From d58a499905b8be41cb14b69132f79bd541106ff8 Mon Sep 17 00:00:00 2001 From: Andrew Diego Beers Date: Thu, 6 Jun 2024 21:24:26 -0400 Subject: [PATCH] Add in new changes to architecture and reflect new saga pattern --- .../Hubs/GameHub.cs | 31 +++++ .../Sagas/GameSagaOrchestrator.cs | 70 ++++++---- .../Services/GameStateService.cs | 69 ++++++++++ ...TheOmenDen.CrowsAgainstHumility.Api.csproj | 120 +++++++++--------- .../Enums/GameStatus.cs | 16 ++- .../Identifiers/SessionId.cs | 4 + .../Models/GameContext.cs | 6 +- .../Models/GameSessionDto.cs | 16 +++ 8 files changed, 242 insertions(+), 90 deletions(-) create mode 100644 TheOmenDen.CrowsAgainstHumility.Api/Hubs/GameHub.cs create mode 100644 TheOmenDen.CrowsAgainstHumility.Api/Services/GameStateService.cs create mode 100644 TheOmenDen.CrowsAgainstHumility.Core/Identifiers/SessionId.cs create mode 100644 TheOmenDen.CrowsAgainstHumility.Core/Models/GameSessionDto.cs diff --git a/TheOmenDen.CrowsAgainstHumility.Api/Hubs/GameHub.cs b/TheOmenDen.CrowsAgainstHumility.Api/Hubs/GameHub.cs new file mode 100644 index 000000000..00e9707a5 --- /dev/null +++ b/TheOmenDen.CrowsAgainstHumility.Api/Hubs/GameHub.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.SignalR; +using TheOmenDen.CrowsAgainstHumility.Api.Sagas; +using TheOmenDen.CrowsAgainstHumility.Core.Identifiers; + +namespace TheOmenDen.CrowsAgainstHumility.Api.Hubs; + +public class GameHub(ILogger logger, GameSagaOrchestrator gameSagaOrchestrator) + : Hub +{ + public Task StartGame(SessionId sessionId, CancellationToken cancellationToken = default) => gameSagaOrchestrator.StartGameAsync(sessionId, cancellationToken); + + public async Task JoinGame(SessionId sessionId, CancellationToken cancellationToken = default) + { + await Groups.AddToGroupAsync(Context.ConnectionId, sessionId.ToString(), cancellationToken); + var gameState = await gameSagaOrchestrator.GetGameStateAsync(sessionId, cancellationToken); + await Clients.Caller.SendAsync("InitializeGameState", gameState, cancellationToken); + } + + public async Task SendPlayerAction(SessionId sessionId, PlayerAction playerAction, CancellationToken cancellationToken = default) + { + var updatedGameState = await gameSagaOrchestrator.ProccessPlayerAction(sessionId, playerAction, cancellationToken); + + await Clients.Group(sessionId.ToString()).SendAsync("GameStateUpdated", updatedGameState, cancellationToken); + } + + public async Task LeaveGame(SessionId sessionId, CancellationToken cancellationToken = default) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, sessionId.ToString(), cancellationToken); + await Clients.Group(sessionId.ToString()).SendAsync("PlayerLeft", Context.ConnectionId, cancellationToken); + } +} \ No newline at end of file diff --git a/TheOmenDen.CrowsAgainstHumility.Api/Sagas/GameSagaOrchestrator.cs b/TheOmenDen.CrowsAgainstHumility.Api/Sagas/GameSagaOrchestrator.cs index a218538e8..5c658c8f4 100644 --- a/TheOmenDen.CrowsAgainstHumility.Api/Sagas/GameSagaOrchestrator.cs +++ b/TheOmenDen.CrowsAgainstHumility.Api/Sagas/GameSagaOrchestrator.cs @@ -1,35 +1,59 @@ -using System.Linq.Expressions; -using TheOmenDen.CrowsAgainstHumility.Core.Models; -using TheOmenDen.CrowsAgainstHumility.Core.Rules; +using TheOmenDen.CrowsAgainstHumility.Core.Enums; namespace TheOmenDen.CrowsAgainstHumility.Api.Sagas; -public sealed class GameSagaOrchestrator(RuleEngine ruleEngine, GameEngine gameEngine, ILogger logger) +public sealed class GameSagaOrchestrator(IGameStateService gameStateService, ILogger logger) { - public async ValueTask ExecuteGameSagaAsync(GameSessionDto session, CancellationToken cancellationToken = default) + public async Task StartGameAsync(SessionId sessionId, CancellationToken cancellationToken = default) { - try - { - await gameEngine.SetupGame(session, cancellationToken); - while (!gameEngine.IsGameOver) - { - await ExecutePlayPhaseAsync(session, cancellationToken); - } - await gameEngine.FinalizeGameAsync(session, cancellationToken); - } - catch (Exception ex) + var initialState = await gameStateService.InitializeGameStateAsync(sessionId, cancellationToken); + + await gameStateService.SaveGameStateAsync(initialState, cancellationToken); + + await NotifyPlayersAsync(sessionId, "Game has started", cancellationToken); + + await ExecuteGameLoopAsync(sessionId, cancellationToken); + } + + private async Task ExecuteGameLoopAsync(SessionId sessionId, CancellationToken cancellationToken) + { + var gameIsRunning = true; + while (gameIsRunning) { - logger.LogError(ex, "An error occurred while executing the game saga: {Message}", ex.Message); - return false; + var gameState = await gameStateService.GetGameStateAsync(sessionId, cancellationToken); + + // Handle game round Logic + await HandleRoundAsync(gameState); + + // Check if game should continue + gameIsRunning = CheckGameForContinuation(gameState); } + + // Finalize Game and clean up + await FinalizeGameAsync(sessionId, cancellationToken); } - private async ValueTask ExecutePlayPhaseAsync(GameSessionDto session, CancellationToken cancellationToken = default) + private async Task HandleRoundAsync(GameSessionDto gameState) { - var tasks = session.Players.Select(player => - gameEngine.ProcessPlayerMoveAsync(session.Id, player.Id, player.NextMove, cancellationToken)); - await Task.WhenAll(tasks); - await ruleEngine.ApplyRoundRules(session, cancellationToken); - await gameEngine.UpdateGameStateAsync(session, cancellationToken); + // Stub this out later + } + + private bool CheckGameForContinuation(GameSessionDto gameState) => gameState.Status != GameStatus.Completed; + + private async Task FinalizeGameAsync(SessionId sessionId, CancellationToken cancellationToken) + { + await gameStateService.UpdateGameStatusAsync(sessionId, new PlayerAction { Type = ActionType.EndGame }, cancellationToken); + + await NotifyPlayersAsync(sessionId, "Game has ended", cancellationToken); + + await gameStateService.DeleteGameStateAsync(sessionId, cancellationToken); + } + + + private async Task NotifyPlayersAsync(SessionId sessionId, string message, CancellationToken cancellationToken) + { + var players = await gameStateService.GetPlayersAsync(sessionId, cancellationToken); + + // Notify Players via SignalR Hub } } diff --git a/TheOmenDen.CrowsAgainstHumility.Api/Services/GameStateService.cs b/TheOmenDen.CrowsAgainstHumility.Api/Services/GameStateService.cs new file mode 100644 index 000000000..01972b0a2 --- /dev/null +++ b/TheOmenDen.CrowsAgainstHumility.Api/Services/GameStateService.cs @@ -0,0 +1,69 @@ +using EasyCaching.Core; +using FastEndpoints; +using TheOmenDen.CrowsAgainstHumility.Core.Identifiers; +using TheOmenDen.CrowsAgainstHumility.Core.Models; + +namespace TheOmenDen.CrowsAgainstHumility.Api.Services; + +public interface IGameStateService +{ + Task InitializeGameStateAsync(SessionId sessionId, CancellationToken cancellationToken = default); + Task GetGameStateAsync(SessionId sessionId, CancellationToken cancellationToken = default); + Task SaveGameStateAsync(GameSessionDto gameState, CancellationToken cancellationToken = default); + Task UpdateGameStateAsync(SessionId sessionId, PlayerAction playerAction, CancellationToken cancellationToken = default); +} + +[RegisterService(LifeTime.Scoped)] +internal sealed class GameStateService(IEasyCachingProvider provider, IGameRepository gameRepository) +: IGameStateService +{ + public async Task InitializeGameStateAsync(SessionId sessionId, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task GetGameStateAsync(SessionId sessionId, CancellationToken cancellationToken = default) + { + var cacheKey = $"gamestate-{sessionId}"; + + return provider.GetAsync( + cacheKey, + async () => await gameRepository.GetGameStateAsync(sessionId), + TimeSpan.FromSeconds(600), + cancellationToken); + } + + + public async Task SaveGameStateAsync(GameSessionDto gameState, CancellationToken cancellationToken = default) + { + var cacheKey = $"gamestate-{gameState.Id}"; + + await gameRepository.SaveGameStateAsync(gameState, cancellationToken); + await provider.SetAsync(cacheKey, gameState, TimeSpan.FromSeconds(600), cancellationToken); + } + + public async Task UpdateGameStateAsync(SessionId sessionId, PlayerAction playerAction, CancellationToken cancellationToken = default) + { + var cacheKey = $"gamestate-{sessionId}"; + + var gameState = await GetGameStateAsync(sessionId, cancellationToken); + + if (gameState is null) + { + return; + } + + var updatedGameState = ApplyActionToGameState(gameState, playerAction); + await gameRepository.UpdateGameStateAsync(updatedGameState); + await provider.SetAsync(cacheKey, updatedGameState, TimeSpan.FromSeconds(600), cancellationToken); + } + + private GameSessionDto ApplyActionToGameState(GameSessionDto gameState, PlayerAction playerAction) + { + return gameState with + { + // Apply the player action to the game state + }; + } + +} \ No newline at end of file diff --git a/TheOmenDen.CrowsAgainstHumility.Api/TheOmenDen.CrowsAgainstHumility.Api.csproj b/TheOmenDen.CrowsAgainstHumility.Api/TheOmenDen.CrowsAgainstHumility.Api.csproj index 432ca1d45..48d25fa7a 100644 --- a/TheOmenDen.CrowsAgainstHumility.Api/TheOmenDen.CrowsAgainstHumility.Api.csproj +++ b/TheOmenDen.CrowsAgainstHumility.Api/TheOmenDen.CrowsAgainstHumility.Api.csproj @@ -1,65 +1,65 @@ - + - - net8.0 - enable - enable - aspnet-TheOmenDen.CrowsAgainstHumility.Api-ffe9d17d-ae78-41b3-9d74-3c12b6e839e4 - Linux - + + net8.0 + enable + enable + aspnet-TheOmenDen.CrowsAgainstHumility.Api-ffe9d17d-ae78-41b3-9d74-3c12b6e839e4 + Linux + - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + diff --git a/TheOmenDen.CrowsAgainstHumility.Core/Enums/GameStatus.cs b/TheOmenDen.CrowsAgainstHumility.Core/Enums/GameStatus.cs index f4833701d..ad432036a 100644 --- a/TheOmenDen.CrowsAgainstHumility.Core/Enums/GameStatus.cs +++ b/TheOmenDen.CrowsAgainstHumility.Core/Enums/GameStatus.cs @@ -5,6 +5,7 @@ namespace TheOmenDen.CrowsAgainstHumility.Core.Enums; public abstract class GameStatus(string name, int value) : SmartEnum(name, value) { + public static readonly GameStatus Setup = new SetupType(); public static readonly GameStatus WaitingToStart = new WaitingToStartType(); public static readonly GameStatus InProgress = new InProgressType(); public static readonly GameStatus Paused = new PausedType(); @@ -22,7 +23,16 @@ public override async Task HandleStateAsync(GameState context) } } - private sealed class InProgressType() : GameStatus("InProgress", 2) + private sealed class SetupType() : GameStatus("Setup", 2) + { + public override async Task HandleStateAsync(GameState context) + { + // Setup the game + await Task.CompletedTask; + } + } + + private sealed class InProgressType() : GameStatus("InProgress", 3) { public override async Task HandleStateAsync(GameState context) { @@ -31,7 +41,7 @@ public override async Task HandleStateAsync(GameState context) } } - private sealed class PausedType() : GameStatus("Paused", 3) + private sealed class PausedType() : GameStatus("Paused", 4) { public override async Task HandleStateAsync(GameState context) { @@ -40,7 +50,7 @@ public override async Task HandleStateAsync(GameState context) } } - private sealed class CompletedType() : GameStatus("Completed", 4) + private sealed class CompletedType() : GameStatus("Completed", 5) { public override async Task HandleStateAsync(GameState context) { diff --git a/TheOmenDen.CrowsAgainstHumility.Core/Identifiers/SessionId.cs b/TheOmenDen.CrowsAgainstHumility.Core/Identifiers/SessionId.cs new file mode 100644 index 000000000..a3359aa5a --- /dev/null +++ b/TheOmenDen.CrowsAgainstHumility.Core/Identifiers/SessionId.cs @@ -0,0 +1,4 @@ +namespace TheOmenDen.CrowsAgainstHumility.Core.Identifiers; + +[StronglyTypedId] +public partial struct SessionId; \ No newline at end of file diff --git a/TheOmenDen.CrowsAgainstHumility.Core/Models/GameContext.cs b/TheOmenDen.CrowsAgainstHumility.Core/Models/GameContext.cs index c4f2d170a..525747dd4 100644 --- a/TheOmenDen.CrowsAgainstHumility.Core/Models/GameContext.cs +++ b/TheOmenDen.CrowsAgainstHumility.Core/Models/GameContext.cs @@ -4,9 +4,7 @@ namespace TheOmenDen.CrowsAgainstHumility.Core.Models; public class GameContext { - public Deck BlackDeck { get; set; } - public Deck WhiteDeck { get; set; } - - + public Deck BlackDeck { get; set; } + public Deck WhiteDeck { get; set; } } \ No newline at end of file diff --git a/TheOmenDen.CrowsAgainstHumility.Core/Models/GameSessionDto.cs b/TheOmenDen.CrowsAgainstHumility.Core/Models/GameSessionDto.cs new file mode 100644 index 000000000..17e791843 --- /dev/null +++ b/TheOmenDen.CrowsAgainstHumility.Core/Models/GameSessionDto.cs @@ -0,0 +1,16 @@ +using TheOmenDen.CrowsAgainstHumility.Core.Enums; +using TheOmenDen.CrowsAgainstHumility.Core.Identifiers; +using TheOmenDen.CrowsAgainstHumility.Core.Models.Cards; + +namespace TheOmenDen.CrowsAgainstHumility.Core.Models; + +public sealed record GameSessionDto( + SessionId Id, + IReadOnlyList Players, + ImmutableBlackCard CurrentBlackCard, + IReadOnlyDictionary> PlayerSubmissions, + PlayerState CurrentJudge, + GameStatus Status, + TimeSpan BlackCardReadingTimerDuration, + TimeSpan SubmissionTimerDuration, + TimeSpan ReviewTimerDuration); \ No newline at end of file