예제 소스
장기로 이해하는 규칙 엔진과 평가 함수 탐색 · 파일 59개
ai.ts
import { Game, type GameSnapshot } from "./game";
import { makePass, materialValue, opposite, pieceEmpty, type Move, type Piece, type Pos } from "./types";
function clamp(n: number, lo: number, hi: number): number {
return Math.max(lo, Math.min(hi, n));
}
function positionBonus(game: Game, pos: Pos, piece: Piece): number {
let bonus = 0;
if (piece.type === "pawn") {
if (piece.side === "cho") bonus += (6 - pos.rank) * 3;
else bonus += (pos.rank - 3) * 3;
}
if (piece.type === "horse" || piece.type === "elephant") {
bonus += (4 - Math.abs(pos.file - 4)) * 2;
bonus += 4 - Math.abs(pos.rank - 4);
}
if (piece.type === "chariot") {
const enemyKing = game.kingPos(opposite(piece.side));
if (pos.file === enemyKing.file || pos.rank === enemyKing.rank) bonus += 8;
}
if (piece.type === "king") {
const center: Pos = piece.side === "han" ? { file: 4, rank: 1 } : { file: 4, rank: 8 };
if (pos.file === center.file && pos.rank === center.rank) bonus += 6;
}
return bonus;
}
function evaluate(game: Game): number {
let cho = 0;
let han = 15;
for (let rank = 0; rank < 10; rank++) {
for (let file = 0; file < 9; file++) {
const pos = { file, rank };
const piece = game.at(pos);
if (pieceEmpty(piece) || piece.type === "king") continue;
const value = materialValue(piece.type) + positionBonus(game, pos, piece);
if (piece.side === "cho") cho += value;
else han += value;
}
}
if (game.isCheck("cho")) cho -= 18;
if (game.isCheck("han")) han -= 18;
const score = cho - han;
return game.sideToMove() === "cho" ? score : -score;
}
function negamax(game: Game, depth: number, alpha: number, beta: number): number {
if (game.result() !== "playing") {
if (game.result() === "draw") return 0;
const choWon = game.result() === "cho_win";
const choToMove = game.sideToMove() === "cho";
const mate = 50000 + depth;
return choWon === choToMove ? mate : -mate;
}
if (depth <= 0) return evaluate(game);
let moves = game.legalMoves();
if (moves.length === 0) {
if (game.isCheck(game.sideToMove())) return -50000 - depth;
const next = game.clone();
next.passTurn();
return -negamax(next, depth - 1, -beta, -alpha);
}
moves = [...moves].sort(
(a, b) => materialValue(b.captured.type) - materialValue(a.captured.type),
);
let best = -2000000;
let a = alpha;
for (const move of moves) {
const next = game.clone();
next.applyTrusted(move);
next.finishTurn();
const score = -negamax(next, depth - 1, -beta, -a);
best = Math.max(best, score);
a = Math.max(a, score);
if (a >= beta) break;
}
return best;
}
export function chooseMove(snapshot: GameSnapshot, depth: number): Move {
const game = Game.fromSnapshot(snapshot);
const d = clamp(depth, 1, 5);
let moves = game.legalMoves();
if (moves.length === 0) return makePass();
moves = [...moves].sort(
(a, b) => materialValue(b.captured.type) - materialValue(a.captured.type),
);
let bestScore = -2000000;
const best: Move[] = [];
for (const move of moves) {
const next = game.clone();
next.applyTrusted(move);
next.finishTurn();
const score = -negamax(next, d - 1, -2000000, 2000000);
if (score > bestScore) {
bestScore = score;
best.length = 0;
best.push(move);
} else if (score === bestScore) {
best.push(move);
}
}
if (best.length === 0) return moves[0]!;
return best[Math.floor(Math.random() * best.length)]!;
}
export type AiRequest = {
requestId: number;
snapshot: GameSnapshot;
depth: number;
};
export type AiResponse = {
requestId: number;
move: Move;
};