chore: восстановление репозитория из снапшота v0.3.1
Прежняя git-история утрачена при переносе проекта на машину владельца (снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной истории. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
1c85091186
184 changed files with 33303 additions and 0 deletions
81
packages/ai/src/test-utils.ts
Normal file
81
packages/ai/src/test-utils.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Тестовые помощники: построение позиции из 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 };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue