723 lines
27 KiB
C#
723 lines
27 KiB
C#
using System.Text.Json;
|
|
using Fluxor;
|
|
using Koogle.Application.DTOs;
|
|
using Koogle.Application.Games;
|
|
using Koogle.Application.Interfaces;
|
|
using Koogle.Domain.Enums;
|
|
using Koogle.Web.Hubs;
|
|
using Koogle.Web.Services;
|
|
|
|
namespace Koogle.Web.Store.GameState;
|
|
|
|
/// <summary>
|
|
/// Side effects for game state management.
|
|
/// Handles async operations like persistence, state recovery, and SignalR broadcasts.
|
|
/// </summary>
|
|
public class GameEffects
|
|
{
|
|
private readonly ILogger<GameEffects> _logger;
|
|
private readonly IState<GameState> _gameState;
|
|
private readonly IGamePersistenceService _persistenceService;
|
|
private readonly ICurrentClubContext _clubContext;
|
|
private readonly GameDefinitionRegistry _gameRegistry;
|
|
private readonly GameHubService _hubService;
|
|
|
|
// Debounce timer for save operations
|
|
private Timer? _saveDebounceTimer;
|
|
private const int SaveDebounceMs = 500;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the GameEffects class.
|
|
/// </summary>
|
|
public GameEffects(
|
|
ILogger<GameEffects> logger,
|
|
IState<GameState> gameState,
|
|
IGamePersistenceService persistenceService,
|
|
ICurrentClubContext clubContext,
|
|
GameDefinitionRegistry gameRegistry,
|
|
GameHubService hubService)
|
|
{
|
|
_logger = logger;
|
|
_gameState = gameState;
|
|
_persistenceService = persistenceService;
|
|
_clubContext = clubContext;
|
|
_gameRegistry = gameRegistry;
|
|
_hubService = hubService;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles StartGameAction - creates game entity in database.
|
|
/// </summary>
|
|
[EffectMethod]
|
|
public async Task HandleStartGame(StartGameAction action, IDispatcher dispatcher)
|
|
{
|
|
try
|
|
{
|
|
var initialState = new GameStateSerializationDto
|
|
{
|
|
ThrowPanelAfter = MapThrowPanelToDto(ThrowPanelState.Initial with
|
|
{
|
|
IsStarted = true,
|
|
ThrowsPerRound = action.ThrowsPerRound,
|
|
ThrowMode = action.ThrowMode
|
|
}),
|
|
ThrowPanelBefore = MapThrowPanelToDto(ThrowPanelState.Initial with
|
|
{
|
|
IsStarted = true,
|
|
ThrowsPerRound = action.ThrowsPerRound,
|
|
ThrowMode = action.ThrowMode
|
|
}),
|
|
Participants = new ParticipantsStateDto
|
|
{
|
|
PlayerIds = action.PlayerIds,
|
|
CurrentPlayerIndex = 0,
|
|
Mode = (int)action.ParticipantsMode
|
|
},
|
|
GameModel = action.InitialGameModel != null
|
|
? JsonSerializer.SerializeToElement(action.InitialGameModel, GameStateSerializationDto.JsonOptions)
|
|
: null,
|
|
Setup = action.Setup != null
|
|
? JsonSerializer.SerializeToElement(action.Setup, GameStateSerializationDto.JsonOptions)
|
|
: null,
|
|
UndoStack = []
|
|
};
|
|
|
|
var createDto = new CreateGameDto
|
|
{
|
|
DayId = action.DayId,
|
|
ClubId = _clubContext.ClubId,
|
|
GameType = action.GameTypeName,
|
|
PlayerIds = action.PlayerIds,
|
|
InitialGameStateJson = JsonSerializer.Serialize(initialState, GameStateSerializationDto.JsonOptions)
|
|
};
|
|
|
|
var gameId = await _persistenceService.CreateGameAsync(createDto);
|
|
|
|
var throwPanel = ThrowPanelState.Initial with
|
|
{
|
|
IsStarted = true,
|
|
ThrowsPerRound = action.ThrowsPerRound,
|
|
ThrowMode = action.ThrowMode
|
|
};
|
|
|
|
var participants = new ParticipantsState
|
|
{
|
|
PlayerIds = action.PlayerIds,
|
|
CurrentPlayerIndex = 0,
|
|
Mode = action.ParticipantsMode
|
|
};
|
|
|
|
dispatcher.Dispatch(new StartGameSuccessAction(
|
|
gameId,
|
|
action.GameTypeName,
|
|
throwPanel,
|
|
participants,
|
|
action.InitialGameModel,
|
|
action.Setup));
|
|
|
|
// Broadcast game started via SignalR
|
|
await _hubService.BroadcastGameStartedAsync(action.DayId, gameId, action.GameTypeName);
|
|
|
|
_logger.LogInformation("Game started: {GameId}, Type: {GameType}", gameId, action.GameTypeName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to start game");
|
|
dispatcher.Dispatch(new StartGameFailureAction($"Spiel konnte nicht gestartet werden: {ex.Message}"));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles EndGameAction - updates game status in database.
|
|
/// </summary>
|
|
[EffectMethod]
|
|
public async Task HandleEndGame(EndGameAction action, IDispatcher dispatcher)
|
|
{
|
|
var state = _gameState.Value;
|
|
if (!state.ActiveGameId.HasValue)
|
|
{
|
|
dispatcher.Dispatch(new EndGameFailureAction("Kein aktives Spiel vorhanden"));
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Save final state before ending
|
|
await SaveCurrentStateAsync(state.ActiveGameId.Value);
|
|
|
|
await _persistenceService.EndGameAsync(state.ActiveGameId.Value, action.FinalStatus);
|
|
|
|
var summary = new GameSummaryDto
|
|
{
|
|
Id = state.ActiveGameId.Value,
|
|
GameTypeName = state.GameTypeName ?? "",
|
|
Status = action.FinalStatus,
|
|
StartedAt = DateTime.UtcNow, // Will be overwritten by actual value from DB
|
|
CompletedAt = DateTime.UtcNow,
|
|
ParticipantCount = state.Participants.PlayerIds.Length
|
|
};
|
|
|
|
dispatcher.Dispatch(new EndGameSuccessAction(summary));
|
|
|
|
// Broadcast game ended via SignalR
|
|
var result = new GameResultDto
|
|
{
|
|
GameId = state.ActiveGameId.Value,
|
|
WinnerId = action.WinnerId,
|
|
WinnerName = action.WinnerName,
|
|
FinalScores = action.FinalScores,
|
|
GameTypeName = state.GameTypeName ?? "",
|
|
CompletedAt = DateTime.UtcNow
|
|
};
|
|
await _hubService.BroadcastGameEndedAsync(state.ActiveGameId.Value, result);
|
|
|
|
_logger.LogInformation("Game ended: {GameId}, Status: {Status}", state.ActiveGameId, action.FinalStatus);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to end game: {GameId}", state.ActiveGameId);
|
|
dispatcher.Dispatch(new EndGameFailureAction($"Spiel konnte nicht beendet werden: {ex.Message}"));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles LoadActiveGameAction - loads active game from database for recovery.
|
|
/// </summary>
|
|
[EffectMethod]
|
|
public async Task HandleLoadActiveGame(LoadActiveGameAction action, IDispatcher dispatcher)
|
|
{
|
|
try
|
|
{
|
|
var activeGame = await _persistenceService.LoadActiveGameAsync(action.DayId);
|
|
|
|
if (activeGame == null)
|
|
{
|
|
dispatcher.Dispatch(new NoActiveGameAction());
|
|
return;
|
|
}
|
|
|
|
var stateDto = JsonSerializer.Deserialize<GameStateSerializationDto>(
|
|
activeGame.GameStateJson,
|
|
GameStateSerializationDto.JsonOptions);
|
|
|
|
if (stateDto == null)
|
|
{
|
|
_logger.LogWarning("Failed to deserialize game state for game: {GameId}", activeGame.Id);
|
|
dispatcher.Dispatch(new NoActiveGameAction());
|
|
return;
|
|
}
|
|
|
|
var throwPanelBefore = MapDtoToThrowPanel(stateDto.ThrowPanelBefore);
|
|
var throwPanelAfter = MapDtoToThrowPanel(stateDto.ThrowPanelAfter);
|
|
var participants = MapDtoToParticipants(stateDto.Participants);
|
|
var undoStack = stateDto.UndoStack
|
|
.Select(s => new GameSnapshot
|
|
{
|
|
ThrowPanelBefore = MapDtoToThrowPanel(s.ThrowPanelBefore),
|
|
ThrowPanelAfter = MapDtoToThrowPanel(s.ThrowPanelAfter),
|
|
Participants = MapDtoToParticipants(s.Participants),
|
|
GameModel = s.GameModel.HasValue ? (object?)s.GameModel.Value : null
|
|
})
|
|
.ToList();
|
|
var redoStack = stateDto.RedoStack
|
|
.Select(s => new GameSnapshot
|
|
{
|
|
ThrowPanelBefore = MapDtoToThrowPanel(s.ThrowPanelBefore),
|
|
ThrowPanelAfter = MapDtoToThrowPanel(s.ThrowPanelAfter),
|
|
Participants = MapDtoToParticipants(s.Participants),
|
|
GameModel = s.GameModel.HasValue ? (object?)s.GameModel.Value : null
|
|
})
|
|
.ToList();
|
|
|
|
// Deserialize setup if present (may be null for legacy games)
|
|
IGameSetupModel? setup = null;
|
|
if (stateDto.Setup.HasValue)
|
|
{
|
|
setup = JsonSerializer.Deserialize<IGameSetupModel>(
|
|
stateDto.Setup.Value.GetRawText(),
|
|
GameStateSerializationDto.JsonOptions);
|
|
}
|
|
|
|
dispatcher.Dispatch(new LoadActiveGameSuccessAction(
|
|
activeGame.Id,
|
|
activeGame.GameType,
|
|
throwPanelBefore,
|
|
throwPanelAfter,
|
|
participants,
|
|
stateDto.GameModel.HasValue ? (object?)stateDto.GameModel.Value : null,
|
|
undoStack,
|
|
redoStack,
|
|
setup));
|
|
|
|
_logger.LogInformation("Active game loaded: {GameId}, Type: {GameType}", activeGame.Id, activeGame.GameType);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to load active game for day: {DayId}", action.DayId);
|
|
dispatcher.Dispatch(new LoadActiveGameFailureAction($"Spiel konnte nicht geladen werden: {ex.Message}"));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles LoadCompletedGamesAction - loads completed games list from database.
|
|
/// </summary>
|
|
[EffectMethod]
|
|
public async Task HandleLoadCompletedGames(LoadCompletedGamesAction action, IDispatcher dispatcher)
|
|
{
|
|
try
|
|
{
|
|
var games = await _persistenceService.GetCompletedGamesAsync(action.DayId);
|
|
|
|
// Map Application DTOs to Web DTOs
|
|
var webGames = games.Select(g => new GameSummaryDto
|
|
{
|
|
Id = g.Id,
|
|
GameTypeName = g.GameTypeName,
|
|
Status = g.Status,
|
|
StartedAt = g.StartedAt,
|
|
CompletedAt = g.CompletedAt,
|
|
ParticipantCount = g.ParticipantCount
|
|
}).ToList();
|
|
|
|
dispatcher.Dispatch(new LoadCompletedGamesSuccessAction(webGames));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to load completed games for day: {DayId}", action.DayId);
|
|
dispatcher.Dispatch(new LoadCompletedGamesFailureAction($"Spiele konnten nicht geladen werden: {ex.Message}"));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RecordThrowAction - calls ProcessThrow and applies game logic.
|
|
/// </summary>
|
|
[EffectMethod]
|
|
public Task HandleRecordThrow(RecordThrowAction action, IDispatcher dispatcher)
|
|
{
|
|
var state = _gameState.Value;
|
|
var gameTypeName = state.GameTypeName;
|
|
var currentPlayerId = state.Participants.CurrentPlayerId;
|
|
|
|
if (string.IsNullOrEmpty(gameTypeName) || !currentPlayerId.HasValue)
|
|
{
|
|
_logger.LogWarning("Cannot process throw: missing game type or player");
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// Get game logic service (may be null if game type not registered)
|
|
IGameLogicService? gameLogicService = null;
|
|
try
|
|
{
|
|
gameLogicService = _gameRegistry.GetLogicService(gameTypeName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Could not resolve game logic service for {GameType}", gameTypeName);
|
|
}
|
|
|
|
// Calculate new throw panel state with incremented counters
|
|
var newThrowPanel = action.AfterThrowState with
|
|
{
|
|
ThrowCounterPerRound = action.AfterThrowState.ThrowCounterPerRound + 1,
|
|
TotalThrowCounter = action.AfterThrowState.TotalThrowCounter + 1,
|
|
BellValue = false // Reset bell after throw
|
|
};
|
|
|
|
// Check if round is complete BEFORE applying pin reset
|
|
bool isRoundComplete = newThrowPanel.ThrowCounterPerRound >= newThrowPanel.ThrowsPerRound;
|
|
|
|
// Create AfterThrowState for game logic with updated counters
|
|
var beforeSnapshot = CreateThrowPanelSnapshot(action.BeforeThrowState);
|
|
var afterSnapshot = CreateThrowPanelSnapshot(newThrowPanel);
|
|
var afterThrowState = afterSnapshot.CreateAfterThrowState(beforeSnapshot, currentPlayerId.Value, action.IsGutter);
|
|
|
|
// Default values
|
|
bool shouldRotatePlayer = false;
|
|
Guid? nextPlayerId = null;
|
|
PinStatus[]? pinPattern = null;
|
|
bool isGameOver = false;
|
|
Guid? winnerId = null;
|
|
object? updatedGameModel = state.GameModel;
|
|
|
|
// Call ProcessThrow if game logic service is available
|
|
if (gameLogicService != null && state.GameModel != null)
|
|
{
|
|
try
|
|
{
|
|
var (updatedModel, throwResult) = gameLogicService.ProcessThrow(state.GameModel, afterThrowState);
|
|
updatedGameModel = updatedModel;
|
|
shouldRotatePlayer = throwResult.ShouldRotatePlayer;
|
|
isGameOver = throwResult.IsGameOver;
|
|
winnerId = throwResult.WinnerId;
|
|
|
|
// Check for overrides from game logic
|
|
if (throwResult.Overrides != null)
|
|
{
|
|
nextPlayerId = throwResult.Overrides.NextPlayerId;
|
|
pinPattern = throwResult.Overrides.PinPattern;
|
|
|
|
// If game logic provides pin pattern, use it
|
|
if (pinPattern != null && pinPattern.Length == 9)
|
|
{
|
|
newThrowPanel = ApplyPinPattern(newThrowPanel, pinPattern);
|
|
}
|
|
else if (!throwResult.Overrides.SkipStandardPinReset)
|
|
{
|
|
// Apply standard pin reset behavior
|
|
newThrowPanel = ApplyStandardPinReset(newThrowPanel);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No overrides - apply standard pin reset behavior
|
|
newThrowPanel = ApplyStandardPinReset(newThrowPanel);
|
|
}
|
|
|
|
_logger.LogDebug(
|
|
"ProcessThrow completed: pins={Pins}, rotate={Rotate}, gameOver={GameOver}",
|
|
afterThrowState.PinsKnocked, shouldRotatePlayer, isGameOver);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error in ProcessThrow for game type {GameType}", gameTypeName);
|
|
// Continue with standard behavior on error
|
|
newThrowPanel = ApplyStandardPinReset(newThrowPanel);
|
|
shouldRotatePlayer = isRoundComplete;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// No game logic service - use standard behavior
|
|
newThrowPanel = ApplyStandardPinReset(newThrowPanel);
|
|
shouldRotatePlayer = isRoundComplete;
|
|
}
|
|
|
|
// Dispatch result action
|
|
dispatcher.Dispatch(new ProcessThrowResultAction(
|
|
newThrowPanel,
|
|
updatedGameModel,
|
|
shouldRotatePlayer,
|
|
nextPlayerId,
|
|
pinPattern,
|
|
isGameOver,
|
|
winnerId));
|
|
|
|
// Debounce save operations to avoid excessive DB writes
|
|
_saveDebounceTimer?.Dispose();
|
|
_saveDebounceTimer = new Timer(
|
|
async _ => await DebouncedSaveAsync(dispatcher),
|
|
null,
|
|
SaveDebounceMs,
|
|
Timeout.Infinite);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private static ThrowPanelSnapshot CreateThrowPanelSnapshot(ThrowPanelState state)
|
|
{
|
|
return ThrowPanelSnapshot.FromPins(
|
|
state.GetPins(),
|
|
state.ThrowsPerRound,
|
|
state.ThrowCounterPerRound,
|
|
state.TotalThrowCounter,
|
|
state.ThrowMode,
|
|
state.BellValue);
|
|
}
|
|
|
|
private static ThrowPanelState ApplyPinPattern(ThrowPanelState state, PinStatus[] pattern)
|
|
{
|
|
return state with
|
|
{
|
|
Pin1 = pattern[0],
|
|
Pin2 = pattern[1],
|
|
Pin3 = pattern[2],
|
|
Pin4 = pattern[3],
|
|
Pin5 = pattern[4],
|
|
Pin6 = pattern[5],
|
|
Pin7 = pattern[6],
|
|
Pin8 = pattern[7],
|
|
Pin9 = pattern[8]
|
|
};
|
|
}
|
|
|
|
private static ThrowPanelState ApplyStandardPinReset(ThrowPanelState state)
|
|
{
|
|
// Check if round is complete
|
|
if (state.ThrowCounterPerRound >= state.ThrowsPerRound)
|
|
{
|
|
// Reset throw counter and all pins at end of round
|
|
var resetState = state with { ThrowCounterPerRound = 0 };
|
|
return resetState.ResetPins();
|
|
}
|
|
|
|
// Within round - behavior depends on throw mode
|
|
if (state.ThrowMode == ThrowMode.Reposition)
|
|
{
|
|
// Reposition: reset pins after each throw
|
|
return state.ResetPins();
|
|
}
|
|
|
|
if (state.ThrowMode == ThrowMode.Decrease)
|
|
{
|
|
// Decrease: mark fallen as disabled, check for all-down reset
|
|
var newState = state.MarkFallenAsDisabled();
|
|
if (newState.AllPinsDown())
|
|
{
|
|
return newState.ResetPins();
|
|
}
|
|
return newState;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles UndoThrowSuccessAction - triggers debounced save after undo.
|
|
/// </summary>
|
|
[EffectMethod]
|
|
public Task HandleUndoThrowSuccess(UndoThrowSuccessAction action, IDispatcher dispatcher)
|
|
{
|
|
var stateAfterReducer = _gameState.Value;
|
|
_logger.LogInformation(
|
|
"UNDO SUCCESS: State after reducer - pins fallen={Fallen}, total={Total}, UndoStack={UndoCount}, RedoStack={RedoCount}",
|
|
stateAfterReducer.ThrowPanelAfter.CountFallenPins(),
|
|
stateAfterReducer.ThrowPanelAfter.TotalThrowCounter,
|
|
stateAfterReducer.UndoStack.Count,
|
|
stateAfterReducer.RedoStack.Count);
|
|
|
|
// Save after undo as well
|
|
_saveDebounceTimer?.Dispose();
|
|
_saveDebounceTimer = new Timer(
|
|
async _ => await DebouncedSaveAsync(dispatcher),
|
|
null,
|
|
SaveDebounceMs,
|
|
Timeout.Infinite);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles UndoThrowAction - pops last snapshot from stack and restores state.
|
|
/// </summary>
|
|
[EffectMethod]
|
|
public Task HandleUndoThrow(UndoThrowAction action, IDispatcher dispatcher)
|
|
{
|
|
var state = _gameState.Value;
|
|
|
|
if (state.UndoStack.Count == 0)
|
|
{
|
|
_logger.LogWarning("Undo requested but undo stack is empty");
|
|
dispatcher.Dispatch(new UndoThrowFailureAction("Keine Würfe zum Rückgängigmachen vorhanden"));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
var lastSnapshot = state.UndoStack[^1];
|
|
|
|
_logger.LogInformation(
|
|
"UNDO: Stack={StackSize}, Current pins fallen={CurrentFallen}, Snapshot pins fallen={SnapshotFallen}, Current total={CurrentTotal}, Snapshot total={SnapshotTotal}",
|
|
state.UndoStack.Count,
|
|
state.ThrowPanelAfter.CountFallenPins(),
|
|
lastSnapshot.ThrowPanelAfter.CountFallenPins(),
|
|
state.ThrowPanelAfter.TotalThrowCounter,
|
|
lastSnapshot.ThrowPanelAfter.TotalThrowCounter);
|
|
|
|
dispatcher.Dispatch(new UndoThrowSuccessAction(
|
|
lastSnapshot.ThrowPanelAfter,
|
|
lastSnapshot.Participants,
|
|
lastSnapshot.GameModel));
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RedoThrowAction - pops last snapshot from redo stack and restores state.
|
|
/// </summary>
|
|
[EffectMethod]
|
|
public Task HandleRedoThrow(RedoThrowAction action, IDispatcher dispatcher)
|
|
{
|
|
var state = _gameState.Value;
|
|
|
|
if (state.RedoStack.Count == 0)
|
|
{
|
|
_logger.LogWarning("Redo requested but redo stack is empty");
|
|
dispatcher.Dispatch(new RedoThrowFailureAction("Keine Würfe zum Wiederherstellen vorhanden"));
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
var lastSnapshot = state.RedoStack[^1];
|
|
|
|
_logger.LogDebug(
|
|
"Redoing throw. Stack size before: {StackSize}, Restoring to throw counter: {ThrowCounter}",
|
|
state.RedoStack.Count,
|
|
lastSnapshot.ThrowPanelAfter.TotalThrowCounter);
|
|
|
|
dispatcher.Dispatch(new RedoThrowSuccessAction(
|
|
lastSnapshot.ThrowPanelAfter,
|
|
lastSnapshot.Participants,
|
|
lastSnapshot.GameModel));
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles RedoThrowSuccessAction - triggers debounced save after redo.
|
|
/// </summary>
|
|
[EffectMethod]
|
|
public Task HandleRedoThrowSuccess(RedoThrowSuccessAction action, IDispatcher dispatcher)
|
|
{
|
|
// Save after redo
|
|
_saveDebounceTimer?.Dispose();
|
|
_saveDebounceTimer = new Timer(
|
|
async _ => await DebouncedSaveAsync(dispatcher),
|
|
null,
|
|
SaveDebounceMs,
|
|
Timeout.Infinite);
|
|
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles SaveGameStateAction - explicit save to database.
|
|
/// </summary>
|
|
[EffectMethod]
|
|
public async Task HandleSaveGameState(SaveGameStateAction action, IDispatcher dispatcher)
|
|
{
|
|
var state = _gameState.Value;
|
|
if (!state.ActiveGameId.HasValue)
|
|
{
|
|
dispatcher.Dispatch(new SaveGameStateFailureAction("Kein aktives Spiel"));
|
|
return;
|
|
}
|
|
|
|
var success = await SaveCurrentStateAsync(state.ActiveGameId.Value);
|
|
|
|
if (success)
|
|
{
|
|
dispatcher.Dispatch(new SaveGameStateSuccessAction());
|
|
}
|
|
else
|
|
{
|
|
dispatcher.Dispatch(new ConcurrencyConflictAction(state.ActiveGameId.Value));
|
|
}
|
|
}
|
|
|
|
private async Task DebouncedSaveAsync(IDispatcher dispatcher)
|
|
{
|
|
var state = _gameState.Value;
|
|
if (!state.ActiveGameId.HasValue || !state.IsGameActive)
|
|
return;
|
|
|
|
var success = await SaveCurrentStateAsync(state.ActiveGameId.Value);
|
|
|
|
if (success)
|
|
{
|
|
dispatcher.Dispatch(new RecordThrowSuccessAction());
|
|
|
|
// Broadcast state update via SignalR
|
|
var stateDto = CreateSerializationDto(state);
|
|
await _hubService.BroadcastGameStateAsync(state.ActiveGameId.Value, stateDto);
|
|
}
|
|
else
|
|
{
|
|
dispatcher.Dispatch(new ConcurrencyConflictAction(state.ActiveGameId.Value));
|
|
}
|
|
}
|
|
|
|
private static GameStateSerializationDto CreateSerializationDto(GameState state)
|
|
{
|
|
return new GameStateSerializationDto
|
|
{
|
|
ThrowPanelBefore = MapThrowPanelToDto(state.ThrowPanelBefore),
|
|
ThrowPanelAfter = MapThrowPanelToDto(state.ThrowPanelAfter),
|
|
Participants = new ParticipantsStateDto
|
|
{
|
|
PlayerIds = state.Participants.PlayerIds,
|
|
CurrentPlayerIndex = state.Participants.CurrentPlayerIndex,
|
|
Mode = (int)state.Participants.Mode
|
|
},
|
|
GameModel = state.GameModel != null
|
|
? System.Text.Json.JsonSerializer.SerializeToElement(state.GameModel, GameStateSerializationDto.JsonOptions)
|
|
: null,
|
|
Setup = state.Setup != null
|
|
? System.Text.Json.JsonSerializer.SerializeToElement(state.Setup, GameStateSerializationDto.JsonOptions)
|
|
: null,
|
|
UndoStack = state.UndoStack.Select(s => new GameSnapshotDto
|
|
{
|
|
ThrowPanelAfter = MapThrowPanelToDto(s.ThrowPanelAfter),
|
|
Participants = new ParticipantsStateDto
|
|
{
|
|
PlayerIds = s.Participants.PlayerIds,
|
|
CurrentPlayerIndex = s.Participants.CurrentPlayerIndex,
|
|
Mode = (int)s.Participants.Mode
|
|
},
|
|
GameModel = s.GameModel != null
|
|
? System.Text.Json.JsonSerializer.SerializeToElement(s.GameModel, GameStateSerializationDto.JsonOptions)
|
|
: null
|
|
}).ToList(),
|
|
RedoStack = state.RedoStack.Select(s => new GameSnapshotDto
|
|
{
|
|
ThrowPanelAfter = MapThrowPanelToDto(s.ThrowPanelAfter),
|
|
Participants = new ParticipantsStateDto
|
|
{
|
|
PlayerIds = s.Participants.PlayerIds,
|
|
CurrentPlayerIndex = s.Participants.CurrentPlayerIndex,
|
|
Mode = (int)s.Participants.Mode
|
|
},
|
|
GameModel = s.GameModel != null
|
|
? System.Text.Json.JsonSerializer.SerializeToElement(s.GameModel, GameStateSerializationDto.JsonOptions)
|
|
: null
|
|
}).ToList()
|
|
};
|
|
}
|
|
|
|
private async Task<bool> SaveCurrentStateAsync(Guid gameId)
|
|
{
|
|
var state = _gameState.Value;
|
|
var stateDto = CreateSerializationDto(state);
|
|
var json = JsonSerializer.Serialize(stateDto, GameStateSerializationDto.JsonOptions);
|
|
return await _persistenceService.SaveGameStateAsync(gameId, json);
|
|
}
|
|
|
|
private static ThrowPanelStateDto MapThrowPanelToDto(ThrowPanelState state) => new()
|
|
{
|
|
IsStarted = state.IsStarted,
|
|
Pins =
|
|
[
|
|
(int)state.Pin1, (int)state.Pin2, (int)state.Pin3,
|
|
(int)state.Pin4, (int)state.Pin5, (int)state.Pin6,
|
|
(int)state.Pin7, (int)state.Pin8, (int)state.Pin9
|
|
],
|
|
ThrowsPerRound = state.ThrowsPerRound,
|
|
ThrowCounterPerRound = state.ThrowCounterPerRound,
|
|
ThrowMode = (int)state.ThrowMode,
|
|
TotalThrowCounter = state.TotalThrowCounter,
|
|
BellValue = state.BellValue
|
|
};
|
|
|
|
private static ThrowPanelState MapDtoToThrowPanel(ThrowPanelStateDto dto) => new()
|
|
{
|
|
IsStarted = dto.IsStarted,
|
|
Pin1 = (PinStatus)dto.Pins[0],
|
|
Pin2 = (PinStatus)dto.Pins[1],
|
|
Pin3 = (PinStatus)dto.Pins[2],
|
|
Pin4 = (PinStatus)dto.Pins[3],
|
|
Pin5 = (PinStatus)dto.Pins[4],
|
|
Pin6 = (PinStatus)dto.Pins[5],
|
|
Pin7 = (PinStatus)dto.Pins[6],
|
|
Pin8 = (PinStatus)dto.Pins[7],
|
|
Pin9 = (PinStatus)dto.Pins[8],
|
|
ThrowsPerRound = dto.ThrowsPerRound,
|
|
ThrowCounterPerRound = dto.ThrowCounterPerRound,
|
|
ThrowMode = (ThrowMode)dto.ThrowMode,
|
|
TotalThrowCounter = dto.TotalThrowCounter,
|
|
BellValue = dto.BellValue
|
|
};
|
|
|
|
private static ParticipantsState MapDtoToParticipants(ParticipantsStateDto dto) => new()
|
|
{
|
|
PlayerIds = dto.PlayerIds,
|
|
CurrentPlayerIndex = dto.CurrentPlayerIndex,
|
|
Mode = (ParticipantsMode)dto.Mode
|
|
};
|
|
}
|