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,
|
|
|
|
|
};
|
|
|
|
|
}
|