게시판장기로 이해하는 규칙 엔진과 평가 함수 탐색예제 소스

예제 소스

장기로 이해하는 규칙 엔진과 평가 함수 탐색 · 파일 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;
};