-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add in new changes to architecture and reflect new saga pattern
- Loading branch information
1 parent
79fc105
commit d58a499
Showing
8 changed files
with
242 additions
and
90 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
70
TheOmenDen.CrowsAgainstHumility.Api/Sagas/GameSagaOrchestrator.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
69
TheOmenDen.CrowsAgainstHumility.Api/Services/GameStateService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; | ||
} | ||
|
||
} |
120 changes: 60 additions & 60 deletions
120
TheOmenDen.CrowsAgainstHumility.Api/TheOmenDen.CrowsAgainstHumility.Api.csproj
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
4 changes: 4 additions & 0 deletions
4
TheOmenDen.CrowsAgainstHumility.Core/Identifiers/SessionId.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
namespace TheOmenDen.CrowsAgainstHumility.Core.Identifiers; | ||
|
||
[StronglyTypedId] | ||
public partial struct SessionId; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
TheOmenDen.CrowsAgainstHumility.Core/Models/GameSessionDto.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |