// Mirrors of the Go view types (internal/game/view.go). export type Suit = 'red' | 'blue' | 'yellow' export type CardKind = 'pet' | 'food' | 'ailment' export type Phase = 'lobby' | 'shop' | 'arrange' | 'battle' | 'gameover' export interface Card { id: string kind: CardKind name: string tier?: number power?: number suit?: Suit effects?: unknown[] effectText?: string food?: string perk?: boolean temporary?: boolean ailment?: string // 'spooked' | 'exposed' (Unicorn pack; kind === 'ailment') } export interface PlayerView { id: string name: string seat: number coins: number trophies: number roundWins?: number[] // rounds this player won a battle in (public) ready: boolean connected: boolean isBot?: boolean deckSize: number petCount: number avocados?: number // set-aside Avocado tokens (Golden pack) firstBuyFree?: boolean // Manta Ray: next buy is free (self only) buysThisRound?: number // Blue-Ringed Octopus counter (self only) trumpets?: number // banked Trumpets for next battle (Bird of Paradise, self only) mana?: number // persistent Mana pool (Unicorn pack; public) shopPeek?: Card // Bigfoot's look at the top of the shop deck (self only) deck?: Card[] } export interface PendingTrade { playerId: string tier: number options: [Card, Card] } // Cockatoo (Golden pack): the buyer must reveal one of their pets. export interface PendingReveal { playerId: string source: string options?: string[] // eligible pet card ids (buyer only) apples?: number // fixed apple reward (Quetzalcoatl); 0 = revealed pet's power (Cockatoo) } // Water of Youth (Unicorn pack): the buyer must sacrifice one of their pets to // upgrade into a next-tier card. export interface PendingSacrifice { playerId: string source: string tier: number options?: string[] // eligible pet card ids to sacrifice (buyer only) } export interface BattleEvent { type: | 'prep' | 'reveal' | 'summon' | 'mill' | 'rock' | 'clash' | 'shield' | 'strip' | 'steal' | 'eat' | 'heal' | 'setaside' | 'release' | 'trumpet' // a side gains (+) or spends/loses (-) Trumpets (Golden pack) | 'prevent' // a Cone Snail shaves damage off a hit (Golden pack) | 'mana' // a side gains (+) or spends (-) Mana (Unicorn pack) | 'ailment' // a pet gains Ailment cards (Unicorn pack) | 'bounce' // a pet is sent to the bottom of its deck (Unicorn: Loveland Frogman) seat?: number target?: number card?: Card count?: number // clash damage?: number[] died?: boolean[] // rock / strip / steal roll?: number dice?: number[] // rock: individual die faces (each 0, 1, or 2) damageAfter?: number targetDied?: boolean // eat (new bonus total) / reveal (starting bonus) bonus?: number // human-readable description of this step for the event log text?: string } // A round runs one battle per pairing, so a six-player round has three of // these. Everything inside is indexed by *side* — 0 or 1 within this battle — // rather than by seat at the table, including BattleEvent.seat/target. `seats` // maps the two apart. winnerSeat is the exception: it's a real seat. export interface BattleResult { round: number seats: number[] // the two seats fighting, first player first stackSizes: number[] lineups?: Card[][] // each side's arranged deck (top first); public for peeking events: BattleEvent[] | null winnerSeat: number trophies: number } // sideOf maps a seat to its side index in a battle, or -1 if it wasn't in it. export function sideOf(battle: BattleResult, seat: number): number { return battle.seats?.indexOf(seat) ?? -1 } export interface LogEntry { seq: number round: number phase: Phase seat: number // acting seat, or -1 icon?: string kind?: string // e.g. 'result' (battle outcome) text: string source?: string // card id that caused a spawn spawn?: string // 'apple' | 'bee' } export interface PackInfo { id: string name: string emoji: string playable: boolean } export interface GameView { gameId: string code: string phase: Phase round: number maxRounds: number maxPets: number packs: string[] // selected packs, shuffled together packCatalog: PackInfo[] // the choices offered in the lobby packsNeeded: number // packs the current table size requires hostSeat: number minPlayers: number maxPlayers: number playerCounts: number[] // table sizes a game can start at (2, 4, 6) youSeat: number turn: number prioritySeat: number shopRow: Card[] deckCounts: number[] players: PlayerView[] matchups?: [number, number][] // this round's battle pairings (public) yourOpponent: number // the seat you face this round, or -1 pending?: PendingTrade pendingReveal?: PendingReveal pendingSacrifice?: PendingSacrifice battle?: BattleResult // your own battle this round battles?: BattleResult[] // every table's, all replayable winnerSeat: number // outright winner, or -1 when shared winnerSeats?: number[] // everyone holding the title log?: LogEntry[] debug?: boolean // server DEBUG mode: unlocks the buy-any-card panel } export type ClientMessage = | { type: 'setPacks'; packs: string[] } | { type: 'addBot'; difficulty: string } | { type: 'removePlayer'; target: string } | { type: 'start' } | { type: 'buy'; row: number } | { type: 'buyAvocado'; row: number } | { type: 'sell'; cards: string[] } | { type: 'trade'; cards: string[] } | { type: 'tradeChoose'; pick: number } | { type: 'revealChoose'; card: string } | { type: 'sacrificeChoose'; card: string } | { type: 'peek' } | { type: 'pass' } | { type: 'arrange'; order: string[] } | { type: 'ready' } | { type: 'debugAdd'; name: string } export interface Session { gameId: string code: string playerId: string token: string }