chore: восстановление репозитория из снапшота v0.3.1

Прежняя git-история утрачена при переносе проекта на машину владельца
(снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной
истории.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
sab.code.lab 2026-08-07 09:34:02 +02:00
commit 1c85091186
184 changed files with 33303 additions and 0 deletions

View 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 };
}