diff --git a/src/Koogle.Web/Store/GameState/GameEffects.cs b/src/Koogle.Web/Store/GameState/GameEffects.cs index eb8effb..6c9dbd8 100644 --- a/src/Koogle.Web/Store/GameState/GameEffects.cs +++ b/src/Koogle.Web/Store/GameState/GameEffects.cs @@ -303,6 +303,13 @@ public class GameEffects var gameTypeName = state.GameTypeName; var currentPlayerId = state.Participants.CurrentPlayerId; + // Block input if game is already over + if (state.IsGameOver) + { + _logger.LogWarning("Cannot process throw: game is already over"); + return Task.CompletedTask; + } + if (string.IsNullOrEmpty(gameTypeName) || !currentPlayerId.HasValue) { _logger.LogWarning("Cannot process throw: missing game type or player"); @@ -407,6 +414,12 @@ public class GameEffects isGameOver, winnerId)); + // If game is over, skip save (user must confirm end via UI) + if (isGameOver) + { + return Task.CompletedTask; + } + // Debounce save operations to avoid excessive DB writes _saveDebounceTimer?.Dispose(); _saveDebounceTimer = new Timer( @@ -428,6 +441,13 @@ public class GameEffects var gameTypeName = state.GameTypeName; var currentPlayerId = state.Participants.CurrentPlayerId; + // Block input if game is already over + if (state.IsGameOver) + { + dispatcher.Dispatch(new ExecuteGameActionFailureAction("Cannot execute action: game is already over")); + return Task.CompletedTask; + } + if (string.IsNullOrEmpty(gameTypeName) || !currentPlayerId.HasValue || state.GameModel == null) { dispatcher.Dispatch(new ExecuteGameActionFailureAction("Cannot execute action: missing game state")); @@ -451,6 +471,16 @@ public class GameEffects result.IsGameOver, result.WinnerId)); + _logger.LogDebug( + "Game action executed: {ActionId}, rotate={Rotate}, gameOver={GameOver}", + action.ActionId, result.ShouldRotatePlayer, result.IsGameOver); + + // If game is over, skip save (user must confirm end via UI) + if (result.IsGameOver) + { + return Task.CompletedTask; + } + // Debounce save operations _saveDebounceTimer?.Dispose(); _saveDebounceTimer = new Timer( @@ -458,10 +488,6 @@ public class GameEffects null, SaveDebounceMs, Timeout.Infinite); - - _logger.LogDebug( - "Game action executed: {ActionId}, rotate={Rotate}, gameOver={GameOver}", - action.ActionId, result.ShouldRotatePlayer, result.IsGameOver); } else { diff --git a/src/Koogle.Web/Store/GameState/GameReducers.cs b/src/Koogle.Web/Store/GameState/GameReducers.cs index dc2ee48..b2a35c3 100644 --- a/src/Koogle.Web/Store/GameState/GameReducers.cs +++ b/src/Koogle.Web/Store/GameState/GameReducers.cs @@ -42,7 +42,9 @@ public static class GameReducers UndoStack = [], RedoStack = [], IsLoading = false, - Error = null + Error = null, + IsGameOver = false, + WinnerId = null }; /// @@ -84,7 +86,9 @@ public static class GameReducers UndoStack = [], RedoStack = [], CompletedGames = [.. state.CompletedGames, action.Summary], - IsLoading = false + IsLoading = false, + IsGameOver = false, + WinnerId = null }; /// @@ -235,25 +239,30 @@ public static class GameReducers ThrowPanelAfter = action.NewThrowPanelState, GameModel = action.UpdatedGameModel, IsLoading = false, - IsSaving = true + IsSaving = !action.IsGameOver, // Don't save if game is over (EndGame handles it) + IsGameOver = action.IsGameOver, + WinnerId = action.WinnerId }; - // Apply player rotation or override - if (action.NextPlayerId.HasValue) + // Apply player rotation or override (only if game not over) + if (!action.IsGameOver) { - // Game logic specifies exact next player - newState = newState with + if (action.NextPlayerId.HasValue) { - Participants = newState.Participants.SetCurrentPlayer(action.NextPlayerId.Value) - }; - } - else if (action.ShouldRotatePlayer) - { - // Standard rotation to next player - newState = newState with + // Game logic specifies exact next player + newState = newState with + { + Participants = newState.Participants.SetCurrentPlayer(action.NextPlayerId.Value) + }; + } + else if (action.ShouldRotatePlayer) { - Participants = newState.Participants.NextPlayer() - }; + // Standard rotation to next player + newState = newState with + { + Participants = newState.Participants.NextPlayer() + }; + } } return newState; @@ -569,11 +578,13 @@ public static class GameReducers { GameModel = action.UpdatedGameModel, IsLoading = false, - IsSaving = true + IsSaving = !action.IsGameOver, // Don't save if game is over (EndGame handles it) + IsGameOver = action.IsGameOver, + WinnerId = action.WinnerId }; - // Apply player rotation if requested - if (action.ShouldRotatePlayer) + // Apply player rotation if requested (only if game not over) + if (!action.IsGameOver && action.ShouldRotatePlayer) { newState = newState with { diff --git a/src/Koogle.Web/Store/GameState/GameState.cs b/src/Koogle.Web/Store/GameState/GameState.cs index 99458af..67c25a4 100644 --- a/src/Koogle.Web/Store/GameState/GameState.cs +++ b/src/Koogle.Web/Store/GameState/GameState.cs @@ -91,6 +91,16 @@ public record GameState /// public bool IsConcurrencyConflict { get; init; } + /// + /// Indicates the game has ended (show results, block input). + /// + public bool IsGameOver { get; init; } + + /// + /// Winner ID if game is over. + /// + public Guid? WinnerId { get; init; } + /// /// Private constructor for Fluxor initialization. /// @@ -115,7 +125,9 @@ public record GameState IsLoading = false, IsSaving = false, Error = null, - IsConcurrencyConflict = false + IsConcurrencyConflict = false, + IsGameOver = false, + WinnerId = null }; }