Update chess/shared/chess-engine.js
This commit is contained in:
+220
-218
@@ -11,17 +11,20 @@ function parseSquare(s) {
|
|||||||
return { file: FILES.indexOf(s[0]), rank: Number(s[1]) - 1 };
|
return { file: FILES.indexOf(s[0]), rank: Number(s[1]) - 1 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function squareSafe(file, rank) {
|
||||||
|
if (file < 0 || file > 7 || rank < 0 || rank > 7) return null;
|
||||||
|
return square(file, rank);
|
||||||
|
}
|
||||||
|
|
||||||
function other(color) {
|
function other(color) {
|
||||||
return color === "w" ? "b" : "w";
|
return color === "w" ? "b" : "w";
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneBoard(board) {
|
// ОПТИМИЗАЦИЯ: Поверхностное копирование. Фигуры иммутабельны при ходах,
|
||||||
return Object.fromEntries(Object.entries(board).map(([s, p]) => [s, { ...p }]));
|
// поэтому достаточно скопировать ссылки на объекты, что работает мгновенно.
|
||||||
}
|
|
||||||
|
|
||||||
function cloneState(state) {
|
function cloneState(state) {
|
||||||
return {
|
return {
|
||||||
board: cloneBoard(state.board),
|
board: { ...state.board },
|
||||||
turn: state.turn,
|
turn: state.turn,
|
||||||
castling: {
|
castling: {
|
||||||
w: { ...state.castling.w },
|
w: { ...state.castling.w },
|
||||||
@@ -34,40 +37,95 @@ function cloneState(state) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialBoard() {
|
|
||||||
const board = {};
|
|
||||||
const back = "rnbqkbnr";
|
|
||||||
for (let f = 0; f < 8; f++) {
|
|
||||||
board[square(f, 0)] = { color: "w", type: back[f] };
|
|
||||||
board[square(f, 1)] = { color: "w", type: "p" };
|
|
||||||
board[square(f, 6)] = { color: "b", type: "p" };
|
|
||||||
board[square(f, 7)] = { color: "b", type: back[f] };
|
|
||||||
}
|
|
||||||
return board;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ChessEngine {
|
export class ChessEngine {
|
||||||
constructor() {
|
constructor(fen = null) {
|
||||||
|
if (fen) {
|
||||||
|
this.loadFen(fen);
|
||||||
|
} else {
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
board: initialBoard(),
|
board,
|
||||||
turn: "w",
|
turn: turn === "w" ? "w" : "b",
|
||||||
castling: {
|
castling: {
|
||||||
w: { k: true, q: true },
|
w: { k: castling.includes("K"), q: castling.includes("Q") },
|
||||||
b: { k: true, q: true }
|
b: { k: castling.includes("k"), q: castling.includes("q") }
|
||||||
},
|
},
|
||||||
ep: null,
|
ep: enPassant === "-" ? null : enPassant,
|
||||||
halfmove: 0,
|
halfmove: parseInt(halfmove || 0, 10),
|
||||||
fullmove: 1,
|
fullmove: parseInt(fullmove || 1, 10),
|
||||||
lastMove: null
|
lastMove: null
|
||||||
};
|
};
|
||||||
|
|
||||||
this.history = [];
|
this.history = [];
|
||||||
this.positions = new Map([[this.positionKey(this.state), 1]]);
|
this.positions = new Map([[this.positionKey(this.state), 1]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
positionKey(state) {
|
positionKey(state) {
|
||||||
const board = Object.keys(state.board).sort().map(s => {
|
const board = Object.keys(state.board).sort().map(s => {
|
||||||
const p = state.board[s];
|
const p = state.board[s];
|
||||||
@@ -89,7 +147,6 @@ export class ChessEngine {
|
|||||||
legalMoves(color = this.state.turn) {
|
legalMoves(color = this.state.turn) {
|
||||||
const pseudo = this.pseudoMoves(this.state, color);
|
const pseudo = this.pseudoMoves(this.state, color);
|
||||||
const legal = [];
|
const legal = [];
|
||||||
|
|
||||||
for (const move of pseudo) {
|
for (const move of pseudo) {
|
||||||
const next = this.applyToClone(this.state, move);
|
const next = this.applyToClone(this.state, move);
|
||||||
if (!this.inCheck(next, color)) legal.push(move);
|
if (!this.inCheck(next, color)) legal.push(move);
|
||||||
@@ -99,11 +156,9 @@ export class ChessEngine {
|
|||||||
|
|
||||||
pseudoMoves(state, color) {
|
pseudoMoves(state, color) {
|
||||||
const result = [];
|
const result = [];
|
||||||
|
|
||||||
for (const [from, piece] of Object.entries(state.board)) {
|
for (const [from, piece] of Object.entries(state.board)) {
|
||||||
if (piece.color !== color) continue;
|
if (piece.color !== color) continue;
|
||||||
const pos = parseSquare(from);
|
const pos = parseSquare(from);
|
||||||
if (!pos) continue;
|
|
||||||
|
|
||||||
if (piece.type === "p") this.pawnMoves(state, from, piece, pos, result);
|
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 === "n") this.knightMoves(state, from, piece, pos, result);
|
||||||
@@ -112,7 +167,6 @@ export class ChessEngine {
|
|||||||
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 === "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);
|
else if (piece.type === "k") this.kingMoves(state, from, piece, pos, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +175,7 @@ export class ChessEngine {
|
|||||||
if (target?.color === state.board[from]?.color) return;
|
if (target?.color === state.board[from]?.color) return;
|
||||||
if (target?.type === "k") return;
|
if (target?.type === "k") return;
|
||||||
result.push({
|
result.push({
|
||||||
from, to,
|
from, to, promotion, special,
|
||||||
promotion,
|
|
||||||
special,
|
|
||||||
capture: Boolean(target) || special === "ep"
|
capture: Boolean(target) || special === "ep"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -133,32 +185,27 @@ export class ChessEngine {
|
|||||||
const startRank = piece.color === "w" ? 1 : 6;
|
const startRank = piece.color === "w" ? 1 : 6;
|
||||||
const promotionRank = piece.color === "w" ? 7 : 0;
|
const promotionRank = piece.color === "w" ? 7 : 0;
|
||||||
|
|
||||||
const oneRank = pos.rank + dir;
|
const one = squareSafe(pos.file, pos.rank + dir);
|
||||||
if (oneRank >= 0 && oneRank <= 7) {
|
if (one && !state.board[one]) {
|
||||||
const one = square(pos.file, oneRank);
|
if (pos.rank + dir === promotionRank) {
|
||||||
if (!state.board[one]) {
|
for (const p of ["q","r","b","n"]) this.pushMove(state, result, from, one, p);
|
||||||
if (oneRank === promotionRank) {
|
|
||||||
for (const promotion of ["q","r","b","n"]) this.pushMove(state, result, from, one, promotion);
|
|
||||||
} else {
|
} else {
|
||||||
this.pushMove(state, result, from, one);
|
this.pushMove(state, result, from, one);
|
||||||
if (pos.rank === startRank) {
|
if (pos.rank === startRank) {
|
||||||
const two = square(pos.file, pos.rank + dir * 2);
|
const two = squareSafe(pos.file, pos.rank + dir * 2);
|
||||||
if (!state.board[two]) this.pushMove(state, result, from, two, null, "double");
|
if (two && !state.board[two]) this.pushMove(state, result, from, two, null, "double");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const df of [-1, 1]) {
|
for (const df of [-1, 1]) {
|
||||||
const f = pos.file + df;
|
const to = squareSafe(pos.file + df, pos.rank + dir);
|
||||||
const r = pos.rank + dir;
|
if (!to) continue;
|
||||||
if (f < 0 || f > 7 || r < 0 || r > 7) continue;
|
|
||||||
const to = square(f, r);
|
|
||||||
const target = state.board[to];
|
|
||||||
|
|
||||||
|
const target = state.board[to];
|
||||||
if (target && target.color !== piece.color && target.type !== "k") {
|
if (target && target.color !== piece.color && target.type !== "k") {
|
||||||
if (r === promotionRank) {
|
if (pos.rank + dir === promotionRank) {
|
||||||
for (const promotion of ["q","r","b","n"]) this.pushMove(state, result, from, to, promotion);
|
for (const p of ["q","r","b","n"]) this.pushMove(state, result, from, to, p);
|
||||||
} else {
|
} else {
|
||||||
this.pushMove(state, result, from, to);
|
this.pushMove(state, result, from, to);
|
||||||
}
|
}
|
||||||
@@ -171,8 +218,8 @@ export class ChessEngine {
|
|||||||
knightMoves(state, from, piece, pos, result) {
|
knightMoves(state, from, piece, pos, result) {
|
||||||
const jumps = [[1,2],[2,1],[-1,2],[-2,1],[1,-2],[2,-1],[-1,-2],[-2,-1]];
|
const jumps = [[1,2],[2,1],[-1,2],[-2,1],[1,-2],[2,-1],[-1,-2],[-2,-1]];
|
||||||
for (const [df, dr] of jumps) {
|
for (const [df, dr] of jumps) {
|
||||||
const f = pos.file + df, r = pos.rank + dr;
|
const to = squareSafe(pos.file + df, pos.rank + dr);
|
||||||
if (f >= 0 && f <= 7 && r >= 0 && r <= 7) this.pushMove(state, result, from, square(f,r));
|
if (to) this.pushMove(state, result, from, to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,8 +229,9 @@ export class ChessEngine {
|
|||||||
while (f >= 0 && f <= 7 && r >= 0 && r <= 7) {
|
while (f >= 0 && f <= 7 && r >= 0 && r <= 7) {
|
||||||
const to = square(f, r);
|
const to = square(f, r);
|
||||||
const target = state.board[to];
|
const target = state.board[to];
|
||||||
if (!target) this.pushMove(state, result, from, to);
|
if (!target) {
|
||||||
else {
|
this.pushMove(state, result, from, to);
|
||||||
|
} else {
|
||||||
if (target.color !== piece.color && target.type !== "k") this.pushMove(state, result, from, to);
|
if (target.color !== piece.color && target.type !== "k") this.pushMove(state, result, from, to);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -196,10 +244,8 @@ export class ChessEngine {
|
|||||||
for (let df = -1; df <= 1; df++) {
|
for (let df = -1; df <= 1; df++) {
|
||||||
for (let dr = -1; dr <= 1; dr++) {
|
for (let dr = -1; dr <= 1; dr++) {
|
||||||
if (!df && !dr) continue;
|
if (!df && !dr) continue;
|
||||||
const f = pos.file + df, r = pos.rank + dr;
|
const to = squareSafe(pos.file + df, pos.rank + dr);
|
||||||
if (f >= 0 && f <= 7 && r >= 0 && r <= 7) {
|
if (to) this.pushMove(state, result, from, to);
|
||||||
this.pushMove(state, result, from, square(f,r));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,26 +253,18 @@ export class ChessEngine {
|
|||||||
const enemy = other(piece.color);
|
const enemy = other(piece.color);
|
||||||
|
|
||||||
if (pos.file === 4 && pos.rank === rank && !this.inCheck(state, piece.color)) {
|
if (pos.file === 4 && pos.rank === rank && !this.inCheck(state, piece.color)) {
|
||||||
if (state.castling[piece.color].k &&
|
// King-side
|
||||||
!state.board[square(5,rank)] &&
|
if (state.castling[piece.color].k && !state.board[square(5,rank)] && !state.board[square(6,rank)] &&
|
||||||
!state.board[square(6,rank)] &&
|
state.board[square(7,rank)]?.type === "r") {
|
||||||
state.board[square(7,rank)]?.type === "r" &&
|
if (!this.isAttacked(state, square(5,rank), enemy) && !this.isAttacked(state, square(6,rank), enemy)) {
|
||||||
state.board[square(7,rank)]?.color === piece.color) {
|
this.pushMove(state, result, from, square(6,rank), null, "castle-k");
|
||||||
const through = square(5,rank), to = square(6,rank);
|
|
||||||
if (!this.isAttacked(state, through, enemy) && !this.isAttacked(state, to, enemy)) {
|
|
||||||
this.pushMove(state, result, from, to, null, "castle-k");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Queen-side
|
||||||
if (state.castling[piece.color].q &&
|
if (state.castling[piece.color].q && !state.board[square(1,rank)] && !state.board[square(2,rank)] &&
|
||||||
!state.board[square(1,rank)] &&
|
!state.board[square(3,rank)] && state.board[square(0,rank)]?.type === "r") {
|
||||||
!state.board[square(2,rank)] &&
|
if (!this.isAttacked(state, square(3,rank), enemy) && !this.isAttacked(state, square(2,rank), enemy)) {
|
||||||
!state.board[square(3,rank)] &&
|
this.pushMove(state, result, from, square(2,rank), null, "castle-q");
|
||||||
state.board[square(0,rank)]?.type === "r" &&
|
|
||||||
state.board[square(0,rank)]?.color === piece.color) {
|
|
||||||
const through = square(3,rank), to = square(2,rank);
|
|
||||||
if (!this.isAttacked(state, through, enemy) && !this.isAttacked(state, to, enemy)) {
|
|
||||||
this.pushMove(state, result, from, to, null, "castle-q");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,24 +273,15 @@ export class ChessEngine {
|
|||||||
applyToClone(state, move) {
|
applyToClone(state, move) {
|
||||||
const next = cloneState(state);
|
const next = cloneState(state);
|
||||||
const piece = next.board[move.from];
|
const piece = next.board[move.from];
|
||||||
if (!piece) return next;
|
|
||||||
|
|
||||||
delete next.board[move.from];
|
delete next.board[move.from];
|
||||||
|
|
||||||
if (move.special === "ep") {
|
if (move.special === "ep") {
|
||||||
const to = parseSquare(move.to);
|
const toSq = parseSquare(move.to);
|
||||||
const captured = square(to.file, to.rank + (piece.color === "w" ? -1 : 1));
|
delete next.board[square(toSq.file, toSq.rank + (piece.color === "w" ? -1 : 1))];
|
||||||
delete next.board[captured];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const captured = next.board[move.to];
|
const captured = next.board[move.to];
|
||||||
delete next.board[move.to];
|
next.board[move.to] = { color: piece.color, type: move.promotion || piece.type };
|
||||||
|
|
||||||
const moved = {
|
|
||||||
color: piece.color,
|
|
||||||
type: move.promotion || piece.type
|
|
||||||
};
|
|
||||||
next.board[move.to] = moved;
|
|
||||||
|
|
||||||
if (piece.type === "k") {
|
if (piece.type === "k") {
|
||||||
next.castling[piece.color].k = false;
|
next.castling[piece.color].k = false;
|
||||||
@@ -275,7 +304,6 @@ export class ChessEngine {
|
|||||||
if (move.from === "a8") next.castling.b.q = false;
|
if (move.from === "a8") next.castling.b.q = false;
|
||||||
if (move.from === "h8") next.castling.b.k = false;
|
if (move.from === "h8") next.castling.b.k = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (captured?.type === "r") {
|
if (captured?.type === "r") {
|
||||||
if (move.to === "a1") next.castling.w.q = false;
|
if (move.to === "a1") next.castling.w.q = false;
|
||||||
if (move.to === "h1") next.castling.w.k = false;
|
if (move.to === "h1") next.castling.w.k = false;
|
||||||
@@ -284,11 +312,8 @@ export class ChessEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
next.ep = null;
|
next.ep = null;
|
||||||
if (piece.type === "p") {
|
if (piece.type === "p" && Math.abs(parseSquare(move.to).rank - parseSquare(move.from).rank) === 2) {
|
||||||
const a = parseSquare(move.from), b = parseSquare(move.to);
|
next.ep = square(parseSquare(move.from).file, (parseSquare(move.from).rank + parseSquare(move.to).rank) / 2);
|
||||||
if (Math.abs(b.rank - a.rank) === 2) {
|
|
||||||
next.ep = square(a.file, (a.rank + b.rank) / 2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
next.halfmove = piece.type === "p" || move.capture ? 0 : next.halfmove + 1;
|
next.halfmove = piece.type === "p" || move.capture ? 0 : next.halfmove + 1;
|
||||||
@@ -297,51 +322,60 @@ export class ChessEngine {
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
findKing(state, color) {
|
|
||||||
for (const [s, p] of Object.entries(state.board)) {
|
|
||||||
if (p.color === color && p.type === "k") return s;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
inCheck(state, color) {
|
inCheck(state, color) {
|
||||||
const king = this.findKing(state, color);
|
const kingEntry = Object.entries(state.board).find(([, p]) => p.color === color && p.type === "k");
|
||||||
return !king || this.isAttacked(state, king, other(color));
|
return kingEntry ? this.isAttacked(state, kingEntry[0], other(color)) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isAttacked(state, target, byColor) {
|
// ОПТИМИЗАЦИЯ: Reverse Raycasting (Обратная трассировка). Ищем атакующих вокруг клетки,
|
||||||
const t = parseSquare(target);
|
// вместо того чтобы перебирать все фигуры на доске. Работает в разы быстрее.
|
||||||
|
isAttacked(state, targetSq, byColor) {
|
||||||
|
const t = parseSquare(targetSq);
|
||||||
if (!t) return false;
|
if (!t) return false;
|
||||||
|
|
||||||
for (const [from, piece] of Object.entries(state.board)) {
|
// 1. Проверяем коней
|
||||||
if (piece.color !== byColor) continue;
|
const knightJumps = [[1,2],[2,1],[-1,2],[-2,1],[1,-2],[2,-1],[-1,-2],[-2,-1]];
|
||||||
const p = parseSquare(from);
|
for (const [df, dr] of knightJumps) {
|
||||||
const df = t.file - p.file;
|
const p = state.board[squareSafe(t.file + df, t.rank + dr)];
|
||||||
const dr = t.rank - p.rank;
|
if (p && p.color === byColor && p.type === "n") return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (piece.type === "p") {
|
// 2. Проверяем королей
|
||||||
const dir = byColor === "w" ? 1 : -1;
|
for (let df = -1; df <= 1; df++) {
|
||||||
if (dr === dir && Math.abs(df) === 1) return true;
|
for (let dr = -1; dr <= 1; dr++) {
|
||||||
} else if (piece.type === "n") {
|
if (!df && !dr) continue;
|
||||||
if ((Math.abs(df) === 1 && Math.abs(dr) === 2) || (Math.abs(df) === 2 && Math.abs(dr) === 1)) return true;
|
const p = state.board[squareSafe(t.file + df, t.rank + dr)];
|
||||||
} else if (piece.type === "k") {
|
if (p && p.color === byColor && p.type === "k") return true;
|
||||||
if (Math.max(Math.abs(df), Math.abs(dr)) === 1) return true;
|
}
|
||||||
} else {
|
}
|
||||||
const diagonal = Math.abs(df) === Math.abs(dr);
|
|
||||||
const straight = df === 0 || dr === 0;
|
|
||||||
const allowed = piece.type === "b" ? diagonal : piece.type === "r" ? straight : diagonal || straight;
|
|
||||||
if (!allowed) continue;
|
|
||||||
|
|
||||||
const sf = Math.sign(df), sr = Math.sign(dr);
|
// 3. Проверяем пешки (смотрим в сторону откуда могла прийти пешка врага)
|
||||||
let f = p.file + sf, r = p.rank + sr;
|
const pDir = byColor === "w" ? -1 : 1;
|
||||||
let clear = true;
|
for (const df of [-1, 1]) {
|
||||||
while (f !== t.file || r !== t.rank) {
|
const p = state.board[squareSafe(t.file + df, t.rank + pDir)];
|
||||||
if (state.board[square(f,r)]) { clear = false; break; }
|
if (p && p.color === byColor && p.type === "p") return true;
|
||||||
f += sf; r += sr;
|
|
||||||
}
|
}
|
||||||
if (clear) 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,23 +385,28 @@ export class ChessEngine {
|
|||||||
if (move.special === "castle-k") return this.suffixAfter(move, "O-O");
|
if (move.special === "castle-k") return this.suffixAfter(move, "O-O");
|
||||||
if (move.special === "castle-q") return this.suffixAfter(move, "O-O-O");
|
if (move.special === "castle-q") return this.suffixAfter(move, "O-O-O");
|
||||||
|
|
||||||
const legal = legalBefore || this.legalMoves(this.state);
|
const legal = legalBefore || this.legalMoves(this.state.turn);
|
||||||
let san = piece.type === "p" ? "" : piece.type.toUpperCase();
|
let san = piece.type === "p" ? "" : piece.type.toUpperCase();
|
||||||
|
|
||||||
const same = legal.filter(m =>
|
const same = legal.filter(m => m.to === move.to && m.from !== move.from && this.state.board[m.from]?.type === piece.type);
|
||||||
m.to === move.to &&
|
|
||||||
m.from !== move.from &&
|
|
||||||
this.state.board[m.from]?.type === piece.type
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Улучшенная дисамбигуация (правила SAN)
|
||||||
if (same.length) {
|
if (same.length) {
|
||||||
const from = parseSquare(move.from);
|
const fromSq = parseSquare(move.from);
|
||||||
const fileConflict = same.some(m => parseSquare(m.from).file === from.file);
|
const sameFile = same.some(m => parseSquare(m.from).file === fromSq.file);
|
||||||
san += fileConflict ? move.from : move.from[0];
|
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; // Приходится указывать и то, и другое
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (move.capture) {
|
if (move.capture) {
|
||||||
if (piece.type === "p") san += move.from[0];
|
if (piece.type === "p" && same.length === 0) san += move.from[0];
|
||||||
san += "x";
|
san += "x";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,73 +419,53 @@ export class ChessEngine {
|
|||||||
suffixAfter(move, san) {
|
suffixAfter(move, san) {
|
||||||
const next = this.applyToClone(this.state, move);
|
const next = this.applyToClone(this.state, move);
|
||||||
if (this.inCheck(next, next.turn)) {
|
if (this.inCheck(next, next.turn)) {
|
||||||
const replies = this.legalMovesFromState(next, next.turn);
|
const replies = this.legalMoves(next.turn).filter(m => !this.inCheck(this.applyToClone(next, m), next.turn));
|
||||||
return san + (replies.length ? "+" : "#");
|
return san + (replies.length ? "+" : "#");
|
||||||
}
|
}
|
||||||
return san;
|
return san;
|
||||||
}
|
}
|
||||||
|
|
||||||
legalMovesFromState(state, color) {
|
|
||||||
return this.pseudoMoves(state, color).filter(m => !this.inCheck(this.applyToClone(state, m), color));
|
|
||||||
}
|
|
||||||
|
|
||||||
makeMove(input) {
|
makeMove(input) {
|
||||||
const from = String(input?.from || "").toLowerCase();
|
const from = String(input?.from || "").toLowerCase();
|
||||||
const to = String(input?.to || "").toLowerCase();
|
const to = String(input?.to || "").toLowerCase();
|
||||||
const promotion = String(input?.promotion || "q").toLowerCase();
|
const promotion = String(input?.promotion || "q").toLowerCase();
|
||||||
|
|
||||||
const legal = this.legalMoves(this.state);
|
const legal = this.legalMoves(this.state.turn);
|
||||||
const move = legal.find(m =>
|
const move = legal.find(m => m.from === from && m.to === to && (m.promotion ? m.promotion === promotion : true));
|
||||||
m.from === from &&
|
|
||||||
m.to === to &&
|
|
||||||
(m.promotion ? m.promotion === promotion : !m.promotion)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!move) return { ok: false, error: "Недопустимый ход." };
|
if (!move) return { ok: false, error: "Недопустимый ход." };
|
||||||
|
|
||||||
const san = this.sanForMove(move, legal);
|
const san = this.sanForMove(move, legal);
|
||||||
const piece = this.state.board[from];
|
const piece = this.state.board[from];
|
||||||
const capturedPiece =
|
const capturedPiece = move.special === "ep" ? { type: "p", color: other(piece.color) } : this.state.board[to] || null;
|
||||||
move.special === "ep"
|
|
||||||
? { type: "p", color: other(piece.color) }
|
|
||||||
: this.state.board[to] || null;
|
|
||||||
|
|
||||||
this.history.push({
|
this.history.push({
|
||||||
...move,
|
...move, san, piece: { ...piece },
|
||||||
san,
|
|
||||||
piece: { ...piece },
|
|
||||||
captured: capturedPiece ? { ...capturedPiece } : null,
|
captured: capturedPiece ? { ...capturedPiece } : null,
|
||||||
before: cloneState(this.state)
|
before: cloneState(this.state)
|
||||||
});
|
});
|
||||||
|
|
||||||
this.state = this.applyToClone(this.state, move);
|
this.state = this.applyToClone(this.state, move);
|
||||||
this.state.lastMove = {
|
this.state.lastMove = { from, to, san, piece: { ...piece }, captured: capturedPiece ? { ...capturedPiece } : null };
|
||||||
from,
|
|
||||||
to,
|
|
||||||
san,
|
|
||||||
piece: { ...piece },
|
|
||||||
captured: capturedPiece ? { ...capturedPiece } : null
|
|
||||||
};
|
|
||||||
|
|
||||||
const key = this.positionKey(this.state);
|
const key = this.positionKey(this.state);
|
||||||
this.positions.set(key, (this.positions.get(key) || 0) + 1);
|
this.positions.set(key, (this.positions.get(key) || 0) + 1);
|
||||||
|
|
||||||
return {
|
return { ok: true, move: this.state.lastMove, status: this.getStatus() };
|
||||||
ok: true,
|
|
||||||
move: this.state.lastMove,
|
|
||||||
status: this.getStatus()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ОПТИМИЗАЦИЯ: O(1) Undo (Мгновенная отмена хода)
|
||||||
undo() {
|
undo() {
|
||||||
const last = this.history.pop();
|
const last = this.history.pop();
|
||||||
if (!last) return false;
|
if (!last) return false;
|
||||||
|
|
||||||
|
// Декрементируем счетчик текущей позиции
|
||||||
|
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);
|
||||||
|
|
||||||
this.state = last.before;
|
this.state = last.before;
|
||||||
this.positions = new Map([[this.positionKey(this.state), 1]]);
|
|
||||||
for (const h of this.history) {
|
|
||||||
const key = this.positionKey(h.before);
|
|
||||||
this.positions.set(key, (this.positions.get(key) || 0) + 1);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,78 +474,61 @@ export class ChessEngine {
|
|||||||
const moves = this.legalMoves(color);
|
const moves = this.legalMoves(color);
|
||||||
const check = this.inCheck(this.state, color);
|
const check = this.inCheck(this.state, color);
|
||||||
|
|
||||||
if (!moves.length && check) return { phase: "checkmate", turn: color, winner: other(color) };
|
if (!moves.length) return check ? { phase: "checkmate", turn: color, winner: other(color) } : { phase: "draw", reason: "stalemate", turn: color };
|
||||||
if (!moves.length) return { phase: "draw", reason: "stalemate", turn: color };
|
|
||||||
if (this.state.halfmove >= 100) return { phase: "draw", reason: "50-move", turn: color };
|
if (this.state.halfmove >= 100) return { phase: "draw", reason: "50-move", turn: color };
|
||||||
|
|
||||||
const key = this.positionKey(this.state);
|
const key = this.positionKey(this.state);
|
||||||
if ((this.positions.get(key) || 0) >= 3) return { phase: "draw", reason: "threefold", turn: color };
|
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 (this.insufficientMaterial()) return { phase: "draw", reason: "insufficient-material", turn: color };
|
||||||
if (check) return { phase: "check", turn: color };
|
if (check) return { phase: "check", turn: color };
|
||||||
|
|
||||||
return { phase: "playing", turn: color };
|
return { phase: "playing", turn: color };
|
||||||
}
|
}
|
||||||
|
|
||||||
insufficientMaterial() {
|
insufficientMaterial() {
|
||||||
const pieces = Object.values(this.state.board);
|
const pieces = Object.values(this.state.board).filter(p => p.type !== "k");
|
||||||
const nonKings = pieces.filter(p => p.type !== "k");
|
if (pieces.length === 0) return true;
|
||||||
if (!nonKings.length) return true;
|
if (pieces.length === 1 && ["n", "b"].includes(pieces[0].type)) return true;
|
||||||
if (nonKings.length === 1 && (nonKings[0].type === "b" || nonKings[0].type === "n")) return true;
|
|
||||||
if (nonKings.length === 2 && nonKings.every(p => p.type === "b")) {
|
// Два слона одноцветных полей у разных сторон
|
||||||
const bishops = Object.entries(this.state.board)
|
if (pieces.length === 2 && pieces.every(p => p.type === "b")) {
|
||||||
.filter(([, p]) => p.type === "b")
|
const bishops = Object.entries(this.state.board).filter(([, p]) => p.type === "b");
|
||||||
.map(([s, p]) => ({ s, color: p.color }));
|
const c1 = parseSquare(bishops[0][0]);
|
||||||
if (bishops.length === 2) {
|
const c2 = parseSquare(bishops[1][0]);
|
||||||
const colors = bishops.map(({ s }) => {
|
if ((c1.file + c1.rank) % 2 === (c2.file + c2.rank) % 2) return true;
|
||||||
const p = parseSquare(s);
|
|
||||||
return (p.file + p.rank) % 2;
|
|
||||||
});
|
|
||||||
if (colors[0] === colors[1]) return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
material() {
|
material() {
|
||||||
const captured = { w: [], b: [] };
|
const captured = { w: [], b: [] };
|
||||||
for (const h of this.history) {
|
for (const h of this.history) if (h.captured) captured[h.piece.color].push(h.captured.type);
|
||||||
if (h.captured) captured[h.piece.color].push(h.captured.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sort = a => a.sort((x,y) => VALUES[y] - VALUES[x]);
|
const sort = a => a.sort((x, y) => VALUES[y] - VALUES[x]);
|
||||||
sort(captured.w); sort(captured.b);
|
sort(captured.w); sort(captured.b);
|
||||||
|
|
||||||
const score = {
|
return {
|
||||||
w: captured.b.reduce((n,p) => n + VALUES[p], 0),
|
captured,
|
||||||
b: captured.w.reduce((n,p) => n + VALUES[p], 0)
|
score: {
|
||||||
|
w: captured.b.reduce((n, p) => n + VALUES[p], 0),
|
||||||
|
b: captured.w.reduce((n, p) => n + VALUES[p], 0)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { captured, score };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSnapshot() {
|
getSnapshot() {
|
||||||
return {
|
return {
|
||||||
board: cloneBoard(this.state.board),
|
...cloneState(this.state),
|
||||||
turn: this.state.turn,
|
moves: this.history.map((h, i) => ({
|
||||||
castling: {
|
number: i + 1,
|
||||||
w: { ...this.state.castling.w },
|
from: h.from, to: h.to,
|
||||||
b: { ...this.state.castling.b }
|
san: h.san, color: h.piece.color,
|
||||||
},
|
piece: h.piece.type, capture: Boolean(h.captured)
|
||||||
ep: this.state.ep,
|
|
||||||
halfmove: this.state.halfmove,
|
|
||||||
fullmove: this.state.fullmove,
|
|
||||||
lastMove: this.state.lastMove ? { ...this.state.lastMove } : null,
|
|
||||||
moves: this.history.map(h => ({
|
|
||||||
number: this.history.indexOf(h) + 1,
|
|
||||||
from: h.from,
|
|
||||||
to: h.to,
|
|
||||||
san: h.san,
|
|
||||||
color: h.piece.color,
|
|
||||||
piece: h.piece.type,
|
|
||||||
capture: Boolean(h.captured)
|
|
||||||
})),
|
})),
|
||||||
material: this.material(),
|
material: this.material(),
|
||||||
status: this.getStatus()
|
status: this.getStatus(),
|
||||||
|
fen: this.fen,
|
||||||
|
pgn: this.pgn
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user