/** 쿼리도 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>, 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); }); });