Update chess/public/js/ai.js
This commit is contained in:
+137
-494
@@ -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')
|
||||||
]);
|
|
||||||
|
// Относительная горизонталь: 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([
|
let bonus = 0;
|
||||||
"c3",
|
|
||||||
"d3",
|
switch (piece.type) {
|
||||||
"e3",
|
case "p":
|
||||||
"f3",
|
bonus += (relativeRank * relativeRank) * 2; // Мощный бонус за продвижение
|
||||||
"c4",
|
if (file > 2 && file < 5) bonus += 15; // Центральные пешки сильнее
|
||||||
"d4",
|
break;
|
||||||
"e4",
|
case "n":
|
||||||
"f4",
|
bonus += centerScore * 8; // Кони требуют центра
|
||||||
"c5",
|
if (file === 0 || file === 7 || rank === 0 || rank === 7) bonus -= 25; // Кони на краю - позор
|
||||||
"d5",
|
break;
|
||||||
"e5",
|
case "b":
|
||||||
"f5",
|
bonus += centerScore * 5; // Слонам нравятся центральные диагонали
|
||||||
"c6",
|
if (relativeRank === 0) bonus -= 10; // Штраф за пассивность на задней линии
|
||||||
"d6",
|
break;
|
||||||
"e6",
|
case "r":
|
||||||
"f6",
|
if (relativeRank === 6) bonus += 35; // Ладьи на 7-й горизонтали смертоносны
|
||||||
]);
|
if (file === 3 || file === 4) bonus += 15; // Давим на центральные вертикали
|
||||||
|
break;
|
||||||
function cloneEngine(engine) {
|
case "q":
|
||||||
return new engine.constructor(
|
bonus += centerScore * 3; // Ферзю легкий бонус за центр
|
||||||
engine.getSnapshot(),
|
break;
|
||||||
);
|
case "k":
|
||||||
}
|
if (relativeRank < 2) {
|
||||||
|
if (file < 2 || file > 5) bonus += 40; // Безопасность (рокировка)
|
||||||
function opposite(color) {
|
if (file === 3 || file === 4) bonus -= 20; // Опасность в центре
|
||||||
return color === "w"
|
} else {
|
||||||
? "b"
|
bonus -= relativeRank * 15; // В миттельшпиле королю не стоит гулять
|
||||||
: "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;
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
const pieceMoves =
|
|
||||||
engine.getMovesFrom(
|
|
||||||
square,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const move of pieceMoves) {
|
|
||||||
moves.push(move);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return bonus;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. БЫСТРЫЙ ОЦЕНЩИК
|
||||||
function evaluate(engine, color) {
|
function evaluate(engine, color) {
|
||||||
const status =
|
let score = 0;
|
||||||
engine.state.status;
|
// Перебираем только словарь активных фигур (вместо пустых клеток 8х8)
|
||||||
|
for (const [sq, piece] of Object.entries(engine.state.board)) {
|
||||||
if (
|
const val = PIECE_VALUE[piece.type] || 0;
|
||||||
status?.phase ===
|
const positional = getPositionalBonus(piece, sq);
|
||||||
"checkmate"
|
|
||||||
) {
|
const total = val + positional;
|
||||||
if (
|
|
||||||
status.winner === color
|
if (piece.color === color) {
|
||||||
) {
|
score += total;
|
||||||
return 1_000_000;
|
} else {
|
||||||
|
score -= total;
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1_000_000;
|
|
||||||
}
|
}
|
||||||
|
return score;
|
||||||
if (
|
|
||||||
status?.phase === "draw"
|
|
||||||
) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
evaluateMaterial(
|
|
||||||
engine,
|
|
||||||
color,
|
|
||||||
) +
|
|
||||||
evaluatePosition(
|
|
||||||
engine,
|
|
||||||
color,
|
|
||||||
) +
|
|
||||||
evaluateMobility(
|
|
||||||
engine,
|
|
||||||
color,
|
|
||||||
) +
|
|
||||||
evaluateKings(
|
|
||||||
engine,
|
|
||||||
color,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function orderMoves(
|
// 3. MVV-LVA СОРТИРОВКА (Most Valuable Victim - Least Valuable Attacker)
|
||||||
engine,
|
function orderMoves(engine, moves) {
|
||||||
moves,
|
for (const move of moves) {
|
||||||
) {
|
let score = 0;
|
||||||
return [...moves].sort(
|
if (move.capture) {
|
||||||
(a, b) => {
|
const attacker = engine.getPiece(move.from);
|
||||||
const captureA =
|
const victim = engine.getPiece(move.to) || { type: "p" }; // Фолбэк для взятия на проходе
|
||||||
a.capture ? 1 : 0;
|
if (attacker && victim) {
|
||||||
|
// Если пешка(100) бьет ферзя(900) -> огромный приоритет
|
||||||
const captureB =
|
score += 1000 + PIECE_VALUE[victim.type] - PIECE_VALUE[attacker.type];
|
||||||
b.capture ? 1 : 0;
|
}
|
||||||
|
}
|
||||||
const promotionA =
|
if (move.promotion) {
|
||||||
a.promotion ? 1 : 0;
|
score += PIECE_VALUE[move.promotion] + 900;
|
||||||
|
}
|
||||||
const promotionB =
|
move._score = score;
|
||||||
b.promotion ? 1 : 0;
|
}
|
||||||
|
return moves.sort((a, b) => b._score - a._score);
|
||||||
return (
|
|
||||||
captureB -
|
|
||||||
captureA ||
|
|
||||||
promotionB -
|
|
||||||
promotionA
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function minimax(
|
// 4. МИНИМАКС С ОТСЕЧЕНИЕМ И МГНОВЕННОЙ ОТМЕНОЙ (UNDO)
|
||||||
engine,
|
function minimax(engine, depth, alpha, beta, maximizingColor) {
|
||||||
depth,
|
// Быстрая проверка на ничью (правило 50 ходов)
|
||||||
alpha,
|
if (engine.state.halfmove >= 100) return 0;
|
||||||
beta,
|
|
||||||
maximizingColor,
|
if (depth <= 0) {
|
||||||
) {
|
return evaluate(engine, maximizingColor);
|
||||||
const status =
|
|
||||||
engine.state.status;
|
|
||||||
|
|
||||||
if (
|
|
||||||
depth <= 0 ||
|
|
||||||
status?.phase ===
|
|
||||||
"checkmate" ||
|
|
||||||
status?.phase === "draw"
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
score: evaluate(
|
|
||||||
engine,
|
|
||||||
maximizingColor,
|
|
||||||
),
|
|
||||||
move: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const turn =
|
const turn = engine.state.turn;
|
||||||
engine.state.turn;
|
const moves = engine.legalMoves(turn);
|
||||||
|
|
||||||
const moves =
|
|
||||||
orderMoves(
|
|
||||||
engine,
|
|
||||||
getAllMoves(
|
|
||||||
engine,
|
|
||||||
turn,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!moves.length) {
|
if (!moves.length) {
|
||||||
return {
|
if (engine.inCheck(engine.state, turn)) {
|
||||||
score: evaluate(
|
// Предпочитаем самые быстрые маты, прибавляя остаточную глубину
|
||||||
engine,
|
return turn === maximizingColor ? -1000000 - depth : 1000000 + depth;
|
||||||
maximizingColor,
|
}
|
||||||
),
|
return 0; // Пат
|
||||||
move: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const maximizing =
|
orderMoves(engine, moves);
|
||||||
turn === maximizingColor;
|
|
||||||
|
|
||||||
let bestScore = maximizing
|
const maximizing = turn === maximizingColor;
|
||||||
? -Infinity
|
let bestScore = maximizing ? -Infinity : Infinity;
|
||||||
: Infinity;
|
|
||||||
|
|
||||||
let bestMove = null;
|
|
||||||
|
|
||||||
for (const move of moves) {
|
for (const move of moves) {
|
||||||
const child =
|
// Делаем ход напрямую в оригинальном движке
|
||||||
cloneEngine(engine);
|
engine.makeMove({ from: move.from, to: move.to, promotion: move.promotion });
|
||||||
|
|
||||||
const result =
|
const score = minimax(engine, depth - 1, alpha, beta, maximizingColor);
|
||||||
child.makeMove({
|
|
||||||
from: move.from,
|
// Моментально откатываем
|
||||||
to: move.to,
|
engine.undo();
|
||||||
promotion:
|
|
||||||
move.promotion ||
|
|
||||||
null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.ok) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultNode =
|
|
||||||
minimax(
|
|
||||||
child,
|
|
||||||
depth - 1,
|
|
||||||
alpha,
|
|
||||||
beta,
|
|
||||||
maximizingColor,
|
|
||||||
);
|
|
||||||
|
|
||||||
const score =
|
|
||||||
resultNode.score;
|
|
||||||
|
|
||||||
if (maximizing) {
|
if (maximizing) {
|
||||||
if (
|
bestScore = Math.max(bestScore, score);
|
||||||
score >
|
alpha = Math.max(alpha, bestScore);
|
||||||
bestScore
|
|
||||||
) {
|
|
||||||
bestScore = score;
|
|
||||||
bestMove = move;
|
|
||||||
}
|
|
||||||
|
|
||||||
alpha = Math.max(
|
|
||||||
alpha,
|
|
||||||
bestScore,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
if (
|
bestScore = Math.min(bestScore, score);
|
||||||
score <
|
beta = Math.min(beta, bestScore);
|
||||||
bestScore
|
|
||||||
) {
|
|
||||||
bestScore = score;
|
|
||||||
bestMove = move;
|
|
||||||
}
|
|
||||||
|
|
||||||
beta = Math.min(
|
|
||||||
beta,
|
|
||||||
bestScore,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (beta <= alpha) {
|
if (beta <= alpha) break; // Альфа-бета отсечение
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return bestScore;
|
||||||
score: bestScore,
|
|
||||||
move: bestMove,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 =
|
const moves = engine.legalMoves(color);
|
||||||
LEVELS[level] ||
|
if (!moves.length) return null;
|
||||||
LEVELS.intermediate;
|
|
||||||
|
|
||||||
const color =
|
|
||||||
engine.state.turn;
|
|
||||||
|
|
||||||
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 =
|
|
||||||
child.makeMove({
|
ranked.push({ move, score });
|
||||||
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(
|
ranked.sort((a, b) => b.score - a.score);
|
||||||
(a, b) =>
|
|
||||||
b.score - a.score,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!ranked.length) {
|
if (!ranked.length) return moves[0];
|
||||||
return moves[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
// Применяем процент случайности для уровней ниже эксперта
|
||||||
Math.random() <
|
if (Math.random() < config.randomness) {
|
||||||
config.randomness
|
const count = Math.min(3, ranked.length);
|
||||||
) {
|
return ranked[Math.floor(Math.random() * count)].move;
|
||||||
const count =
|
|
||||||
Math.min(
|
|
||||||
3,
|
|
||||||
ranked.length,
|
|
||||||
);
|
|
||||||
|
|
||||||
return ranked[
|
|
||||||
Math.floor(
|
|
||||||
Math.random() *
|
|
||||||
count,
|
|
||||||
)
|
|
||||||
].move;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ranked[0].move;
|
return ranked[0].move;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user