/** * Тестовые помощники: построение позиции из ASCII и прогон партии бот-бот. * В публичный API (index.ts) не экспортируются. */ import { applyMove, createBoard, createSeededRng } from '@go-learn/core'; import type { BoardSize, BoardState, CellState, Color, Move, Point } from '@go-learn/core'; import type { AiLevel } from './ai.js'; import { chooseMove } from './ai.js'; /** * Позиция из ASCII-строк: 'X' — чёрные, 'O' — белые, '.' — пусто. * positionHashes пуст: для сконструированных позиций суперко-проверка * против истории не нужна (повторов «пустого» прошлого быть не может). */ export function stateFromAscii( size: BoardSize, toPlay: Color, rows: readonly string[], ): BoardState { if (rows.length !== size) throw new Error(`нужно ровно ${size} строк`); const grid: CellState[] = []; for (const row of rows) { if (row.length !== size) throw new Error(`строка должна быть длины ${size}: "${row}"`); for (const char of row) { if (char === 'X') grid.push('black'); else if (char === 'O') grid.push('white'); else if (char === '.') grid.push('empty'); else throw new Error(`неизвестный символ клетки: "${char}"`); } } return { size, grid, toPlay, captures: { black: 0, white: 0 }, koPoint: null, positionHashes: [], over: false, }; } /** Запись партии бот-бот: журнал ходов и финальное состояние. */ export interface BotGameRecord { readonly moves: readonly string[]; readonly finalState: BoardState; } /** Компактная запись хода для сравнения партий. */ function formatMove(move: Move): string { return move.kind === 'play' ? `${move.point.x},${move.point.y}` : move.kind; } /** * Партия бот-бот от пустой доски до over. Каждый ход бота применяется через * applyMove: нелегальный ход или превышение maxMoves — ошибка теста. */ export function playBotGame( seed: number, level: AiLevel, size: BoardSize, maxMoves: number, ): BotGameRecord { const rng = createSeededRng(seed); let state = createBoard(size); const moves: string[] = []; let lastOpponentMove: Point | null = null; while (!state.over) { if (moves.length >= maxMoves) { throw new Error(`партия (seed ${seed}) не завершилась за ${maxMoves} ходов`); } const result = chooseMove({ state, level, rng, lastOpponentMove }); const applied = applyMove(state, result.move); if (!applied.ok) { throw new Error(`бот сделал нелегальный ход (seed ${seed}): ${applied.error}`); } moves.push(formatMove(result.move)); if (result.move.kind === 'play') lastOpponentMove = result.move.point; state = applied.state; } return { moves, finalState: state }; }