import { Card } from '../cards/Card'; import { Suit } from '../cards/Suit'; export type PlayerId = string; /** Seat index into `GameState.players`. */ export type Seat = number; export enum Phase { /** The attacker must lay the opening card of the turn. Passing is not allowed. */ Attack = 'attack', /** At least one attacking card is unbeaten; the defender must beat it or take. */ Defence = 'defence', /** Nothing left to beat (or a take was declared); attackers may add or stop. */ Reinforce = 'reinforce', Finished = 'finished', } /** One attacking card on the table, with the card that beat it if there is one. */ export interface TableSlot { attack: Card; defence: Card | null; } export interface PlayerState { readonly id: PlayerId; hand: Card[]; /** True once the player has run out of cards and left the game. */ out: boolean; } /** * The complete state of a match. Plain data — no methods, no engine types, no * wall-clock time. Everything that can vary is in here, including the RNG, so a * state plus a sequence of actions replays exactly. */ export interface GameState { /** Seating order, clockwise. Fixed for the whole game, including players who are out. */ players: PlayerState[]; /** Face-down draw pile. Index 0 is the next card drawn; the last entry is the trump card. */ deck: Card[]; /** The face-up card that set the trump suit. Kept for display after it has been drawn. */ trumpCard: Card; trumpSuit: Suit; table: TableSlot[]; discard: Card[]; phase: Phase; attacker: Seat; defender: Seat; /** 1-based. The first turn of a game allows only five attacking cards. */ turnNumber: number; /** True once the defender has declared a take for the current turn. */ defenderTook: boolean; /** Per seat: this attacker has declared they are adding nothing more this turn. */ passed: boolean[]; rngState: number; /** Set when the game is over: the "fool", or null for the rare mutual finish. */ loser: PlayerId | null; } export function cloneState(state: GameState): GameState { return { players: state.players.map((p) => ({ id: p.id, hand: p.hand.slice(), out: p.out })), deck: state.deck.slice(), trumpCard: state.trumpCard, trumpSuit: state.trumpSuit, table: state.table.map((s) => ({ attack: s.attack, defence: s.defence })), discard: state.discard.slice(), phase: state.phase, attacker: state.attacker, defender: state.defender, turnNumber: state.turnNumber, defenderTook: state.defenderTook, passed: state.passed.slice(), rngState: state.rngState, loser: state.loser, }; } /** JSON-safe form of a state, for bug reports and future mid-match saves. */ export interface SerializedGameState { players: { id: PlayerId; hand: number[]; out: boolean }[]; deck: number[]; trumpCard: number; table: { attack: number; defence: number | null }[]; discard: number[]; phase: Phase; attacker: Seat; defender: Seat; turnNumber: number; defenderTook: boolean; passed: boolean[]; rngState: number; loser: PlayerId | null; } export function serializeState(state: GameState): SerializedGameState { return { players: state.players.map((p) => ({ id: p.id, hand: p.hand.map((c) => c.id), out: p.out })), deck: state.deck.map((c) => c.id), trumpCard: state.trumpCard.id, table: state.table.map((s) => ({ attack: s.attack.id, defence: s.defence ? s.defence.id : null })), discard: state.discard.map((c) => c.id), phase: state.phase, attacker: state.attacker, defender: state.defender, turnNumber: state.turnNumber, defenderTook: state.defenderTook, passed: state.passed.slice(), rngState: state.rngState, loser: state.loser, }; } export function deserializeState(data: SerializedGameState): GameState { const trumpCard = Card.byId(data.trumpCard); return { players: data.players.map((p) => ({ id: p.id, hand: p.hand.map(Card.byId), out: p.out })), deck: data.deck.map(Card.byId), trumpCard, trumpSuit: trumpCard.suit, table: data.table.map((s) => ({ attack: Card.byId(s.attack), defence: s.defence === null ? null : Card.byId(s.defence), })), discard: data.discard.map(Card.byId), phase: data.phase, attacker: data.attacker, defender: data.defender, turnNumber: data.turnNumber, defenderTook: data.defenderTook, passed: data.passed.slice(), rngState: data.rngState, loser: data.loser, }; }