M2: бот и headless-прогон матчей
- PlayerView: проекция состояния, в которой боту видно только то, что видно живому игроку (своя рука, стол, размеры чужих рук, счётчики) - BasicBot: политика из GAMEPLAY.md, полностью детерминированная - Match: обёртка с состоянием и подпиской на события (её же возьмёт UI) - runMatch: прогон матча без движка и без таймеров - soak-тесты: 3500 полных матчей (боты и случайные легальные ходы, 2-4 игрока), инварианты состояния проверяются после каждого действия - npm run stats:bot — распределение исходов самоигры Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
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<PlayerId, Bot>, 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user