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