M9: 체스 컴퓨터 상대를 새 체스 수 생성기(체스960)에 맞춤

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-06 13:46:46 +09:00
parent fde665b13e
commit c78d0fd1fc
2 changed files with 22 additions and 17 deletions

View File

@@ -39,8 +39,8 @@ describe('chess bot', () => {
const a = decide(make(fen), seed) as Extract<ChessAction, { type: 'move' }>; const a = decide(make(fen), seed) as Extract<ChessAction, { type: 'move' }>;
expect(a.from).toBe('d5'); expect(a.from).toBe('d5');
const c = load(fen); const c = load(fen);
c.move({ from: a.from, to: a.to }); c.make(c.findMove(a.from, a.to)!);
expect(c.isAttacked(a.to as 'a1', 'b')).toBe(false); expect(c.isAttacked(a.to, 'b')).toBe(false);
} }
}); });

View File

@@ -3,23 +3,24 @@
* checks > development / centre / pawn pushes. Promotions are scored by the new piece, so it queens * checks > development / centre / pawn pushes. Promotions are scored by the new piece, so it queens
* (and under-promotes only when queening would stalemate). * (and under-promotes only when queening would stalemate).
*/ */
import type { PieceSymbol, Square } from 'chess.js';
import type { BotStrategy } from '../bots/types'; import type { BotStrategy } from '../bots/types';
import { answerRequests, best, pick, playable, typeOf } from '../bots/policy'; import { answerRequests, best, pick, playable, typeOf } from '../bots/policy';
import type { ChessView } from './index'; import type { ChessView } from './index';
import { moveInfo, type PieceType } from './movegen';
import { PIECE_VALUE, insufficientMaterial, load, pieces } from './rules'; 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 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 NEAR_CENTER = new Set(['c3', 'd3', 'e3', 'f3', 'c4', 'f4', 'c5', 'f5', 'c6', 'd6', 'e6', 'f6']);
const SAN_PIECE: Record<string, PieceSymbol> = { 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<ChessView> = ({ view, legal, random }) => { export const chessBot: BotStrategy<ChessView> = ({ view, legal, random }) => {
const req = answerRequests(legal); const req = answerRequests(legal);
if (req) return req; if (req) return req;
const moves = legal.filter((a) => typeOf(a) === 'move') as Move[]; const moves = legal.filter((a) => typeOf(a) === 'move') as Move[];
if (!moves.length) return pick(playable(legal), random); 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 us = c.turn();
const them = us === 'w' ? 'b' : 'w'; const them = us === 'w' ? 'b' : 'w';
let balance = 0; let balance = 0;
@@ -30,14 +31,17 @@ export const chessBot: BotStrategy<ChessView> = ({ view, legal, random }) => {
/** Worst material loss the opponent can inflict right away (mate counts as everything). */ /** Worst material loss the opponent can inflict right away (mate counts as everything). */
const threat = (): number => { const threat = (): number => {
let worst = 0; let worst = 0;
for (const san of c.moves()) { for (const r of c.moves()) {
if (san.endsWith('#')) return 1000; c.make(r);
if (!san.includes('x')) continue; const mate = c.isCheckmate();
const to = san.match(/[a-h][1-8]/g)!.at(-1)! as Square; c.unmake();
const victim = c.get(to)?.type ?? 'p'; // empty target = en passant if (mate) return 1000;
const attacker = SAN_PIECE[san[0]!] ?? 'p'; 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). // 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]; const loss = defended ? Math.max(0, PIECE_VALUE[victim] - PIECE_VALUE[attacker]) : PIECE_VALUE[victim];
worst = Math.max(worst, loss); worst = Math.max(worst, loss);
} }
@@ -46,17 +50,18 @@ export const chessBot: BotStrategy<ChessView> = ({ view, legal, random }) => {
const score = (m: Move): number => { const score = (m: Move): number => {
const piece = c.get(m.from)!; 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 { try {
if (c.isCheckmate()) return 1e6; 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. // 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(); if (c.isStalemate() || insufficientMaterial(c)) return (balance + gain > 0 ? -5000 : balance + gain < -2 ? 5000 : 0) + random();
let s = (gain - threat()) * 100; let s = (gain - threat()) * 100;
if (c.inCheck()) s += 25; if (c.inCheck()) s += 25;
// Squeeze a lone king (helps finish won endgames): fewer replies is better. // Squeeze a lone king (helps finish won endgames): fewer replies is better.
if (theirPieces === 0) s -= c.moves().length * 3; 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; else if (piece.type === 'k' && opening) s -= 30;
if (CENTER.has(m.to)) s += piece.type === 'p' ? 12 : 6; if (CENTER.has(m.to)) s += piece.type === 'p' ? 12 : 6;
else if (NEAR_CENTER.has(m.to)) s += 4; else if (NEAR_CENTER.has(m.to)) s += 4;
@@ -67,7 +72,7 @@ export const chessBot: BotStrategy<ChessView> = ({ view, legal, random }) => {
if (piece.type === 'p' && !opening) s += (us === 'w' ? Number(m.to[1]) : 9 - Number(m.to[1])) * 2; if (piece.type === 'p' && !opening) s += (us === 'w' ? Number(m.to[1]) : 9 - Number(m.to[1])) * 2;
return s + random() * 5; return s + random() * 5;
} finally { } finally {
c.undo(); c.unmake();
} }
}; };
return best(moves, score, random); return best(moves, score, random);