// The bot: a short alpha-beta search over the legal moves, scored by what // the game is about — the king's road to safety, the pieces on the board, // and the attackers pressing around the king. A function of the state // alone, so a ledger replays to the same choice. import { cellOf, cornersOf, DIRECTIONS, escapeAt, isEdge, movesOf, neighbors, playOn, type Input, type Piece, type Side } from './board'; import { RULESETS, type Ruleset } from './rules'; import type { SeatId } from './spec'; import type { State } from './index'; /** Plies of lookahead: three is well under a second a move on either board. */ const DEPTH = 3; /** The board as the search sees it: pieces, who moves, and a verdict when one has fallen. */ interface Node { cells: Piece[]; toMove: Side; /** The side that has won, once the king is taken or away. */ won: Side | null; } function stepNode(set: Ruleset, node: Node, from: number, to: number): Node { const piece = node.cells[from]; const { cells } = playOn(set, node.cells, from, to); const kingAt = cells.indexOf('k'); let won: Side | null = null; if (kingAt === -1) won = 'attackers'; else if (piece === 'k' && escapeAt(set, kingAt)) won = 'defenders'; return { cells, toMove: node.toMove === 'attackers' ? 'defenders' : 'attackers', won }; } /** Positive is good for the attackers. */ function evaluate(set: Ruleset, node: Node): number { if (node.won) return node.won === 'attackers' ? 10_000 : -10_000; const size = set.size; const cells = node.cells; let score = 0; let attackers = 0; let defenders = 0; let kingAt = -1; cells.forEach((p, i) => { if (p === 'a') attackers += 1; else if (p === 'd') defenders += 1; else if (p === 'k') kingAt = i; }); score += attackers * 10 - defenders * 16; if (kingAt === -1) return 10_000; const k = cellOf(size, kingAt); // The king's distance to safety, and how open his roads are. const distance = set.escape === 'corner' ? Math.min(...cornersOf(size).map((i) => cellOf(size, i)).map((g) => Math.abs(g.x - k.x) + Math.abs(g.y - k.y))) : Math.min(k.x, k.y, size - 1 - k.x, size - 1 - k.y); score += distance * 6; // Open lines from the king straight to an escape square are worth a great deal. for (const [dx, dy] of DIRECTIONS) { let x = k.x + dx; let y = k.y + dy; let open = true; while (x >= 0 && y >= 0 && x < size && y < size) { if (cells[y * size + x] !== '.') { open = false; break; } x += dx; y += dy; } if (open && escapeAt(set, (y - dy) * size + (x - dx))) score -= 400; } // Attackers pressing the king. score += neighbors(size, kingAt).filter((n) => cells[n] === 'a').length * 12; return score; } function orderMoves(set: Ruleset, cells: Piece[], moves: Input[]): Input[] { // King moves and edge moves first: they change the position most. const king = cells.indexOf('k'); return moves .map((m) => ({ m, w: (m.from === king ? 2 : 0) + (isEdge(set.size, m.to) ? 1 : 0) })) .sort((a, b) => b.w - a.w) .map((x) => x.m); } function search(set: Ruleset, node: Node, depth: number, alpha: number, beta: number, forSide: Side): number { if (depth === 0 || node.won) return evaluate(set, node) * (forSide === 'attackers' ? 1 : -1); const maximizing = node.toMove === forSide; let best = maximizing ? -Infinity : Infinity; for (const m of orderMoves(set, node.cells, movesOf(set, node.cells, node.toMove))) { const next = stepNode(set, node, m.from, m.to); const v = search(set, next, depth - 1, alpha, beta, forSide); if (maximizing) { best = Math.max(best, v); alpha = Math.max(alpha, v); } else { best = Math.min(best, v); beta = Math.min(beta, v); } if (beta <= alpha) break; } return best; } export function chooseMove(state: State, seat: SeatId): Input { const set = RULESETS[state.set]; const side = state.players[seat].side; const root: Node = { cells: state.cells, toMove: side, won: null }; // A side with no move has already lost in applyMove, so there is always one. const moves = orderMoves(set, state.cells, movesOf(set, state.cells, side)); let best = moves[0]; let bestScore = -Infinity; for (const m of moves) { const next = stepNode(set, root, m.from, m.to); const v = search(set, next, DEPTH - 1, -Infinity, Infinity, side); if (v > bestScore) { bestScore = v; best = m; } } return { from: best.from, to: best.to }; }