diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 5efc564..b5c47f5 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,7 +2,8 @@ "permissions": { "allow": [ "Bash(dotnet test:*)", - "Bash(dotnet build)" + "Bash(dotnet build)", + "Bash(dir:*)" ], "deny": [], "ask": [] diff --git a/JoshHeaps.Net.UiTests/MultiplayerTests.cs b/JoshHeaps.Net.UiTests/MultiplayerTests.cs index 510b53d..cbd2660 100644 --- a/JoshHeaps.Net.UiTests/MultiplayerTests.cs +++ b/JoshHeaps.Net.UiTests/MultiplayerTests.cs @@ -81,18 +81,4 @@ public class MultiplayerTests : PageTest // Should see SignalR connected message in console Assert.That(consoleLogs, Does.Contain("βœ… SignalR connected").Or.Contain("SignalR connected")); } - - [Test] - public async Task Move_Updates_Are_Sent_Via_SignalR() - { - // This would require more complex setup with two players - // For now, just verify the SignalR methods exist in the JavaScript - var jsContent = await Page.EvaluateAsync(@" - () => { - return document.querySelector('script[src*=""chessLogic.js""]') ? 'chessLogic.js loaded' : 'not found'; - } - "); - - Assert.That(jsContent, Is.EqualTo("chessLogic.js loaded")); - } } \ No newline at end of file diff --git a/JoshHeaps.Net/Controllers/ChessController.cs b/JoshHeaps.Net/Controllers/ChessController.cs index b19b3a0..157cdd7 100644 --- a/JoshHeaps.Net/Controllers/ChessController.cs +++ b/JoshHeaps.Net/Controllers/ChessController.cs @@ -9,14 +9,21 @@ namespace JoshHeaps.Net.Controllers; [ApiController] [Route("api/[controller]")] -public class ChessController(IChessService chessService, IHubContext chessHub, IBackgroundTaskQueue queue) : ControllerBase +public class ChessController( + IChessService chessService, + IHubContext chessHub, + IBackgroundTaskQueue queue) : ControllerBase { /// /// Store of ongoing games. /// private static readonly ConcurrentDictionary _games = []; + private static readonly ConcurrentDictionary _gameRemovalTasks = []; + private static readonly ConcurrentDictionary _gameRemovalCancellationTokens = []; - private static ConcurrentDictionary _gameRemovalTasks = []; + private static readonly TimeSpan _computerGameTimeout = TimeSpan.FromHours(1); + private static readonly TimeSpan _multiplayerGameTimeout = TimeSpan.FromSeconds(5); + private static readonly TimeSpan _gameCleanupTimeout = TimeSpan.FromMinutes(1); /// /// Create a new chess game and store it in-memory. @@ -54,7 +61,7 @@ public class ChessController(IChessService chessService, IHubContext c }); } - ScheduleRemoveGame(gameState.GameId, TimeSpan.FromHours(1)); + ScheduleRemoveGame(gameState.GameId, _computerGameTimeout); return Ok(new { @@ -96,7 +103,7 @@ public class ChessController(IChessService chessService, IHubContext c isWhite = false; } - ScheduleRemoveGame(gameState.GameId, TimeSpan.FromDays(1)); + ScheduleRemoveGame(gameState.GameId, _multiplayerGameTimeout); return Ok(new { @@ -175,18 +182,11 @@ public class ChessController(IChessService chessService, IHubContext c return BadRequest(result); if (result.IsCheckmate || result.IsStalemate) - { - // queue game removal - ScheduleRemoveGame(gameState.GameId, TimeSpan.FromMinutes(1)); - } + ScheduleRemoveGame(gameState.GameId, _gameCleanupTimeout); + else if (gameState.IsVsComputer) + ScheduleRemoveGame(gameState.GameId, _computerGameTimeout); else - { - // increase timeout if play continues. - if (gameState.IsVsComputer) - ScheduleRemoveGame(gameState.GameId, TimeSpan.FromHours(1)); - else - ScheduleRemoveGame(gameState.GameId, TimeSpan.FromDays(1)); - } + ScheduleRemoveGame(gameState.GameId, _multiplayerGameTimeout); if (gameState.IsVsComputer && gameState.Computer is not null) queue.Queue(() => gameState.Computer.MakeMove(gameState, chessHub, chessService)); @@ -229,31 +229,36 @@ public class ChessController(IChessService chessService, IHubContext c private static void ScheduleRemoveGame(Guid id, TimeSpan delay) { - if (_gameRemovalTasks.ContainsKey(id)) + if (_gameRemovalCancellationTokens.TryRemove(id, out var oldCts)) { - _gameRemovalTasks[id] = Task.Run(async () => - { - await Task.Delay(delay); - - if (_games[id].Computer is not null) - await _games[id].Computer!.DisposeAsync(); - - _games.Remove(id, out _); - _gameRemovalTasks.Remove(id, out _); - }); - - return; + oldCts.Cancel(); + oldCts.Dispose(); } - _gameRemovalTasks.TryAdd(id, Task.Run(async () => + var cts = new CancellationTokenSource(); + _gameRemovalCancellationTokens[id] = cts; + + _gameRemovalTasks[id] = Task.Run(async () => { - await Task.Delay(delay); + try + { + await Task.Delay(delay, cts.Token); - if (_games[id].Computer is not null) - await _games[id].Computer!.DisposeAsync(); + if (_games.TryGetValue(id, out var game) && game.Computer is not null) + await game.Computer.DisposeAsync(); - _games.Remove(id, out _); - _gameRemovalTasks.Remove(id, out _); - })); + _games.Remove(id, out _); + } + catch (OperationCanceledException) { } + finally + { + if (_gameRemovalCancellationTokens.TryGetValue(id, out var currentCts) && currentCts == cts) + { + _gameRemovalCancellationTokens.TryRemove(id, out _); + } + + cts.Dispose(); + } + }); } } diff --git a/JoshHeaps.Net/Pages/Index.cshtml b/JoshHeaps.Net/Pages/Index.cshtml index f3ce34d..da8f1ea 100644 --- a/JoshHeaps.Net/Pages/Index.cshtml +++ b/JoshHeaps.Net/Pages/Index.cshtml @@ -1,270 +1,93 @@ ο»Ώ@page @model JoshHeaps.Net.Pages.IndexModel @{ - ViewData["Title"] = "Happy 5th Anniversary, Morgan!"; + ViewData["Title"] = "JoshHeaps.Net"; Layout = "_Layout"; } - - -
-

Happy 5th Anniversary, Morgan!

- -
- -
πŸ’– πŸ’• πŸ’—
- -