/** 체스 computer player: mates, wins material without hanging it, promotes to a queen. */ import { describe, expect, test } from 'bun:test'; import { SeededRng } from '@bg/engine'; import { decideBotAction } from '../bots/index'; import { chess, type ChessAction, type ChessState } from './index'; import { fullFen, keyOf, load } from './rules'; /** White = 'A' (the bot in these tests), black = 'B'; both clocks running. */ function make(fen: string): ChessState { const options = chess.optionsSchema.parse({ colorAssignment: 'hostWhite' }); const s = chess.setup({ players: ['A', 'B'], options, rng: SeededRng.fromSeed([1, 2, 3, 4]), now: 0 }); s.startFen = fullFen(load(fen)); s.fen = s.startFen; s.repetition = { [keyOf(load(fen))]: 1 }; s.clock.started = { white: true, black: true }; return s; } const decide = (s: ChessState, seed = 1) => { const r = SeededRng.fromSeed([seed, 7, 7, 7]); return decideBotAction(chess, s, 'A', { now: 1000, random: () => r.next() }) as ChessAction; }; const uci = (a: ChessAction) => (a.type === 'move' ? `${a.from}${a.to}${a.promotion ?? ''}` : a.type); describe('chess bot', () => { test('plays mate in one', () => { for (let seed = 1; seed <= 5; seed++) expect(uci(decide(make('6k1/5ppp/8/8/8/8/8/R5K1 w - - 0 1'), seed))).toBe('a1a8'); }); test('takes a hanging queen', () => { expect(uci(decide(make('4k3/8/8/3q4/8/8/8/3RK3 w - - 0 1')))).toBe('d1d5'); }); test('does not grab a defended pawn with the queen, and moves an attacked queen away', () => { // Qxd5 would lose the queen to cxd5. for (let seed = 1; seed <= 5; seed++) expect(uci(decide(make('4k3/8/2p5/3p4/8/8/8/3QK3 w - - 0 1'), seed))).not.toBe('d1d5'); // The queen on d5 is attacked by the c6 pawn: it must leave, to a square nothing black attacks. for (let seed = 1; seed <= 5; seed++) { const fen = '4k3/8/2p5/3Q4/8/8/8/4K3 w - - 0 1'; const a = decide(make(fen), seed) as Extract; expect(a.from).toBe('d5'); const c = load(fen); c.make(c.findMove(a.from, a.to)!); expect(c.isAttacked(a.to, 'b')).toBe(false); } }); test('promotes to a queen', () => { expect(uci(decide(make('8/P7/8/8/8/8/k7/4K3 w - - 0 1')))).toBe('a7a8q'); }); test("accepts the opponent's undo request", () => { let s = make('4k3/8/8/8/8/8/4P3/4K3 w - - 0 1'); s = chess.apply(s, 'A', { type: 'move', from: 'e2', to: 'e4' }, { rng: SeededRng.fromSeed([1, 1, 1, 1]), now: 500 }).state; s = chess.apply(s, 'B', { type: 'move', from: 'e8', to: 'd7' }, { rng: SeededRng.fromSeed([1, 1, 1, 1]), now: 600 }).state; s = chess.apply(s, 'B', { type: 'requestUndo' }, { rng: SeededRng.fromSeed([1, 1, 1, 1]), now: 700 }).state; expect(decide(s)).toEqual({ type: 'respondUndo', accept: true }); }); });