diff --git a/src/Koogle.Application/DTOs/GameStateSerializationDto.cs b/src/Koogle.Application/DTOs/GameStateSerializationDto.cs index 593180d..869500f 100644 --- a/src/Koogle.Application/DTOs/GameStateSerializationDto.cs +++ b/src/Koogle.Application/DTOs/GameStateSerializationDto.cs @@ -9,10 +9,15 @@ namespace Koogle.Application.DTOs; /// public record GameStateSerializationDto { + /// + /// State of the throw panel before current throw. + /// + public ThrowPanelStateDto ThrowPanelBefore { get; init; } = new(); + /// /// Current state of the throw panel. /// - public ThrowPanelStateDto ThrowPanel { get; init; } = new(); + public ThrowPanelStateDto ThrowPanelAfter { get; init; } = new(); /// /// Current state of game participants. @@ -120,7 +125,12 @@ public record GameSnapshotDto /// /// Snapshot of the throw panel state. /// - public ThrowPanelStateDto ThrowPanel { get; init; } = new(); + public ThrowPanelStateDto ThrowPanelBefore { get; init; } = new(); + + /// + /// Snapshot of the throw panel state. + /// + public ThrowPanelStateDto ThrowPanelAfter { get; init; } = new(); /// /// Snapshot of the participants state. diff --git a/src/Koogle.Web/Components/Game/GameInputPanel.razor b/src/Koogle.Web/Components/Game/GameInputPanel.razor index e05e96a..4b43f87 100644 --- a/src/Koogle.Web/Components/Game/GameInputPanel.razor +++ b/src/Koogle.Web/Components/Game/GameInputPanel.razor @@ -7,6 +7,12 @@ @inject IState GameState @inject IDispatcher Dispatcher +@* @GameState.Value.ThrowPanelAfter + +
+ +@GameState.Value.ThrowPanelBefore *@ +
@* Current player display *@
@@ -28,13 +34,13 @@ @* Pin input area *@
-
- @if (@GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Reposition) + @if (@GameState.Value.ThrowPanelAfter.ThrowMode == ThrowMode.Reposition) { @* Number quick-entry *@
@@ -52,7 +58,7 @@ @* Throw info and controls *@
-
@@ -162,7 +168,7 @@ private int? _selectedNumber; private bool _hasModifiedPins; - private ThrowPanelState? _beforeThrowState; + // private ThrowPanelState? _beforeThrowState; private int _lastKnownThrowCounter; private bool IsInteractive => GameState.Value.IsGameActive && !GameState.Value.IsLoading; @@ -173,35 +179,46 @@ GameState.StateChanged += OnGameStateChanged; // Store initial state for before/after comparison CaptureBeforeThrowState(); - _lastKnownThrowCounter = GameState.Value.ThrowPanel.TotalThrowCounter; + _lastKnownThrowCounter = GameState.Value.ThrowPanelAfter.TotalThrowCounter; } private void OnGameStateChanged(object? sender, EventArgs e) { - var currentCounter = GameState.Value.ThrowPanel.TotalThrowCounter; + var tp = GameState.Value.ThrowPanelAfter; + var currentCounter = tp.TotalThrowCounter; // Detect external state changes (Undo/Redo/SignalR) by comparing throw counter - // If counter changed externally, reset local state and capture new before state + // If counter changed externally (went backwards = Undo, or different = external change) if (currentCounter != _lastKnownThrowCounter) { _hasModifiedPins = false; _selectedNumber = null; - CaptureBeforeThrowState(); + + // // In Reposition mode after Undo, reset pins for clean re-entry + // // Counter going backwards indicates Undo + // if (currentCounter < _lastKnownThrowCounter && + // tp.ThrowMode == ThrowMode.Reposition) + // { + // // Reset pins to standing for clean re-entry + // Dispatcher.Dispatch(new ResetPinsAction()); + // } + _lastKnownThrowCounter = currentCounter; } - else if (!_hasModifiedPins) + + // Always capture before state when not actively modifying pins + if (!_hasModifiedPins) { - // Normal state change (e.g., after throw processed), capture new before state CaptureBeforeThrowState(); } - // Force re-render to update child components (ThrowPanel, PinPanel, etc.) + // Force re-render to update child components (ThrowPanelAfter, PinPanel, etc.) InvokeAsync(StateHasChanged); } private void CaptureBeforeThrowState() { - _beforeThrowState = GameState.Value.ThrowPanel; + // _beforeThrowState = GameState.Value.ThrowPanelAfter; } public void Dispose() @@ -217,7 +234,7 @@ var currentStatus = GetPinStatus(pinNumber); // In Decrease mode, fallen pins cannot be reset to standing - if (GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Decrease && currentStatus == PinStatus.Disabled) + if (GameState.Value.ThrowPanelAfter.ThrowMode == ThrowMode.Decrease && currentStatus == PinStatus.Disabled) { return; } @@ -232,7 +249,7 @@ private void HandleNumberClick(int number) { // Quick entry: set pins 1-N as fallen, reset as standing - var newState = GameState.Value.ThrowPanel.ResetPins(); + var newState = GameState.Value.ThrowPanelAfter.ResetPins(); for (int i = 1; i <= 9; i++) { @@ -254,7 +271,7 @@ private void HandleBellClick() { - Dispatcher.Dispatch(new SetBellValueAction(!GameState.Value.ThrowPanel.BellValue)); + Dispatcher.Dispatch(new SetBellValueAction(!GameState.Value.ThrowPanelAfter.BellValue)); } private async Task HandleGutterClick(bool isLeft) @@ -273,7 +290,7 @@ private async Task ConfirmThrow(bool isGutter, bool isLeftGutter) { - var currentThrowPanel = GameState.Value.ThrowPanel; + var currentThrowPanel = GameState.Value.ThrowPanelAfter; var fallenPins = currentThrowPanel.CountFallenPins(); var bellValue = currentThrowPanel.BellValue; @@ -288,7 +305,8 @@ }; // Get before state (captured at start of input session) - var beforeState = _beforeThrowState ?? currentThrowPanel; + // var beforeState = _beforeThrowState ?? currentThrowPanel; + var beforeState = GameState.Value.ThrowPanelBefore; // Dispatch record throw action - game logic will handle pin reset and player rotation Dispatcher.Dispatch(new RecordThrowAction(beforeState, currentThrowPanel, isGutter, isLeftGutter)); @@ -310,15 +328,15 @@ private PinStatus GetPinStatus(int pinNumber) => pinNumber switch { - 1 => GameState.Value.ThrowPanel.Pin1, - 2 => GameState.Value.ThrowPanel.Pin2, - 3 => GameState.Value.ThrowPanel.Pin3, - 4 => GameState.Value.ThrowPanel.Pin4, - 5 => GameState.Value.ThrowPanel.Pin5, - 6 => GameState.Value.ThrowPanel.Pin6, - 7 => GameState.Value.ThrowPanel.Pin7, - 8 => GameState.Value.ThrowPanel.Pin8, - 9 => GameState.Value.ThrowPanel.Pin9, + 1 => GameState.Value.ThrowPanelAfter.Pin1, + 2 => GameState.Value.ThrowPanelAfter.Pin2, + 3 => GameState.Value.ThrowPanelAfter.Pin3, + 4 => GameState.Value.ThrowPanelAfter.Pin4, + 5 => GameState.Value.ThrowPanelAfter.Pin5, + 6 => GameState.Value.ThrowPanelAfter.Pin6, + 7 => GameState.Value.ThrowPanelAfter.Pin7, + 8 => GameState.Value.ThrowPanelAfter.Pin8, + 9 => GameState.Value.ThrowPanelAfter.Pin9, _ => PinStatus.Standing }; diff --git a/src/Koogle.Web/Store/GameState/GameActions.cs b/src/Koogle.Web/Store/GameState/GameActions.cs index f4e41c2..dda5588 100644 --- a/src/Koogle.Web/Store/GameState/GameActions.cs +++ b/src/Koogle.Web/Store/GameState/GameActions.cs @@ -68,7 +68,8 @@ public record LoadActiveGameAction(Guid DayId); public record LoadActiveGameSuccessAction( Guid GameId, string GameTypeName, - ThrowPanelState ThrowPanel, + ThrowPanelState ThrowPanelBefore, + ThrowPanelState ThrowPanelAfter, ParticipantsState Participants, object? GameModel, IReadOnlyList UndoStack, diff --git a/src/Koogle.Web/Store/GameState/GameEffects.cs b/src/Koogle.Web/Store/GameState/GameEffects.cs index 7ffebdc..90dcc7f 100644 --- a/src/Koogle.Web/Store/GameState/GameEffects.cs +++ b/src/Koogle.Web/Store/GameState/GameEffects.cs @@ -55,7 +55,13 @@ public class GameEffects { var initialState = new GameStateSerializationDto { - ThrowPanel = MapThrowPanelToDto(ThrowPanelState.Initial with + ThrowPanelAfter = MapThrowPanelToDto(ThrowPanelState.Initial with + { + IsStarted = true, + ThrowsPerRound = action.ThrowsPerRound, + ThrowMode = action.ThrowMode + }), + ThrowPanelBefore = MapThrowPanelToDto(ThrowPanelState.Initial with { IsStarted = true, ThrowsPerRound = action.ThrowsPerRound, @@ -201,12 +207,14 @@ public class GameEffects return; } - var throwPanel = MapDtoToThrowPanel(stateDto.ThrowPanel); + var throwPanelBefore = MapDtoToThrowPanel(stateDto.ThrowPanelBefore); + var throwPanelAfter = MapDtoToThrowPanel(stateDto.ThrowPanelAfter); var participants = MapDtoToParticipants(stateDto.Participants); var undoStack = stateDto.UndoStack .Select(s => new GameSnapshot { - ThrowPanel = MapDtoToThrowPanel(s.ThrowPanel), + ThrowPanelBefore = MapDtoToThrowPanel(s.ThrowPanelBefore), + ThrowPanelAfter = MapDtoToThrowPanel(s.ThrowPanelAfter), Participants = MapDtoToParticipants(s.Participants), GameModel = s.GameModel.HasValue ? (object?)s.GameModel.Value : null }) @@ -214,7 +222,8 @@ public class GameEffects var redoStack = stateDto.RedoStack .Select(s => new GameSnapshot { - ThrowPanel = MapDtoToThrowPanel(s.ThrowPanel), + ThrowPanelBefore = MapDtoToThrowPanel(s.ThrowPanelBefore), + ThrowPanelAfter = MapDtoToThrowPanel(s.ThrowPanelAfter), Participants = MapDtoToParticipants(s.Participants), GameModel = s.GameModel.HasValue ? (object?)s.GameModel.Value : null }) @@ -232,7 +241,8 @@ public class GameEffects dispatcher.Dispatch(new LoadActiveGameSuccessAction( activeGame.Id, activeGame.GameType, - throwPanel, + throwPanelBefore, + throwPanelAfter, participants, stateDto.GameModel.HasValue ? (object?)stateDto.GameModel.Value : null, undoStack, @@ -467,13 +477,13 @@ public class GameEffects [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.ThrowPanel.CountFallenPins(), - // stateAfterReducer.ThrowPanel.TotalThrowCounter, - // stateAfterReducer.UndoStack.Count, - // stateAfterReducer.RedoStack.Count); + 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(); @@ -506,13 +516,13 @@ public class GameEffects _logger.LogInformation( "UNDO: Stack={StackSize}, Current pins fallen={CurrentFallen}, Snapshot pins fallen={SnapshotFallen}, Current total={CurrentTotal}, Snapshot total={SnapshotTotal}", state.UndoStack.Count, - state.ThrowPanel.CountFallenPins(), - lastSnapshot.ThrowPanel.CountFallenPins(), - state.ThrowPanel.TotalThrowCounter, - lastSnapshot.ThrowPanel.TotalThrowCounter); + state.ThrowPanelAfter.CountFallenPins(), + lastSnapshot.ThrowPanelAfter.CountFallenPins(), + state.ThrowPanelAfter.TotalThrowCounter, + lastSnapshot.ThrowPanelAfter.TotalThrowCounter); dispatcher.Dispatch(new UndoThrowSuccessAction( - lastSnapshot.ThrowPanel, + lastSnapshot.ThrowPanelAfter, lastSnapshot.Participants, lastSnapshot.GameModel)); @@ -539,10 +549,10 @@ public class GameEffects _logger.LogDebug( "Redoing throw. Stack size before: {StackSize}, Restoring to throw counter: {ThrowCounter}", state.RedoStack.Count, - lastSnapshot.ThrowPanel.TotalThrowCounter); + lastSnapshot.ThrowPanelAfter.TotalThrowCounter); dispatcher.Dispatch(new RedoThrowSuccessAction( - lastSnapshot.ThrowPanel, + lastSnapshot.ThrowPanelAfter, lastSnapshot.Participants, lastSnapshot.GameModel)); @@ -617,7 +627,8 @@ public class GameEffects { return new GameStateSerializationDto { - ThrowPanel = MapThrowPanelToDto(state.ThrowPanel), + ThrowPanelBefore = MapThrowPanelToDto(state.ThrowPanelBefore), + ThrowPanelAfter = MapThrowPanelToDto(state.ThrowPanelAfter), Participants = new ParticipantsStateDto { PlayerIds = state.Participants.PlayerIds, @@ -632,7 +643,7 @@ public class GameEffects : null, UndoStack = state.UndoStack.Select(s => new GameSnapshotDto { - ThrowPanel = MapThrowPanelToDto(s.ThrowPanel), + ThrowPanelAfter = MapThrowPanelToDto(s.ThrowPanelAfter), Participants = new ParticipantsStateDto { PlayerIds = s.Participants.PlayerIds, @@ -645,7 +656,7 @@ public class GameEffects }).ToList(), RedoStack = state.RedoStack.Select(s => new GameSnapshotDto { - ThrowPanel = MapThrowPanelToDto(s.ThrowPanel), + ThrowPanelAfter = MapThrowPanelToDto(s.ThrowPanelAfter), Participants = new ParticipantsStateDto { PlayerIds = s.Participants.PlayerIds, diff --git a/src/Koogle.Web/Store/GameState/GameReducers.cs b/src/Koogle.Web/Store/GameState/GameReducers.cs index d22e15f..5fad56d 100644 --- a/src/Koogle.Web/Store/GameState/GameReducers.cs +++ b/src/Koogle.Web/Store/GameState/GameReducers.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Fluxor; using Koogle.Application.Games; using Koogle.Domain.Enums; +using Microsoft.Extensions.Logging; namespace Koogle.Web.Store.GameState; @@ -34,7 +35,7 @@ public static class GameReducers IsGameActive = true, ActiveGameId = action.GameId, GameTypeName = action.GameTypeName, - ThrowPanel = action.ThrowPanel, + ThrowPanelAfter = action.ThrowPanel, Participants = action.Participants, GameModel = action.GameModel, Setup = action.Setup, @@ -76,7 +77,7 @@ public static class GameReducers IsGameActive = false, ActiveGameId = null, GameTypeName = null, - ThrowPanel = ThrowPanelState.Initial, + ThrowPanelAfter = ThrowPanelState.Initial, Participants = ParticipantsState.Initial, GameModel = null, Setup = null, @@ -119,7 +120,7 @@ public static class GameReducers IsGameActive = true, ActiveGameId = action.GameId, GameTypeName = action.GameTypeName, - ThrowPanel = action.ThrowPanel, + ThrowPanelAfter = action.ThrowPanelAfter, Participants = action.Participants, GameModel = action.GameModel, Setup = action.Setup, @@ -198,11 +199,17 @@ public static class GameReducers var snapshot = new GameSnapshot { - ThrowPanel = state.ThrowPanel, + ThrowPanelAfter = state.ThrowPanelAfter, Participants = state.Participants, GameModel = clonedGameModel }; + Console.WriteLine($"[OnRecordThrow] Creating snapshot - state.ThrowPanelAfter pins: " + + $"[{(int)state.ThrowPanelAfter.Pin1},{(int)state.ThrowPanelAfter.Pin2},{(int)state.ThrowPanelAfter.Pin3}," + + $"{(int)state.ThrowPanelAfter.Pin4},{(int)state.ThrowPanelAfter.Pin5},{(int)state.ThrowPanelAfter.Pin6}," + + $"{(int)state.ThrowPanelAfter.Pin7},{(int)state.ThrowPanelAfter.Pin8},{(int)state.ThrowPanelAfter.Pin9}] " + + $"TotalThrows={state.ThrowPanelAfter.TotalThrowCounter}"); + return state with { UndoStack = state.UndoStack.Add(snapshot), @@ -217,9 +224,15 @@ public static class GameReducers [ReducerMethod] public static GameState OnProcessThrowResult(GameState state, ProcessThrowResultAction action) { + Console.WriteLine($"[OnProcessThrowResult] Setting new ThrowPanelAfter pins: " + + $"[{(int)action.NewThrowPanelState.Pin1},{(int)action.NewThrowPanelState.Pin2},{(int)action.NewThrowPanelState.Pin3}," + + $"{(int)action.NewThrowPanelState.Pin4},{(int)action.NewThrowPanelState.Pin5},{(int)action.NewThrowPanelState.Pin6}," + + $"{(int)action.NewThrowPanelState.Pin7},{(int)action.NewThrowPanelState.Pin8},{(int)action.NewThrowPanelState.Pin9}] " + + $"TotalThrows={action.NewThrowPanelState.TotalThrowCounter}"); + var newState = state with { - ThrowPanel = action.NewThrowPanelState, + ThrowPanelAfter = action.NewThrowPanelState, GameModel = action.UpdatedGameModel, IsLoading = false, IsSaving = true @@ -274,7 +287,7 @@ public static class GameReducers public static GameState OnSetPinStatus(GameState state, SetPinStatusAction action) => state with { - ThrowPanel = state.ThrowPanel.SetPin(action.PinNumber, action.Status) + ThrowPanelAfter = state.ThrowPanelAfter.SetPin(action.PinNumber, action.Status) }; /// @@ -284,7 +297,7 @@ public static class GameReducers public static GameState OnResetPins(GameState state) => state with { - ThrowPanel = state.ThrowPanel.ResetPins() + ThrowPanelAfter = state.ThrowPanelAfter.ResetPins() }; /// @@ -294,7 +307,7 @@ public static class GameReducers public static GameState OnSetBellValue(GameState state, SetBellValueAction action) => state with { - ThrowPanel = state.ThrowPanel with { BellValue = action.Value } + ThrowPanelAfter = state.ThrowPanelAfter with { BellValue = action.Value } }; // Player Reducers @@ -327,10 +340,16 @@ public static class GameReducers [ReducerMethod] public static GameState OnUndoThrowSuccess(GameState state, UndoThrowSuccessAction action) { + Console.WriteLine($"[OnUndoThrowSuccess] Restoring - action.ThrowPanelAfter pins: " + + $"[{(int)action.ThrowPanel.Pin1},{(int)action.ThrowPanel.Pin2},{(int)action.ThrowPanel.Pin3}," + + $"{(int)action.ThrowPanel.Pin4},{(int)action.ThrowPanel.Pin5},{(int)action.ThrowPanel.Pin6}," + + $"{(int)action.ThrowPanel.Pin7},{(int)action.ThrowPanel.Pin8},{(int)action.ThrowPanel.Pin9}] " + + $"TotalThrows={action.ThrowPanel.TotalThrowCounter}"); + // Create snapshot of current state for redo (deep clone GameModel) var currentSnapshot = new GameSnapshot { - ThrowPanel = state.ThrowPanel, + ThrowPanelAfter = state.ThrowPanelAfter, Participants = state.Participants, GameModel = CloneGameModel(state.GameModel) }; @@ -342,7 +361,7 @@ public static class GameReducers return state with { - ThrowPanel = action.ThrowPanel, + ThrowPanelAfter = action.ThrowPanel, Participants = action.Participants, GameModel = action.GameModel, UndoStack = newUndoStack, @@ -372,7 +391,7 @@ public static class GameReducers // Create snapshot of current state for undo (deep clone GameModel) var currentSnapshot = new GameSnapshot { - ThrowPanel = state.ThrowPanel, + ThrowPanelAfter = state.ThrowPanelAfter, Participants = state.Participants, GameModel = CloneGameModel(state.GameModel) }; @@ -384,7 +403,7 @@ public static class GameReducers return state with { - ThrowPanel = action.ThrowPanel, + ThrowPanelAfter = action.ThrowPanel, Participants = action.Participants, GameModel = action.GameModel, UndoStack = state.UndoStack.Add(currentSnapshot), @@ -466,7 +485,7 @@ public static class GameReducers public static GameState OnGameStateUpdatedFromHub(GameState state, GameStateUpdatedFromHubAction action) => state with { - ThrowPanel = action.ThrowPanel, + ThrowPanelAfter = action.ThrowPanel, Participants = action.Participants, GameModel = action.GameModel }; @@ -481,7 +500,7 @@ public static class GameReducers IsGameActive = false, ActiveGameId = null, GameTypeName = null, - ThrowPanel = ThrowPanelState.Initial, + ThrowPanelAfter = ThrowPanelState.Initial, Participants = ParticipantsState.Initial, GameModel = null, Setup = null, diff --git a/src/Koogle.Web/Store/GameState/GameState.cs b/src/Koogle.Web/Store/GameState/GameState.cs index b0f89bd..99458af 100644 --- a/src/Koogle.Web/Store/GameState/GameState.cs +++ b/src/Koogle.Web/Store/GameState/GameState.cs @@ -31,10 +31,15 @@ public record GameState /// public Guid? ActiveGameId { get; init; } + /// + /// State of the throw panel before the current throw. + /// + public ThrowPanelState ThrowPanelBefore { get; init; } = ThrowPanelState.Initial; + /// /// Current state of the throw panel. /// - public ThrowPanelState ThrowPanel { get; init; } = ThrowPanelState.Initial; + public ThrowPanelState ThrowPanelAfter { get; init; } = ThrowPanelState.Initial; /// /// Current state of game participants. @@ -100,7 +105,7 @@ public record GameState GameTypeName = null, DayId = null, ActiveGameId = null, - ThrowPanel = ThrowPanelState.Initial, + ThrowPanelAfter = ThrowPanelState.Initial, Participants = ParticipantsState.Initial, GameModel = null, Setup = null, @@ -350,7 +355,12 @@ public record GameSnapshot /// /// Snapshot of the throw panel state. /// - public ThrowPanelState ThrowPanel { get; init; } = ThrowPanelState.Initial; + public ThrowPanelState ThrowPanelBefore { get; init; } = ThrowPanelState.Initial; + + /// + /// Snapshot of the throw panel state. + /// + public ThrowPanelState ThrowPanelAfter { get; init; } = ThrowPanelState.Initial; /// /// Snapshot of the participants state. diff --git a/src/KoogleApp/Components/Pages/Game.razor b/src/KoogleApp/Components/Pages/Game.razor index dff96ec..08c02fd 100644 --- a/src/KoogleApp/Components/Pages/Game.razor +++ b/src/KoogleApp/Components/Pages/Game.razor @@ -41,7 +41,7 @@ @inject IGameStatusDataService DataService; -@* @ThrowPanelState.Value *@ +@* @ThrowPanelAfterAfterState.Value *@ @switch (_gameView) {