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 bool _hasModifiedPins;
private ThrowPanelState? _beforeThrowState;
private int _lastKnownThrowCounter;
private bool IsInteractive => GameState.Value.IsGameActive && !GameState.Value.IsLoading;
@ -172,15 +173,30 @@
GameState.StateChanged += OnGameStateChanged;
// Store initial state for before/after comparison
CaptureBeforeThrowState();
_lastKnownThrowCounter = GameState.Value.ThrowPanel.TotalThrowCounter;
}
private void OnGameStateChanged(object? sender, EventArgs e)
{
// After throw is processed and pins are reset, capture new before state
if (!_hasModifiedPins)
var currentCounter = GameState.Value.ThrowPanel.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 (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();
}
// Force re-render to update child components (ThrowPanel, PinPanel, etc.)
InvokeAsync(StateHasChanged);
}
private void CaptureBeforeThrowState()
@ -201,7 +217,7 @@
var currentStatus = GetPinStatus(pinNumber);
// 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;
}

View File

@ -467,6 +467,14 @@ 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);
// Save after undo as well
_saveDebounceTimer?.Dispose();
_saveDebounceTimer = new Timer(
@ -495,9 +503,12 @@ public class GameEffects
var lastSnapshot = state.UndoStack[^1];
_logger.LogDebug(
"Undoing throw. Stack size before: {StackSize}, Restoring to throw counter: {ThrowCounter}",
_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);
dispatcher.Dispatch(new UndoThrowSuccessAction(

View File

@ -1,5 +1,7 @@
using System.Collections.Immutable;
using System.Text.Json;
using Fluxor;
using Koogle.Application.Games;
using Koogle.Domain.Enums;
namespace Koogle.Web.Store.GameState;
@ -191,11 +193,14 @@ public static class GameReducers
public static GameState OnRecordThrow(GameState state, RecordThrowAction action)
{
// 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
{
ThrowPanel = state.ThrowPanel,
Participants = state.Participants,
GameModel = state.GameModel
GameModel = clonedGameModel
};
return state with
@ -322,12 +327,12 @@ public static class GameReducers
[ReducerMethod]
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
{
ThrowPanel = state.ThrowPanel,
Participants = state.Participants,
GameModel = state.GameModel
GameModel = CloneGameModel(state.GameModel)
};
// Remove the last snapshot from undo stack
@ -364,12 +369,12 @@ public static class GameReducers
[ReducerMethod]
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
{
ThrowPanel = state.ThrowPanel,
Participants = state.Participants,
GameModel = state.GameModel
GameModel = CloneGameModel(state.GameModel)
};
// Remove the last snapshot from redo stack
@ -508,4 +513,18 @@ public static class GameReducers
{
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);
}
}