Прежняя git-история утрачена при переносе проекта на машину владельца (снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной истории. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
/**
|
||
* Тестовые помощники: построение позиции из 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 };
|
||
}
|