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,120 @@
/**
* Тесты lib/goals.ts: все 4 предиката целей + формальный глаз v1
* (позитив/негатив: область 4 пунктов не глаз; область у края глаз).
* Позиции строятся buildPosition, ходы applyMove из @go-learn/core.
*/
import { applyMove, buildPosition } from '@go-learn/core';
import type { BoardState, Point } from '@go-learn/core';
import { describe, expect, it } from 'vitest';
import { goalReached } from './goals.js';
function p(x: number, y: number): Point {
return { x, y };
}
/** Ход чёрных/белых, падает с ошибкой теста при нелегальности. */
function play(state: BoardState, color: 'black' | 'white', point: Point): BoardState {
const result = applyMove(state, { kind: 'play', color, point });
if (!result.ok) throw new Error(`тест: ход нелегален: ${result.error}`);
return result.state;
}
describe('goals: capture-target', () => {
// Белый камень (4,4) в атари: чёрные (3,4),(5,4),(4,3), дамэ — (4,5).
const start = buildPosition(9, [p(3, 4), p(5, 4), p(4, 3)], [p(4, 4)], 'black');
const target = [p(4, 4)];
it('цель на доске → не выполнено; захват → выполнено', () => {
expect(goalReached(start, { type: 'capture-target' }, target)).toBe(false);
const after = play(start, 'black', p(4, 5));
expect(goalReached(after, { type: 'capture-target' }, target)).toBe(true);
});
it('пустой target → false', () => {
expect(goalReached(start, { type: 'capture-target' }, [])).toBe(false);
});
});
describe('goals: liberties', () => {
// Чёрный (2,2) прижат белыми (1,2),(3,2),(2,1): одно дамэ (2,3).
const start = buildPosition(9, [p(2, 2)], [p(1, 2), p(3, 2), p(2, 1)], 'black');
const target = [p(2, 2)];
it('группа с 1 дамэ < count=2 → false', () => {
expect(goalReached(start, { type: 'liberties', count: 2 }, target)).toBe(false);
});
it('после выхода (2,3) у группы 3 дамэ ≥ 2 → true', () => {
const after = play(start, 'black', p(2, 3));
expect(goalReached(after, { type: 'liberties', count: 2 }, target)).toBe(true);
expect(goalReached(after, { type: 'liberties', count: 4 }, target)).toBe(false);
});
});
describe('goals: connect', () => {
// Два чёрных камня (4,2) и (4,4), соединяются ходом (4,3).
const start = buildPosition(9, [p(4, 2), p(4, 4)], [], 'black');
const target = [p(4, 2), p(4, 4)];
it('разные группы → false; после соединения → true', () => {
expect(goalReached(start, { type: 'connect' }, target)).toBe(false);
const after = play(start, 'black', p(4, 3));
expect(goalReached(after, { type: 'connect' }, target)).toBe(true);
});
it('камни разных цветов не могут быть одной группой → false', () => {
const mixed = buildPosition(9, [p(4, 2)], [p(4, 4)], 'black');
expect(goalReached(mixed, { type: 'connect' }, target)).toBe(false);
});
});
describe('goals: two-eyes', () => {
// Кольцо чёрных вокруг линии из 3 пустых пунктов (3,4),(4,4),(5,4):
// один формальный глаз (область из 3 пунктов). Ход (4,4) делит область
// на два глаза по 1 пункту.
const ring = [p(2, 4), p(6, 4), p(3, 3), p(4, 3), p(5, 3), p(3, 5), p(4, 5), p(5, 5)];
const start = buildPosition(9, ring, [], 'black');
const target = [p(4, 3)];
it('один глаз из 3 пунктов → false', () => {
expect(goalReached(start, { type: 'two-eyes' }, target)).toBe(false);
});
it('после разделения области — два глаза → true', () => {
const after = play(start, 'black', p(4, 4));
expect(goalReached(after, { type: 'two-eyes' }, target)).toBe(true);
});
});
describe('goals: формальный глаз v1 (границы определения)', () => {
it('область из 4 пустых пунктов — не глаз', () => {
// Кольцо вокруг линии из 4 пунктов (3,4)..(6,4): формальных глаз нет.
const ring4 = [
p(2, 4),
p(7, 4),
p(3, 3),
p(4, 3),
p(5, 3),
p(6, 3),
p(3, 5),
p(4, 5),
p(5, 5),
p(6, 5),
];
const state = buildPosition(9, ring4, [], 'black');
expect(goalReached(state, { type: 'two-eyes' }, [p(4, 3)])).toBe(false);
});
it('глаз у края доски засчитывается (край = «свой» сосед)', () => {
// Группа чёрных у левого края с двумя глазами: (0,0) и (0,2).
const black = [p(1, 0), p(1, 1), p(0, 1), p(1, 2), p(1, 3), p(0, 3)];
const state = buildPosition(9, black, [], 'white');
expect(goalReached(state, { type: 'two-eyes' }, [p(1, 1)])).toBe(true);
});
it('область, смежная с двумя цветами, — не глаз', () => {
// Пустой пункт (0,0) между чёрным (1,0) и белым (0,1) — не глаз.
const state = buildPosition(9, [p(1, 0)], [p(0, 1)], 'black');
expect(goalReached(state, { type: 'two-eyes' }, [p(1, 0)])).toBe(false);
});
});