diff --git a/chess/public/js/ai.js b/chess/public/js/ai.js index d45b13c..b9a5354 100644 --- a/chess/public/js/ai.js +++ b/chess/public/js/ai.js @@ -1,543 +1,186 @@ +// Ценность фигур (обрати внимание, ключи теперь в нижнем регистре под новый движок) const PIECE_VALUE = { - P: 100, - N: 320, - B: 330, - R: 500, - Q: 900, - K: 20_000, + p: 100, + n: 320, + b: 330, + r: 500, + q: 900, + k: 20000, }; 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, - }, + 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", -]); +// 1. АЛГОРИТМИЧЕСКАЯ ПОЗИЦИОННАЯ ОЦЕНКА +// Вычисляет бонус для фигуры в зависимости от ее положения на доске +function getPositionalBonus(piece, sqStr) { + const file = sqStr.charCodeAt(0) - 97; // 0 до 7 (от 'a' до 'h') + const rank = sqStr.charCodeAt(1) - 49; // 0 до 7 (от '1' до '8') + + // Относительная горизонталь: 0 - домашняя линия, 7 - линия превращения + const relativeRank = piece.color === "w" ? rank : 7 - rank; + + // Близость к центру (чем больше, тем ближе) + const centerDistX = Math.abs(3.5 - file); + const centerDistY = Math.abs(3.5 - relativeRank); + const centerScore = 7 - (centerDistX + centerDistY); -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; + let bonus = 0; + + switch (piece.type) { + case "p": + bonus += (relativeRank * relativeRank) * 2; // Мощный бонус за продвижение + if (file > 2 && file < 5) bonus += 15; // Центральные пешки сильнее + break; + case "n": + bonus += centerScore * 8; // Кони требуют центра + if (file === 0 || file === 7 || rank === 0 || rank === 7) bonus -= 25; // Кони на краю - позор + break; + case "b": + bonus += centerScore * 5; // Слонам нравятся центральные диагонали + if (relativeRank === 0) bonus -= 10; // Штраф за пассивность на задней линии + break; + case "r": + if (relativeRank === 6) bonus += 35; // Ладьи на 7-й горизонтали смертоносны + if (file === 3 || file === 4) bonus += 15; // Давим на центральные вертикали + break; + case "q": + bonus += centerScore * 3; // Ферзю легкий бонус за центр + break; + case "k": + if (relativeRank < 2) { + if (file < 2 || file > 5) bonus += 40; // Безопасность (рокировка) + if (file === 3 || file === 4) bonus -= 20; // Опасность в центре + } else { + bonus -= relativeRank * 15; // В миттельшпиле королю не стоит гулять } - - const pieceMoves = - engine.getMovesFrom( - square, - ); - - for (const move of pieceMoves) { - moves.push(move); - } - } + break; } - - 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; + return bonus; } +// 2. БЫСТРЫЙ ОЦЕНЩИК function evaluate(engine, color) { - const status = - engine.state.status; - - if ( - status?.phase === - "checkmate" - ) { - if ( - status.winner === color - ) { - return 1_000_000; + let score = 0; + // Перебираем только словарь активных фигур (вместо пустых клеток 8х8) + for (const [sq, piece] of Object.entries(engine.state.board)) { + const val = PIECE_VALUE[piece.type] || 0; + const positional = getPositionalBonus(piece, sq); + + const total = val + positional; + + if (piece.color === color) { + score += total; + } else { + score -= total; } - - return -1_000_000; } - - if ( - status?.phase === "draw" - ) { - return 0; - } - - return ( - evaluateMaterial( - engine, - color, - ) + - evaluatePosition( - engine, - color, - ) + - evaluateMobility( - engine, - color, - ) + - evaluateKings( - engine, - color, - ) - ); + return score; } -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 - ); - }, - ); +// 3. MVV-LVA СОРТИРОВКА (Most Valuable Victim - Least Valuable Attacker) +function orderMoves(engine, moves) { + for (const move of moves) { + let score = 0; + if (move.capture) { + const attacker = engine.getPiece(move.from); + const victim = engine.getPiece(move.to) || { type: "p" }; // Фолбэк для взятия на проходе + if (attacker && victim) { + // Если пешка(100) бьет ферзя(900) -> огромный приоритет + score += 1000 + PIECE_VALUE[victim.type] - PIECE_VALUE[attacker.type]; + } + } + if (move.promotion) { + score += PIECE_VALUE[move.promotion] + 900; + } + move._score = score; + } + return moves.sort((a, b) => b._score - a._score); } -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, - }; +// 4. МИНИМАКС С ОТСЕЧЕНИЕМ И МГНОВЕННОЙ ОТМЕНОЙ (UNDO) +function minimax(engine, depth, alpha, beta, maximizingColor) { + // Быстрая проверка на ничью (правило 50 ходов) + if (engine.state.halfmove >= 100) return 0; + + if (depth <= 0) { + return evaluate(engine, maximizingColor); } - const turn = - engine.state.turn; - - const moves = - orderMoves( - engine, - getAllMoves( - engine, - turn, - ), - ); + const turn = engine.state.turn; + const moves = engine.legalMoves(turn); if (!moves.length) { - return { - score: evaluate( - engine, - maximizingColor, - ), - move: null, - }; + if (engine.inCheck(engine.state, turn)) { + // Предпочитаем самые быстрые маты, прибавляя остаточную глубину + return turn === maximizingColor ? -1000000 - depth : 1000000 + depth; + } + return 0; // Пат } - const maximizing = - turn === maximizingColor; + orderMoves(engine, moves); - let bestScore = maximizing - ? -Infinity - : Infinity; - - let bestMove = null; + const maximizing = turn === maximizingColor; + let bestScore = maximizing ? -Infinity : Infinity; 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; + // Делаем ход напрямую в оригинальном движке + engine.makeMove({ from: move.from, to: move.to, promotion: move.promotion }); + + const score = minimax(engine, depth - 1, alpha, beta, maximizingColor); + + // Моментально откатываем + engine.undo(); if (maximizing) { - if ( - score > - bestScore - ) { - bestScore = score; - bestMove = move; - } - - alpha = Math.max( - alpha, - bestScore, - ); + bestScore = Math.max(bestScore, score); + alpha = Math.max(alpha, bestScore); } else { - if ( - score < - bestScore - ) { - bestScore = score; - bestMove = move; - } - - beta = Math.min( - beta, - bestScore, - ); + bestScore = Math.min(bestScore, score); + beta = Math.min(beta, bestScore); } - if (beta <= alpha) { - break; - } + if (beta <= alpha) break; // Альфа-бета отсечение } - return { - score: bestScore, - move: bestMove, - }; + return bestScore; } 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; - } +export function chooseComputerMove(engine, level = "intermediate") { + const config = LEVELS[level] || LEVELS.intermediate; + const color = engine.state.turn; + + const moves = engine.legalMoves(color); + if (!moves.length) return null; + // Первичная сортировка для эффективного начала + orderMoves(engine, moves); 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, - }); + engine.makeMove({ from: move.from, to: move.to, promotion: move.promotion }); + const score = minimax(engine, Math.max(0, config.depth - 1), -Infinity, Infinity, color); + engine.undo(); + + ranked.push({ move, score }); } - ranked.sort( - (a, b) => - b.score - a.score, - ); + ranked.sort((a, b) => b.score - a.score); - if (!ranked.length) { - return moves[0]; - } + 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; + // Применяем процент случайности для уровней ниже эксперта + if (Math.random() < config.randomness) { + const count = Math.min(3, ranked.length); + return ranked[Math.floor(Math.random() * count)].move; } return ranked[0].move; -} \ No newline at end of file +} +