import { Card } from '@core/cards/Card'; import { Suit } from '@core/cards/Suit'; import { GameState, Phase, PlayerId } from '@core/rules/GameState'; /** * Builds an exact position from a readable description, so a rule test can say * what it is testing instead of playing twenty moves to get there. * * Cards are written as codes: `'6C 10H KS'`. Seating order is the key order of * `hands`. */ export interface StateSpec { hands: Record; /** Suit letter of the trump, e.g. `'S'`. */ trump: string; /** Draw pile, top card first. The trump card is appended at the bottom. */ deck?: string; /** Explicit trump card; defaults to the six of the trump suit. */ trumpCard?: string; table?: { attack: string; defence?: string }[]; discard?: string; phase?: Phase; attacker?: PlayerId; defender?: PlayerId; turnNumber?: number; defenderTook?: boolean; /** Attackers who have already declared they are done this turn. */ passed?: PlayerId[]; out?: PlayerId[]; seed?: number; } export function makeState(spec: StateSpec): GameState { const ids = Object.keys(spec.hands); const trumpSuit = Card.parse(`6${spec.trump}`).suit; const trumpCard = spec.trumpCard ? Card.parse(spec.trumpCard) : Card.parse(`6${spec.trump}`); const outSet = new Set(spec.out ?? []); const drawPile = spec.deck ? Card.parseAll(spec.deck) : []; // The trump card always sits at the bottom of a non-empty pile. const deck = drawPile.length > 0 ? [...drawPile, trumpCard] : []; const attacker = ids.indexOf(spec.attacker ?? ids[0]); const defender = ids.indexOf(spec.defender ?? ids[(attacker + 1) % ids.length]); return { players: ids.map((id) => ({ id, hand: Card.parseAll(spec.hands[id]), out: outSet.has(id), })), deck, trumpCard, trumpSuit, table: (spec.table ?? []).map((s) => ({ attack: Card.parse(s.attack), defence: s.defence ? Card.parse(s.defence) : null, })), discard: spec.discard ? Card.parseAll(spec.discard) : [], phase: spec.phase ?? Phase.Attack, attacker, defender, turnNumber: spec.turnNumber ?? 2, defenderTook: spec.defenderTook ?? false, passed: ids.map((id) => (spec.passed ?? []).includes(id)), rngState: spec.seed ?? 1, loser: null, }; } /** Hand of `player` as sorted card codes, for readable assertions. */ export function handOf(state: GameState, player: PlayerId): string[] { const p = state.players.find((x) => x.id === player); if (!p) throw new Error(`No player ${player}`); return p.hand.map((c) => c.code); } /** Table as `'6C/7C'` pairs (`'6C/-'` when unbeaten). */ export function tableOf(state: GameState): string[] { return state.table.map((s) => `${s.attack.code}/${s.defence ? s.defence.code : '-'}`); } export const SUITS = { clubs: Suit.Clubs, diamonds: Suit.Diamonds, hearts: Suit.Hearts, spades: Suit.Spades, };