Files
website-collection/chess/public/js/ai.js
T

1464 lines
27 KiB
JavaScript
Raw Normal View History

2026-08-23 07:42:19 +00:00
/*
=========================================================
CHESS PARTY — STRONG AI
=========================================================
Особенности:
- Iterative Deepening
- Alpha-Beta pruning
- Quiescence Search
- Transposition Table
- Killer Moves
- History Heuristic
- MVV-LVA
- Piece Square Tables
- Pawn structure evaluation
- Passed pawns
- Open / semi-open files
- Mobility
- King safety
- Endgame evaluation
- Mate search
Ожидаемый API движка:
engine.state.board
engine.state.turn
engine.state.halfmove
engine.legalMoves(color)
engine.makeMove({ from, to, promotion })
engine.undo()
engine.getPiece(square)
engine.inCheck(state, color)
=========================================================
*/
/* =========================================================
MATERIAL
========================================================= */
2026-08-22 17:36:51 +00:00
const PIECE_VALUE = {
2026-08-23 06:53:40 +00:00
p: 100,
n: 320,
b: 330,
r: 500,
q: 900,
k: 20000,
2026-08-22 17:36:51 +00:00
};
2026-08-23 07:42:19 +00:00
/* =========================================================
LEVELS
========================================================= */
2026-08-22 17:36:51 +00:00
const LEVELS = {
2026-08-23 07:42:19 +00:00
easy: {
depth: 2,
time: 350,
randomness: 0.12,
},
intermediate: {
depth: 4,
time: 1000,
randomness: 0.025,
},
hard: {
depth: 5,
time: 2500,
randomness: 0,
},
expert: {
depth: 6,
time: 5000,
randomness: 0,
},
master: {
depth: 7,
time: 10000,
randomness: 0,
},
2026-08-22 17:36:51 +00:00
};
2026-08-23 07:42:19 +00:00
/* =========================================================
PIECE SQUARE TABLES
========================================================= */
const PST = {
p: [
0, 0, 0, 0, 0, 0, 0, 0,
50, 50, 50, 50, 50, 50, 50, 50,
10, 10, 20, 30, 30, 20, 10, 10,
5, 5, 10, 27, 27, 10, 5, 5,
0, 0, 0, 25, 25, 0, 0, 0,
5, -5, -10, 0, 0, -10, -5, 5,
5, 10, 10, -25, -25, 10, 10, 5,
0, 0, 0, 0, 0, 0, 0, 0,
],
n: [
-50, -40, -30, -30, -30, -30, -40, -50,
-40, -20, 0, 5, 5, 0, -20, -40,
-30, 5, 10, 15, 15, 10, 5, -30,
-30, 0, 15, 20, 20, 15, 0, -30,
-30, 5, 15, 20, 20, 15, 5, -30,
-30, 0, 10, 15, 15, 10, 0, -30,
-40, -20, 0, 0, 0, 0, -20, -40,
-50, -40, -30, -30, -30, -30, -40, -50,
],
b: [
-20, -10, -10, -10, -10, -10, -10, -20,
-10, 5, 0, 0, 0, 0, 5, -10,
-10, 10, 10, 10, 10, 10, 10, -10,
-10, 0, 10, 10, 10, 10, 0, -10,
-10, 5, 5, 10, 10, 5, 5, -10,
-10, 0, 5, 10, 10, 5, 0, -10,
-10, 0, 0, 0, 0, 0, 0, -10,
-20, -10, -10, -10, -10, -10, -10, -20,
],
r: [
0, 0, 0, 5, 5, 0, 0, 0,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
5, 10, 10, 10, 10, 10, 10, 5,
0, 0, 0, 0, 0, 0, 0, 0,
],
q: [
-20, -10, -10, -5, -5, -10, -10, -20,
-10, 0, 0, 0, 0, 0, 0, -10,
-10, 0, 5, 5, 5, 5, 0, -10,
-5, 0, 5, 5, 5, 5, 0, -5,
0, 0, 5, 5, 5, 5, 0, -5,
-10, 5, 5, 5, 5, 5, 0, -10,
-10, 0, 5, 0, 0, 0, 0, -10,
-20, -10, -10, -5, -5, -10, -10, -20,
],
k: [
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-20, -30, -30, -40, -40, -30, -30, -20,
-10, -20, -20, -20, -20, -20, -20, -10,
20, 20, 0, 0, 0, 0, 20, 20,
20, 30, 10, 0, 0, 10, 30, 20,
],
};
/* =========================================================
ENDGAME KING TABLE
========================================================= */
const KING_ENDGAME = [
-50, -30, -30, -30, -30, -30, -30, -50,
-30, -10, 0, 5, 5, 0, -10, -30,
-30, 0, 20, 30, 30, 20, 0, -30,
-30, 5, 30, 40, 40, 30, 5, -30,
-30, 5, 30, 40, 40, 30, 5, -30,
-30, 0, 20, 30, 30, 20, 0, -30,
-30, -10, 0, 0, 0, 0, -10, -30,
-50, -30, -30, -30, -30, -30, -30, -50,
];
/* =========================================================
SEARCH STATE
========================================================= */
let searchStart = 0;
let searchLimit = 5000;
let stopSearch = false;
const transpositionTable = new Map();
const killerMoves = new Map();
const historyTable = new Map();
const MAX_TT_SIZE = 100000;
/* =========================================================
UTILS
========================================================= */
function opposite(color) {
return color === "w" ? "b" : "w";
2026-08-22 17:36:51 +00:00
}
2026-08-23 07:42:19 +00:00
function squareIndex(square) {
if (!square || square.length < 2) {
return 0;
}
const file = square.charCodeAt(0) - 97;
const rank = Number(square[1]) - 1;
if (
file < 0 ||
file > 7 ||
rank < 0 ||
rank > 7
) {
return 0;
}
return rank * 8 + file;
}
function mirroredIndex(square, color) {
const index = squareIndex(square);
if (color === "w") {
return index;
}
const rank = Math.floor(index / 8);
const file = index % 8;
return (7 - rank) * 8 + file;
}
function moveKey(move) {
return `${move.from}-${move.to}-${move.promotion || ""}`;
}
function isCapture(move) {
return !!move.capture;
}
/* =========================================================
POSITION KEY
========================================================= */
function positionKey(engine) {
let key = `${engine.state.turn}|`;
const board = engine.state.board || {};
const squares = Object.keys(board).sort();
for (const sq of squares) {
const piece = board[sq];
if (!piece) continue;
key += `${sq}${piece.color}${piece.type};`;
}
return key;
}
/* =========================================================
ENDGAME DETECTION
========================================================= */
function isEndgame(engine) {
let nonPawnPieces = 0;
let queens = 0;
let rooks = 0;
for (const piece of Object.values(engine.state.board || {})) {
if (!piece) continue;
if (piece.type === "q") queens++;
if (piece.type === "r") rooks++;
if (
piece.type !== "p" &&
piece.type !== "k"
) {
nonPawnPieces++;
2026-08-22 17:36:51 +00:00
}
}
2026-08-23 07:42:19 +00:00
return (
queens === 0 &&
nonPawnPieces <= 4 &&
rooks <= 2
);
}
/* =========================================================
MATERIAL COUNT
========================================================= */
function materialScore(engine, color) {
let score = 0;
for (const piece of Object.values(engine.state.board || {})) {
if (!piece) continue;
const value = PIECE_VALUE[piece.type] || 0;
if (piece.color === color) {
score += value;
} else {
score -= value;
}
}
2026-08-23 06:53:40 +00:00
return score;
2026-08-22 17:36:51 +00:00
}
2026-08-23 07:42:19 +00:00
/* =========================================================
PAWN STRUCTURE
========================================================= */
function evaluatePawns(engine, color) {
const board = engine.state.board || {};
const ownPawns = [];
const enemyPawns = [];
for (const [sq, piece] of Object.entries(board)) {
if (!piece || piece.type !== "p") continue;
if (piece.color === color) {
ownPawns.push(sq);
} else {
enemyPawns.push(sq);
}
}
let score = 0;
const ownFiles = Array(8).fill(0);
const enemyFiles = Array(8).fill(0);
for (const sq of ownPawns) {
ownFiles[sq.charCodeAt(0) - 97]++;
}
for (const sq of enemyPawns) {
enemyFiles[sq.charCodeAt(0) - 97]++;
}
/* doubled pawns */
for (const count of ownFiles) {
if (count > 1) {
score -= (count - 1) * 18;
}
}
/* isolated pawns */
for (let file = 0; file < 8; file++) {
if (ownFiles[file] !== 0) {
const left =
file > 0 ? ownFiles[file - 1] : 0;
const right =
file < 7 ? ownFiles[file + 1] : 0;
if (!left && !right) {
score -= 12;
2026-08-23 06:53:40 +00:00
}
}
}
2026-08-23 07:42:19 +00:00
/* passed pawns */
for (const sq of ownPawns) {
const file = sq.charCodeAt(0) - 97;
const rank = Number(sq[1]) - 1;
let blocked = false;
for (const enemySq of enemyPawns) {
const ef =
enemySq.charCodeAt(0) - 97;
const er =
Number(enemySq[1]) - 1;
if (Math.abs(ef - file) > 1) {
continue;
}
if (
color === "w" &&
er > rank
) {
blocked = true;
break;
}
if (
color === "b" &&
er < rank
) {
blocked = true;
break;
}
}
if (!blocked) {
const advancement =
color === "w"
? rank
: 7 - rank;
score += 15 + advancement * 12;
}
}
return score;
2026-08-22 17:36:51 +00:00
}
2026-08-23 07:42:19 +00:00
/* =========================================================
OPEN FILES
========================================================= */
2026-08-22 17:36:51 +00:00
2026-08-23 07:42:19 +00:00
function evaluateFiles(engine, color) {
const board = engine.state.board || {};
let score = 0;
for (let file = 0; file < 8; file++) {
let ownPawn = false;
let enemyPawn = false;
for (let rank = 1; rank <= 8; rank++) {
const sq =
String.fromCharCode(97 + file) + rank;
const piece = board[sq];
if (!piece || piece.type !== "p") {
continue;
}
if (piece.color === color) {
ownPawn = true;
} else {
enemyPawn = true;
}
}
if (!ownPawn && !enemyPawn) {
score += 8;
} else if (!ownPawn) {
score += 15;
2026-08-23 06:53:40 +00:00
}
2026-08-22 17:36:51 +00:00
}
2026-08-23 07:42:19 +00:00
return score;
}
2026-08-22 17:36:51 +00:00
2026-08-23 07:42:19 +00:00
/* =========================================================
PIECE POSITION
========================================================= */
function positionalScore(piece, square, endgame) {
if (!piece) return 0;
const index =
mirroredIndex(square, piece.color);
if (
piece.type === "k" &&
endgame
) {
return KING_ENDGAME[index];
}
const table = PST[piece.type];
if (!table) return 0;
return table[index];
}
/* =========================================================
KING SAFETY
========================================================= */
function evaluateKingSafety(engine, color) {
const board = engine.state.board || {};
let ownKing = null;
let enemyKing = null;
for (const [sq, piece] of Object.entries(board)) {
if (!piece || piece.type !== "k") continue;
if (piece.color === color) {
ownKing = sq;
} else {
enemyKing = sq;
}
}
if (!ownKing || !enemyKing) {
return 0;
}
let score = 0;
const file =
ownKing.charCodeAt(0) - 97;
const rank =
Number(ownKing[1]);
/* Corner / castled king */
if (
file <= 1 ||
file >= 6
) {
score += 18;
}
/* King in center */
if (
file >= 2 &&
file <= 5 &&
rank >= 3 &&
rank <= 6
) {
score -= 18;
}
/* Pawn shield */
const direction =
color === "w" ? 1 : -1;
for (let df = -1; df <= 1; df++) {
const f = file + df;
if (f < 0 || f > 7) continue;
const pawnRank = rank + direction;
if (
pawnRank >= 1 &&
pawnRank <= 8
) {
const sq =
String.fromCharCode(97 + f) +
pawnRank;
const piece = board[sq];
if (
piece &&
piece.color === color &&
piece.type === "p"
) {
score += 8;
} else {
score -= 6;
}
}
}
return score;
}
/* =========================================================
MOBILITY
========================================================= */
function evaluateMobility(engine, color) {
let ownMoves = 0;
let enemyMoves = 0;
try {
ownMoves =
engine.legalMoves(color).length;
enemyMoves =
engine.legalMoves(opposite(color)).length;
} catch {
return 0;
}
return (
(ownMoves - enemyMoves) * 3
);
}
/* =========================================================
FULL POSITION EVALUATION
========================================================= */
function evaluate(engine, color) {
const enemy = opposite(color);
const endgame = isEndgame(engine);
let score = 0;
score += materialScore(engine, color);
score += evaluatePawns(engine, color);
score -= evaluatePawns(engine, enemy);
score += evaluateFiles(engine, color);
score -= evaluateFiles(engine, enemy);
score += evaluateKingSafety(engine, color);
score -= evaluateKingSafety(engine, enemy);
score += evaluateMobility(engine, color);
/* positional pieces */
for (
const [square, piece]
of Object.entries(engine.state.board || {})
) {
if (!piece) continue;
const positional =
positionalScore(
piece,
square,
endgame
);
if (piece.color === color) {
score += positional;
} else {
score -= positional;
}
}
/* Bishop pair */
let ownBishops = 0;
let enemyBishops = 0;
for (
const piece
of Object.values(engine.state.board || {})
) {
if (!piece || piece.type !== "b") {
continue;
}
if (piece.color === color) {
ownBishops++;
} else {
enemyBishops++;
}
}
if (ownBishops >= 2) {
score += 35;
}
if (enemyBishops >= 2) {
score -= 35;
}
return score;
}
/* =========================================================
MOVE ORDERING
========================================================= */
function moveOrderingScore(
engine,
move,
ply
) {
let score = 0;
const key = moveKey(move);
/* TT move */
if (
move._ttMove
) {
score += 10000000;
}
/* Promotion */
if (move.promotion) {
score +=
900000 +
(PIECE_VALUE[move.promotion] || 0);
}
/* Capture */
if (move.capture) {
const attacker =
engine.getPiece(move.from);
const victim =
engine.getPiece(move.to);
const victimValue =
victim
? PIECE_VALUE[victim.type] || 0
: 100;
const attackerValue =
attacker
? PIECE_VALUE[attacker.type] || 0
: 100;
score +=
500000 +
victimValue * 10 -
attackerValue;
}
/* Killer */
const killers =
killerMoves.get(ply) || [];
if (killers.includes(key)) {
score += 300000;
}
/* History */
score +=
historyTable.get(key) || 0;
return score;
}
function orderMoves(
engine,
moves,
ply = 0,
ttMove = null
) {
const ttKey =
ttMove
? moveKey(ttMove)
: null;
2026-08-22 17:36:51 +00:00
for (const move of moves) {
2026-08-23 07:42:19 +00:00
move._ttMove =
ttKey &&
moveKey(move) === ttKey;
move._order =
moveOrderingScore(
engine,
move,
ply
);
}
moves.sort(
(a, b) =>
b._order - a._order
);
return moves;
}
/* =========================================================
QUIESCENCE
========================================================= */
function quiescence(
engine,
alpha,
beta,
maximizingColor,
ply = 0
) {
if (
Date.now() - searchStart >
searchLimit
) {
stopSearch = true;
return 0;
}
const standPat =
evaluate(
engine,
maximizingColor
);
if (standPat >= beta) {
return beta;
}
if (standPat > alpha) {
alpha = standPat;
}
let moves =
engine.legalMoves(
engine.state.turn
);
moves =
moves.filter(
move =>
move.capture ||
move.promotion
);
orderMoves(
engine,
moves,
ply
);
for (const move of moves) {
if (stopSearch) break;
engine.makeMove({
from: move.from,
to: move.to,
promotion: move.promotion
});
const score =
-quiescence(
engine,
-beta,
-alpha,
opposite(maximizingColor),
ply + 1
);
2026-08-23 06:53:40 +00:00
engine.undo();
2026-08-22 17:36:51 +00:00
2026-08-23 07:42:19 +00:00
if (score >= beta) {
return beta;
2026-08-22 17:36:51 +00:00
}
2026-08-23 07:42:19 +00:00
if (score > alpha) {
alpha = score;
}
2026-08-22 17:36:51 +00:00
}
2026-08-23 07:42:19 +00:00
return alpha;
}
/* =========================================================
MINIMAX / NEGAMAX
========================================================= */
function search(
engine,
depth,
alpha,
beta,
color,
ply = 0
) {
if (
Date.now() - searchStart >
searchLimit
) {
stopSearch = true;
return 0;
}
if (depth <= 0) {
return quiescence(
engine,
alpha,
beta,
color,
ply
);
}
if (
engine.state.halfmove >= 100
) {
return 0;
}
const key =
positionKey(engine);
const cached =
transpositionTable.get(key);
if (
cached &&
cached.depth >= depth
) {
if (
cached.flag === "EXACT"
) {
return cached.score;
}
if (
cached.flag === "LOWER" &&
cached.score > alpha
) {
alpha = cached.score;
}
if (
cached.flag === "UPPER" &&
cached.score < beta
) {
beta = cached.score;
}
if (alpha >= beta) {
return cached.score;
}
}
const turn =
engine.state.turn;
let moves =
engine.legalMoves(turn);
if (!moves.length) {
if (
engine.inCheck(
engine.state,
turn
)
) {
/*
* Чем меньше ply,
* тем быстрее мат.
*/
if (turn === color) {
return -1000000 + ply;
}
return 1000000 - ply;
}
return 0;
}
const alphaOriginal = alpha;
let bestScore = -Infinity;
let bestMove = null;
let ttMove =
cached?.bestMove || null;
orderMoves(
engine,
moves,
ply,
ttMove
);
for (const move of moves) {
if (stopSearch) {
break;
}
engine.makeMove({
from: move.from,
to: move.to,
promotion: move.promotion
});
const score =
-search(
engine,
depth - 1,
-beta,
-alpha,
opposite(color),
ply + 1
);
engine.undo();
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
if (score > alpha) {
alpha = score;
/*
* History heuristic
*/
const keyMove =
moveKey(move);
const previous =
historyTable.get(
keyMove
) || 0;
historyTable.set(
keyMove,
Math.min(
100000,
previous +
depth * depth
)
);
}
/*
* Killer move
*/
if (
!move.capture &&
!move.promotion
) {
if (score > alphaOriginal) {
const killers =
killerMoves.get(ply) || [];
const keyMove =
moveKey(move);
if (
!killers.includes(
keyMove
)
) {
killers.unshift(
keyMove
);
if (killers.length > 2) {
killers.pop();
}
killerMoves.set(
ply,
killers
);
}
}
}
if (alpha >= beta) {
break;
}
}
if (stopSearch) {
return bestScore;
}
let flag = "EXACT";
if (bestScore <= alphaOriginal) {
flag = "UPPER";
} else if (bestScore >= beta) {
flag = "LOWER";
}
/*
* Prevent unlimited memory growth.
*/
if (
transpositionTable.size >
MAX_TT_SIZE
) {
transpositionTable.clear();
}
transpositionTable.set(
key,
{
depth,
score: bestScore,
flag,
bestMove,
}
);
2026-08-23 06:53:40 +00:00
return bestScore;
2026-08-22 17:36:51 +00:00
}
2026-08-23 07:42:19 +00:00
/* =========================================================
ROOT SEARCH
========================================================= */
2026-08-22 17:36:51 +00:00
2026-08-23 07:42:19 +00:00
function rootSearch(
engine,
depth,
color
) {
let moves =
engine.legalMoves(color);
if (!moves.length) {
return null;
}
const rootKey =
positionKey(engine);
const cached =
transpositionTable.get(
rootKey
);
orderMoves(
engine,
moves,
0,
cached?.bestMove
);
let bestMove =
moves[0];
let bestScore =
-Infinity;
let alpha =
-Infinity;
const beta =
Infinity;
2026-08-22 17:36:51 +00:00
for (const move of moves) {
2026-08-23 07:42:19 +00:00
if (stopSearch) {
break;
}
engine.makeMove({
from: move.from,
to: move.to,
promotion: move.promotion
});
const score =
-search(
engine,
depth - 1,
-beta,
-alpha,
opposite(color),
1
);
2026-08-23 06:53:40 +00:00
engine.undo();
2026-08-23 07:42:19 +00:00
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
if (score > alpha) {
alpha = score;
}
2026-08-22 17:36:51 +00:00
}
2026-08-23 07:42:19 +00:00
transpositionTable.set(
rootKey,
{
depth,
score: bestScore,
flag: "EXACT",
bestMove,
}
);
2026-08-22 17:36:51 +00:00
2026-08-23 07:42:19 +00:00
return {
move: bestMove,
score: bestScore,
};
2026-08-23 06:53:40 +00:00
}
2026-08-23 07:42:19 +00:00
/* =========================================================
ITERATIVE DEEPENING
========================================================= */
function iterativeSearch(
engine,
color,
maxDepth
) {
let bestMove = null;
let bestScore = 0;
for (
let depth = 1;
depth <= maxDepth;
depth++
) {
if (stopSearch) {
break;
}
const result =
rootSearch(
engine,
depth,
color
);
if (!result) {
break;
}
/*
* Сохраняем только
* полностью законченный слой.
*/
if (!stopSearch) {
bestMove =
result.move;
bestScore =
result.score;
}
}
return {
move: bestMove,
score: bestScore,
};
}
/* =========================================================
PUBLIC AI
========================================================= */
export const AI_LEVELS =
LEVELS;
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;
}
/*
* Сбрасываем состояние поиска.
*/
searchStart =
Date.now();
searchLimit =
config.time;
stopSearch = false;
/*
* Не даём старым killer/history
* слишком сильно влиять на новую игру.
*/
if (
transpositionTable.size >
MAX_TT_SIZE
) {
transpositionTable.clear();
}
const result =
iterativeSearch(
engine,
color,
config.depth
);
let selected =
result.move || moves[0];
/*
* Лёгкий уровень может иногда
* выбрать один из лучших ходов.
*/
if (
config.randomness > 0 &&
Math.random() <
config.randomness
) {
const candidates =
moves
.slice()
.sort(
(a, b) =>
(b._order || 0) -
(a._order || 0)
)
.slice(0, 3);
if (candidates.length) {
selected =
candidates[
Math.floor(
Math.random() *
candidates.length
)
];
}
}
/*
* Не передаём внутренние поля
* движку.
*/
return {
from: selected.from,
to: selected.to,
promotion:
selected.promotion
};
}
/* =========================================================
OPTIONAL HELPERS
========================================================= */
export function clearAI() {
transpositionTable.clear();
killerMoves.clear();
historyTable.clear();
}
export function getAIStats() {
return {
transpositionTable:
transpositionTable.size,
killerMoves:
killerMoves.size,
historyMoves:
historyTable.size,
};
}