Skip to content

Commit c86d817

Browse files
committed
runnable in Telegram
1 parent 68f9446 commit c86d817

File tree

8 files changed

+136
-26
lines changed

8 files changed

+136
-26
lines changed

SukaLambdaEngineTests/StartGameTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class StartGameTest
88
[TestMethod]
99
public void StartGame()
1010
{
11-
RootController controller = new RootController(GamePlatform.Undefined);
11+
RootController controller = new RootController("TestChat", GamePlatform.Undefined);
1212
controller.cmdRouter.ExecuteCommand(
1313
"TestAccount",
1414
"/i68".TrimStart().TrimStart('/'),

src/CommandRouter.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public readonly
4141
public readonly
4242
Dictionary<string, // command name
4343
Tuple<OutGameCommand,
44-
Func<string, // account
44+
Func<string, // account
4545
string, // command body
4646
RootController, bool>>> outGameMethod = new();
4747

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

6868
public void ExecuteCommand(string account, string command, RootController controller)
6969
{
70+
if (command.ToLower() == "start")
71+
{
72+
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
73+
{
74+
foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Static))
75+
{
76+
OutGameCommand? attribute = method.GetCustomAttribute<OutGameCommand>();
77+
if (attribute is null) continue;
78+
controller.logCollector.Log(LogCollector.LogType.OutGame, $"/{attribute.name} {attribute.regex} {attribute.help}");
79+
}
80+
}
81+
return;
82+
}
7083
string[] cmdSplitted = Regex.Split(command, @"\s+");
7184
string commandName = cmdSplitted[0];
7285
string commandBody = String.Join(" ", cmdSplitted[1..]);
@@ -85,7 +98,7 @@ public void ExecuteCommand(string account, string command, RootController contro
8598
outGameMethod[commandName].Item2(account, commandBody, controller);
8699
}
87100

88-
public void RegisterCommandsForCharacter(Character character)
101+
public void RegisterCommandsForCharacter(Character character, SukaLambdaEngine vm)
89102
{
90103
accountToInGameMethod.TryAdd(character.accountId, new());
91104
accountToInGameMethod[character.accountId].TryAdd(character, new());
@@ -99,6 +112,7 @@ public void RegisterCommandsForCharacter(Character character)
99112
accountToInGameMethod[character.accountId][character][attribute.name] =
100113
new Tuple<InGameCommand, Func<string, SukaLambdaEngine, bool>>
101114
(attribute, method.CreateDelegate<Func<string, SukaLambdaEngine, bool>>(skill));
115+
vm.rootController.logCollector.Log(LogCollector.LogType.Character, $"{character.GetType().Name} /{attribute.name} {attribute.regex} {attribute.help}");
102116
}
103117
}
104118
}

