go-learn/packages/ai/src/test-utils.ts
sab.code.lab 1c85091186 chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца
(снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной
истории.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 09:34:02 +02:00

81 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Тестовые помощники: построение позиции из 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 };
}