import { Bot } from '../bot/Bot'; import { describeAction } from '../rules/Action'; import { describeEvent } from '../rules/GameEvent'; import { PlayerId } from '../rules/GameState'; import { Match } from './Match'; export interface RunOptions { /** Safety net: a match this long means the rules have a cycle in them. */ maxActions?: number; /** Collects a human-readable trace, for debugging a bad seed. */ trace?: string[]; } export interface RunResult { loser: PlayerId | null; actions: number; turns: number; } /** * Plays a match out between bots, with no engine and no clock. The soak test * leans on this; so does anyone reproducing a bug from a seed. * * Stops early and returns if it reaches a seat with no bot — that is how the * same driver can fast-forward a match that a human is also playing. */ export function runMatch(match: Match, bots: Record, options: RunOptions = {}): RunResult { const maxActions = options.maxActions ?? 2000; let actions = 0; while (!match.finished) { const actor = match.currentActor; if (actor === null) break; const bot = bots[actor]; if (!bot) break; const action = bot.decide(match.viewFor(actor)); if (options.trace) { options.trace.push(describeAction(action)); } const events = match.perform(action); if (options.trace) { for (const event of events) { options.trace.push(` ${describeEvent(event)}`); } } actions += 1; if (actions > maxActions) { throw new Error(`Match did not finish within ${maxActions} actions`); } } return { loser: match.loser, actions, turns: match.state.turnNumber }; }