KoogleApp/src/Koogle.Web/Store/GameState/GameState.cs

411 lines
12 KiB
C#

using System.Collections.Immutable;
using Fluxor;
using Koogle.Application.Games;
using Koogle.Domain.Enums;
namespace Koogle.Web.Store.GameState;
/// <summary>
/// Fluxor state for game management.
/// </summary>
[FeatureState]
public record GameState
{
/// <summary>
/// Indicates whether a game is currently active.
/// </summary>
public bool IsGameActive { get; init; }
/// <summary>
/// Name of the active game type (e.g., "Training", "Shit").
/// </summary>
public string? GameTypeName { get; init; }
/// <summary>
/// ID of the day the game belongs to.
/// </summary>
public Guid? DayId { get; init; }
/// <summary>
/// ID of the currently active game.
/// </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;
/// <summary>
/// Current state of game participants.
/// </summary>
public ParticipantsState Participants { get; init; } = ParticipantsState.Initial;
/// <summary>
/// Game-specific model data (serialized to JSON).
/// </summary>
public object? GameModel { get; init; }
/// <summary>
/// Game setup configuration (persisted for recovery).
/// </summary>
public IGameSetupModel? Setup { get; init; }
/// <summary>
/// Stack of game snapshots for undo functionality (unlimited).
/// </summary>
public ImmutableList<GameSnapshot> UndoStack { get; init; } = [];
/// <summary>
/// Stack of game snapshots for redo functionality (cleared on new throw).
/// </summary>
public ImmutableList<GameSnapshot> RedoStack { get; init; } = [];
/// <summary>
/// List of completed games for the current day.
/// </summary>
public IReadOnlyList<GameSummaryDto> CompletedGames { get; init; } = [];
/// <summary>
/// Indicates whether a game operation is in progress.
/// </summary>
public bool IsLoading { get; init; }
/// <summary>
/// Indicates whether game is being saved.
/// </summary>
public bool IsSaving { get; init; }
/// <summary>
/// Error message if operation failed.
/// </summary>
public string? Error { get; init; }
/// <summary>
/// Indicates a concurrency conflict occurred (RowVersion mismatch).
/// </summary>
public bool IsConcurrencyConflict { get; init; }
/// <summary>
/// Private constructor for Fluxor initialization.
/// </summary>
private GameState() { }
/// <summary>
/// Creates the initial state.
/// </summary>
public static GameState Initial => new()
{
IsGameActive = false,
GameTypeName = null,
DayId = null,
ActiveGameId = null,
ThrowPanelAfter = ThrowPanelState.Initial,
Participants = ParticipantsState.Initial,
GameModel = null,
Setup = null,
UndoStack = [],
RedoStack = [],
CompletedGames = [],
IsLoading = false,
IsSaving = false,
Error = null,
IsConcurrencyConflict = false
};
}
/// <summary>
/// Represents the state of the throw panel with pin status and throw counters.
/// </summary>
public record ThrowPanelState
{
/// <summary>
/// Indicates whether the throw panel has been started/initialized.
/// </summary>
public bool IsStarted { get; init; }
/// <summary>
/// Status of pin 1 (top).
/// </summary>
public PinStatus Pin1 { get; init; }
/// <summary>
/// Status of pin 2 (second row left).
/// </summary>
public PinStatus Pin2 { get; init; }
/// <summary>
/// Status of pin 3 (second row right).
/// </summary>
public PinStatus Pin3 { get; init; }
/// <summary>
/// Status of pin 4 (third row left).
/// </summary>
public PinStatus Pin4 { get; init; }
/// <summary>
/// Status of pin 5 (third row center).
/// </summary>
public PinStatus Pin5 { get; init; }
/// <summary>
/// Status of pin 6 (third row right).
/// </summary>
public PinStatus Pin6 { get; init; }
/// <summary>
/// Status of pin 7 (fourth row left).
/// </summary>
public PinStatus Pin7 { get; init; }
/// <summary>
/// Status of pin 8 (fourth row right).
/// </summary>
public PinStatus Pin8 { get; init; }
/// <summary>
/// Status of pin 9 (bottom).
/// </summary>
public PinStatus Pin9 { get; init; }
/// <summary>
/// Number of throws allowed per round.
/// </summary>
public int ThrowsPerRound { get; init; }
/// <summary>
/// Current throw counter within the round.
/// </summary>
public int ThrowCounterPerRound { get; init; }
/// <summary>
/// Current throw mode (Reposition or Decrease).
/// </summary>
public ThrowMode ThrowMode { get; init; }
/// <summary>
/// Total throw counter across all rounds.
/// </summary>
public int TotalThrowCounter { get; init; }
/// <summary>
/// Current bell value (for certain game modes).
/// </summary>
public bool BellValue { get; init; }
/// <summary>
/// Initial state with all pins standing.
/// </summary>
public static ThrowPanelState Initial => new()
{
IsStarted = false,
Pin1 = PinStatus.Standing,
Pin2 = PinStatus.Standing,
Pin3 = PinStatus.Standing,
Pin4 = PinStatus.Standing,
Pin5 = PinStatus.Standing,
Pin6 = PinStatus.Standing,
Pin7 = PinStatus.Standing,
Pin8 = PinStatus.Standing,
Pin9 = PinStatus.Standing,
ThrowsPerRound = 3,
ThrowCounterPerRound = 0,
ThrowMode = ThrowMode.Reposition,
TotalThrowCounter = 0,
BellValue = false
};
/// <summary>
/// Gets all pin statuses as an array.
/// </summary>
public PinStatus[] GetPins() => [Pin1, Pin2, Pin3, Pin4, Pin5, Pin6, Pin7, Pin8, Pin9];
/// <summary>
/// Counts the number of pins knocked down (Fallen status).
/// </summary>
public int CountFallenPins() => GetPins().Count(p => p == PinStatus.Fallen);
/// <summary>
/// Counts the number of pins still standing.
/// </summary>
public int CountStandingPins() => GetPins().Count(p => p == PinStatus.Standing);
/// <summary>
/// Creates a new state with all pins reset to standing.
/// </summary>
public ThrowPanelState ResetPins() => this with
{
Pin1 = PinStatus.Standing,
Pin2 = PinStatus.Standing,
Pin3 = PinStatus.Standing,
Pin4 = PinStatus.Standing,
Pin5 = PinStatus.Standing,
Pin6 = PinStatus.Standing,
Pin7 = PinStatus.Standing,
Pin8 = PinStatus.Standing,
Pin9 = PinStatus.Standing
};
/// <summary>
/// Sets a specific pin status by index (1-9).
/// </summary>
public ThrowPanelState SetPin(int pinNumber, PinStatus status) => pinNumber switch
{
1 => this with { Pin1 = status },
2 => this with { Pin2 = status },
3 => this with { Pin3 = status },
4 => this with { Pin4 = status },
5 => this with { Pin5 = status },
6 => this with { Pin6 = status },
7 => this with { Pin7 = status },
8 => this with { Pin8 = status },
9 => this with { Pin9 = status },
_ => this
};
/// <summary>
/// Marks all fallen pins as disabled (for Decrease mode).
/// </summary>
public ThrowPanelState MarkFallenAsDisabled() => this with
{
Pin1 = Pin1 == PinStatus.Fallen ? PinStatus.Disabled : Pin1,
Pin2 = Pin2 == PinStatus.Fallen ? PinStatus.Disabled : Pin2,
Pin3 = Pin3 == PinStatus.Fallen ? PinStatus.Disabled : Pin3,
Pin4 = Pin4 == PinStatus.Fallen ? PinStatus.Disabled : Pin4,
Pin5 = Pin5 == PinStatus.Fallen ? PinStatus.Disabled : Pin5,
Pin6 = Pin6 == PinStatus.Fallen ? PinStatus.Disabled : Pin6,
Pin7 = Pin7 == PinStatus.Fallen ? PinStatus.Disabled : Pin7,
Pin8 = Pin8 == PinStatus.Fallen ? PinStatus.Disabled : Pin8,
Pin9 = Pin9 == PinStatus.Fallen ? PinStatus.Disabled : Pin9
};
/// <summary>
/// Checks if all pins are knocked down (fallen or disabled).
/// </summary>
public bool AllPinsDown() => GetPins().All(p => p != PinStatus.Standing);
}
/// <summary>
/// Represents the state of game participants.
/// </summary>
public record ParticipantsState
{
/// <summary>
/// IDs of players in the game (in turn order).
/// </summary>
public Guid[] PlayerIds { get; init; } = [];
/// <summary>
/// Index of the current player in the PlayerIds array.
/// </summary>
public int CurrentPlayerIndex { get; init; }
/// <summary>
/// Mode for managing player turns.
/// </summary>
public ParticipantsMode Mode { get; init; }
/// <summary>
/// Initial state with no players.
/// </summary>
public static ParticipantsState Initial => new()
{
PlayerIds = [],
CurrentPlayerIndex = 0,
Mode = ParticipantsMode.GameLogic
};
/// <summary>
/// Gets the current player ID, or null if no players.
/// </summary>
public Guid? CurrentPlayerId =>
PlayerIds.Length > 0 && CurrentPlayerIndex < PlayerIds.Length
? PlayerIds[CurrentPlayerIndex]
: null;
/// <summary>
/// Advances to the next player (wraps around).
/// </summary>
public ParticipantsState NextPlayer() =>
PlayerIds.Length == 0
? this
: this with { CurrentPlayerIndex = (CurrentPlayerIndex + 1) % PlayerIds.Length };
/// <summary>
/// Sets the current player by ID.
/// </summary>
public ParticipantsState SetCurrentPlayer(Guid playerId)
{
var index = Array.IndexOf(PlayerIds, playerId);
return index >= 0 ? this with { CurrentPlayerIndex = index } : this;
}
}
/// <summary>
/// Represents a snapshot of the game state for undo functionality.
/// </summary>
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;
/// <summary>
/// Snapshot of the participants state.
/// </summary>
public ParticipantsState Participants { get; init; } = ParticipantsState.Initial;
/// <summary>
/// Snapshot of the game-specific model.
/// </summary>
public object? GameModel { get; init; }
}
/// <summary>
/// Summary DTO for a completed game.
/// </summary>
public record GameSummaryDto
{
/// <summary>
/// Game ID.
/// </summary>
public Guid Id { get; init; }
/// <summary>
/// Game type name.
/// </summary>
public string GameTypeName { get; init; } = string.Empty;
/// <summary>
/// Game status.
/// </summary>
public GameStatus Status { get; init; }
/// <summary>
/// When the game was started.
/// </summary>
public DateTime? StartedAt { get; init; }
/// <summary>
/// When the game was completed.
/// </summary>
public DateTime? CompletedAt { get; init; }
/// <summary>
/// Number of participants.
/// </summary>
public int ParticipantCount { get; init; }
}