Update chess/public/js/ai.js

This commit is contained in:
2026-08-23 06:53:40 +00:00
parent e852d0eea4
commit d67496fb19
+152 -509
View File
@@ -1,543 +1,186 @@
// Ценность фигур (обрати внимание, ключи теперь в нижнем регистре под новый движок)
const PIECE_VALUE = { const PIECE_VALUE = {
P: 100, p: 100,
N: 320, n: 320,
B: 330, b: 330,
R: 500, r: 500,
Q: 900, q: 900,
K: 20_000, k: 20000,
}; };
const LEVELS = { const LEVELS = {
easy: { easy: { depth: 1, randomness: 0.35 },
depth: 1, intermediate: { depth: 2, randomness: 0.08 },
randomness: 0.35, hard: { depth: 3, randomness: 0.01 },
}, expert: { depth: 4, randomness: 0 },
intermediate: {
depth: 2,
randomness: 0.08,
},
hard: {
depth: 3,
randomness: 0.01,
},
expert: {
depth: 4,
randomness: 0,
},
}; };
const CENTER = new Set([ // 1. АЛГОРИТМИЧЕСКАЯ ПОЗИЦИОННАЯ ОЦЕНКА
"d4", // Вычисляет бонус для фигуры в зависимости от ее положения на доске
"e4", function getPositionalBonus(piece, sqStr) {
"d5", const file = sqStr.charCodeAt(0) - 97; // 0 до 7 (от 'a' до 'h')
"e5", const rank = sqStr.charCodeAt(1) - 49; // 0 до 7 (от '1' до '8')
]);
const EXTENDED_CENTER = new Set([ // Относительная горизонталь: 0 - домашняя линия, 7 - линия превращения
"c3", const relativeRank = piece.color === "w" ? rank : 7 - rank;
"d3",
"e3",
"f3",
"c4",
"d4",
"e4",
"f4",
"c5",
"d5",
"e5",
"f5",
"c6",
"d6",
"e6",
"f6",
]);
function cloneEngine(engine) { // Близость к центру (чем больше, тем ближе)
return new engine.constructor( const centerDistX = Math.abs(3.5 - file);
engine.getSnapshot(), const centerDistY = Math.abs(3.5 - relativeRank);
); const centerScore = 7 - (centerDistX + centerDistY);
}
function opposite(color) { let bonus = 0;
return color === "w"
? "b"
: "w";
}
function squareName(x, y) { switch (piece.type) {
return String.fromCharCode( case "p":
97 + x, bonus += (relativeRank * relativeRank) * 2; // Мощный бонус за продвижение
) + (8 - y); if (file > 2 && file < 5) bonus += 15; // Центральные пешки сильнее
} break;
case "n":
function getAllMoves(engine, color) { bonus += centerScore * 8; // Кони требуют центра
const moves = []; if (file === 0 || file === 7 || rank === 0 || rank === 7) bonus -= 25; // Кони на краю - позор
break;
for (let y = 0; y < 8; y++) { case "b":
for (let x = 0; x < 8; x++) { bonus += centerScore * 5; // Слонам нравятся центральные диагонали
const square = if (relativeRank === 0) bonus -= 10; // Штраф за пассивность на задней линии
squareName(x, y); break;
case "r":
const piece = if (relativeRank === 6) bonus += 35; // Ладьи на 7-й горизонтали смертоносны
engine.state.board[y][x]; if (file === 3 || file === 4) bonus += 15; // Давим на центральные вертикали
break;
if ( case "q":
!piece || bonus += centerScore * 3; // Ферзю легкий бонус за центр
piece.color !== color break;
) { case "k":
continue; if (relativeRank < 2) {
} if (file < 2 || file > 5) bonus += 40; // Безопасность (рокировка)
if (file === 3 || file === 4) bonus -= 20; // Опасность в центре
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 { } else {
score += 35; bonus -= relativeRank * 15; // В миттельшпиле королю не стоит гулять
} }
}
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; break;
} }
return bonus;
}
// 2. БЫСТРЫЙ ОЦЕНЩИК
function evaluate(engine, color) {
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 score;
}
// 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);
}
// 4. МИНИМАКС С ОТСЕЧЕНИЕМ И МГНОВЕННОЙ ОТМЕНОЙ (UNDO)
function minimax(engine, depth, alpha, beta, maximizingColor) {
// Быстрая проверка на ничью (правило 50 ходов)
if (engine.state.halfmove >= 100) return 0;
if (depth <= 0) {
return evaluate(engine, maximizingColor);
} }
return { const turn = engine.state.turn;
score: bestScore, const moves = engine.legalMoves(turn);
move: bestMove,
}; if (!moves.length) {
if (engine.inCheck(engine.state, turn)) {
// Предпочитаем самые быстрые маты, прибавляя остаточную глубину
return turn === maximizingColor ? -1000000 - depth : 1000000 + depth;
}
return 0; // Пат
}
orderMoves(engine, moves);
const maximizing = turn === maximizingColor;
let bestScore = maximizing ? -Infinity : Infinity;
for (const move of moves) {
// Делаем ход напрямую в оригинальном движке
engine.makeMove({ from: move.from, to: move.to, promotion: move.promotion });
const score = minimax(engine, depth - 1, alpha, beta, maximizingColor);
// Моментально откатываем
engine.undo();
if (maximizing) {
bestScore = Math.max(bestScore, score);
alpha = Math.max(alpha, bestScore);
} else {
bestScore = Math.min(bestScore, score);
beta = Math.min(beta, bestScore);
}
if (beta <= alpha) break; // Альфа-бета отсечение
}
return bestScore;
} }
export const AI_LEVELS = LEVELS; export const AI_LEVELS = LEVELS;
export function chooseComputerMove( export function chooseComputerMove(engine, level = "intermediate") {
engine, const config = LEVELS[level] || LEVELS.intermediate;
level = "intermediate", const color = engine.state.turn;
) {
const config =
LEVELS[level] ||
LEVELS.intermediate;
const color = const moves = engine.legalMoves(color);
engine.state.turn; if (!moves.length) return null;
const moves =
getAllMoves(
engine,
color,
);
if (!moves.length) {
return null;
}
// Первичная сортировка для эффективного начала
orderMoves(engine, moves);
const ranked = []; const ranked = [];
for (const move of moves) { for (const move of moves) {
const child = engine.makeMove({ from: move.from, to: move.to, promotion: move.promotion });
cloneEngine(engine); const score = minimax(engine, Math.max(0, config.depth - 1), -Infinity, Infinity, color);
engine.undo();
const result = ranked.push({ move, score });
child.makeMove({
from: move.from,
to: move.to,
promotion:
move.promotion ||
null,
});
if (!result.ok) {
continue;
} }
const score = ranked.sort((a, b) => b.score - a.score);
minimax(
child,
Math.max(
0,
config.depth - 1,
),
-Infinity,
Infinity,
color,
).score;
ranked.push({ if (!ranked.length) return moves[0];
move,
score,
});
}
ranked.sort( // Применяем процент случайности для уровней ниже эксперта
(a, b) => if (Math.random() < config.randomness) {
b.score - a.score, const count = Math.min(3, ranked.length);
); return ranked[Math.floor(Math.random() * count)].move;
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; return ranked[0].move;
} }