Files
joke-app/packages/games/src/chess/bot.test.ts
2026-10-06 13:46:46 +09:00

59 lines
2.8 KiB
TypeScript

/** 체스 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<ChessAction, { type: 'move' }>;
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 });
});
});