From c78d0fd1fc24983200ae150d9c1d7b2bf8922215 Mon Sep 17 00:00:00 2001 From: EJClaw Date: Tue, 6 Oct 2026 13:46:46 +0900 Subject: [PATCH] =?UTF-8?q?M9:=20=EC=B2=B4=EC=8A=A4=20=EC=BB=B4=ED=93=A8?= =?UTF-8?q?=ED=84=B0=20=EC=83=81=EB=8C=80=EB=A5=BC=20=EC=83=88=20=EC=B2=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=88=98=20=EC=83=9D=EC=84=B1=EA=B8=B0(=EC=B2=B4?= =?UTF-8?q?=EC=8A=A4960)=EC=97=90=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- packages/games/src/chess/bot.test.ts | 4 ++-- packages/games/src/chess/bot.ts | 35 ++++++++++++++++------------ 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/packages/games/src/chess/bot.test.ts b/packages/games/src/chess/bot.test.ts index 8bfede7..84ddb16 100644 --- a/packages/games/src/chess/bot.test.ts +++ b/packages/games/src/chess/bot.test.ts @@ -39,8 +39,8 @@ describe('chess bot', () => { const a = decide(make(fen), seed) as Extract; expect(a.from).toBe('d5'); const c = load(fen); - c.move({ from: a.from, to: a.to }); - expect(c.isAttacked(a.to as 'a1', 'b')).toBe(false); + c.make(c.findMove(a.from, a.to)!); + expect(c.isAttacked(a.to, 'b')).toBe(false); } }); diff --git a/packages/games/src/chess/bot.ts b/packages/games/src/chess/bot.ts index 14219c8..aee6330 100644 --- a/packages/games/src/chess/bot.ts +++ b/packages/games/src/chess/bot.ts @@ -3,23 +3,24 @@ * checks > development / centre / pawn pushes. Promotions are scored by the new piece, so it queens * (and under-promotes only when queening would stalemate). */ -import type { PieceSymbol, Square } from 'chess.js'; import type { BotStrategy } from '../bots/types'; import { answerRequests, best, pick, playable, typeOf } from '../bots/policy'; import type { ChessView } from './index'; +import { moveInfo, type PieceType } from './movegen'; import { PIECE_VALUE, insufficientMaterial, load, pieces } from './rules'; -type Move = { type: 'move'; from: Square; to: Square; promotion?: 'q' | 'r' | 'b' | 'n' }; +type Move = { type: 'move'; from: string; to: string; promotion?: 'q' | 'r' | 'b' | 'n' }; const CENTER = new Set(['d4', 'e4', 'd5', 'e5']); const NEAR_CENTER = new Set(['c3', 'd3', 'e3', 'f3', 'c4', 'f4', 'c5', 'f5', 'c6', 'd6', 'e6', 'f6']); -const SAN_PIECE: Record = { N: 'n', B: 'b', R: 'r', Q: 'q', K: 'k' }; +/** Piece type of a raw generator move field (1..6). */ +const TYPE = (n: number) => ' pnbrqk'[n] as PieceType; export const chessBot: BotStrategy = ({ view, legal, random }) => { const req = answerRequests(legal); if (req) return req; const moves = legal.filter((a) => typeOf(a) === 'move') as Move[]; if (!moves.length) return pick(playable(legal), random); - const c = load(view.fen); + const c = load(view.fen, view.chess960Index !== null); const us = c.turn(); const them = us === 'w' ? 'b' : 'w'; let balance = 0; @@ -30,14 +31,17 @@ export const chessBot: BotStrategy = ({ view, legal, random }) => { /** Worst material loss the opponent can inflict right away (mate counts as everything). */ const threat = (): number => { let worst = 0; - for (const san of c.moves()) { - if (san.endsWith('#')) return 1000; - if (!san.includes('x')) continue; - const to = san.match(/[a-h][1-8]/g)!.at(-1)! as Square; - const victim = c.get(to)?.type ?? 'p'; // empty target = en passant - const attacker = SAN_PIECE[san[0]!] ?? 'p'; + for (const r of c.moves()) { + c.make(r); + const mate = c.isCheckmate(); + c.unmake(); + if (mate) return 1000; + if (!r.captured) continue; + const to = moveInfo.to(r); + const victim = TYPE(r.captured); + const attacker = TYPE(r.piece); // A defended victim only costs the difference (they lose the attacker to our recapture). - const defended = c.attackers(to, us).length > 0; + const defended = c.isAttacked(to, us); const loss = defended ? Math.max(0, PIECE_VALUE[victim] - PIECE_VALUE[attacker]) : PIECE_VALUE[victim]; worst = Math.max(worst, loss); } @@ -46,17 +50,18 @@ export const chessBot: BotStrategy = ({ view, legal, random }) => { const score = (m: Move): number => { const piece = c.get(m.from)!; - const done = c.move({ from: m.from, to: m.to, promotion: m.promotion }); + const done = c.findMove(m.from, m.to, m.promotion)!; + c.make(done); try { if (c.isCheckmate()) return 1e6; - const gain = (done.captured ? PIECE_VALUE[done.captured] : 0) + (done.promotion ? PIECE_VALUE[done.promotion] - 1 : 0); + const gain = (done.captured ? PIECE_VALUE[TYPE(done.captured)] : 0) + (done.promo ? PIECE_VALUE[TYPE(done.promo)] - 1 : 0); // A draw is bad when ahead, welcome when clearly behind. if (c.isStalemate() || insufficientMaterial(c)) return (balance + gain > 0 ? -5000 : balance + gain < -2 ? 5000 : 0) + random(); let s = (gain - threat()) * 100; if (c.inCheck()) s += 25; // Squeeze a lone king (helps finish won endgames): fewer replies is better. if (theirPieces === 0) s -= c.moves().length * 3; - if (done.isKingsideCastle() || done.isQueensideCastle()) s += 40; + if (moveInfo.isCastle(done)) s += 40; else if (piece.type === 'k' && opening) s -= 30; if (CENTER.has(m.to)) s += piece.type === 'p' ? 12 : 6; else if (NEAR_CENTER.has(m.to)) s += 4; @@ -67,7 +72,7 @@ export const chessBot: BotStrategy = ({ view, legal, random }) => { if (piece.type === 'p' && !opening) s += (us === 'w' ? Number(m.to[1]) : 9 - Number(m.to[1])) * 2; return s + random() * 5; } finally { - c.undo(); + c.unmake(); } }; return best(moves, score, random);