2026-08-22 17:39:10 +00:00
|
|
|
const FILES = "abcdefgh";
|
2026-08-23 07:17:16 +00:00
|
|
|
|
|
|
|
|
const VALUES = {
|
|
|
|
|
p: 1,
|
|
|
|
|
n: 3,
|
|
|
|
|
b: 3,
|
|
|
|
|
r: 5,
|
|
|
|
|
q: 9,
|
|
|
|
|
k: 0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const PIECES = ["q", "r", "b", "n"];
|
2026-08-22 17:39:10 +00:00
|
|
|
|
2026-08-22 18:57:39 +00:00
|
|
|
function square(file, rank) {
|
2026-08-23 07:17:16 +00:00
|
|
|
return `${FILES[file]}${rank + 1}`;
|
2026-08-22 17:39:10 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
function parseSquare(value) {
|
|
|
|
|
if (!/^[a-h][1-8]$/.test(value || "")) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
file: FILES.indexOf(value[0]),
|
|
|
|
|
rank: Number(value[1]) - 1
|
|
|
|
|
};
|
2026-08-22 17:39:10 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
function safeSquare(file, rank) {
|
|
|
|
|
if (
|
|
|
|
|
file < 0 ||
|
|
|
|
|
file > 7 ||
|
|
|
|
|
rank < 0 ||
|
|
|
|
|
rank > 7
|
|
|
|
|
) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return square(file, rank);
|
2026-08-23 06:50:00 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
function opposite(color) {
|
|
|
|
|
return color === "w" ? "b" : "w";
|
2026-08-22 17:39:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cloneState(state) {
|
2026-08-23 07:17:16 +00:00
|
|
|
const board = {};
|
|
|
|
|
|
|
|
|
|
for (const [sq, piece] of Object.entries(state.board)) {
|
|
|
|
|
board[sq] = {
|
|
|
|
|
color: piece.color,
|
|
|
|
|
type: piece.type
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
board,
|
|
|
|
|
|
|
|
|
|
turn: state.turn,
|
|
|
|
|
|
|
|
|
|
castling: {
|
|
|
|
|
w: { ...state.castling.w },
|
|
|
|
|
b: { ...state.castling.b }
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
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 07:17:16 +00:00
|
|
|
constructor(fen = null) {
|
|
|
|
|
this.reset();
|
2026-08-23 06:50:00 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
if (fen) {
|
|
|
|
|
this.loadFen(fen);
|
2026-08-23 06:50:00 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
reset() {
|
|
|
|
|
this.loadFen(
|
|
|
|
|
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
|
|
|
|
);
|
2026-08-23 06:50:00 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
loadFen(fen) {
|
2026-08-22 17:39:10 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const parts = String(fen).trim().split(/\s+/);
|
2026-08-22 17:39:10 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const placement = parts[0];
|
|
|
|
|
const turn = parts[1] || "w";
|
|
|
|
|
const castling = parts[2] || "-";
|
|
|
|
|
const ep = parts[3] || "-";
|
|
|
|
|
const halfmove = Number(parts[4] || 0);
|
|
|
|
|
const fullmove = Number(parts[5] || 1);
|
2026-08-22 17:39:10 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const board = {};
|
2026-08-22 17:39:10 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
let rank = 7;
|
|
|
|
|
let file = 0;
|
2026-08-22 17:39:10 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
for (const char of placement) {
|
2026-08-22 17:39:10 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
if (char === "/") {
|
|
|
|
|
rank--;
|
|
|
|
|
file = 0;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-08-22 17:39:10 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
if (/^\d$/.test(char)) {
|
|
|
|
|
file += Number(char);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const color =
|
|
|
|
|
char === char.toUpperCase()
|
|
|
|
|
? "w"
|
|
|
|
|
: "b";
|
|
|
|
|
|
|
|
|
|
board[square(file, rank)] = {
|
|
|
|
|
color,
|
|
|
|
|
type: char.toLowerCase()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
file++;
|
2026-08-22 17:39:10 +00:00
|
|
|
}
|
2026-08-23 07:17:16 +00:00
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
|
board,
|
|
|
|
|
|
|
|
|
|
turn:
|
|
|
|
|
turn === "b"
|
|
|
|
|
? "b"
|
|
|
|
|
: "w",
|
|
|
|
|
|
|
|
|
|
castling: {
|
|
|
|
|
w: {
|
|
|
|
|
k: castling.includes("K"),
|
|
|
|
|
q: castling.includes("Q")
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
b: {
|
|
|
|
|
k: castling.includes("k"),
|
|
|
|
|
q: castling.includes("q")
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
ep:
|
|
|
|
|
ep === "-"
|
|
|
|
|
? null
|
|
|
|
|
: ep,
|
|
|
|
|
|
|
|
|
|
halfmove,
|
|
|
|
|
fullmove,
|
|
|
|
|
|
|
|
|
|
lastMove: null
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.history = [];
|
|
|
|
|
|
|
|
|
|
this.positions = new Map();
|
|
|
|
|
|
|
|
|
|
this.positions.set(
|
|
|
|
|
this.positionKey(this.state),
|
|
|
|
|
1
|
|
|
|
|
);
|
2026-08-22 17:39:10 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
get fen() {
|
|
|
|
|
|
|
|
|
|
let result = "";
|
|
|
|
|
|
|
|
|
|
for (let rank = 7; rank >= 0; rank--) {
|
|
|
|
|
|
|
|
|
|
let empty = 0;
|
|
|
|
|
|
|
|
|
|
for (let file = 0; file < 8; file++) {
|
|
|
|
|
|
|
|
|
|
const piece =
|
|
|
|
|
this.state.board[
|
|
|
|
|
square(file, rank)
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if (!piece) {
|
|
|
|
|
empty++;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (empty) {
|
|
|
|
|
result += empty;
|
|
|
|
|
empty = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result +=
|
|
|
|
|
piece.color === "w"
|
|
|
|
|
? piece.type.toUpperCase()
|
|
|
|
|
: piece.type;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (empty) {
|
|
|
|
|
result += empty;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rank > 0) {
|
|
|
|
|
result += "/";
|
|
|
|
|
}
|
2026-08-22 18:57:39 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
let rights = "";
|
|
|
|
|
|
|
|
|
|
if (this.state.castling.w.k) rights += "K";
|
|
|
|
|
if (this.state.castling.w.q) rights += "Q";
|
|
|
|
|
if (this.state.castling.b.k) rights += "k";
|
|
|
|
|
if (this.state.castling.b.q) rights += "q";
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
result,
|
|
|
|
|
this.state.turn,
|
|
|
|
|
rights || "-",
|
|
|
|
|
this.state.ep || "-",
|
|
|
|
|
this.state.halfmove,
|
|
|
|
|
this.state.fullmove
|
|
|
|
|
].join(" ");
|
2026-08-22 18:57:39 +00:00
|
|
|
}
|
2026-08-23 07:17:16 +00:00
|
|
|
|
|
|
|
|
positionKey(state) {
|
|
|
|
|
|
|
|
|
|
const pieces = Object
|
|
|
|
|
.keys(state.board)
|
|
|
|
|
.sort()
|
|
|
|
|
.map(sq => {
|
|
|
|
|
const p = state.board[sq];
|
|
|
|
|
|
|
|
|
|
return `${sq}${p.color}${p.type}`;
|
|
|
|
|
})
|
|
|
|
|
.join(",");
|
|
|
|
|
|
|
|
|
|
let rights = "";
|
|
|
|
|
|
|
|
|
|
if (state.castling.w.k) rights += "K";
|
|
|
|
|
if (state.castling.w.q) rights += "Q";
|
|
|
|
|
if (state.castling.b.k) rights += "k";
|
|
|
|
|
if (state.castling.b.q) rights += "q";
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
pieces,
|
|
|
|
|
state.turn,
|
|
|
|
|
rights || "-",
|
|
|
|
|
state.ep || "-"
|
|
|
|
|
].join("|");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getPiece(squareName) {
|
|
|
|
|
return this.state.board[squareName] || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
movesFrom(from) {
|
|
|
|
|
|
|
|
|
|
const piece = this.state.board[from];
|
|
|
|
|
|
|
|
|
|
if (!piece) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (piece.color !== this.state.turn) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.legalMoves(
|
|
|
|
|
this.state.turn
|
|
|
|
|
).filter(
|
|
|
|
|
move => move.from === from
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
legalMoves(color = this.state.turn) {
|
|
|
|
|
|
|
|
|
|
const pseudo =
|
|
|
|
|
this.pseudoMoves(
|
|
|
|
|
this.state,
|
|
|
|
|
color
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const result = [];
|
|
|
|
|
|
|
|
|
|
for (const move of pseudo) {
|
|
|
|
|
|
|
|
|
|
const next =
|
|
|
|
|
this.applyMoveToState(
|
|
|
|
|
this.state,
|
|
|
|
|
move
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!this.inCheck(next, color)) {
|
|
|
|
|
result.push(move);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pseudoMoves(state, color) {
|
|
|
|
|
|
|
|
|
|
const result = [];
|
|
|
|
|
|
|
|
|
|
for (const [from, piece] of Object.entries(state.board)) {
|
|
|
|
|
|
|
|
|
|
if (piece.color !== color) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pos = parseSquare(from);
|
|
|
|
|
|
|
|
|
|
if (!pos) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (piece.type) {
|
|
|
|
|
|
|
|
|
|
case "p":
|
|
|
|
|
this.pawnMoves(
|
|
|
|
|
state,
|
|
|
|
|
from,
|
|
|
|
|
piece,
|
|
|
|
|
pos,
|
|
|
|
|
result
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "n":
|
|
|
|
|
this.knightMoves(
|
|
|
|
|
state,
|
|
|
|
|
from,
|
|
|
|
|
piece,
|
|
|
|
|
pos,
|
|
|
|
|
result
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "b":
|
|
|
|
|
this.slideMoves(
|
|
|
|
|
state,
|
|
|
|
|
from,
|
|
|
|
|
piece,
|
|
|
|
|
pos,
|
|
|
|
|
result,
|
|
|
|
|
[
|
|
|
|
|
[1, 1],
|
|
|
|
|
[1, -1],
|
|
|
|
|
[-1, 1],
|
|
|
|
|
[-1, -1]
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "r":
|
|
|
|
|
this.slideMoves(
|
|
|
|
|
state,
|
|
|
|
|
from,
|
|
|
|
|
piece,
|
|
|
|
|
pos,
|
|
|
|
|
result,
|
|
|
|
|
[
|
|
|
|
|
[1, 0],
|
|
|
|
|
[-1, 0],
|
|
|
|
|
[0, 1],
|
|
|
|
|
[0, -1]
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "q":
|
|
|
|
|
this.slideMoves(
|
|
|
|
|
state,
|
|
|
|
|
from,
|
|
|
|
|
piece,
|
|
|
|
|
pos,
|
|
|
|
|
result,
|
|
|
|
|
[
|
|
|
|
|
[1, 1],
|
|
|
|
|
[1, -1],
|
|
|
|
|
[-1, 1],
|
|
|
|
|
[-1, -1],
|
|
|
|
|
[1, 0],
|
|
|
|
|
[-1, 0],
|
|
|
|
|
[0, 1],
|
|
|
|
|
[0, -1]
|
|
|
|
|
]
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case "k":
|
|
|
|
|
this.kingMoves(
|
|
|
|
|
state,
|
|
|
|
|
from,
|
|
|
|
|
piece,
|
|
|
|
|
pos,
|
|
|
|
|
result
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
to,
|
|
|
|
|
promotion = null,
|
|
|
|
|
special = null
|
|
|
|
|
) {
|
2026-08-22 18:57:39 +00:00
|
|
|
|
|
|
|
|
const target = state.board[to];
|
2026-08-23 07:17:16 +00:00
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
target &&
|
|
|
|
|
target.color === state.board[from].color
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
target &&
|
|
|
|
|
target.type === "k"
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result.push({
|
|
|
|
|
from,
|
|
|
|
|
to,
|
|
|
|
|
promotion,
|
|
|
|
|
special,
|
|
|
|
|
capture:
|
|
|
|
|
Boolean(target) ||
|
|
|
|
|
special === "ep"
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pawnMoves(
|
|
|
|
|
state,
|
|
|
|
|
from,
|
|
|
|
|
piece,
|
|
|
|
|
pos,
|
|
|
|
|
result
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const direction =
|
|
|
|
|
piece.color === "w"
|
|
|
|
|
? 1
|
|
|
|
|
: -1;
|
|
|
|
|
|
|
|
|
|
const startRank =
|
|
|
|
|
piece.color === "w"
|
|
|
|
|
? 1
|
|
|
|
|
: 6;
|
|
|
|
|
|
|
|
|
|
const promotionRank =
|
|
|
|
|
piece.color === "w"
|
|
|
|
|
? 7
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
const one =
|
|
|
|
|
safeSquare(
|
|
|
|
|
pos.file,
|
|
|
|
|
pos.rank + direction
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
one &&
|
|
|
|
|
!state.board[one]
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
pos.rank + direction ===
|
|
|
|
|
promotionRank
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
for (const type of PIECES) {
|
|
|
|
|
this.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
one,
|
|
|
|
|
type
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
this.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
one
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
pos.rank === startRank
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const two =
|
|
|
|
|
safeSquare(
|
|
|
|
|
pos.file,
|
|
|
|
|
pos.rank +
|
|
|
|
|
direction * 2
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
two &&
|
|
|
|
|
!state.board[two]
|
|
|
|
|
) {
|
|
|
|
|
this.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
two,
|
|
|
|
|
null,
|
|
|
|
|
"double"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const df of [-1, 1]) {
|
|
|
|
|
|
|
|
|
|
const to =
|
|
|
|
|
safeSquare(
|
|
|
|
|
pos.file + df,
|
|
|
|
|
pos.rank + direction
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!to) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const target =
|
|
|
|
|
state.board[to];
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
target &&
|
|
|
|
|
target.color !== piece.color &&
|
|
|
|
|
target.type !== "k"
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
pos.rank + direction ===
|
|
|
|
|
promotionRank
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
for (const type of PIECES) {
|
|
|
|
|
this.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
to,
|
|
|
|
|
type
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
this.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
to
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if (
|
|
|
|
|
state.ep === to
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
this.addMove(
|
|
|
|
|
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) {
|
|
|
|
|
|
|
|
|
|
const to =
|
|
|
|
|
safeSquare(
|
|
|
|
|
pos.file + df,
|
|
|
|
|
pos.rank + dr
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (to) {
|
|
|
|
|
this.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
to
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slideMoves(
|
|
|
|
|
state,
|
|
|
|
|
from,
|
|
|
|
|
piece,
|
|
|
|
|
pos,
|
|
|
|
|
result,
|
|
|
|
|
directions
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
for (const [df, dr] of directions) {
|
|
|
|
|
|
|
|
|
|
let file =
|
|
|
|
|
pos.file + df;
|
|
|
|
|
|
|
|
|
|
let rank =
|
|
|
|
|
pos.rank + dr;
|
|
|
|
|
|
|
|
|
|
while (
|
|
|
|
|
file >= 0 &&
|
|
|
|
|
file < 8 &&
|
|
|
|
|
rank >= 0 &&
|
|
|
|
|
rank < 8
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const to =
|
|
|
|
|
square(file, rank);
|
|
|
|
|
|
|
|
|
|
const target =
|
|
|
|
|
state.board[to];
|
|
|
|
|
|
|
|
|
|
if (!target) {
|
|
|
|
|
|
|
|
|
|
this.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
to
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
target.color !== piece.color &&
|
|
|
|
|
target.type !== "k"
|
|
|
|
|
) {
|
|
|
|
|
this.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
to
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file += df;
|
|
|
|
|
rank += 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const to =
|
|
|
|
|
safeSquare(
|
|
|
|
|
pos.file + df,
|
|
|
|
|
pos.rank + dr
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (to) {
|
|
|
|
|
this.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
to
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rank =
|
|
|
|
|
piece.color === "w"
|
|
|
|
|
? 0
|
|
|
|
|
: 7;
|
|
|
|
|
|
|
|
|
|
const enemy =
|
|
|
|
|
opposite(piece.color);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
pos.file !== 4 ||
|
|
|
|
|
pos.rank !== rank
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
this.inCheck(
|
|
|
|
|
state,
|
|
|
|
|
piece.color
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
square(6, rank),
|
|
|
|
|
null,
|
|
|
|
|
"castle-k"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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.addMove(
|
|
|
|
|
state,
|
|
|
|
|
result,
|
|
|
|
|
from,
|
|
|
|
|
square(2, rank),
|
|
|
|
|
null,
|
|
|
|
|
"castle-q"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applyMoveToState(
|
|
|
|
|
state,
|
|
|
|
|
move
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const next =
|
|
|
|
|
cloneState(state);
|
|
|
|
|
|
|
|
|
|
const piece =
|
|
|
|
|
next.board[move.from];
|
|
|
|
|
|
|
|
|
|
if (!piece) {
|
|
|
|
|
return next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delete next.board[move.from];
|
|
|
|
|
|
|
|
|
|
if (move.special === "ep") {
|
|
|
|
|
|
|
|
|
|
const target =
|
|
|
|
|
parseSquare(move.to);
|
|
|
|
|
|
|
|
|
|
const capturedSquare =
|
|
|
|
|
square(
|
|
|
|
|
target.file,
|
|
|
|
|
target.rank +
|
|
|
|
|
(piece.color === "w" ? -1 : 1)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
delete next.board[capturedSquare];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const captured =
|
|
|
|
|
next.board[move.to];
|
|
|
|
|
|
|
|
|
|
next.board[move.to] = {
|
|
|
|
|
color: piece.color,
|
|
|
|
|
type:
|
|
|
|
|
move.promotion ||
|
|
|
|
|
piece.type
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (piece.type === "k") {
|
|
|
|
|
|
|
|
|
|
next.castling[piece.color].k = false;
|
|
|
|
|
next.castling[piece.color].q = false;
|
|
|
|
|
|
|
|
|
|
const rank =
|
|
|
|
|
piece.color === "w"
|
|
|
|
|
? 0
|
|
|
|
|
: 7;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
move.special === "castle-k"
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
delete next.board[
|
|
|
|
|
square(7, rank)
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
next.board[
|
|
|
|
|
square(5, rank)
|
|
|
|
|
] = {
|
|
|
|
|
color: piece.color,
|
|
|
|
|
type: "r"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
move.special === "castle-q"
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
piece.type === "p"
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const from =
|
|
|
|
|
parseSquare(move.from);
|
|
|
|
|
|
|
|
|
|
const to =
|
|
|
|
|
parseSquare(move.to);
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
Math.abs(
|
|
|
|
|
to.rank -
|
|
|
|
|
from.rank
|
|
|
|
|
) === 2
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
next.ep =
|
|
|
|
|
square(
|
|
|
|
|
from.file,
|
|
|
|
|
(from.rank + to.rank) / 2
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next.halfmove =
|
|
|
|
|
piece.type === "p" ||
|
|
|
|
|
move.capture
|
|
|
|
|
? 0
|
|
|
|
|
: next.halfmove + 1;
|
|
|
|
|
|
|
|
|
|
if (piece.color === "b") {
|
|
|
|
|
next.fullmove++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next.turn =
|
|
|
|
|
opposite(piece.color);
|
|
|
|
|
|
|
|
|
|
return next;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inCheck(
|
|
|
|
|
state,
|
|
|
|
|
color
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const king =
|
|
|
|
|
Object.entries(
|
|
|
|
|
state.board
|
|
|
|
|
).find(
|
|
|
|
|
([, piece]) =>
|
|
|
|
|
piece.color === color &&
|
|
|
|
|
piece.type === "k"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!king) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.isAttacked(
|
|
|
|
|
state,
|
|
|
|
|
king[0],
|
|
|
|
|
opposite(color)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isAttacked(
|
|
|
|
|
state,
|
|
|
|
|
target,
|
|
|
|
|
attacker
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const pos =
|
|
|
|
|
parseSquare(target);
|
|
|
|
|
|
|
|
|
|
if (!pos) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const knightMoves = [
|
|
|
|
|
[1, 2],
|
|
|
|
|
[2, 1],
|
|
|
|
|
[-1, 2],
|
|
|
|
|
[-2, 1],
|
|
|
|
|
[1, -2],
|
|
|
|
|
[2, -1],
|
|
|
|
|
[-1, -2],
|
|
|
|
|
[-2, -1]
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const [df, dr] of knightMoves) {
|
|
|
|
|
|
|
|
|
|
const sq =
|
|
|
|
|
safeSquare(
|
|
|
|
|
pos.file + df,
|
|
|
|
|
pos.rank + dr
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const piece =
|
|
|
|
|
sq
|
|
|
|
|
? state.board[sq]
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
piece &&
|
|
|
|
|
piece.color === attacker &&
|
|
|
|
|
piece.type === "n"
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let df = -1; df <= 1; df++) {
|
|
|
|
|
for (let dr = -1; dr <= 1; dr++) {
|
|
|
|
|
|
|
|
|
|
if (!df && !dr) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sq =
|
|
|
|
|
safeSquare(
|
|
|
|
|
pos.file + df,
|
|
|
|
|
pos.rank + dr
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const piece =
|
|
|
|
|
sq
|
|
|
|
|
? state.board[sq]
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
piece &&
|
|
|
|
|
piece.color === attacker &&
|
|
|
|
|
piece.type === "k"
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pawnDirection =
|
|
|
|
|
attacker === "w"
|
|
|
|
|
? -1
|
|
|
|
|
: 1;
|
|
|
|
|
|
|
|
|
|
for (const df of [-1, 1]) {
|
|
|
|
|
|
|
|
|
|
const sq =
|
|
|
|
|
safeSquare(
|
|
|
|
|
pos.file + df,
|
|
|
|
|
pos.rank + pawnDirection
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const piece =
|
|
|
|
|
sq
|
|
|
|
|
? state.board[sq]
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
piece &&
|
|
|
|
|
piece.color === attacker &&
|
|
|
|
|
piece.type === "p"
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rays = [
|
|
|
|
|
{
|
|
|
|
|
df: 1,
|
|
|
|
|
dr: 1,
|
|
|
|
|
pieces: ["b", "q"]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
df: -1,
|
|
|
|
|
dr: -1,
|
|
|
|
|
pieces: ["b", "q"]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
df: 1,
|
|
|
|
|
dr: -1,
|
|
|
|
|
pieces: ["b", "q"]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
df: -1,
|
|
|
|
|
dr: 1,
|
|
|
|
|
pieces: ["b", "q"]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
df: 1,
|
|
|
|
|
dr: 0,
|
|
|
|
|
pieces: ["r", "q"]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
df: -1,
|
|
|
|
|
dr: 0,
|
|
|
|
|
pieces: ["r", "q"]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
df: 0,
|
|
|
|
|
dr: 1,
|
|
|
|
|
pieces: ["r", "q"]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
df: 0,
|
|
|
|
|
dr: -1,
|
|
|
|
|
pieces: ["r", "q"]
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const ray of rays) {
|
|
|
|
|
|
|
|
|
|
let file =
|
|
|
|
|
pos.file + ray.df;
|
|
|
|
|
|
|
|
|
|
let rank =
|
|
|
|
|
pos.rank + ray.dr;
|
|
|
|
|
|
|
|
|
|
while (
|
|
|
|
|
file >= 0 &&
|
|
|
|
|
file < 8 &&
|
|
|
|
|
rank >= 0 &&
|
|
|
|
|
rank < 8
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const sq =
|
|
|
|
|
square(file, rank);
|
|
|
|
|
|
|
|
|
|
const piece =
|
|
|
|
|
state.board[sq];
|
|
|
|
|
|
|
|
|
|
if (piece) {
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
piece.color === attacker &&
|
|
|
|
|
ray.pieces.includes(piece.type)
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file += ray.df;
|
|
|
|
|
rank += ray.dr;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
makeMove(input) {
|
|
|
|
|
|
|
|
|
|
const from =
|
|
|
|
|
String(input?.from || "")
|
|
|
|
|
.toLowerCase();
|
|
|
|
|
|
|
|
|
|
const to =
|
|
|
|
|
String(input?.to || "")
|
|
|
|
|
.toLowerCase();
|
|
|
|
|
|
|
|
|
|
let promotion =
|
|
|
|
|
String(
|
|
|
|
|
input?.promotion || "q"
|
|
|
|
|
).toLowerCase();
|
|
|
|
|
|
|
|
|
|
if (!["q", "r", "b", "n"].includes(promotion)) {
|
|
|
|
|
promotion = "q";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const legal =
|
|
|
|
|
this.legalMoves(
|
|
|
|
|
this.state.turn
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const candidates =
|
|
|
|
|
legal.filter(
|
|
|
|
|
move =>
|
|
|
|
|
move.from === from &&
|
|
|
|
|
move.to === to
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let move = null;
|
|
|
|
|
|
|
|
|
|
if (candidates.length === 1) {
|
|
|
|
|
|
|
|
|
|
move = candidates[0];
|
|
|
|
|
|
2026-08-23 06:50:00 +00:00
|
|
|
} else {
|
2026-08-23 07:17:16 +00:00
|
|
|
|
|
|
|
|
move =
|
|
|
|
|
candidates.find(
|
|
|
|
|
candidate =>
|
|
|
|
|
candidate.promotion === promotion
|
|
|
|
|
);
|
2026-08-22 18:57:39 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
if (!move) {
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: "Недопустимый ход."
|
|
|
|
|
};
|
2026-08-22 18:57:39 +00:00
|
|
|
}
|
2026-08-23 07:17:16 +00:00
|
|
|
|
|
|
|
|
const piece =
|
|
|
|
|
this.state.board[from];
|
|
|
|
|
|
|
|
|
|
const captured =
|
|
|
|
|
move.special === "ep"
|
|
|
|
|
? {
|
|
|
|
|
color: opposite(piece.color),
|
|
|
|
|
type: "p"
|
|
|
|
|
}
|
|
|
|
|
: this.state.board[to] || null;
|
|
|
|
|
|
|
|
|
|
const before =
|
|
|
|
|
cloneState(
|
|
|
|
|
this.state
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.state =
|
|
|
|
|
this.applyMoveToState(
|
|
|
|
|
this.state,
|
|
|
|
|
move
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const san =
|
|
|
|
|
this.createSan(
|
|
|
|
|
before,
|
|
|
|
|
move,
|
|
|
|
|
legal
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.state.lastMove = {
|
|
|
|
|
from,
|
|
|
|
|
to,
|
|
|
|
|
san,
|
|
|
|
|
piece: {
|
|
|
|
|
...piece
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
captured:
|
|
|
|
|
captured
|
|
|
|
|
? { ...captured }
|
|
|
|
|
: null
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.history.push({
|
|
|
|
|
...move,
|
|
|
|
|
|
|
|
|
|
san,
|
|
|
|
|
|
|
|
|
|
piece: {
|
|
|
|
|
...piece
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
captured:
|
|
|
|
|
captured
|
|
|
|
|
? { ...captured }
|
|
|
|
|
: null,
|
|
|
|
|
|
|
|
|
|
before
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const key =
|
|
|
|
|
this.positionKey(
|
|
|
|
|
this.state
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
this.positions.set(
|
|
|
|
|
key,
|
|
|
|
|
(this.positions.get(key) || 0) + 1
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
move: this.state.lastMove,
|
|
|
|
|
status: this.getStatus()
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
createSan(
|
|
|
|
|
before,
|
|
|
|
|
move,
|
|
|
|
|
legalBefore
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const piece =
|
|
|
|
|
before.board[move.from];
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
move.special === "castle-k"
|
|
|
|
|
) {
|
|
|
|
|
return this.sanSuffix(
|
|
|
|
|
before,
|
|
|
|
|
move,
|
|
|
|
|
"O-O"
|
|
|
|
|
);
|
2026-08-22 18:57:39 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
if (
|
|
|
|
|
move.special === "castle-q"
|
|
|
|
|
) {
|
|
|
|
|
return this.sanSuffix(
|
|
|
|
|
before,
|
|
|
|
|
move,
|
|
|
|
|
"O-O-O"
|
|
|
|
|
);
|
2026-08-23 06:50:00 +00:00
|
|
|
}
|
2026-08-23 07:17:16 +00:00
|
|
|
|
|
|
|
|
let san =
|
|
|
|
|
piece.type === "p"
|
|
|
|
|
? ""
|
|
|
|
|
: piece.type.toUpperCase();
|
|
|
|
|
|
|
|
|
|
const conflicts =
|
|
|
|
|
legalBefore.filter(
|
|
|
|
|
other =>
|
|
|
|
|
other.from !== move.from &&
|
|
|
|
|
other.to === move.to &&
|
|
|
|
|
before.board[other.from]?.type === piece.type
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (conflicts.length) {
|
|
|
|
|
|
|
|
|
|
const from =
|
|
|
|
|
parseSquare(move.from);
|
|
|
|
|
|
|
|
|
|
const sameFile =
|
|
|
|
|
conflicts.some(
|
|
|
|
|
item =>
|
|
|
|
|
parseSquare(item.from).file === from.file
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const sameRank =
|
|
|
|
|
conflicts.some(
|
|
|
|
|
item =>
|
|
|
|
|
parseSquare(item.from).rank === from.rank
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!sameFile) {
|
|
|
|
|
san += move.from[0];
|
|
|
|
|
} else if (!sameRank) {
|
|
|
|
|
san += move.from[1];
|
|
|
|
|
} else {
|
|
|
|
|
san += move.from;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (move.capture) {
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
piece.type === "p"
|
|
|
|
|
) {
|
|
|
|
|
san += move.from[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
san += "x";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
san += move.to;
|
|
|
|
|
|
|
|
|
|
if (move.promotion) {
|
|
|
|
|
san +=
|
|
|
|
|
"=" +
|
|
|
|
|
move.promotion.toUpperCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.sanSuffix(
|
|
|
|
|
before,
|
|
|
|
|
move,
|
|
|
|
|
san
|
|
|
|
|
);
|
2026-08-23 06:50:00 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
sanSuffix(
|
|
|
|
|
before,
|
|
|
|
|
move,
|
|
|
|
|
san
|
|
|
|
|
) {
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const next =
|
|
|
|
|
this.applyMoveToState(
|
|
|
|
|
before,
|
|
|
|
|
move
|
|
|
|
|
);
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
if (
|
|
|
|
|
this.inCheck(
|
|
|
|
|
next,
|
|
|
|
|
next.turn
|
|
|
|
|
)
|
|
|
|
|
) {
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const replies =
|
|
|
|
|
this.legalMovesForState(
|
|
|
|
|
next,
|
|
|
|
|
next.turn
|
|
|
|
|
);
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
return (
|
|
|
|
|
san +
|
|
|
|
|
(
|
|
|
|
|
replies.length
|
|
|
|
|
? "+"
|
|
|
|
|
: "#"
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-08-23 06:50:00 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
return san;
|
2026-08-22 18:57:39 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
legalMovesForState(
|
|
|
|
|
state,
|
|
|
|
|
color
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const pseudo =
|
|
|
|
|
this.pseudoMoves(
|
|
|
|
|
state,
|
|
|
|
|
color
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return pseudo.filter(
|
|
|
|
|
move => {
|
|
|
|
|
|
|
|
|
|
const next =
|
|
|
|
|
this.applyMoveToState(
|
|
|
|
|
state,
|
|
|
|
|
move
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return !this.inCheck(
|
|
|
|
|
next,
|
|
|
|
|
color
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
);
|
2026-08-22 18:57:39 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
getStatus() {
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const color =
|
|
|
|
|
this.state.turn;
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const moves =
|
|
|
|
|
this.legalMoves(color);
|
|
|
|
|
|
|
|
|
|
const check =
|
|
|
|
|
this.inCheck(
|
|
|
|
|
this.state,
|
|
|
|
|
color
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!moves.length) {
|
|
|
|
|
|
|
|
|
|
if (check) {
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
phase: "checkmate",
|
|
|
|
|
turn: color,
|
|
|
|
|
winner: opposite(color)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
phase: "draw",
|
|
|
|
|
reason: "stalemate",
|
|
|
|
|
turn: color
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
this.state.halfmove >= 100
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
phase: "draw",
|
|
|
|
|
reason: "50-move",
|
|
|
|
|
turn: color
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
phase: "playing",
|
|
|
|
|
turn: color
|
|
|
|
|
};
|
2026-08-22 18:57:39 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
insufficientMaterial() {
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const pieces =
|
|
|
|
|
Object.values(
|
|
|
|
|
this.state.board
|
|
|
|
|
).filter(
|
|
|
|
|
piece =>
|
|
|
|
|
piece.type !== "k"
|
|
|
|
|
);
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
if (!pieces.length) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
if (
|
|
|
|
|
pieces.length === 1 &&
|
|
|
|
|
["b", "n"].includes(
|
|
|
|
|
pieces[0].type
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
if (
|
|
|
|
|
pieces.length === 2 &&
|
|
|
|
|
pieces.every(
|
|
|
|
|
piece =>
|
|
|
|
|
piece.type === "b"
|
|
|
|
|
)
|
|
|
|
|
) {
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const bishops =
|
|
|
|
|
Object.entries(
|
|
|
|
|
this.state.board
|
|
|
|
|
).filter(
|
|
|
|
|
([, piece]) =>
|
|
|
|
|
piece.type === "b"
|
|
|
|
|
);
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const a =
|
|
|
|
|
parseSquare(
|
|
|
|
|
bishops[0][0]
|
|
|
|
|
);
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const b =
|
|
|
|
|
parseSquare(
|
|
|
|
|
bishops[1][0]
|
|
|
|
|
);
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
if (
|
|
|
|
|
(a.file + a.rank) % 2 ===
|
|
|
|
|
(b.file + b.rank) % 2
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-23 06:50:00 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
return false;
|
2026-08-22 18:57:39 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
material() {
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
const captured = {
|
|
|
|
|
w: [],
|
|
|
|
|
b: []
|
|
|
|
|
};
|
2026-08-22 18:57:39 +00:00
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
for (const move of this.history) {
|
|
|
|
|
|
|
|
|
|
if (!move.captured) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
captured[
|
|
|
|
|
move.piece.color
|
|
|
|
|
].push(
|
|
|
|
|
move.captured.type
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sort =
|
|
|
|
|
list =>
|
|
|
|
|
list.sort(
|
|
|
|
|
(a, b) =>
|
|
|
|
|
VALUES[b] -
|
|
|
|
|
VALUES[a]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
sort(captured.w);
|
|
|
|
|
sort(captured.b);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
captured,
|
|
|
|
|
|
|
|
|
|
score: {
|
|
|
|
|
w: captured.b.reduce(
|
|
|
|
|
(sum, piece) =>
|
|
|
|
|
sum + VALUES[piece],
|
|
|
|
|
0
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
b: captured.w.reduce(
|
|
|
|
|
(sum, piece) =>
|
|
|
|
|
sum + VALUES[piece],
|
|
|
|
|
0
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getSnapshot() {
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...cloneState(
|
|
|
|
|
this.state
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
moves:
|
|
|
|
|
this.history.map(
|
|
|
|
|
(move, index) => ({
|
|
|
|
|
number: index + 1,
|
|
|
|
|
from: move.from,
|
|
|
|
|
to: move.to,
|
|
|
|
|
san: move.san,
|
|
|
|
|
color: move.piece.color,
|
|
|
|
|
piece: move.piece.type,
|
|
|
|
|
capture:
|
|
|
|
|
Boolean(
|
|
|
|
|
move.captured
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
material:
|
|
|
|
|
this.material(),
|
|
|
|
|
|
|
|
|
|
status:
|
|
|
|
|
this.getStatus(),
|
|
|
|
|
|
|
|
|
|
fen:
|
|
|
|
|
this.fen,
|
|
|
|
|
|
|
|
|
|
pgn:
|
|
|
|
|
this.pgn
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get pgn() {
|
|
|
|
|
|
|
|
|
|
let result = "";
|
|
|
|
|
|
|
|
|
|
for (
|
|
|
|
|
let i = 0;
|
|
|
|
|
i < this.history.length;
|
|
|
|
|
i++
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
const move =
|
|
|
|
|
this.history[i];
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
move.piece.color === "w"
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
result +=
|
|
|
|
|
`${Math.floor(i / 2) + 1}. ${move.san} `;
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
result +=
|
|
|
|
|
`${move.san} `;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const status =
|
|
|
|
|
this.getStatus();
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
status.phase === "checkmate"
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
result +=
|
|
|
|
|
status.winner === "w"
|
|
|
|
|
? "1-0"
|
|
|
|
|
: "0-1";
|
|
|
|
|
|
|
|
|
|
} else if (
|
|
|
|
|
status.phase === "draw"
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
result += "1/2-1/2";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result.trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
undo() {
|
|
|
|
|
|
|
|
|
|
const last =
|
|
|
|
|
this.history.pop();
|
|
|
|
|
|
|
|
|
|
if (!last) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const current =
|
|
|
|
|
this.positionKey(
|
|
|
|
|
this.state
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const count =
|
|
|
|
|
this.positions.get(
|
|
|
|
|
current
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (count <= 1) {
|
|
|
|
|
this.positions.delete(
|
|
|
|
|
current
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
this.positions.set(
|
|
|
|
|
current,
|
|
|
|
|
count - 1
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.state =
|
|
|
|
|
last.before;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2026-08-22 17:39:10 +00:00
|
|
|
}
|
|
|
|
|
|
2026-08-23 07:17:16 +00:00
|
|
|
export {
|
|
|
|
|
FILES,
|
|
|
|
|
VALUES
|
|
|
|
|
};
|