Skip to content

Commit

Permalink
Add in new changes to architecture and reflect new saga pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
AluTheCrow committed Jun 7, 2024
1 parent 79fc105 commit d58a499
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 90 deletions.
31 changes: 31 additions & 0 deletions TheOmenDen.CrowsAgainstHumility.Api/Hubs/GameHub.cs
Original file line number Diff line number Diff line change
@@ -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<GameHub> 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);
}
}
70 changes: 47 additions & 23 deletions TheOmenDen.CrowsAgainstHumility.Api/Sagas/GameSagaOrchestrator.cs
Original file line number Diff line number Diff line change
@@ -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<GameSagaOrchestrator> logger)
public sealed class GameSagaOrchestrator(IGameStateService gameStateService, ILogger<GameSagaOrchestrator> logger)
{
public async ValueTask<bool> 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
}
}
69 changes: 69 additions & 0 deletions TheOmenDen.CrowsAgainstHumility.Api/Services/GameStateService.cs
Original file line number Diff line number Diff line change
@@ -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<GameSessionDto> InitializeGameStateAsync(SessionId sessionId, CancellationToken cancellationToken = default);
Task<GameSessionDto> GetGameStateAsync(SessionId sessionId, CancellationToken cancellationToken = default);
Task SaveGameStateAsync(GameSessionDto gameState, CancellationToken cancellationToken = default);
Task UpdateGameStateAsync(SessionId sessionId, PlayerAction playerAction, CancellationToken cancellationToken = default);
}

[RegisterService<IGameStateService>(LifeTime.Scoped)]
internal sealed class GameStateService(IEasyCachingProvider provider, IGameRepository gameRepository)
: IGameStateService
{
public async Task<GameSessionDto> InitializeGameStateAsync(SessionId sessionId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}

public Task<GameSessionDto> GetGameStateAsync(SessionId sessionId, CancellationToken cancellationToken = default)
{
var cacheKey = $"gamestate-{sessionId}";

return provider.GetAsync<GameSessionDto>(
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
};
}

}
Original file line number Diff line number Diff line change
@@ -1,65 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-TheOmenDen.CrowsAgainstHumility.Api-ffe9d17d-ae78-41b3-9d74-3c12b6e839e4</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>aspnet-TheOmenDen.CrowsAgainstHumility.Api-ffe9d17d-ae78-41b3-9d74-3c12b6e839e4</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Ardalis.SmartEnum" Version="8.0.0" />
<PackageReference Include="Ardalis.SmartEnum.SystemTextJson" Version="8.0.0" />
<PackageReference Include="Azure.Core" Version="1.39.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.1" />
<PackageReference Include="Azure.Identity" Version="1.11.3" />
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.6.0" />
<PackageReference Include="FastEndpoints" Version="5.25.0" />
<PackageReference Include="FastEndpoints.ClientGen.Kiota" Version="5.25.0" />
<PackageReference Include="FastEndpoints.Generator" Version="5.25.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FastEndpoints.Security" Version="5.25.0" />
<PackageReference Include="FastEndpoints.Swagger" Version="5.25.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" NoWarn="NU1605" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.6" NoWarn="NU1605" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" Version="8.0.6" />
<PackageReference Include="Microsoft.Azure.SignalR" Version="1.25.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.5.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Graph" Version="5.54.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="2.19.0" />
<PackageReference Include="Microsoft.Identity.Web.DownstreamApi" Version="2.19.0" />
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="2.19.0" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="2.19.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0" />
<PackageReference Include="Serilog.Enrichers.Memory" Version="1.0.4" />
<PackageReference Include="Serilog.Enrichers.Process" Version="2.0.2" />
<PackageReference Include="Serilog.Enrichers.Sensitive" Version="1.7.3" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.File.GZip" Version="1.0.2" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Ardalis.SmartEnum" Version="8.0.0" />
<PackageReference Include="Ardalis.SmartEnum.SystemTextJson" Version="8.0.0" />
<PackageReference Include="Azure.Core" Version="1.39.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.3.1" />
<PackageReference Include="Azure.Identity" Version="1.11.3" />
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.6.0" />
<PackageReference Include="FastEndpoints" Version="5.25.0" />
<PackageReference Include="FastEndpoints.ClientGen.Kiota" Version="5.25.0" />
<PackageReference Include="FastEndpoints.Generator" Version="5.25.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FastEndpoints.Security" Version="5.25.0" />
<PackageReference Include="FastEndpoints.Swagger" Version="5.25.0" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" NoWarn="NU1605" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.6" NoWarn="NU1605" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" Version="8.0.6" />
<PackageReference Include="Microsoft.Azure.SignalR" Version="1.25.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="8.0.6" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="8.5.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Graph" Version="5.54.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="2.19.0" />
<PackageReference Include="Microsoft.Identity.Web.DownstreamApi" Version="2.19.0" />
<PackageReference Include="Microsoft.Identity.Web.MicrosoftGraph" Version="2.19.0" />
<PackageReference Include="Microsoft.Identity.Web.UI" Version="2.19.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.20.1" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.3.0" />
<PackageReference Include="Serilog.Enrichers.Memory" Version="1.0.4" />
<PackageReference Include="Serilog.Enrichers.Process" Version="2.0.2" />
<PackageReference Include="Serilog.Enrichers.Sensitive" Version="1.7.3" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.ApplicationInsights" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.File.GZip" Version="1.0.2" />
<PackageReference Include="SerilogAnalyzer" Version="0.15.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\TheOmenDen.CrowsAgainstHumility.Core\TheOmenDen.CrowsAgainstHumility.Core.csproj" />
<ProjectReference Include="..\TheOmenDen.CrowsAgainstHumility.Data\TheOmenDen.CrowsAgainstHumility.Data.csproj" />
<ProjectReference Include="..\TheOmenDen.CrowsAgainstHumility.ServiceDefaults\TheOmenDen.CrowsAgainstHumility.ServiceDefaults.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TheOmenDen.CrowsAgainstHumility.Core\TheOmenDen.CrowsAgainstHumility.Core.csproj" />
<ProjectReference Include="..\TheOmenDen.CrowsAgainstHumility.Data\TheOmenDen.CrowsAgainstHumility.Data.csproj" />
<ProjectReference Include="..\TheOmenDen.CrowsAgainstHumility.ServiceDefaults\TheOmenDen.CrowsAgainstHumility.ServiceDefaults.csproj" />
</ItemGroup>