src/Map/Predefined/Island68.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
{
33
public class Island68 : Map
44
{
5-
[OutGameCommand("i68", "i68", "Start game on island 68")]
5+
[OutGameCommand("i68", ".*", "Start game on island 68")]
66
public static bool Start(string account, string commandBody, RootController controller)
77
{
8-
if (controller.vm != null) return false;
9-
Map map = new Island68($"file:{nameof(Island68)}?mode=memory&cache=shared");
8+
if (controller.vm != null)
9+
{
10+
controller.logCollector.Log(LogCollector.LogType.Map, "Another game running!");
11+
return false;
12+
}
13+
Map map = new Island68($"file:{nameof(Island68)}-{controller.gamePlatform}-{controller.chatId}.db3?cache=shared");
1014
Lakhesh lakhesh = new Lakhesh(account);
1115
GetWater getWater = new GetWater(lakhesh);
1216
lakhesh.skills.Add(getWater);
@@ -27,6 +31,16 @@ public static bool Start(string account, string commandBody, RootController cont
2731

2832
SukaLambdaEngine vm = new(controller, map: map);
2933
vm.AddCharacter(lakhesh, 0, 0, new Heading(HeadingDirection.E));
34+
35+
controller.logCollector.Log(LogCollector.LogType.Map, @"アイランド68へようこそ!
36+
Take a tour with Lakhesh (菈) around in the forests and lawns of Island 68.
37+
/mv EEESS to move towards the east for 3 blocks, and then south for 2 blocks.
38+
Lakhesh has a mobility of 5 blocks in each round.
39+
It costs 3 mobility to move from forest (森林)
40+
and 0 mobility to move from Warehouse (仓)
41+
Get a bucket of water with /water when Lakhesh is next to a water block (水)
42+
and return to Warehouse (仓) to win the game!");
43+
controller.logCollector.Log(LogCollector.LogType.Map, map.RenderAsText(Language.cn));
3044
return true;
3145
}
3246

@@ -49,7 +63,9 @@ public override List<NumericEffect> Execute(SkillExecution skillExecution, SukaL
4963
{
5064
hasWater = true;
5165
executionSuccess = true;
52-
owner.statusCommitted.Mobility -= (long)Math.Abs(owner.statusCommitted.Mobility * 0.4);
66+
long mobilityReduction = (long)Math.Abs(owner.statusCommitted.Mobility * 0.4);
67+
owner.statusCommitted.Mobility -= mobilityReduction;
68+
owner.statusTemporary.Mobility -= mobilityReduction;
5369
break;
5470
}
5571
return result;

src/RootController.cs

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace sukalambda
1+
using static sukalambda.Island68;
2+
3+
namespace sukalambda
24
{
35
public enum GamePlatform
46
{
@@ -7,14 +9,46 @@ public enum GamePlatform
79
}
810
public class RootController
911
{
10-
public GamePlatform gamePlatform;
12+
public string chatId { get; init; }
13+
public GamePlatform gamePlatform { get; init; }
1114
public CommandRouter cmdRouter = new();
1215
public LogCollector logCollector = new();
1316
public SukaLambdaEngine? vm = null;
1417

15-
public RootController(GamePlatform gamePlatform = GamePlatform.Undefined)
18+
public RootController(string chatId, GamePlatform gamePlatform = GamePlatform.Undefined)
1619
{
20+
this.chatId = chatId;
1721
this.gamePlatform = gamePlatform;
1822
}
23+
24+
[OutGameCommand("exit", ".*", "Exit the game!")]
25+
public static bool Exit(string account, string commandBody, RootController controller)
26+
{
27+
if (controller.vm != null)
28+
{
29+
controller.vm.gameEnded = true;
30+
}
31+
controller.logCollector.Log(LogCollector.LogType.Map, "Will exit the game!");
32+
return true;
33+
}
34+
35+
[OutGameCommand("pause", ".*", "Pause the game!")]
36+
public static bool Pause(string account, string commandBody, RootController controller)
37+
{
38+
if (controller.vm != null)
39+
{
40+
if (!controller.vm.gamePaused)
41+
{
42+
controller.vm.gamePaused = true;
43+
controller.logCollector.Log(LogCollector.LogType.Map, "Paused the game. Send /pause again to continue");
44+
}
45+
else
46+
{
47+
controller.vm.gamePaused = false;
48+
controller.logCollector.Log(LogCollector.LogType.Map, "Game continued.");
49+
}
50+
}
51+
return true;
52+
}
1953
}
2054
}

src/Skill/Skill.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ public override List<NumericEffect> Execute(SkillExecution skillExecution, SukaL
241241
}
242242

243243
[InGameCommand("mv", @"^[↑↓←→NSWEnsweUDLRudlr]+$",
244-
"`mv NNESWWW` for moving 2 blks up, 1 right, 1 down, 3 left")]
244+
"`/mv NNESWWW` for moving 2 blks up, 1 right, 1 down, 3 left")]
245245
public override bool PlanUseSkill(string commandBody, SukaLambdaEngine vm)
246246
{
247247
if (owner.altitude != Altitude.Surface) throw new NotImplementedException();

src/SukaLambdaEngine.cs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
namespace sukalambda
1+
using System;
2+
3+
namespace sukalambda
24
{
35
public class CONFIG
46
{
@@ -45,6 +47,7 @@ public class SukaLambdaEngine
4547

4648
public int timeStarted = DateTime.Now.Second;
4749
public bool gameStarted = false;
50+
public bool gamePaused = false;
4851
public bool gameEnded = false;
4952

5053
public Random rand { get; init; }
@@ -84,7 +87,7 @@ public void AddCharacter(Character character, ushort x, ushort y, Heading headin
8487
if (gameEnded) return;
8588
if (map == null) throw new InvalidOperationException("Map is null!");
8689
semaphore.WaitOne(5000);
87-
rootController.cmdRouter.RegisterCommandsForCharacter(character);
90+
rootController.cmdRouter.RegisterCommandsForCharacter(character, this);
8891
characters[character.persistedStatus.id] = character;
8992
if (alignment != null) character.alignment = alignment;
9093
map.AddCharacter(character, x, y, heading, alignment ?? character.alignment);
@@ -97,7 +100,7 @@ public void AddCharacter(Character character, Alignment? alignment = null)
97100
if (gameEnded) return;
98101
if (map != null) throw new InvalidOperationException("Map had been initialized!");
99102
semaphore.WaitOne(5000);
100-
rootController.cmdRouter.RegisterCommandsForCharacter(character);
103+
rootController.cmdRouter.RegisterCommandsForCharacter(character, this);
101104
if (alignment != null) character.alignment = alignment;
102105
characters[character.persistedStatus.id] = character;
103106
semaphore.Release();
@@ -130,9 +133,9 @@ public void RemoveSkillOfCharacterAndType(Character character, Skill? type=null,
130133
semaphore.WaitOne(500);
131134
if (rounds[currentRoundPointer + roundBias] == null) rounds[currentRoundPointer + roundBias] = new();
132135
Round round = rounds[currentRoundPointer + roundBias];
133-
foreach (SkillExecution execution in round)
134-
if (execution.fromCharacter == character && (type == null || execution.skill.GetType() == type.GetType()))
135-
round.Remove(execution);
136+
round.RemoveAll(execution =>
137+
execution.fromCharacter == character
138+
&& (type == null || execution.skill.GetType() == type.GetType()));
136139
semaphore.Release();
137140
}
138141

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

157160
public void ExecuteRound(bool releaseSemaphore = true)
158161
{
159-
if (gameEnded) return;
162+
if (gameEnded || gamePaused) return;
160163
semaphore.WaitOne(5000);
161164
foreach (var kvp in characters)
162165
kvp.Value.statusTemporary = kvp.Value.statusCommitted.Clone();
163166
if (currentRoundPointer == 0) OnStartGame();
164167
OnStartRound();
165168
HashSet<SkillExecution> executed = new();
169+
if (rounds[currentRoundPointer] == null)
170+
rounds[currentRoundPointer] = new();
166171
for (int currentSkillPointer = 0; currentSkillPointer < rounds[currentRoundPointer].Count; ++currentSkillPointer)
167172
{
168173
rounds[currentRoundPointer].Sort((l, r) =>

src/Utils/Database.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public class GameDbContext : DbContext
2121
public GameDbContext(string dbPath)
2222
{
2323
this.dbPath = dbPath;
24+
this.Database.EnsureDeleted();
2425
this.Database.EnsureCreated(); // works only when there is no table
2526
}
2627
protected override void OnConfiguring(DbContextOptionsBuilder options) => options.UseSqlite($"Data Source={dbPath}");

telegram/Program.cs

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Text;
1010

1111
ConcurrentDictionary<long, RootController> chatIdToController = new();
12+
ConcurrentDictionary<long, Task> chatIdToTimedExecution = new();
1213

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

@@ -64,23 +65,62 @@ async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, Cancel
6465
return;
6566
}
6667

68+
void ExecuteOneRoundAfterMilliseconds(int milliseconds, RootController controller)
69+
{
70+
while (true)
71+
{
72+
if (controller.vm != null && controller.vm.gameEnded) // Ended by external force
73+
{
74+
controller.vm = null;
75+
chatIdToController.Remove(chatId, out _);
76+
chatIdToTimedExecution.Remove(chatId, out _);
77+
botClient.SendTextMessageAsync(
78+
chatId: chatId,
79+
text: "Ready for the next game",
80+
cancellationToken: cancellationToken);
81+
break;
82+
}
83+
Thread.Sleep(milliseconds);
84+
if (controller.vm != null && !controller.vm.gameEnded && !controller.vm.gamePaused)
85+
{
86+
controller.vm.ExecuteRound(releaseSemaphore: false);
87+
List<string> logs = controller.logCollector.PopGameLog();
88+
string contentToSend = String.Join("\r\n", logs);
89+
if (contentToSend != "")
90+
botClient.SendTextMessageAsync(
91+
chatId: chatId,
92+
text: contentToSend,
93+
cancellationToken: cancellationToken);
94+
if (controller.vm.map != null)
95+
botClient.SendTextMessageAsync(chatId: chatId, text: controller.vm.map.RenderAsText(Language.cn), cancellationToken: cancellationToken);
96+
controller.vm.semaphore.Release();
97+
}
98+
}
99+
}
100+
101+
67102
if (message.From != null)
68103
{
69104
long senderId = message.From.Id;
70-
RootController controller = chatIdToController.GetOrAdd(chatId, new RootController(GamePlatform.Telegram));
105+
RootController controller = chatIdToController.GetOrAdd(
106+
chatId, new RootController(chatId.ToString(), GamePlatform.Telegram)
107+
);
71108
controller.cmdRouter.ExecuteCommand(
72109
senderId.ToString(),
73110
messageText.TrimStart().TrimStart('/'),
74111
controller
75112
);
76113
List<string> logs = controller.logCollector.PopGameLog();
77-
foreach (string log in logs)
78-
if (log != "")
79-
sentMessage = await botClient.SendTextMessageAsync(
80-
chatId: chatId,
81-
text: log,
82-
cancellationToken: cancellationToken
83-
);
114+
string contentToSend = String.Join("\r\n", logs);
115+
if (contentToSend != "")
116+
sentMessage = await botClient.SendTextMessageAsync(
117+
chatId: chatId,
118+
text: contentToSend,
119+
cancellationToken: cancellationToken
120+
);
121+
if (controller.vm?.map != null && !chatIdToTimedExecution.ContainsKey(chatId))
122+
_ = chatIdToTimedExecution.GetOrAdd(chatId,
123+
Task.Run(() => ExecuteOneRoundAfterMilliseconds(15000, controller)));
84124
}
85125
}
86126

0 commit comments

Comments
 (0)