fix undo/redo handling

This commit is contained in:
beo3000 2025-12-27 20:51:03 +01:00
parent 0a7fb1f0af
commit 191fb3db3f
3 changed files with 56 additions and 10 deletions

View File

@ -163,6 +163,7 @@
private int? _selectedNumber; private int? _selectedNumber;
private bool _hasModifiedPins; private bool _hasModifiedPins;
private ThrowPanelState? _beforeThrowState; private ThrowPanelState? _beforeThrowState;
private int _lastKnownThrowCounter;
private bool IsInteractive => GameState.Value.IsGameActive && !GameState.Value.IsLoading; private bool IsInteractive => GameState.Value.IsGameActive && !GameState.Value.IsLoading;
@ -172,15 +173,30 @@
GameState.StateChanged += OnGameStateChanged; GameState.StateChanged += OnGameStateChanged;
// Store initial state for before/after comparison // Store initial state for before/after comparison
CaptureBeforeThrowState(); CaptureBeforeThrowState();
_lastKnownThrowCounter = GameState.Value.ThrowPanel.TotalThrowCounter;
} }
private void OnGameStateChanged(object? sender, EventArgs e) private void OnGameStateChanged(object? sender, EventArgs e)
{ {
// After throw is processed and pins are reset, capture new before state var currentCounter = GameState.Value.ThrowPanel.TotalThrowCounter;
if (!_hasModifiedPins)
// Detect external state changes (Undo/Redo/SignalR) by comparing throw counter
// If counter changed externally, reset local state and capture new before state
if (currentCounter != _lastKnownThrowCounter)
{ {
_hasModifiedPins = false;
_selectedNumber = null;
CaptureBeforeThrowState();
_lastKnownThrowCounter = currentCounter;
}
else if (!_hasModifiedPins)
{
// Normal state change (e.g., after throw processed), capture new before state
CaptureBeforeThrowState(); CaptureBeforeThrowState();
} }
// Force re-render to update child components (ThrowPanel, PinPanel, etc.)
InvokeAsync(StateHasChanged);
} }
private void CaptureBeforeThrowState() private void CaptureBeforeThrowState()
@ -201,7 +217,7 @@
var currentStatus = GetPinStatus(pinNumber); var currentStatus = GetPinStatus(pinNumber);
// In Decrease mode, fallen pins cannot be reset to standing // In Decrease mode, fallen pins cannot be reset to standing
if (GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Decrease && currentStatus == PinStatus.Fallen) if (GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Decrease && currentStatus == PinStatus.Disabled)
{ {
return; return;
} }

View File

@ -467,6 +467,14 @@ public class GameEffects
[EffectMethod] [EffectMethod]
public Task HandleUndoThrowSuccess(UndoThrowSuccessAction action, IDispatcher dispatcher) 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);
// Save after undo as well // Save after undo as well
_saveDebounceTimer?.Dispose(); _saveDebounceTimer?.Dispose();
_saveDebounceTimer = new Timer( _saveDebounceTimer = new Timer(
@ -495,9 +503,12 @@ public class GameEffects
var lastSnapshot = state.UndoStack[^1]; var lastSnapshot = state.UndoStack[^1];
_logger.LogDebug( _logger.LogInformation(
"Undoing throw. Stack size before: {StackSize}, Restoring to throw counter: {ThrowCounter}", "UNDO: Stack={StackSize}, Current pins fallen={CurrentFallen}, Snapshot pins fallen={SnapshotFallen}, Current total={CurrentTotal}, Snapshot total={SnapshotTotal}",
state.UndoStack.Count, state.UndoStack.Count,
state.ThrowPanel.CountFallenPins(),
lastSnapshot.ThrowPanel.CountFallenPins(),
state.ThrowPanel.TotalThrowCounter,
lastSnapshot.ThrowPanel.TotalThrowCounter); lastSnapshot.ThrowPanel.TotalThrowCounter);
dispatcher.Dispatch(new UndoThrowSuccessAction( dispatcher.Dispatch(new UndoThrowSuccessAction(

View File

@ -1,5 +1,7 @@
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Text.Json;
using Fluxor; using Fluxor;
using Koogle.Application.Games;
using Koogle.Domain.Enums; using Koogle.Domain.Enums;
namespace Koogle.Web.Store.GameState; namespace Koogle.Web.Store.GameState;
@ -191,11 +193,14 @@ public static class GameReducers
public static GameState OnRecordThrow(GameState state, RecordThrowAction action) public static GameState OnRecordThrow(GameState state, RecordThrowAction action)
{ {
// Create snapshot of current state before throw is processed // Create snapshot of current state before throw is processed
// Deep clone GameModel to avoid reference sharing issues
object? clonedGameModel = CloneGameModel(state.GameModel);
var snapshot = new GameSnapshot var snapshot = new GameSnapshot
{ {
ThrowPanel = state.ThrowPanel, ThrowPanel = state.ThrowPanel,
Participants = state.Participants, Participants = state.Participants,
GameModel = state.GameModel GameModel = clonedGameModel
}; };
return state with return state with
@ -322,12 +327,12 @@ public static class GameReducers
[ReducerMethod] [ReducerMethod]
public static GameState OnUndoThrowSuccess(GameState state, UndoThrowSuccessAction action) public static GameState OnUndoThrowSuccess(GameState state, UndoThrowSuccessAction action)
{ {
// Create snapshot of current state for redo // Create snapshot of current state for redo (deep clone GameModel)
var currentSnapshot = new GameSnapshot var currentSnapshot = new GameSnapshot
{ {
ThrowPanel = state.ThrowPanel, ThrowPanel = state.ThrowPanel,
Participants = state.Participants, Participants = state.Participants,
GameModel = state.GameModel GameModel = CloneGameModel(state.GameModel)
}; };
// Remove the last snapshot from undo stack // Remove the last snapshot from undo stack
@ -364,12 +369,12 @@ public static class GameReducers
[ReducerMethod] [ReducerMethod]
public static GameState OnRedoThrowSuccess(GameState state, RedoThrowSuccessAction action) public static GameState OnRedoThrowSuccess(GameState state, RedoThrowSuccessAction action)
{ {
// Create snapshot of current state for undo // Create snapshot of current state for undo (deep clone GameModel)
var currentSnapshot = new GameSnapshot var currentSnapshot = new GameSnapshot
{ {
ThrowPanel = state.ThrowPanel, ThrowPanel = state.ThrowPanel,
Participants = state.Participants, Participants = state.Participants,
GameModel = state.GameModel GameModel = CloneGameModel(state.GameModel)
}; };
// Remove the last snapshot from redo stack // Remove the last snapshot from redo stack
@ -508,4 +513,18 @@ public static class GameReducers
{ {
GameModel = action.GameModel GameModel = action.GameModel
}; };
// Helper Methods
/// <summary>
/// Deep clones a GameModel using JSON serialization.
/// </summary>
private static object? CloneGameModel(object? gameModel)
{
if (gameModel == null)
return null;
var json = JsonSerializer.Serialize(gameModel, gameModel.GetType(), GameModelFactory.JsonSerializerOptions);
return JsonSerializer.Deserialize(json, gameModel.GetType(), GameModelFactory.JsonSerializerOptions);
}
} }