const PIECE_VALUE = { P: 100, N: 320, B: 330, R: 500, Q: 900, K: 20_000, }; const LEVELS = { easy: { depth: 1, randomness: 0.35, }, intermediate: { depth: 2, randomness: 0.08, }, hard: { depth: 3, randomness: 0.01, }, expert: { depth: 4, randomness: 0, }, }; const CENTER = new Set([ "d4", "e4", "d5", "e5", ]); const EXTENDED_CENTER = new Set([ "c3", "d3", "e3", "f3", "c4", "d4", "e4", "f4", "c5", "d5", "e5", "f5", "c6", "d6", "e6", "f6", ]); function cloneEngine(engine) { return new engine.constructor( engine.getSnapshot(), ); } function opposite(color) { return color === "w" ? "b" : "w"; } function squareName(x, y) { return String.fromCharCode( 97 + x, ) + (8 - y); } function getAllMoves(engine, color) { const moves = []; for (let y = 0; y < 8; y++) { for (let x = 0; x < 8; x++) { const square = squareName(x, y); const piece = engine.state.board[y][x]; if ( !piece || piece.color !== color ) { continue; } const pieceMoves = engine.getMovesFrom( square, ); for (const move of pieceMoves) { moves.push(move); } } } return moves; } function evaluateMaterial(engine, color) { let score = 0; for (const row of engine.state.board) { for (const piece of row) { if (!piece) { continue; } const value = PIECE_VALUE[piece.type] || 0; score += piece.color === color ? value : -value; } } return score; } function evaluatePosition(engine, color) { let score = 0; for (let y = 0; y < 8; y++) { for (let x = 0; x < 8; x++) { const piece = engine.state.board[y][x]; if (!piece) { continue; } const square = squareName(x, y); const sign = piece.color === color ? 1 : -1; if ( CENTER.has(square) ) { score += sign * (piece.type === "P" ? 30 : 18); } if ( EXTENDED_CENTER.has( square, ) ) { score += sign * 6; } if ( piece.type === "N" ) { const edgePenalty = Math.abs(3.5 - x) + Math.abs(3.5 - y); score += sign * Math.max( 0, 18 - edgePenalty * 4, ); } if ( piece.type === "P" ) { const advancement = piece.color === "w" ? 6 - y : y - 1; score += sign * advancement * 8; } if ( piece.type === "B" ) { score += sign * 4; } if ( piece.type === "R" && (y === 0 || y === 7) ) { score += sign * 5; } } } return score; } function evaluateMobility( engine, color, ) { const own = getAllMoves( engine, color, ).length; const enemy = getAllMoves( engine, opposite(color), ).length; return (own - enemy) * 3; } function evaluateKings(engine, color) { let score = 0; const status = engine.state.status; if (!status) { return 0; } if (status.check) { const turn = engine.state.turn; if (turn === color) { score -= 35; } else { score += 35; } } return score; } function evaluate(engine, color) { const status = engine.state.status; if ( status?.phase === "checkmate" ) { if ( status.winner === color ) { return 1_000_000; } return -1_000_000; } if ( status?.phase === "draw" ) { return 0; } return ( evaluateMaterial( engine, color, ) + evaluatePosition( engine, color, ) + evaluateMobility( engine, color, ) + evaluateKings( engine, color, ) ); } function orderMoves( engine, moves, ) { return [...moves].sort( (a, b) => { const captureA = a.capture ? 1 : 0; const captureB = b.capture ? 1 : 0; const promotionA = a.promotion ? 1 : 0; const promotionB = b.promotion ? 1 : 0; return ( captureB - captureA || promotionB - promotionA ); }, ); } function minimax( engine, depth, alpha, beta, maximizingColor, ) { const status = engine.state.status; if ( depth <= 0 || status?.phase === "checkmate" || status?.phase === "draw" ) { return { score: evaluate( engine, maximizingColor, ), move: null, }; } const turn = engine.state.turn; const moves = orderMoves( engine, getAllMoves( engine, turn, ), ); if (!moves.length) { return { score: evaluate( engine, maximizingColor, ), move: null, }; } const maximizing = turn === maximizingColor; let bestScore = maximizing ? -Infinity : Infinity; let bestMove = null; for (const move of moves) { const child = cloneEngine(engine); const result = child.makeMove({ from: move.from, to: move.to, promotion: move.promotion || null, }); if (!result.ok) { continue; } const resultNode = minimax( child, depth - 1, alpha, beta, maximizingColor, ); const score = resultNode.score; if (maximizing) { if ( score > bestScore ) { bestScore = score; bestMove = move; } alpha = Math.max( alpha, bestScore, ); } else { if ( score < bestScore ) { bestScore = score; bestMove = move; } beta = Math.min( beta, bestScore, ); } if (beta <= alpha) { break; } } return { score: bestScore, move: bestMove, }; } export const AI_LEVELS = LEVELS; export function chooseComputerMove( engine, level = "intermediate", ) { const config = LEVELS[level] || LEVELS.intermediate; const color = engine.state.turn; const moves = getAllMoves( engine, color, ); if (!moves.length) { return null; } const ranked = []; for (const move of moves) { const child = cloneEngine(engine); const result = child.makeMove({ from: move.from, to: move.to, promotion: move.promotion || null, }); if (!result.ok) { continue; } const score = minimax( child, Math.max( 0, config.depth - 1, ), -Infinity, Infinity, color, ).score; ranked.push({ move, score, }); } ranked.sort( (a, b) => b.score - a.score, ); if (!ranked.length) { return moves[0]; } if ( Math.random() < config.randomness ) { const count = Math.min( 3, ranked.length, ); return ranked[ Math.floor( Math.random() * count, ) ].move; } return ranked[0].move; }