// The contract between a game and everything else in this kit. The server, // the client store and the hall know nothing about the game beyond this // file: they create a state from names and a seed, feed it one round of // inputs at a time, ask who still has to move, and hand each viewer the // view they are allowed to see. A new game implements GameSpec once, in // src/lib/game/index.ts, and the rest of the kit works unchanged. // // The shape is simultaneous rounds: every seat that needs input submits, then // the round resolves. Turn-at-a-time games fit too (only one seat needs input // at once). A game of single commands with out-of-turn interruptions, like // Wiz-War, does not fit this contract and needs its own server. /** A seat at the table: a single letter from GameSpec.seatIds. */ export type SeatId = string; /** The viewer with no seat: the Peanut Gallery. Views built for it show only what every seat could see. */ export const SPECTATOR: SeatId = ''; export interface Outcome { winner: SeatId | null; reason: string; } /** A choice the host makes when opening a table: a variant, a side, a length. */ export interface TableOption { key: string; label: string; /** A choice's note is shown under the selector while it is chosen: the one line a player needs to pick. */ choices: { value: string; label: string; note?: string }[]; /** The value a table gets when the host chooses nothing. */ default: string; } /** The host's choices for a table, by option key; only keys the game declares get through. */ export type TableOptions = Record; export interface GameSpec { /** Seats in table order; the table takes at most this many. */ seatIds: SeatId[]; /** What the host may choose when opening a table; empty for a game with one way to play. */ options?: TableOption[]; /** The game's own settings for the preferences panel, kept in the player's browser (see $lib/prefs.svelte). */ preferences?: { key: string; label: string; kind: 'toggle' | 'choice'; choices?: { value: string; label: string }[]; default: boolean | string; note?: string }[]; minSeats: number; /** Names the server draws for bots, in preference order. */ botNames: string[]; /** * The rules revision new games begin under. A game keeps the revision it * started with, recorded on its ledger, so a later fix can keep the old * path for old ledgers behind `state.rules < N`. Bump only when the deploy * gate shows a fix changes how an already-played turn resolves. */ currentRules: number; create(names: Record, seed: number, rules: number, options?: TableOptions): State; /** Resolve one round. Must be a pure function of its arguments: the ledger is replayed through it. */ resolve(state: State, inputs: Record): State; /** Whether this seat's input is needed before the next resolution. */ needsInput(state: State, seat: SeatId): boolean; over(state: State): Outcome | null; /** The round being written, counted from one, for the ledger and the report pin. */ turn(state: State): number; /** What one viewer may see. The server sends nothing else. */ view(state: State, viewer: SeatId): State; botInput(state: State, seat: SeatId): Input; /** Only the shapes the engine understands get through; the engine validates the rest. */ cleanInput(raw: unknown): Input; /** Why this seat may not make this move now, or null when it may. The server answers with the reason. */ validate?(state: State, seat: SeatId, input: Input): string | null; /** A seat's name from the state, for the hall and the chronicle. */ nameOf(state: State, seat: SeatId): string; /** The game's own lines for the hall's tally, counted from a finished game: a label and how many it adds. */ tally?(state: State): Record; }