Add chess/shared/chess-engine.js
This commit is contained in:
@@ -0,0 +1,824 @@
|
|||||||
|
//chess-engine.js
|
||||||
|
|
||||||
|
const FILES = "abcdefgh";
|
||||||
|
const STARTING_BACK_RANK = ["R", "N", "B", "Q", "K", "B", "N", "R"];
|
||||||
|
|
||||||
|
function createPiece(color, type) {
|
||||||
|
return { color, type };
|
||||||
|
}
|
||||||
|
|
||||||
|
function clonePiece(piece) {
|
||||||
|
return piece ? { color: piece.color, type: piece.type } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneBoard(board) {
|
||||||
|
return board.map((row) => row.map((piece) => clonePiece(piece)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorLabel(color) {
|
||||||
|
return color === "w" ? "White" : "Black";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function opponentColor(color) {
|
||||||
|
return color === "w" ? "b" : "w";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toSquare(x, y) {
|
||||||
|
return `${FILES[x]}${8 - y}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fromSquare(square) {
|
||||||
|
if (!square || square.length !== 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = square[0].toLowerCase();
|
||||||
|
const rank = Number(square[1]);
|
||||||
|
const x = FILES.indexOf(file);
|
||||||
|
const y = 8 - rank;
|
||||||
|
|
||||||
|
if (x < 0 || Number.isNaN(y) || y < 0 || y > 7) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
function inBounds(x, y) {
|
||||||
|
return x >= 0 && x < 8 && y >= 0 && y < 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyBoard() {
|
||||||
|
return Array.from({ length: 8 }, () => Array.from({ length: 8 }, () => null));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStartingBoard() {
|
||||||
|
const board = createEmptyBoard();
|
||||||
|
|
||||||
|
for (let index = 0; index < 8; index += 1) {
|
||||||
|
board[0][index] = createPiece("b", STARTING_BACK_RANK[index]);
|
||||||
|
board[1][index] = createPiece("b", "P");
|
||||||
|
board[6][index] = createPiece("w", "P");
|
||||||
|
board[7][index] = createPiece("w", STARTING_BACK_RANK[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return board;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCastlingRights() {
|
||||||
|
return {
|
||||||
|
w: { k: true, q: true },
|
||||||
|
b: { k: true, q: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createInitialState() {
|
||||||
|
return {
|
||||||
|
board: createStartingBoard(),
|
||||||
|
turn: "w",
|
||||||
|
castling: createCastlingRights(),
|
||||||
|
enPassant: null,
|
||||||
|
halfmoveClock: 0,
|
||||||
|
fullmoveNumber: 1,
|
||||||
|
history: [],
|
||||||
|
positionCounts: {},
|
||||||
|
status: null,
|
||||||
|
lastMove: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneState(state) {
|
||||||
|
return {
|
||||||
|
board: cloneBoard(state.board),
|
||||||
|
turn: state.turn,
|
||||||
|
castling: {
|
||||||
|
w: { ...state.castling.w },
|
||||||
|
b: { ...state.castling.b },
|
||||||
|
},
|
||||||
|
enPassant: state.enPassant,
|
||||||
|
halfmoveClock: state.halfmoveClock,
|
||||||
|
fullmoveNumber: state.fullmoveNumber,
|
||||||
|
history: state.history.map((entry) => ({ ...entry })),
|
||||||
|
positionCounts: { ...state.positionCounts },
|
||||||
|
status: state.status ? { ...state.status } : null,
|
||||||
|
lastMove: state.lastMove ? { ...state.lastMove } : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function boardToHash(board) {
|
||||||
|
return board
|
||||||
|
.map((row) =>
|
||||||
|
row
|
||||||
|
.map((piece) => {
|
||||||
|
if (!piece) {
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
return piece.color === "w" ? piece.type : piece.type.toLowerCase();
|
||||||
|
})
|
||||||
|
.join(""),
|
||||||
|
)
|
||||||
|
.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionHash(state) {
|
||||||
|
const castlingText = ["w", "b"]
|
||||||
|
.map((color) => {
|
||||||
|
const rights = [];
|
||||||
|
if (state.castling[color].k) {
|
||||||
|
rights.push("k");
|
||||||
|
}
|
||||||
|
if (state.castling[color].q) {
|
||||||
|
rights.push("q");
|
||||||
|
}
|
||||||
|
return rights.length ? rights.join("") : "-";
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return [
|
||||||
|
boardToHash(state.board),
|
||||||
|
state.turn,
|
||||||
|
castlingText || "-",
|
||||||
|
state.enPassant || "-",
|
||||||
|
].join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPositionCount(state) {
|
||||||
|
const hash = positionHash(state);
|
||||||
|
state.positionCounts[hash] = (state.positionCounts[hash] || 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pieceAt(board, x, y) {
|
||||||
|
if (!inBounds(x, y)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return board[y][x];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPiece(board, x, y, piece) {
|
||||||
|
board[y][x] = piece;
|
||||||
|
}
|
||||||
|
|
||||||
|
function squareColor(x, y) {
|
||||||
|
return (x + y) % 2 === 0 ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKingPosition(board, color) {
|
||||||
|
for (let y = 0; y < 8; y += 1) {
|
||||||
|
for (let x = 0; x < 8; x += 1) {
|
||||||
|
const piece = board[y][x];
|
||||||
|
if (piece && piece.color === color && piece.type === "K") {
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSquareAttacked(state, x, y, byColor) {
|
||||||
|
const board = state.board;
|
||||||
|
const pawnOffset = byColor === "w" ? 1 : -1;
|
||||||
|
|
||||||
|
for (const dx of [-1, 1]) {
|
||||||
|
const piece = pieceAt(board, x + dx, y + pawnOffset);
|
||||||
|
if (piece && piece.color === byColor && piece.type === "P") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const knightOffsets = [
|
||||||
|
[-2, -1],
|
||||||
|
[-2, 1],
|
||||||
|
[-1, -2],
|
||||||
|
[-1, 2],
|
||||||
|
[1, -2],
|
||||||
|
[1, 2],
|
||||||
|
[2, -1],
|
||||||
|
[2, 1],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [dx, dy] of knightOffsets) {
|
||||||
|
const piece = pieceAt(board, x + dx, y + dy);
|
||||||
|
if (piece && piece.color === byColor && piece.type === "N") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const slidingLines = [
|
||||||
|
{ directions: [[1, 0], [-1, 0], [0, 1], [0, -1]], pieces: ["R", "Q"] },
|
||||||
|
{ directions: [[1, 1], [1, -1], [-1, 1], [-1, -1]], pieces: ["B", "Q"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const line of slidingLines) {
|
||||||
|
for (const [dx, dy] of line.directions) {
|
||||||
|
let nx = x + dx;
|
||||||
|
let ny = y + dy;
|
||||||
|
|
||||||
|
while (inBounds(nx, ny)) {
|
||||||
|
const piece = pieceAt(board, nx, ny);
|
||||||
|
if (piece) {
|
||||||
|
if (piece.color === byColor && line.pieces.includes(piece.type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nx += dx;
|
||||||
|
ny += dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let dy = -1; dy <= 1; dy += 1) {
|
||||||
|
for (let dx = -1; dx <= 1; dx += 1) {
|
||||||
|
if (dx === 0 && dy === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const piece = pieceAt(board, x + dx, y + dy);
|
||||||
|
if (piece && piece.color === byColor && piece.type === "K") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInCheck(state, color) {
|
||||||
|
const king = getKingPosition(state.board, color);
|
||||||
|
if (!king) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isSquareAttacked(state, king.x, king.y, opponentColor(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
function addMove(moves, state, x, y, nx, ny, extras = {}) {
|
||||||
|
if (!inBounds(nx, ny)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const piece = state.board[y][x];
|
||||||
|
const target = state.board[ny][nx];
|
||||||
|
|
||||||
|
if (target && target.color === piece.color) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
moves.push({
|
||||||
|
from: toSquare(x, y),
|
||||||
|
to: toSquare(nx, ny),
|
||||||
|
fromX: x,
|
||||||
|
fromY: y,
|
||||||
|
toX: nx,
|
||||||
|
toY: ny,
|
||||||
|
color: piece.color,
|
||||||
|
piece: piece.type,
|
||||||
|
capture: target ? target.type : extras.capture || null,
|
||||||
|
captureColor: target ? target.color : extras.captureColor || null,
|
||||||
|
promotion: extras.promotion || null,
|
||||||
|
castle: extras.castle || null,
|
||||||
|
enPassant: Boolean(extras.enPassant),
|
||||||
|
doublePawnPush: Boolean(extras.doublePawnPush),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePawnMoves(state, x, y, piece, moves) {
|
||||||
|
const direction = piece.color === "w" ? -1 : 1;
|
||||||
|
const startRow = piece.color === "w" ? 6 : 1;
|
||||||
|
const promotionRow = piece.color === "w" ? 0 : 7;
|
||||||
|
const forwardY = y + direction;
|
||||||
|
|
||||||
|
if (inBounds(x, forwardY) && !pieceAt(state.board, x, forwardY)) {
|
||||||
|
if (forwardY === promotionRow) {
|
||||||
|
for (const promotion of ["q", "r", "b", "n"]) {
|
||||||
|
addMove(moves, state, x, y, x, forwardY, { promotion });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addMove(moves, state, x, y, x, forwardY);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doubleForwardY = y + direction * 2;
|
||||||
|
if (y === startRow && !pieceAt(state.board, x, doubleForwardY)) {
|
||||||
|
addMove(moves, state, x, y, x, doubleForwardY, { doublePawnPush: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dx of [-1, 1]) {
|
||||||
|
const nx = x + dx;
|
||||||
|
const ny = y + direction;
|
||||||
|
if (!inBounds(nx, ny)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = pieceAt(state.board, nx, ny);
|
||||||
|
if (target && target.color !== piece.color) {
|
||||||
|
if (ny === promotionRow) {
|
||||||
|
for (const promotion of ["q", "r", "b", "n"]) {
|
||||||
|
addMove(moves, state, x, y, nx, ny, { promotion });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addMove(moves, state, x, y, nx, ny);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.enPassant === toSquare(nx, ny)) {
|
||||||
|
const capturedPawn = pieceAt(state.board, nx, y);
|
||||||
|
if (capturedPawn && capturedPawn.color !== piece.color && capturedPawn.type === "P") {
|
||||||
|
addMove(moves, state, x, y, nx, ny, {
|
||||||
|
enPassant: true,
|
||||||
|
capture: "P",
|
||||||
|
captureColor: capturedPawn.color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSlidingMoves(state, x, y, moves, directions) {
|
||||||
|
for (const [dx, dy] of directions) {
|
||||||
|
let nx = x + dx;
|
||||||
|
let ny = y + dy;
|
||||||
|
|
||||||
|
while (inBounds(nx, ny)) {
|
||||||
|
const target = pieceAt(state.board, nx, ny);
|
||||||
|
if (!target) {
|
||||||
|
addMove(moves, state, x, y, nx, ny);
|
||||||
|
} else {
|
||||||
|
if (target.color !== state.board[y][x].color) {
|
||||||
|
addMove(moves, state, x, y, nx, ny);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
nx += dx;
|
||||||
|
ny += dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function canCastle(state, color, side) {
|
||||||
|
const homeRank = color === "w" ? 7 : 0;
|
||||||
|
const rookFile = side === "k" ? 7 : 0;
|
||||||
|
const king = pieceAt(state.board, 4, homeRank);
|
||||||
|
const rook = pieceAt(state.board, rookFile, homeRank);
|
||||||
|
|
||||||
|
if (!king || king.type !== "K" || king.color !== color) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!rook || rook.type !== "R" || rook.color !== color) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!state.castling[color][side] || isInCheck(state, color)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const betweenSquares = side === "k" ? [5, 6] : [1, 2, 3];
|
||||||
|
for (const file of betweenSquares) {
|
||||||
|
if (pieceAt(state.board, file, homeRank)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const traverseSquares = side === "k" ? [5, 6] : [3, 2];
|
||||||
|
for (const file of traverseSquares) {
|
||||||
|
if (isSquareAttacked(state, file, homeRank, opponentColor(color))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateKingMoves(state, x, y, piece, moves) {
|
||||||
|
for (let dy = -1; dy <= 1; dy += 1) {
|
||||||
|
for (let dx = -1; dx <= 1; dx += 1) {
|
||||||
|
if (dx === 0 && dy === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
addMove(moves, state, x, y, x + dx, y + dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canCastle(state, piece.color, "k")) {
|
||||||
|
addMove(moves, state, x, y, x + 2, y, { castle: "k" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canCastle(state, piece.color, "q")) {
|
||||||
|
addMove(moves, state, x, y, x - 2, y, { castle: "q" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePseudoMoves(state, x, y) {
|
||||||
|
const piece = pieceAt(state.board, x, y);
|
||||||
|
if (!piece) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const moves = [];
|
||||||
|
|
||||||
|
if (piece.type === "P") {
|
||||||
|
generatePawnMoves(state, x, y, piece, moves);
|
||||||
|
} else if (piece.type === "N") {
|
||||||
|
const offsets = [
|
||||||
|
[-2, -1],
|
||||||
|
[-2, 1],
|
||||||
|
[-1, -2],
|
||||||
|
[-1, 2],
|
||||||
|
[1, -2],
|
||||||
|
[1, 2],
|
||||||
|
[2, -1],
|
||||||
|
[2, 1],
|
||||||
|
];
|
||||||
|
for (const [dx, dy] of offsets) {
|
||||||
|
addMove(moves, state, x, y, x + dx, y + dy);
|
||||||
|
}
|
||||||
|
} else if (piece.type === "B") {
|
||||||
|
generateSlidingMoves(state, x, y, moves, [
|
||||||
|
[1, 1],
|
||||||
|
[1, -1],
|
||||||
|
[-1, 1],
|
||||||
|
[-1, -1],
|
||||||
|
]);
|
||||||
|
} else if (piece.type === "R") {
|
||||||
|
generateSlidingMoves(state, x, y, moves, [
|
||||||
|
[1, 0],
|
||||||
|
[-1, 0],
|
||||||
|
[0, 1],
|
||||||
|
[0, -1],
|
||||||
|
]);
|
||||||
|
} else if (piece.type === "Q") {
|
||||||
|
generateSlidingMoves(state, x, y, moves, [
|
||||||
|
[1, 0],
|
||||||
|
[-1, 0],
|
||||||
|
[0, 1],
|
||||||
|
[0, -1],
|
||||||
|
[1, 1],
|
||||||
|
[1, -1],
|
||||||
|
[-1, 1],
|
||||||
|
[-1, -1],
|
||||||
|
]);
|
||||||
|
} else if (piece.type === "K") {
|
||||||
|
generateKingMoves(state, x, y, piece, moves);
|
||||||
|
}
|
||||||
|
|
||||||
|
return moves;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCastlingRights(nextState, move) {
|
||||||
|
const { color, piece, from, to, capture, captureColor } = move;
|
||||||
|
|
||||||
|
if (piece === "K") {
|
||||||
|
nextState.castling[color].k = false;
|
||||||
|
nextState.castling[color].q = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (piece === "R") {
|
||||||
|
if (from === "a1") {
|
||||||
|
nextState.castling.w.q = false;
|
||||||
|
}
|
||||||
|
if (from === "h1") {
|
||||||
|
nextState.castling.w.k = false;
|
||||||
|
}
|
||||||
|
if (from === "a8") {
|
||||||
|
nextState.castling.b.q = false;
|
||||||
|
}
|
||||||
|
if (from === "h8") {
|
||||||
|
nextState.castling.b.k = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capture === "R" && captureColor) {
|
||||||
|
if (to === "a1") {
|
||||||
|
nextState.castling.w.q = false;
|
||||||
|
}
|
||||||
|
if (to === "h1") {
|
||||||
|
nextState.castling.w.k = false;
|
||||||
|
}
|
||||||
|
if (to === "a8") {
|
||||||
|
nextState.castling.b.q = false;
|
||||||
|
}
|
||||||
|
if (to === "h8") {
|
||||||
|
nextState.castling.b.k = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMoveNotation(move, status) {
|
||||||
|
if (move.castle === "k") {
|
||||||
|
return status.reason === "checkmate" ? "O-O#" : status.check ? "O-O+" : "O-O";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (move.castle === "q") {
|
||||||
|
return status.reason === "checkmate" ? "O-O-O#" : status.check ? "O-O-O+" : "O-O-O";
|
||||||
|
}
|
||||||
|
|
||||||
|
const piecePrefix = move.piece === "P" ? "" : move.piece;
|
||||||
|
const pawnCapturePrefix = move.piece === "P" && move.capture ? move.from[0] : "";
|
||||||
|
const captureMarker = move.capture ? "x" : "";
|
||||||
|
const promotionText = move.promotion ? `=${move.promotion.toUpperCase()}` : "";
|
||||||
|
const suffix = status.reason === "checkmate" ? "#" : status.check ? "+" : "";
|
||||||
|
|
||||||
|
return `${piecePrefix}${pawnCapturePrefix}${captureMarker}${move.to}${promotionText}${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMoveRecord(move, status, nextState) {
|
||||||
|
return {
|
||||||
|
from: move.from,
|
||||||
|
to: move.to,
|
||||||
|
color: move.color,
|
||||||
|
piece: move.piece,
|
||||||
|
capture: move.capture,
|
||||||
|
promotion: move.promotion,
|
||||||
|
castle: move.castle,
|
||||||
|
enPassant: move.enPassant,
|
||||||
|
notation: buildMoveNotation(move, status),
|
||||||
|
turnNumber: nextState.fullmoveNumber - (move.color === "w" ? 0 : 1),
|
||||||
|
fenKey: positionHash(nextState),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasInsufficientMaterial(board) {
|
||||||
|
const pieces = [];
|
||||||
|
|
||||||
|
for (const row of board) {
|
||||||
|
for (const piece of row) {
|
||||||
|
if (piece) {
|
||||||
|
pieces.push(piece);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonKings = pieces.filter((piece) => piece.type !== "K");
|
||||||
|
if (nonKings.some((piece) => ["P", "R", "Q"].includes(piece.type))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nonKings.length === 0 || nonKings.length === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nonKings.length === 2) {
|
||||||
|
const [first, second] = nonKings;
|
||||||
|
if (first.color !== second.color) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (first.type === "N" && second.type === "N") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bishops = [];
|
||||||
|
for (let y = 0; y < 8; y += 1) {
|
||||||
|
for (let x = 0; x < 8; x += 1) {
|
||||||
|
const piece = board[y][x];
|
||||||
|
if (piece && piece.type === "B") {
|
||||||
|
bishops.push({ ...piece, x, y });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nonKings.length === 2 && bishops.length === 2 && bishops[0].color !== bishops[1].color) {
|
||||||
|
return squareColor(bishops[0].x, bishops[0].y) === squareColor(bishops[1].x, bishops[1].y);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateStatus(state) {
|
||||||
|
const legalMoves = ChessEngine.generateLegalMovesForState(state, state.turn);
|
||||||
|
const check = isInCheck(state, state.turn);
|
||||||
|
const repetitionCount = state.positionCounts[positionHash(state)] || 0;
|
||||||
|
|
||||||
|
const status = {
|
||||||
|
phase: "playing",
|
||||||
|
winner: null,
|
||||||
|
reason: null,
|
||||||
|
check,
|
||||||
|
legalMoves: legalMoves.length,
|
||||||
|
message: `${colorLabel(state.turn)} to move.`,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (legalMoves.length === 0) {
|
||||||
|
if (check) {
|
||||||
|
status.phase = "checkmate";
|
||||||
|
status.reason = "checkmate";
|
||||||
|
status.winner = opponentColor(state.turn);
|
||||||
|
status.message = `${colorLabel(status.winner)} wins by checkmate.`;
|
||||||
|
} else {
|
||||||
|
status.phase = "draw";
|
||||||
|
status.reason = "stalemate";
|
||||||
|
status.message = "Draw by stalemate.";
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasInsufficientMaterial(state.board)) {
|
||||||
|
status.phase = "draw";
|
||||||
|
status.reason = "insufficient-material";
|
||||||
|
status.message = "Draw by insufficient material.";
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.halfmoveClock >= 100) {
|
||||||
|
status.phase = "draw";
|
||||||
|
status.reason = "fifty-move";
|
||||||
|
status.message = "Draw by fifty-move rule.";
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (repetitionCount >= 3) {
|
||||||
|
status.phase = "draw";
|
||||||
|
status.reason = "threefold";
|
||||||
|
status.message = "Draw by threefold repetition.";
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check) {
|
||||||
|
status.message = `${colorLabel(state.turn)} to move. Check.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMoveToState(state, move, options = {}) {
|
||||||
|
const {
|
||||||
|
recordHistory = false,
|
||||||
|
computeStatus = true,
|
||||||
|
updatePositionCounts = computeStatus || recordHistory,
|
||||||
|
} = options;
|
||||||
|
const nextState = cloneState(state);
|
||||||
|
const movingPiece = pieceAt(nextState.board, move.fromX, move.fromY);
|
||||||
|
|
||||||
|
if (!movingPiece) {
|
||||||
|
throw new Error("No piece found on source square.");
|
||||||
|
}
|
||||||
|
|
||||||
|
setPiece(nextState.board, move.fromX, move.fromY, null);
|
||||||
|
|
||||||
|
if (move.enPassant) {
|
||||||
|
setPiece(nextState.board, move.toX, move.fromY, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const placedPiece = createPiece(
|
||||||
|
movingPiece.color,
|
||||||
|
move.promotion ? move.promotion.toUpperCase() : movingPiece.type,
|
||||||
|
);
|
||||||
|
setPiece(nextState.board, move.toX, move.toY, placedPiece);
|
||||||
|
|
||||||
|
if (move.castle === "k") {
|
||||||
|
const rook = pieceAt(nextState.board, 7, move.fromY);
|
||||||
|
setPiece(nextState.board, 7, move.fromY, null);
|
||||||
|
setPiece(nextState.board, 5, move.fromY, rook);
|
||||||
|
} else if (move.castle === "q") {
|
||||||
|
const rook = pieceAt(nextState.board, 0, move.fromY);
|
||||||
|
setPiece(nextState.board, 0, move.fromY, null);
|
||||||
|
setPiece(nextState.board, 3, move.fromY, rook);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCastlingRights(nextState, move);
|
||||||
|
|
||||||
|
nextState.enPassant = move.doublePawnPush ? toSquare(move.fromX, (move.fromY + move.toY) / 2) : null;
|
||||||
|
nextState.halfmoveClock = move.piece === "P" || move.capture ? 0 : nextState.halfmoveClock + 1;
|
||||||
|
nextState.turn = opponentColor(state.turn);
|
||||||
|
|
||||||
|
if (move.color === "b") {
|
||||||
|
nextState.fullmoveNumber += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextState.lastMove = {
|
||||||
|
from: move.from,
|
||||||
|
to: move.to,
|
||||||
|
color: move.color,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updatePositionCounts) {
|
||||||
|
addPositionCount(nextState);
|
||||||
|
}
|
||||||
|
nextState.status = computeStatus ? evaluateStatus(nextState) : null;
|
||||||
|
|
||||||
|
if (recordHistory) {
|
||||||
|
nextState.history.push(getMoveRecord(move, nextState.status, nextState));
|
||||||
|
}
|
||||||
|
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChessEngine {
|
||||||
|
constructor(snapshot = null) {
|
||||||
|
this.state = snapshot ? cloneState(snapshot) : createInitialState();
|
||||||
|
if (Object.keys(this.state.positionCounts).length === 0) {
|
||||||
|
addPositionCount(this.state);
|
||||||
|
}
|
||||||
|
this.state.status = evaluateStatus(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.state = createInitialState();
|
||||||
|
addPositionCount(this.state);
|
||||||
|
this.state.status = evaluateStatus(this.state);
|
||||||
|
return this.getSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSnapshot() {
|
||||||
|
return cloneState(this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPiece(square) {
|
||||||
|
const coords = fromSquare(square);
|
||||||
|
if (!coords) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return clonePiece(pieceAt(this.state.board, coords.x, coords.y));
|
||||||
|
}
|
||||||
|
|
||||||
|
getMovesFrom(square) {
|
||||||
|
return this.getLegalMoves(this.state.turn).filter((move) => move.from === square);
|
||||||
|
}
|
||||||
|
|
||||||
|
getLegalMoves(color = this.state.turn) {
|
||||||
|
return ChessEngine.generateLegalMovesForState(this.state, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeMove(input) {
|
||||||
|
const legalMove = ChessEngine.findLegalMove(this.state, input);
|
||||||
|
if (!legalMove) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: "Illegal move.",
|
||||||
|
state: this.getSnapshot(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = applyMoveToState(this.state, legalMove, { recordHistory: true });
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
move: legalMove,
|
||||||
|
state: this.getSnapshot(),
|
||||||
|
status: { ...this.state.status },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static positionHash(state) {
|
||||||
|
return positionHash(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static isInCheck(state, color) {
|
||||||
|
return isInCheck(state, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
static evaluateStatus(state) {
|
||||||
|
return evaluateStatus(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
static playMove(state, move, options = {}) {
|
||||||
|
return applyMoveToState(state, move, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
static findLegalMove(state, input) {
|
||||||
|
const from = typeof input.from === "string" ? input.from.toLowerCase() : "";
|
||||||
|
const to = typeof input.to === "string" ? input.to.toLowerCase() : "";
|
||||||
|
const promotion = input.promotion ? input.promotion.toLowerCase() : null;
|
||||||
|
|
||||||
|
return ChessEngine.generateLegalMovesForState(state, state.turn).find(
|
||||||
|
(move) =>
|
||||||
|
move.from === from &&
|
||||||
|
move.to === to &&
|
||||||
|
(move.promotion || null) === (promotion || null),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateLegalMovesForState(state, color = state.turn) {
|
||||||
|
const legalMoves = [];
|
||||||
|
|
||||||
|
for (let y = 0; y < 8; y += 1) {
|
||||||
|
for (let x = 0; x < 8; x += 1) {
|
||||||
|
const piece = pieceAt(state.board, x, y);
|
||||||
|
if (!piece || piece.color !== color) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pseudoMoves = generatePseudoMoves(state, x, y);
|
||||||
|
for (const move of pseudoMoves) {
|
||||||
|
const nextState = applyMoveToState(state, move, {
|
||||||
|
recordHistory: false,
|
||||||
|
computeStatus: false,
|
||||||
|
updatePositionCounts: false,
|
||||||
|
});
|
||||||
|
if (!isInCheck(nextState, color)) {
|
||||||
|
legalMoves.push(move);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return legalMoves;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChessRules = {
|
||||||
|
FILES,
|
||||||
|
colorLabel,
|
||||||
|
fromSquare,
|
||||||
|
toSquare,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user