Compare commits

..

No commits in common. "03364291aaf70ec6024d006063031d3c5fe4d005" and "0a7fb1f0afafc8b20882caba6fd9f1c8872b4651" have entirely different histories.

7 changed files with 60 additions and 175 deletions

View File

@ -9,15 +9,10 @@ namespace Koogle.Application.DTOs;
/// </summary>
public record GameStateSerializationDto
{
/// <summary>
/// State of the throw panel before current throw.
/// </summary>
public ThrowPanelStateDto ThrowPanelBefore { get; init; } = new();
/// <summary>
/// Current state of the throw panel.
/// </summary>
public ThrowPanelStateDto ThrowPanelAfter { get; init; } = new();
public ThrowPanelStateDto ThrowPanel { get; init; } = new();
/// <summary>
/// Current state of game participants.
@ -125,12 +120,7 @@ public record GameSnapshotDto
/// <summary>
/// Snapshot of the throw panel state.
/// </summary>
public ThrowPanelStateDto ThrowPanelBefore { get; init; } = new();
/// <summary>
/// Snapshot of the throw panel state.
/// </summary>
public ThrowPanelStateDto ThrowPanelAfter { get; init; } = new();
public ThrowPanelStateDto ThrowPanel { get; init; } = new();
/// <summary>
/// Snapshot of the participants state.

View File

@ -7,12 +7,6 @@
@inject IState<GameState> GameState
@inject IDispatcher Dispatcher
@* @GameState.Value.ThrowPanelAfter
<hr/>
@GameState.Value.ThrowPanelBefore *@
<div class="game-input-panel">
@* Current player display *@
<div class="current-player-section">
@ -34,13 +28,13 @@
@* Pin input area *@
<div class="pin-input-section">
<PinPanel ThrowPanelState="@GameState.Value.ThrowPanelAfter"
<PinPanel ThrowPanelState="@GameState.Value.ThrowPanel"
IsInteractive="@IsInteractive"
OnPinClicked="HandlePinClick" />
</div>
@if (@GameState.Value.ThrowPanelAfter.ThrowMode == ThrowMode.Reposition)
@if (@GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Reposition)
{
@* Number quick-entry *@
<div class="number-input-section">
@ -58,7 +52,7 @@
@* Throw info and controls *@
<div class="throw-control-section">
<ThrowPanel ThrowPanelState="@GameState.Value.ThrowPanelAfter"
<ThrowPanel ThrowPanelState="@GameState.Value.ThrowPanel"
IsInteractive="@IsInteractive"
OnGutterClicked="HandleGutterClick" />
</div>
@ -168,8 +162,7 @@
private int? _selectedNumber;
private bool _hasModifiedPins;
// private ThrowPanelState? _beforeThrowState;
private int _lastKnownThrowCounter;
private ThrowPanelState? _beforeThrowState;
private bool IsInteractive => GameState.Value.IsGameActive && !GameState.Value.IsLoading;
@ -179,46 +172,20 @@
GameState.StateChanged += OnGameStateChanged;
// Store initial state for before/after comparison
CaptureBeforeThrowState();
_lastKnownThrowCounter = GameState.Value.ThrowPanelAfter.TotalThrowCounter;
}
private void OnGameStateChanged(object? sender, EventArgs e)
{
var tp = GameState.Value.ThrowPanelAfter;
var currentCounter = tp.TotalThrowCounter;
// Detect external state changes (Undo/Redo/SignalR) by comparing throw counter
// If counter changed externally (went backwards = Undo, or different = external change)
if (currentCounter != _lastKnownThrowCounter)
{
_hasModifiedPins = false;
_selectedNumber = null;
// // 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;
}
// Always capture before state when not actively modifying pins
// After throw is processed and pins are reset, capture new before state
if (!_hasModifiedPins)
{
CaptureBeforeThrowState();
}
// Force re-render to update child components (ThrowPanelAfter, PinPanel, etc.)
InvokeAsync(StateHasChanged);
}
private void CaptureBeforeThrowState()
{
// _beforeThrowState = GameState.Value.ThrowPanelAfter;
_beforeThrowState = GameState.Value.ThrowPanel;
}
public void Dispose()
@ -234,7 +201,7 @@
var currentStatus = GetPinStatus(pinNumber);
// In Decrease mode, fallen pins cannot be reset to standing
if (GameState.Value.ThrowPanelAfter.ThrowMode == ThrowMode.Decrease && currentStatus == PinStatus.Disabled)
if (GameState.Value.ThrowPanel.ThrowMode == ThrowMode.Decrease && currentStatus == PinStatus.Fallen)
{
return;
}
@ -249,7 +216,7 @@
private void HandleNumberClick(int number)
{
// Quick entry: set pins 1-N as fallen, reset as standing
var newState = GameState.Value.ThrowPanelAfter.ResetPins();
var newState = GameState.Value.ThrowPanel.ResetPins();
for (int i = 1; i <= 9; i++)
{
@ -271,7 +238,7 @@
private void HandleBellClick()
{
Dispatcher.Dispatch(new SetBellValueAction(!GameState.Value.ThrowPanelAfter.BellValue));
Dispatcher.Dispatch(new SetBellValueAction(!GameState.Value.ThrowPanel.BellValue));
}
private async Task HandleGutterClick(bool isLeft)
@ -290,7 +257,7 @@
private async Task ConfirmThrow(bool isGutter, bool isLeftGutter)
{
var currentThrowPanel = GameState.Value.ThrowPanelAfter;
var currentThrowPanel = GameState.Value.ThrowPanel;
var fallenPins = currentThrowPanel.CountFallenPins();
var bellValue = currentThrowPanel.BellValue;
@ -305,8 +272,7 @@
};
// Get before state (captured at start of input session)
// var beforeState = _beforeThrowState ?? currentThrowPanel;
var beforeState = GameState.Value.ThrowPanelBefore;
var beforeState = _beforeThrowState ?? currentThrowPanel;
// Dispatch record throw action - game logic will handle pin reset and player rotation
Dispatcher.Dispatch(new RecordThrowAction(beforeState, currentThrowPanel, isGutter, isLeftGutter));
@ -328,15 +294,15 @@
private PinStatus GetPinStatus(int pinNumber) => pinNumber switch
{
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,
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,
_ => PinStatus.Standing
};

View File

@ -68,8 +68,7 @@ public record LoadActiveGameAction(Guid DayId);
public record LoadActiveGameSuccessAction(
Guid GameId,
string GameTypeName,
ThrowPanelState ThrowPanelBefore,
ThrowPanelState ThrowPanelAfter,
ThrowPanelState ThrowPanel,
ParticipantsState Participants,
object? GameModel,
IReadOnlyList<GameSnapshot> UndoStack,

View File

@ -55,13 +55,7 @@ public class GameEffects
{
var initialState = new GameStateSerializationDto
{
ThrowPanelAfter = MapThrowPanelToDto(ThrowPanelState.Initial with
{
IsStarted = true,
ThrowsPerRound = action.ThrowsPerRound,
ThrowMode = action.ThrowMode
}),
ThrowPanelBefore = MapThrowPanelToDto(ThrowPanelState.Initial with
ThrowPanel = MapThrowPanelToDto(ThrowPanelState.Initial with
{
IsStarted = true,
ThrowsPerRound = action.ThrowsPerRound,
@ -207,14 +201,12 @@ public class GameEffects
return;
}
var throwPanelBefore = MapDtoToThrowPanel(stateDto.ThrowPanelBefore);
var throwPanelAfter = MapDtoToThrowPanel(stateDto.ThrowPanelAfter);
var throwPanel = MapDtoToThrowPanel(stateDto.ThrowPanel);
var participants = MapDtoToParticipants(stateDto.Participants);
var undoStack = stateDto.UndoStack
.Select(s => new GameSnapshot
{
ThrowPanelBefore = MapDtoToThrowPanel(s.ThrowPanelBefore),
ThrowPanelAfter = MapDtoToThrowPanel(s.ThrowPanelAfter),
ThrowPanel = MapDtoToThrowPanel(s.ThrowPanel),
Participants = MapDtoToParticipants(s.Participants),
GameModel = s.GameModel.HasValue ? (object?)s.GameModel.Value : null
})
@ -222,8 +214,7 @@ public class GameEffects
var redoStack = stateDto.RedoStack
.Select(s => new GameSnapshot
{
ThrowPanelBefore = MapDtoToThrowPanel(s.ThrowPanelBefore),
ThrowPanelAfter = MapDtoToThrowPanel(s.ThrowPanelAfter),
ThrowPanel = MapDtoToThrowPanel(s.ThrowPanel),
Participants = MapDtoToParticipants(s.Participants),
GameModel = s.GameModel.HasValue ? (object?)s.GameModel.Value : null
})
@ -241,8 +232,7 @@ public class GameEffects
dispatcher.Dispatch(new LoadActiveGameSuccessAction(
activeGame.Id,
activeGame.GameType,
throwPanelBefore,
throwPanelAfter,
throwPanel,
participants,
stateDto.GameModel.HasValue ? (object?)stateDto.GameModel.Value : null,
undoStack,
@ -477,14 +467,6 @@ 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.ThrowPanelAfter.CountFallenPins(),
stateAfterReducer.ThrowPanelAfter.TotalThrowCounter,
stateAfterReducer.UndoStack.Count,
stateAfterReducer.RedoStack.Count);
// Save after undo as well
_saveDebounceTimer?.Dispose();
_saveDebounceTimer = new Timer(
@ -513,16 +495,13 @@ public class GameEffects
var lastSnapshot = state.UndoStack[^1];
_logger.LogInformation(
"UNDO: Stack={StackSize}, Current pins fallen={CurrentFallen}, Snapshot pins fallen={SnapshotFallen}, Current total={CurrentTotal}, Snapshot total={SnapshotTotal}",
_logger.LogDebug(
"Undoing throw. Stack size before: {StackSize}, Restoring to throw counter: {ThrowCounter}",
state.UndoStack.Count,
state.ThrowPanelAfter.CountFallenPins(),
lastSnapshot.ThrowPanelAfter.CountFallenPins(),
state.ThrowPanelAfter.TotalThrowCounter,
lastSnapshot.ThrowPanelAfter.TotalThrowCounter);
lastSnapshot.ThrowPanel.TotalThrowCounter);
dispatcher.Dispatch(new UndoThrowSuccessAction(
lastSnapshot.ThrowPanelAfter,
lastSnapshot.ThrowPanel,
lastSnapshot.Participants,
lastSnapshot.GameModel));
@ -549,10 +528,10 @@ public class GameEffects
_logger.LogDebug(
"Redoing throw. Stack size before: {StackSize}, Restoring to throw counter: {ThrowCounter}",
state.RedoStack.Count,
lastSnapshot.ThrowPanelAfter.TotalThrowCounter);
lastSnapshot.ThrowPanel.TotalThrowCounter);
dispatcher.Dispatch(new RedoThrowSuccessAction(
lastSnapshot.ThrowPanelAfter,
lastSnapshot.ThrowPanel,
lastSnapshot.Participants,
lastSnapshot.GameModel));
@ -627,8 +606,7 @@ public class GameEffects
{
return new GameStateSerializationDto
{
ThrowPanelBefore = MapThrowPanelToDto(state.ThrowPanelBefore),
ThrowPanelAfter = MapThrowPanelToDto(state.ThrowPanelAfter),
ThrowPanel = MapThrowPanelToDto(state.ThrowPanel),
Participants = new ParticipantsStateDto
{
PlayerIds = state.Participants.PlayerIds,
@ -643,7 +621,7 @@ public class GameEffects
: null,
UndoStack = state.UndoStack.Select(s => new GameSnapshotDto
{
ThrowPanelAfter = MapThrowPanelToDto(s.ThrowPanelAfter),
ThrowPanel = MapThrowPanelToDto(s.ThrowPanel),
Participants = new ParticipantsStateDto
{
PlayerIds = s.Participants.PlayerIds,
@ -656,7 +634,7 @@ public class GameEffects
}).ToList(),
RedoStack = state.RedoStack.Select(s => new GameSnapshotDto
{
ThrowPanelAfter = MapThrowPanelToDto(s.ThrowPanelAfter),
ThrowPanel = MapThrowPanelToDto(s.ThrowPanel),
Participants = new ParticipantsStateDto
{
PlayerIds = s.Participants.PlayerIds,

View File

@ -1,9 +1,6 @@
using System.Collections.Immutable;
using System.Text.Json;
using Fluxor;
using Koogle.Application.Games;
using Koogle.Domain.Enums;
using Microsoft.Extensions.Logging;
namespace Koogle.Web.Store.GameState;
@ -35,7 +32,7 @@ public static class GameReducers
IsGameActive = true,
ActiveGameId = action.GameId,
GameTypeName = action.GameTypeName,
ThrowPanelAfter = action.ThrowPanel,
ThrowPanel = action.ThrowPanel,
Participants = action.Participants,
GameModel = action.GameModel,
Setup = action.Setup,
@ -77,7 +74,7 @@ public static class GameReducers
IsGameActive = false,
ActiveGameId = null,
GameTypeName = null,
ThrowPanelAfter = ThrowPanelState.Initial,
ThrowPanel = ThrowPanelState.Initial,
Participants = ParticipantsState.Initial,
GameModel = null,
Setup = null,
@ -120,7 +117,7 @@ public static class GameReducers
IsGameActive = true,
ActiveGameId = action.GameId,
GameTypeName = action.GameTypeName,
ThrowPanelAfter = action.ThrowPanelAfter,
ThrowPanel = action.ThrowPanel,
Participants = action.Participants,
GameModel = action.GameModel,
Setup = action.Setup,
@ -194,22 +191,13 @@ 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
{
ThrowPanelAfter = state.ThrowPanelAfter,
ThrowPanel = state.ThrowPanel,
Participants = state.Participants,
GameModel = clonedGameModel
GameModel = state.GameModel
};
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),
@ -224,15 +212,9 @@ 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
{
ThrowPanelAfter = action.NewThrowPanelState,
ThrowPanel = action.NewThrowPanelState,
GameModel = action.UpdatedGameModel,
IsLoading = false,
IsSaving = true
@ -287,7 +269,7 @@ public static class GameReducers
public static GameState OnSetPinStatus(GameState state, SetPinStatusAction action)
=> state with
{
ThrowPanelAfter = state.ThrowPanelAfter.SetPin(action.PinNumber, action.Status)
ThrowPanel = state.ThrowPanel.SetPin(action.PinNumber, action.Status)
};
/// <summary>
@ -297,7 +279,7 @@ public static class GameReducers
public static GameState OnResetPins(GameState state)
=> state with
{
ThrowPanelAfter = state.ThrowPanelAfter.ResetPins()
ThrowPanel = state.ThrowPanel.ResetPins()
};
/// <summary>
@ -307,7 +289,7 @@ public static class GameReducers
public static GameState OnSetBellValue(GameState state, SetBellValueAction action)
=> state with
{
ThrowPanelAfter = state.ThrowPanelAfter with { BellValue = action.Value }
ThrowPanel = state.ThrowPanel with { BellValue = action.Value }
};
// Player Reducers
@ -340,18 +322,12 @@ 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)
// Create snapshot of current state for redo
var currentSnapshot = new GameSnapshot
{
ThrowPanelAfter = state.ThrowPanelAfter,
ThrowPanel = state.ThrowPanel,
Participants = state.Participants,
GameModel = CloneGameModel(state.GameModel)
GameModel = state.GameModel
};
// Remove the last snapshot from undo stack
@ -361,7 +337,7 @@ public static class GameReducers
return state with
{
ThrowPanelAfter = action.ThrowPanel,
ThrowPanel = action.ThrowPanel,
Participants = action.Participants,
GameModel = action.GameModel,
UndoStack = newUndoStack,
@ -388,12 +364,12 @@ public static class GameReducers
[ReducerMethod]
public static GameState OnRedoThrowSuccess(GameState state, RedoThrowSuccessAction action)
{
// Create snapshot of current state for undo (deep clone GameModel)
// Create snapshot of current state for undo
var currentSnapshot = new GameSnapshot
{
ThrowPanelAfter = state.ThrowPanelAfter,
ThrowPanel = state.ThrowPanel,
Participants = state.Participants,
GameModel = CloneGameModel(state.GameModel)
GameModel = state.GameModel
};
// Remove the last snapshot from redo stack
@ -403,7 +379,7 @@ public static class GameReducers
return state with
{
ThrowPanelAfter = action.ThrowPanel,
ThrowPanel = action.ThrowPanel,
Participants = action.Participants,
GameModel = action.GameModel,
UndoStack = state.UndoStack.Add(currentSnapshot),
@ -485,7 +461,7 @@ public static class GameReducers
public static GameState OnGameStateUpdatedFromHub(GameState state, GameStateUpdatedFromHubAction action)
=> state with
{
ThrowPanelAfter = action.ThrowPanel,
ThrowPanel = action.ThrowPanel,
Participants = action.Participants,
GameModel = action.GameModel
};
@ -500,7 +476,7 @@ public static class GameReducers
IsGameActive = false,
ActiveGameId = null,
GameTypeName = null,
ThrowPanelAfter = ThrowPanelState.Initial,
ThrowPanel = ThrowPanelState.Initial,
Participants = ParticipantsState.Initial,
GameModel = null,
Setup = null,
@ -532,18 +508,4 @@ 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);
}
}

View File

@ -31,15 +31,10 @@ public record GameState
/// </summary>
public Guid? ActiveGameId { get; init; }
/// <summary>
/// State of the throw panel before the current throw.
/// </summary>
public ThrowPanelState ThrowPanelBefore { get; init; } = ThrowPanelState.Initial;
/// <summary>
/// Current state of the throw panel.
/// </summary>
public ThrowPanelState ThrowPanelAfter { get; init; } = ThrowPanelState.Initial;
public ThrowPanelState ThrowPanel { get; init; } = ThrowPanelState.Initial;
/// <summary>
/// Current state of game participants.
@ -105,7 +100,7 @@ public record GameState
GameTypeName = null,
DayId = null,
ActiveGameId = null,
ThrowPanelAfter = ThrowPanelState.Initial,
ThrowPanel = ThrowPanelState.Initial,
Participants = ParticipantsState.Initial,
GameModel = null,
Setup = null,
@ -355,12 +350,7 @@ public record GameSnapshot
/// <summary>
/// Snapshot of the throw panel state.
/// </summary>
public ThrowPanelState ThrowPanelBefore { get; init; } = ThrowPanelState.Initial;
/// <summary>
/// Snapshot of the throw panel state.
/// </summary>
public ThrowPanelState ThrowPanelAfter { get; init; } = ThrowPanelState.Initial;
public ThrowPanelState ThrowPanel { get; init; } = ThrowPanelState.Initial;
/// <summary>
/// Snapshot of the participants state.

View File

@ -41,7 +41,7 @@
@inject IGameStatusDataService DataService;
@* @ThrowPanelAfterAfterState.Value *@
@* @ThrowPanelState.Value *@
@switch (_gameView)
{