</Project>
16 changes: 13 additions & 3 deletions TheOmenDen.CrowsAgainstHumility.Core/Enums/GameStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace TheOmenDen.CrowsAgainstHumility.Core.Enums;

public abstract class GameStatus(string name, int value) : SmartEnum<GameStatus>(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();
Expand All @@ -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)
{
Expand All @@ -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)
{
Expand All @@ -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)
{
Expand Down
4 changes: 4 additions & 0 deletions TheOmenDen.CrowsAgainstHumility.Core/Identifiers/SessionId.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
namespace TheOmenDen.CrowsAgainstHumility.Core.Identifiers;

[StronglyTypedId]
public partial struct SessionId;
6 changes: 2 additions & 4 deletions TheOmenDen.CrowsAgainstHumility.Core/Models/GameContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ namespace TheOmenDen.CrowsAgainstHumility.Core.Models;

public class GameContext
{
public Deck<BlackCard> BlackDeck { get; set; }
public Deck<WhiteCard> WhiteDeck { get; set; }


public Deck<ImmutableBlackCard> BlackDeck { get; set; }
public Deck<ImmutableWhiteCard> WhiteDeck { get; set; }

}
16 changes: 16 additions & 0 deletions TheOmenDen.CrowsAgainstHumility.Core/Models/GameSessionDto.cs
Original file line number Diff line number Diff line change
@@ -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<PlayerState> Players,
ImmutableBlackCard CurrentBlackCard,
IReadOnlyDictionary<CardId, SortedList<int, ImmutableWhiteCard>> PlayerSubmissions,
PlayerState CurrentJudge,
GameStatus Status,
TimeSpan BlackCardReadingTimerDuration,
TimeSpan SubmissionTimerDuration,
TimeSpan ReviewTimerDuration);

0 comments on commit d58a499

Please sign in to comment.