411 lines
12 KiB
C#
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; }
|
|
}
|