Files
website-collection/chess/shared/chess-engine.js
T

538 lines
19 KiB
JavaScript
Raw Normal View History

2026-08-22 17:39:10 +00:00
const FILES = "abcdefgh";
2026-08-22 18:57:39 +00:00
const PIECES = ["p", "n", "b", "r", "q", "k"];
const VALUES = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 0 };
2026-08-22 17:39:10 +00:00
2026-08-22 18:57:39 +00:00
function square(file, rank) {
return `${FILES[file]}${rank + 1}`;
2026-08-22 17:39:10 +00:00
}
2026-08-22 18:57:39 +00:00
function parseSquare(s) {
if (!/^[a-h][1-8]$/.test(s || "")) return null;
return { file: FILES.indexOf(s[0]), rank: Number(s[1]) - 1 };
2026-08-22 17:39:10 +00:00
}
2026-08-23 06:50:00 +00:00
function squareSafe(file, rank) {
if (file < 0 || file > 7 || rank < 0 || rank > 7) return null;
return square(file, rank);
}
2026-08-22 18:57:39 +00:00
function other(color) {
2026-08-22 17:39:10 +00:00
return color === "w" ? "b" : "w";
}
2026-08-23 06:50:00 +00:00
// ОПТИМИЗАЦИЯ: Поверхностное копирование. Фигуры иммутабельны при ходах,
// поэтому достаточно скопировать ссылки на объекты, что работает мгновенно.
2026-08-22 17:39:10 +00:00
function cloneState(state) {
return {
2026-08-23 06:50:00 +00:00
board: { ...state.board },
2026-08-22 17:39:10 +00:00
turn: state.turn,
castling: {
w: { ...state.castling.w },
2026-08-22 18:57:39 +00:00
b: { ...state.castling.b }
2026-08-22 17:39:10 +00:00
},
2026-08-22 18:57:39 +00:00
ep: state.ep,
halfmove: state.halfmove,
fullmove: state.fullmove,
lastMove: state.lastMove ? { ...state.lastMove } : null
2026-08-22 17:39:10 +00:00
};
}
export class ChessEngine {
2026-08-23 06:50:00 +00:00
constructor(fen = null) {
if (fen) {
this.loadFen(fen);
} else {
this.reset();
}
2026-08-22 17:39:10 +00:00
}
reset() {
2026-08-23 06:50:00 +00:00
this.loadFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
}
loadFen(fen) {
const [placement, turn, castling, enPassant, halfmove, fullmove] = fen.split(" ");
const board = {};
let rank = 7, file = 0;
for (const char of placement) {
if (char === "/") {
rank--; file = 0;
} else if (/\d/.test(char)) {
file += parseInt(char, 10);
} else {
const color = char === char.toUpperCase() ? "w" : "b";
board[square(file, rank)] = { color, type: char.toLowerCase() };
file++;
}
}
2026-08-22 18:57:39 +00:00
this.state = {
2026-08-23 06:50:00 +00:00
board,
turn: turn === "w" ? "w" : "b",
2026-08-22 18:57:39 +00:00
castling: {
2026-08-23 06:50:00 +00:00
w: { k: castling.includes("K"), q: castling.includes("Q") },
b: { k: castling.includes("k"), q: castling.includes("q") }
2026-08-22 18:57:39 +00:00
},
2026-08-23 06:50:00 +00:00
ep: enPassant === "-" ? null : enPassant,
halfmove: parseInt(halfmove || 0, 10),
fullmove: parseInt(fullmove || 1, 10),
2026-08-22 18:57:39 +00:00
lastMove: null
2026-08-22 17:39:10 +00:00
};
2026-08-23 06:50:00 +00:00
2026-08-22 18:57:39 +00:00
this.history = [];
this.positions = new Map([[this.positionKey(this.state), 1]]);
2026-08-22 17:39:10 +00:00
}
2026-08-23 06:50:00 +00:00
get fen() {
let fen = "";
for (let r = 7; r >= 0; r--) {
let empty = 0;
for (let f = 0; f < 8; f++) {
const p = this.state.board[square(f, r)];
if (p) {
if (empty > 0) { fen += empty; empty = 0; }
fen += p.color === "w" ? p.type.toUpperCase() : p.type;
} else {
empty++;
}
}
if (empty > 0) fen += empty;
if (r > 0) fen += "/";
}
let castling = "";
if (this.state.castling.w.k) castling += "K";
if (this.state.castling.w.q) castling += "Q";
if (this.state.castling.b.k) castling += "k";
if (this.state.castling.b.q) castling += "q";
return `${fen} ${this.state.turn} ${castling || "-"} ${this.state.ep || "-"} ${this.state.halfmove} ${this.state.fullmove}`;
}
get pgn() {
let pgn = "";
for (let i = 0; i < this.history.length; i++) {
if (this.history[i].piece.color === "w") {
pgn += `${Math.floor(i / 2) + 1}. ${this.history[i].san} `;
} else {
pgn += `${this.history[i].san} `;
}
}
const status = this.getStatus();
if (status.phase === "checkmate") pgn += status.winner === "w" ? "1-0" : "0-1";
else if (status.phase === "draw") pgn += "1/2-1/2";
return pgn.trim();
}
2026-08-22 18:57:39 +00:00
positionKey(state) {
const board = Object.keys(state.board).sort().map(s => {
const p = state.board[s];
return `${s}${p.color}${p.type}`;
}).join(",");
return `${board}|${state.turn}|${state.castling.w.k ? "K" : ""}${state.castling.w.q ? "Q" : ""}${state.castling.b.k ? "k" : ""}${state.castling.b.q ? "q" : ""}|${state.ep || "-"}`;
2026-08-22 17:39:10 +00:00
}
2026-08-22 18:57:39 +00:00
getPiece(s) {
return this.state.board[s] || null;
2026-08-22 17:39:10 +00:00
}
2026-08-22 18:57:39 +00:00
movesFrom(from) {
const p = this.state.board[from];
if (!p || p.color !== this.state.turn) return [];
return this.legalMoves(this.state.turn).filter(m => m.from === from);
2026-08-22 17:39:10 +00:00
}
2026-08-22 18:57:39 +00:00
legalMoves(color = this.state.turn) {
const pseudo = this.pseudoMoves(this.state, color);
const legal = [];
for (const move of pseudo) {
const next = this.applyToClone(this.state, move);
if (!this.inCheck(next, color)) legal.push(move);
}
return legal;
2026-08-22 17:39:10 +00:00
}
2026-08-22 18:57:39 +00:00
pseudoMoves(state, color) {
const result = [];
for (const [from, piece] of Object.entries(state.board)) {
if (piece.color !== color) continue;
const pos = parseSquare(from);
2026-08-23 06:50:00 +00:00
2026-08-22 18:57:39 +00:00
if (piece.type === "p") this.pawnMoves(state, from, piece, pos, result);
else if (piece.type === "n") this.knightMoves(state, from, piece, pos, result);
else if (piece.type === "b") this.slideMoves(state, from, piece, pos, result, [[1,1],[1,-1],[-1,1],[-1,-1]]);
else if (piece.type === "r") this.slideMoves(state, from, piece, pos, result, [[1,0],[-1,0],[0,1],[0,-1]]);
else if (piece.type === "q") this.slideMoves(state, from, piece, pos, result, [[1,1],[1,-1],[-1,1],[-1,-1],[1,0],[-1,0],[0,1],[0,-1]]);
else if (piece.type === "k") this.kingMoves(state, from, piece, pos, result);
}
return result;
2026-08-22 17:39:10 +00:00
}
2026-08-22 18:57:39 +00:00
pushMove(state, result, from, to, promotion = null, special = null) {
const target = state.board[to];
if (target?.color === state.board[from]?.color) return;
if (target?.type === "k") return;
result.push({
2026-08-23 06:50:00 +00:00
from, to, promotion, special,
2026-08-22 18:57:39 +00:00
capture: Boolean(target) || special === "ep"
});
}
2026-08-22 17:39:10 +00:00
2026-08-22 18:57:39 +00:00
pawnMoves(state, from, piece, pos, result) {
const dir = piece.color === "w" ? 1 : -1;
const startRank = piece.color === "w" ? 1 : 6;
const promotionRank = piece.color === "w" ? 7 : 0;
2026-08-22 17:39:10 +00:00
2026-08-23 06:50:00 +00:00
const one = squareSafe(pos.file, pos.rank + dir);
if (one && !state.board[one]) {
if (pos.rank + dir === promotionRank) {
for (const p of ["q","r","b","n"]) this.pushMove(state, result, from, one, p);
} else {
this.pushMove(state, result, from, one);
if (pos.rank === startRank) {
const two = squareSafe(pos.file, pos.rank + dir * 2);
if (two && !state.board[two]) this.pushMove(state, result, from, two, null, "double");
2026-08-22 17:39:10 +00:00
}
}
}
2026-08-22 18:57:39 +00:00
for (const df of [-1, 1]) {
2026-08-23 06:50:00 +00:00
const to = squareSafe(pos.file + df, pos.rank + dir);
if (!to) continue;
2026-08-22 18:57:39 +00:00
const target = state.board[to];
if (target && target.color !== piece.color && target.type !== "k") {
2026-08-23 06:50:00 +00:00
if (pos.rank + dir === promotionRank) {
for (const p of ["q","r","b","n"]) this.pushMove(state, result, from, to, p);
2026-08-22 18:57:39 +00:00
} else {
this.pushMove(state, result, from, to);
}
} else if (state.ep === to) {
this.pushMove(state, result, from, to, null, "ep");
}
}
}
knightMoves(state, from, piece, pos, result) {
const jumps = [[1,2],[2,1],[-1,2],[-2,1],[1,-2],[2,-1],[-1,-2],[-2,-1]];
for (const [df, dr] of jumps) {
2026-08-23 06:50:00 +00:00
const to = squareSafe(pos.file + df, pos.rank + dr);
if (to) this.pushMove(state, result, from, to);
2026-08-22 18:57:39 +00:00
}
}
slideMoves(state, from, piece, pos, result, dirs) {
for (const [df, dr] of dirs) {
let f = pos.file + df, r = pos.rank + dr;
while (f >= 0 && f <= 7 && r >= 0 && r <= 7) {
const to = square(f, r);
const target = state.board[to];
2026-08-23 06:50:00 +00:00
if (!target) {
this.pushMove(state, result, from, to);
} else {
2026-08-22 18:57:39 +00:00
if (target.color !== piece.color && target.type !== "k") this.pushMove(state, result, from, to);
break;
}
f += df; r += dr;
}
}
}
kingMoves(state, from, piece, pos, result) {
for (let df = -1; df <= 1; df++) {
for (let dr = -1; dr <= 1; dr++) {
if (!df && !dr) continue;
2026-08-23 06:50:00 +00:00
const to = squareSafe(pos.file + df, pos.rank + dr);
if (to) this.pushMove(state, result, from, to);
2026-08-22 18:57:39 +00:00
}
}
const rank = piece.color === "w" ? 0 : 7;
const enemy = other(piece.color);
if (pos.file === 4 && pos.rank === rank && !this.inCheck(state, piece.color)) {
2026-08-23 06:50:00 +00:00
// King-side
if (state.castling[piece.color].k && !state.board[square(5,rank)] && !state.board[square(6,rank)] &&
state.board[square(7,rank)]?.type === "r") {
if (!this.isAttacked(state, square(5,rank), enemy) && !this.isAttacked(state, square(6,rank), enemy)) {
this.pushMove(state, result, from, square(6,rank), null, "castle-k");
2026-08-22 18:57:39 +00:00
}
}
2026-08-23 06:50:00 +00:00
// Queen-side
if (state.castling[piece.color].q && !state.board[square(1,rank)] && !state.board[square(2,rank)] &&
!state.board[square(3,rank)] && state.board[square(0,rank)]?.type === "r") {
if (!this.isAttacked(state, square(3,rank), enemy) && !this.isAttacked(state, square(2,rank), enemy)) {
this.pushMove(state, result, from, square(2,rank), null, "castle-q");
2026-08-22 18:57:39 +00:00
}
}
}
}
applyToClone(state, move) {
const next = cloneState(state);
const piece = next.board[move.from];
delete next.board[move.from];
if (move.special === "ep") {
2026-08-23 06:50:00 +00:00
const toSq = parseSquare(move.to);
delete next.board[square(toSq.file, toSq.rank + (piece.color === "w" ? -1 : 1))];
2026-08-22 18:57:39 +00:00
}
const captured = next.board[move.to];
2026-08-23 06:50:00 +00:00
next.board[move.to] = { color: piece.color, type: move.promotion || piece.type };
2026-08-22 18:57:39 +00:00
if (piece.type === "k") {
next.castling[piece.color].k = false;
next.castling[piece.color].q = false;
if (move.special === "castle-k") {
const rank = piece.color === "w" ? 0 : 7;
delete next.board[square(7,rank)];
next.board[square(5,rank)] = { color: piece.color, type: "r" };
}
if (move.special === "castle-q") {
const rank = piece.color === "w" ? 0 : 7;
delete next.board[square(0,rank)];
next.board[square(3,rank)] = { color: piece.color, type: "r" };
}
}
if (piece.type === "r") {
if (move.from === "a1") next.castling.w.q = false;
if (move.from === "h1") next.castling.w.k = false;
if (move.from === "a8") next.castling.b.q = false;
if (move.from === "h8") next.castling.b.k = false;
}
if (captured?.type === "r") {
if (move.to === "a1") next.castling.w.q = false;
if (move.to === "h1") next.castling.w.k = false;
if (move.to === "a8") next.castling.b.q = false;
if (move.to === "h8") next.castling.b.k = false;
}
next.ep = null;
2026-08-23 06:50:00 +00:00
if (piece.type === "p" && Math.abs(parseSquare(move.to).rank - parseSquare(move.from).rank) === 2) {
next.ep = square(parseSquare(move.from).file, (parseSquare(move.from).rank + parseSquare(move.to).rank) / 2);
2026-08-22 18:57:39 +00:00
}
next.halfmove = piece.type === "p" || move.capture ? 0 : next.halfmove + 1;
if (piece.color === "b") next.fullmove += 1;
next.turn = other(piece.color);
return next;
}
inCheck(state, color) {
2026-08-23 06:50:00 +00:00
const kingEntry = Object.entries(state.board).find(([, p]) => p.color === color && p.type === "k");
return kingEntry ? this.isAttacked(state, kingEntry[0], other(color)) : false;
2026-08-22 18:57:39 +00:00
}
2026-08-23 06:50:00 +00:00
// ОПТИМИЗАЦИЯ: Reverse Raycasting (Обратная трассировка). Ищем атакующих вокруг клетки,
// вместо того чтобы перебирать все фигуры на доске. Работает в разы быстрее.
isAttacked(state, targetSq, byColor) {
const t = parseSquare(targetSq);
2026-08-22 18:57:39 +00:00
if (!t) return false;
2026-08-23 06:50:00 +00:00
// 1. Проверяем коней
const knightJumps = [[1,2],[2,1],[-1,2],[-2,1],[1,-2],[2,-1],[-1,-2],[-2,-1]];
for (const [df, dr] of knightJumps) {
const p = state.board[squareSafe(t.file + df, t.rank + dr)];
if (p && p.color === byColor && p.type === "n") return true;
}
2026-08-22 18:57:39 +00:00
2026-08-23 06:50:00 +00:00
// 2. Проверяем королей
for (let df = -1; df <= 1; df++) {
for (let dr = -1; dr <= 1; dr++) {
if (!df && !dr) continue;
const p = state.board[squareSafe(t.file + df, t.rank + dr)];
if (p && p.color === byColor && p.type === "k") return true;
2026-08-22 18:57:39 +00:00
}
}
2026-08-23 06:50:00 +00:00
// 3. Проверяем пешки (смотрим в сторону откуда могла прийти пешка врага)
const pDir = byColor === "w" ? -1 : 1;
for (const df of [-1, 1]) {
const p = state.board[squareSafe(t.file + df, t.rank + pDir)];
if (p && p.color === byColor && p.type === "p") return true;
}
// 4. Проверяем дальнобойные фигуры (Слоны, Ладьи, Ферзи)
const dirs = [
{ df: 1, dr: 1, types: ["b", "q"] }, { df: -1, dr: -1, types: ["b", "q"] },
{ df: 1, dr: -1, types: ["b", "q"] }, { df: -1, dr: 1, types: ["b", "q"] },
{ df: 1, dr: 0, types: ["r", "q"] }, { df: -1, dr: 0, types: ["r", "q"] },
{ df: 0, dr: 1, types: ["r", "q"] }, { df: 0, dr: -1, types: ["r", "q"] }
];
for (const { df, dr, types } of dirs) {
let f = t.file + df, r = t.rank + dr;
while (f >= 0 && f <= 7 && r >= 0 && r <= 7) {
const p = state.board[square(f, r)];
if (p) {
if (p.color === byColor && types.includes(p.type)) return true;
break; // Наткнулись на любую другую фигуру — луч блокирован
}
f += df; r += dr;
}
}
2026-08-22 18:57:39 +00:00
return false;
}
sanForMove(move, legalBefore = null) {
const piece = this.state.board[move.from];
if (!piece) return "";
if (move.special === "castle-k") return this.suffixAfter(move, "O-O");
if (move.special === "castle-q") return this.suffixAfter(move, "O-O-O");
2026-08-23 06:50:00 +00:00
const legal = legalBefore || this.legalMoves(this.state.turn);
2026-08-22 18:57:39 +00:00
let san = piece.type === "p" ? "" : piece.type.toUpperCase();
2026-08-23 06:50:00 +00:00
const same = legal.filter(m => m.to === move.to && m.from !== move.from && this.state.board[m.from]?.type === piece.type);
2026-08-22 18:57:39 +00:00
2026-08-23 06:50:00 +00:00
// Улучшенная дисамбигуация (правила SAN)
2026-08-22 18:57:39 +00:00
if (same.length) {
2026-08-23 06:50:00 +00:00
const fromSq = parseSquare(move.from);
const sameFile = same.some(m => parseSquare(m.from).file === fromSq.file);
const sameRank = same.some(m => parseSquare(m.from).rank === fromSq.rank);
if (!sameFile) {
san += move.from[0]; // Отличаются по вертикали
} else if (!sameRank) {
san += move.from[1]; // Отличаются по горизонтали
} else {
san += move.from; // Приходится указывать и то, и другое
}
2026-08-22 18:57:39 +00:00
}
if (move.capture) {
2026-08-23 06:50:00 +00:00
if (piece.type === "p" && same.length === 0) san += move.from[0];
2026-08-22 18:57:39 +00:00
san += "x";
}
san += move.to;
if (move.promotion) san += `=${move.promotion.toUpperCase()}`;
return this.suffixAfter(move, san);
}
suffixAfter(move, san) {
const next = this.applyToClone(this.state, move);
if (this.inCheck(next, next.turn)) {
2026-08-23 06:50:00 +00:00
const replies = this.legalMoves(next.turn).filter(m => !this.inCheck(this.applyToClone(next, m), next.turn));
2026-08-22 18:57:39 +00:00
return san + (replies.length ? "+" : "#");
}
return san;
}
makeMove(input) {
const from = String(input?.from || "").toLowerCase();
const to = String(input?.to || "").toLowerCase();
const promotion = String(input?.promotion || "q").toLowerCase();
2026-08-23 06:50:00 +00:00
const legal = this.legalMoves(this.state.turn);
const move = legal.find(m => m.from === from && m.to === to && (m.promotion ? m.promotion === promotion : true));
2026-08-22 18:57:39 +00:00
if (!move) return { ok: false, error: "Недопустимый ход." };
const san = this.sanForMove(move, legal);
const piece = this.state.board[from];
2026-08-23 06:50:00 +00:00
const capturedPiece = move.special === "ep" ? { type: "p", color: other(piece.color) } : this.state.board[to] || null;
2026-08-22 18:57:39 +00:00
this.history.push({
2026-08-23 06:50:00 +00:00
...move, san, piece: { ...piece },
2026-08-22 18:57:39 +00:00
captured: capturedPiece ? { ...capturedPiece } : null,
before: cloneState(this.state)
});
this.state = this.applyToClone(this.state, move);
2026-08-23 06:50:00 +00:00
this.state.lastMove = { from, to, san, piece: { ...piece }, captured: capturedPiece ? { ...capturedPiece } : null };
2026-08-22 18:57:39 +00:00
const key = this.positionKey(this.state);
this.positions.set(key, (this.positions.get(key) || 0) + 1);
2026-08-23 06:50:00 +00:00
return { ok: true, move: this.state.lastMove, status: this.getStatus() };
2026-08-22 18:57:39 +00:00
}
2026-08-23 06:50:00 +00:00
// ОПТИМИЗАЦИЯ: O(1) Undo (Мгновенная отмена хода)
2026-08-22 18:57:39 +00:00
undo() {
const last = this.history.pop();
if (!last) return false;
2026-08-23 06:50:00 +00:00
// Декрементируем счетчик текущей позиции
const currentKey = this.positionKey(this.state);
const count = this.positions.get(currentKey);
if (count === 1) this.positions.delete(currentKey);
else this.positions.set(currentKey, count - 1);
2026-08-22 18:57:39 +00:00
this.state = last.before;
return true;
}
getStatus() {
const color = this.state.turn;
const moves = this.legalMoves(color);
const check = this.inCheck(this.state, color);
2026-08-23 06:50:00 +00:00
if (!moves.length) return check ? { phase: "checkmate", turn: color, winner: other(color) } : { phase: "draw", reason: "stalemate", turn: color };
2026-08-22 18:57:39 +00:00
if (this.state.halfmove >= 100) return { phase: "draw", reason: "50-move", turn: color };
2026-08-23 06:50:00 +00:00
2026-08-22 18:57:39 +00:00
const key = this.positionKey(this.state);
if ((this.positions.get(key) || 0) >= 3) return { phase: "draw", reason: "threefold", turn: color };
if (this.insufficientMaterial()) return { phase: "draw", reason: "insufficient-material", turn: color };
if (check) return { phase: "check", turn: color };
2026-08-23 06:50:00 +00:00
2026-08-22 18:57:39 +00:00
return { phase: "playing", turn: color };
}
insufficientMaterial() {
2026-08-23 06:50:00 +00:00
const pieces = Object.values(this.state.board).filter(p => p.type !== "k");
if (pieces.length === 0) return true;
if (pieces.length === 1 && ["n", "b"].includes(pieces[0].type)) return true;
// Два слона одноцветных полей у разных сторон
if (pieces.length === 2 && pieces.every(p => p.type === "b")) {
const bishops = Object.entries(this.state.board).filter(([, p]) => p.type === "b");
const c1 = parseSquare(bishops[0][0]);
const c2 = parseSquare(bishops[1][0]);
if ((c1.file + c1.rank) % 2 === (c2.file + c2.rank) % 2) return true;
2026-08-22 18:57:39 +00:00
}
return false;
}
material() {
const captured = { w: [], b: [] };
2026-08-23 06:50:00 +00:00
for (const h of this.history) if (h.captured) captured[h.piece.color].push(h.captured.type);
const sort = a => a.sort((x, y) => VALUES[y] - VALUES[x]);
2026-08-22 18:57:39 +00:00
sort(captured.w); sort(captured.b);
2026-08-23 06:50:00 +00:00
return {
captured,
score: {
w: captured.b.reduce((n, p) => n + VALUES[p], 0),
b: captured.w.reduce((n, p) => n + VALUES[p], 0)
}
2026-08-22 18:57:39 +00:00
};
}
getSnapshot() {
return {
2026-08-23 06:50:00 +00:00
...cloneState(this.state),
moves: this.history.map((h, i) => ({
number: i + 1,
from: h.from, to: h.to,
san: h.san, color: h.piece.color,
piece: h.piece.type, capture: Boolean(h.captured)
2026-08-22 18:57:39 +00:00
})),
material: this.material(),
2026-08-23 06:50:00 +00:00
status: this.getStatus(),
fen: this.fen,
pgn: this.pgn
2026-08-22 18:57:39 +00:00
};
2026-08-22 17:39:10 +00:00
}
}
2026-08-22 18:57:39 +00:00
export { FILES, VALUES };