Skip to content

Commit

Permalink
runnable in Telegram
Browse files Browse the repository at this point in the history
  • Loading branch information
Hecate2 committed Sep 4, 2023
1 parent 68f9446 commit c86d817
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 26 deletions.
2 changes: 1 addition & 1 deletion SukaLambdaEngineTests/StartGameTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class StartGameTest
[TestMethod]
public void StartGame()
{
RootController controller = new RootController(GamePlatform.Undefined);
RootController controller = new RootController("TestChat", GamePlatform.Undefined);
controller.cmdRouter.ExecuteCommand(
"TestAccount",
"/i68".TrimStart().TrimStart('/'),
Expand Down
18 changes: 16 additions & 2 deletions src/CommandRouter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public readonly
public readonly
Dictionary<string, // command name
Tuple<OutGameCommand,
Func<string, // account
Func<string, // account
string, // command body
RootController, bool>>> outGameMethod = new();

Expand All @@ -67,6 +67,19 @@ public void RegisterOutGameCommand()

public void ExecuteCommand(string account, string command, RootController controller)
{
if (command.ToLower() == "start")
{
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
{
foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Static))
{
OutGameCommand? attribute = method.GetCustomAttribute<OutGameCommand>();
if (attribute is null) continue;
controller.logCollector.Log(LogCollector.LogType.OutGame, $"/{attribute.name} {attribute.regex} {attribute.help}");
}
}
return;
}
string[] cmdSplitted = Regex.Split(command, @"\s+");
string commandName = cmdSplitted[0];
string commandBody = String.Join(" ", cmdSplitted[1..]);
Expand All @@ -85,7 +98,7 @@ public void ExecuteCommand(string account, string command, RootController contro
outGameMethod[commandName].Item2(account, commandBody, controller);
}

public void RegisterCommandsForCharacter(Character character)
public void RegisterCommandsForCharacter(Character character, SukaLambdaEngine vm)
{
accountToInGameMethod.TryAdd(character.accountId, new());
accountToInGameMethod[character.accountId].TryAdd(character, new());
Expand All @@ -99,6 +112,7 @@ public void RegisterCommandsForCharacter(Character character)
accountToInGameMethod[character.accountId][character][attribute.name] =
new Tuple<InGameCommand, Func<string, SukaLambdaEngine, bool>>
(attribute, method.CreateDelegate<Func<string, SukaLambdaEngine, bool>>(skill));
vm.rootController.logCollector.Log(LogCollector.LogType.Character, $"{character.GetType().Name} /{attribute.name} {attribute.regex} {attribute.help}");
}
}
}
Expand Down
24 changes: 20 additions & 4 deletions src/Map/Predefined/Island68.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@
{
public class Island68 : Map
{
[OutGameCommand("i68", "i68", "Start game on island 68")]
[OutGameCommand("i68", ".*", "Start game on island 68")]
public static bool Start(string account, string commandBody, RootController controller)
{
if (controller.vm != null) return false;
Map map = new Island68($"file:{nameof(Island68)}?mode=memory&cache=shared");
if (controller.vm != null)
{
controller.logCollector.Log(LogCollector.LogType.Map, "Another game running!");
return false;
}
Map map = new Island68($"file:{nameof(Island68)}-{controller.gamePlatform}-{controller.chatId}.db3?cache=shared");
Lakhesh lakhesh = new Lakhesh(account);
GetWater getWater = new GetWater(lakhesh);
lakhesh.skills.Add(getWater);
Expand All @@ -27,6 +31,16 @@ public static bool Start(string account, string commandBody, RootController cont

SukaLambdaEngine vm = new(controller, map: map);
vm.AddCharacter(lakhesh, 0, 0, new Heading(HeadingDirection.E));

controller.logCollector.Log(LogCollector.LogType.Map, @"アイランド68へようこそ!
Take a tour with Lakhesh (菈) around in the forests and lawns of Island 68.
/mv EEESS to move towards the east for 3 blocks, and then south for 2 blocks.
Lakhesh has a mobility of 5 blocks in each round.
It costs 3 mobility to move from forest (森林)
and 0 mobility to move from Warehouse (仓)
Get a bucket of water with /water when Lakhesh is next to a water block (水)
and return to Warehouse (仓) to win the game!");
controller.logCollector.Log(LogCollector.LogType.Map, map.RenderAsText(Language.cn));
return true;
}

Expand All @@ -49,7 +63,9 @@ public override List<NumericEffect> Execute(SkillExecution skillExecution, SukaL
{
hasWater = true;
executionSuccess = true;
owner.statusCommitted.Mobility -= (long)Math.Abs(owner.statusCommitted.Mobility * 0.4);
long mobilityReduction = (long)Math.Abs(owner.statusCommitted.Mobility * 0.4);
owner.statusCommitted.Mobility -= mobilityReduction;
owner.statusTemporary.Mobility -= mobilityReduction;
break;
}
return result;
Expand Down
40 changes: 37 additions & 3 deletions src/RootController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace sukalambda
using static sukalambda.Island68;

namespace sukalambda
{
public enum GamePlatform
{
Expand All @@ -7,14 +9,46 @@ public enum GamePlatform
}
public class RootController
{
public GamePlatform gamePlatform;
public string chatId { get; init; }
public GamePlatform gamePlatform { get; init; }
public CommandRouter cmdRouter = new();
public LogCollector logCollector = new();
public SukaLambdaEngine? vm = null;

public RootController(GamePlatform gamePlatform = GamePlatform.Undefined)
public RootController(string chatId, GamePlatform gamePlatform = GamePlatform.Undefined)
{
this.chatId = chatId;
this.gamePlatform = gamePlatform;
}

[OutGameCommand("exit", ".*", "Exit the game!")]
public static bool Exit(string account, string commandBody, RootController controller)
{
if (controller.vm != null)
{
controller.vm.gameEnded = true;
}
controller.logCollector.Log(LogCollector.LogType.Map, "Will exit the game!");
return true;
}

[OutGameCommand("pause", ".*", "Pause the game!")]
public static bool Pause(string account, string commandBody, RootController controller)
{
if (controller.vm != null)
{
if (!controller.vm.gamePaused)
{
controller.vm.gamePaused = true;
controller.logCollector.Log(LogCollector.LogType.Map, "Paused the game. Send /pause again to continue");
}
else
{
controller.vm.gamePaused = false;
controller.logCollector.Log(LogCollector.LogType.Map, "Game continued.");
}
}
return true;
}
}
}
2 changes: 1 addition & 1 deletion src/Skill/Skill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ public override List<NumericEffect> Execute(SkillExecution skillExecution, SukaL
}

[InGameCommand("mv", @"^[↑↓←→NSWEnsweUDLRudlr]+$",
"`mv NNESWWW` for moving 2 blks up, 1 right, 1 down, 3 left")]
"`/mv NNESWWW` for moving 2 blks up, 1 right, 1 down, 3 left")]
public override bool PlanUseSkill(string commandBody, SukaLambdaEngine vm)
{
if (owner.altitude != Altitude.Surface) throw new NotImplementedException();
Expand Down
19 changes: 12 additions & 7 deletions src/SukaLambdaEngine.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
namespace sukalambda
using System;

namespace sukalambda
{
public class CONFIG
{
Expand Down Expand Up @@ -45,6 +47,7 @@ public class SukaLambdaEngine

public int timeStarted = DateTime.Now.Second;
public bool gameStarted = false;
public bool gamePaused = false;
public bool gameEnded = false;

public Random rand { get; init; }
Expand Down Expand Up @@ -84,7 +87,7 @@ public void AddCharacter(Character character, ushort x, ushort y, Heading headin
if (gameEnded) return;
if (map == null) throw new InvalidOperationException("Map is null!");
semaphore.WaitOne(5000);
rootController.cmdRouter.RegisterCommandsForCharacter(character);
rootController.cmdRouter.RegisterCommandsForCharacter(character, this);
characters[character.persistedStatus.id] = character;
if (alignment != null) character.alignment = alignment;
map.AddCharacter(character, x, y, heading, alignment ?? character.alignment);
Expand All @@ -97,7 +100,7 @@ public void AddCharacter(Character character, Alignment? alignment = null)
if (gameEnded) return;
if (map != null) throw new InvalidOperationException("Map had been initialized!");
semaphore.WaitOne(5000);
rootController.cmdRouter.RegisterCommandsForCharacter(character);
rootController.cmdRouter.RegisterCommandsForCharacter(character, this);
if (alignment != null) character.alignment = alignment;
characters[character.persistedStatus.id] = character;
semaphore.Release();
Expand Down Expand Up @@ -130,9 +133,9 @@ public void RemoveSkillOfCharacterAndType(Character character, Skill? type=null,
semaphore.WaitOne(500);
if (rounds[currentRoundPointer + roundBias] == null) rounds[currentRoundPointer + roundBias] = new();
Round round = rounds[currentRoundPointer + roundBias];
foreach (SkillExecution execution in round)
if (execution.fromCharacter == character && (type == null || execution.skill.GetType() == type.GetType()))
round.Remove(execution);
round.RemoveAll(execution =>
execution.fromCharacter == character
&& (type == null || execution.skill.GetType() == type.GetType()));
semaphore.Release();
}

Expand All @@ -156,13 +159,15 @@ public void AddEternalEffect(MetaEffect effect)

public void ExecuteRound(bool releaseSemaphore = true)
{
if (gameEnded) return;
if (gameEnded || gamePaused) return;
semaphore.WaitOne(5000);
foreach (var kvp in characters)
kvp.Value.statusTemporary = kvp.Value.statusCommitted.Clone();
if (currentRoundPointer == 0) OnStartGame();
OnStartRound();
HashSet<SkillExecution> executed = new();
if (rounds[currentRoundPointer] == null)
rounds[currentRoundPointer] = new();
for (int currentSkillPointer = 0; currentSkillPointer < rounds[currentRoundPointer].Count; ++currentSkillPointer)
{
rounds[currentRoundPointer].Sort((l, r) =>
Expand Down
1 change: 1 addition & 0 deletions src/Utils/Database.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class GameDbContext : DbContext
public GameDbContext(string dbPath)
{
this.dbPath = dbPath;
this.Database.EnsureDeleted();
this.Database.EnsureCreated(); // works only when there is no table
}
protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlite($"Data Source={dbPath}");
Expand Down
56 changes: 48 additions & 8 deletions telegram/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Text;

ConcurrentDictionary<long, RootController> chatIdToController = new();
ConcurrentDictionary<long, Task> chatIdToTimedExecution = new();

var botClient = new TelegramBotClient(System.IO.File.ReadAllText(@"botToken.txt", Encoding.UTF8).TrimEnd().TrimStart());

Expand Down Expand Up @@ -64,23 +65,62 @@ async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, Cancel
return;
}

void ExecuteOneRoundAfterMilliseconds(int milliseconds, RootController controller)
{
while (true)
{
if (controller.vm != null && controller.vm.gameEnded) // Ended by external force
{
controller.vm = null;
chatIdToController.Remove(chatId, out _);
chatIdToTimedExecution.Remove(chatId, out _);
botClient.SendTextMessageAsync(
chatId: chatId,
text: "Ready for the next game",
cancellationToken: cancellationToken);
break;
}
Thread.Sleep(milliseconds);
if (controller.vm != null && !controller.vm.gameEnded && !controller.vm.gamePaused)
{
controller.vm.ExecuteRound(releaseSemaphore: false);
List<string> logs = controller.logCollector.PopGameLog();
string contentToSend = String.Join("\r\n", logs);
if (contentToSend != "")
botClient.SendTextMessageAsync(
chatId: chatId,
text: contentToSend,
cancellationToken: cancellationToken);
if (controller.vm.map != null)
botClient.SendTextMessageAsync(chatId: chatId, text: controller.vm.map.RenderAsText(Language.cn), cancellationToken: cancellationToken);
controller.vm.semaphore.Release();
}
}
}


if (message.From != null)
{
long senderId = message.From.Id;
RootController controller = chatIdToController.GetOrAdd(chatId, new RootController(GamePlatform.Telegram));
RootController controller = chatIdToController.GetOrAdd(
chatId, new RootController(chatId.ToString(), GamePlatform.Telegram)
);
controller.cmdRouter.ExecuteCommand(
senderId.ToString(),
messageText.TrimStart().TrimStart('/'),
controller
);
List<string> logs = controller.logCollector.PopGameLog();
foreach (string log in logs)
if (log != "")
sentMessage = await botClient.SendTextMessageAsync(
chatId: chatId,
text: log,
cancellationToken: cancellationToken
);
string contentToSend = String.Join("\r\n", logs);
if (contentToSend != "")
sentMessage = await botClient.SendTextMessageAsync(
chatId: chatId,
text: contentToSend,
cancellationToken: cancellationToken
);
if (controller.vm?.map != null && !chatIdToTimedExecution.ContainsKey(chatId))
_ = chatIdToTimedExecution.GetOrAdd(chatId,
Task.Run(() => ExecuteOneRoundAfterMilliseconds(15000, controller)));
}
}

Expand Down

0 comments on commit c86d817

Please sign in to comment.