M9: 체스 컴퓨터 상대를 새 체스 수 생성기(체스960)에 맞춤
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,8 +39,8 @@ describe('chess bot', () => {
|
||||
const a = decide(make(fen), seed) as Extract<ChessAction, { type: 'move' }>;
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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<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 }) => {
|
||||
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<ChessView> = ({ 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<ChessView> = ({ 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<ChessView> = ({ 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);
|
||||
|
||||
Reference in New Issue
Block a user