- 보석 상인 → 스플렌더, 벽 쌓기 길찾기 → 쿼리도, 숫자 타일 → 루미큐브, 주사위 땅따먹기 → 부루마불 - deploy/docker-compose.proxy.yml: Caddy 없이 앱만(.5:2970), HTTPS·헤더는 NPMplus - deploy/npmplus-advanced.conf: Caddyfile과 같은 보안 헤더 - 문서: 2026-10-06 결정, 배포 7절 (e2e 구십구 0 카드 방향 버튼 반영 포함) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 lines
2.3 KiB
TypeScript
47 lines
2.3 KiB
TypeScript
/** 쿼리도 computer player: shortest path, winning step, walls a rival who is about to win. */
|
|
import { describe, expect, test } from 'bun:test';
|
|
import { SeededRng } from '@bg/engine';
|
|
import { decideBotAction } from '../bots/index';
|
|
import { quoridor, type Cell, type QuoridorAction, type QuoridorState, type Seat } from './index';
|
|
import { emptyWallIndex, pathLength, setWall, type Wall } from './board';
|
|
|
|
/** 'e5' → { c: 4, r: 4 }. */
|
|
const at = (l: string): Cell => ({ c: 'abcdefghi'.indexOf(l[0]!), r: Number(l.slice(1)) - 1 });
|
|
/** 'h:d4' → horizontal wall at anchor d4. */
|
|
const wall = (w: string): Wall => ({ o: w[0] as 'h' | 'v', ...at(w.slice(2)) });
|
|
|
|
/** 2 players: A = 남 (the bot here, to move, goal row 9), B = 북 (goal row 1). */
|
|
function position(pos: Partial<Record<Seat, string>>, walls: string[] = []): QuoridorState {
|
|
const options = quoridor.optionsSchema.parse({ seatOrder: 'joinOrder' });
|
|
const s = quoridor.setup({ players: ['A', 'B'], options, rng: SeededRng.fromSeed([1, 2, 3, 4]), now: 0 });
|
|
for (const x of s.seats) if (pos[x.seat]) x.pos = at(pos[x.seat]!);
|
|
s.walls = walls.map(wall);
|
|
s.wallIndex = emptyWallIndex();
|
|
for (const w of s.walls) setWall(s.wallIndex, w, true);
|
|
s.turnIndex = s.seats.findIndex((x) => x.seat === 'S');
|
|
return s;
|
|
}
|
|
const decide = (s: QuoridorState, random = () => 0.5) => decideBotAction(quoridor, s, 'A', { now: 1000, random }) as QuoridorAction;
|
|
|
|
describe('quoridor bot', () => {
|
|
test('walks the shortest way around a wall', () => {
|
|
// The wall above e2/f2 blocks the straight way; d2 → d3 is shortest (f2 is blocked above as well).
|
|
const s = position({ S: 'e2', N: 'e9' }, ['h:e2']);
|
|
expect(decide(s)).toEqual({ type: 'move', to: at('d2') });
|
|
});
|
|
|
|
test('steps onto the goal row even when the rival is one step from winning', () => {
|
|
const s = position({ S: 'e8', N: 'a2' });
|
|
expect(decide(s)).toEqual({ type: 'move', to: at('e9') });
|
|
});
|
|
|
|
test('walls off a rival who is about to win', () => {
|
|
const s = position({ S: 'e5', N: 'e2' });
|
|
const a = decide(s);
|
|
expect(a.type).toBe('placeWall');
|
|
const after = quoridor.apply(s, 'A', a, { rng: SeededRng.fromSeed([1, 1, 1, 1]), now: 1000 }).state;
|
|
const north = after.seats.find((x) => x.seat === 'N')!;
|
|
expect(pathLength(after.wallIndex, north.pos!, 'N')).toBeGreaterThan(1);
|
|
});
|
|
});
|