UNPKG

kokopu

Version:

A JavaScript/TypeScript library implementing the chess game rules and providing tools to read/write the standard chess file formats.

357 lines (307 loc) 13.9 kB
/*! * -------------------------------------------------------------------------- * * * * Kokopu - A JavaScript/TypeScript chess library. * * <https://www.npmjs.com/package/kokopu> * * Copyright (C) 2018-2025 Yoann Le Montagner <yo35 -at- melix.net> * * * * Kokopu is free software: you can redistribute it and/or * * modify it under the terms of the GNU Lesser General Public License * * as published by the Free Software Foundation, either version 3 of * * the License, or (at your option) any later version. * * * * Kokopu is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General * * Public License along with this program. If not, see * * <http://www.gnu.org/licenses/>. * * * * -------------------------------------------------------------------------- */ import { isAttacked } from './attacks'; import { ColorImpl, PieceImpl, CpI, SpI, SquareImpl, GameVariantImpl } from './base_types_impl'; import { PositionImpl } from './impl'; /** * Check whether the given position is legal or not. * * See {@link Position.isLegal} for the description of the check points enforced in this function. */ export function isLegal(position: PositionImpl): boolean { refreshLegalFlagAndKingSquares(position); return position.legal!; } /** * Refresh the legal flag of the given position if it is set to null * (which means that the legality state of the position is unknown). * * Together with the legal flag, the reference to the squares where the white and * black kings lie is updated by this function. */ export function refreshLegalFlagAndKingSquares(position: PositionImpl) { if (position.legal !== null) { return; } position.legal = false; // Condition (1) const whiteKingOK = refreshKingSquare(position, ColorImpl.WHITE); const blackKingOK = refreshKingSquare(position, ColorImpl.BLACK); if (!whiteKingOK || !blackKingOK) { return; } // Extension of (1) for variants that allow a player to have no piece at all... if (position.variant === GameVariantImpl.ANTICHESS) { if (!hasAtLeastOnePiece(position, 1 - position.turn)) { // The player that has just played must have at least one piece in antichess. return; } } else if (position.variant === GameVariantImpl.HORDE) { if (position.turn === ColorImpl.BLACK && !hasAtLeastOnePiece(position, ColorImpl.WHITE)) { // White must have at least one piece if he/she has just played in horde chess. return; } } // Condition (2) if (position.king[1 - position.turn] >= 0 && isAttacked(position, position.king[1 - position.turn], position.turn)) { return; } // Condition (3) const forbiddenCPWhite1 = position.variant === GameVariantImpl.HORDE ? SpI.INVALID : CpI.WP; for (let c = 0; c < 8; ++c) { const cp1 = position.board[SquareImpl.A1 + c]; const cp8 = position.board[SquareImpl.A8 + c]; if (cp1 === forbiddenCPWhite1 || cp8 === CpI.WP || cp1 === CpI.BP || cp8 === CpI.BP) { return; } } // At this point, all the conditions (1) to (4) hold, so the position can be flagged as legal. position.legal = true; } /** * Detect the kings of the given color that are present on the chess board. * * @returns `true` if the number of found king(s) corresponds is compatible with a legal position according to the given variant. */ function refreshKingSquare(position: PositionImpl, color: number) { const target = PieceImpl.KING * 2 + color; position.king[color] = -1; // Expectation: king may be present (even several times), and it has no royal power. if (position.variant === GameVariantImpl.ANTICHESS) { return true; } // Expectation: no king of the given color is supposed to be present on the board. else if (position.variant === GameVariantImpl.NO_KING || position.variant === GameVariantImpl.BLACK_KING_ONLY - color || (position.variant === GameVariantImpl.HORDE && color === ColorImpl.WHITE)) { for (let sq = 0; sq < 120; sq += (sq & 0x7) === 7 ? 9 : 1) { if (position.board[sq] === target) { return false; } } return true; } // Expectation: exactly 1 king of the given color is supposed to be present on the board, and it has royal power. else { for (let sq = 0; sq < 120; sq += (sq & 0x7) === 7 ? 9 : 1) { if (position.board[sq] === target) { // If the targeted king is detected on the square sq, two situations may occur: // 1) No king was detected on the previously visited squares: then the current // square is saved, and loop over the next board squares goes on. if (position.king[color] < 0) { position.king[color] = sq; } // 2) Another king was detected on the previously visited squares: then the buffer position.king[color] // is set to the invalid state (-1), and the loop is interrupted. else { position.king[color] = -1; return false; } } } return position.king[color] >= 0; } } /** * Detect whether the player with the given color has at least one piece or not. */ function hasAtLeastOnePiece(position: PositionImpl, color: number) { for (let sq = 0; sq < 120; sq += (sq & 0x7) === 7 ? 9 : 1) { if (position.board[sq] !== SpI.EMPTY && position.board[sq] % 2 === color) { return true; } } return false; } /** * Check whether the current player king is in check after moving from `from` to `to`. * * This function implements the verification steps (7) to (9) as defined in {@link isMoveLegal}. * * Precondition: {@link refreshLegalFlagAndKingSquares} must have been invoked beforehand. * * @param enPassantSquare - Index of the square where the "en-passant" taken pawn lies if any, `-1` otherwise. */ export function isKingSafeAfterMove(position: PositionImpl, from: number, to: number, enPassantSquare = -1) { if (position.king[position.turn] < 0) { return true; } const fromContent = position.board[from]; const toContent = position.board[to]; const movingPiece = Math.trunc(fromContent / 2); // Step (7) -> Execute the displacement (castling moves are processed separately). position.board[to] = fromContent; position.board[from] = SpI.EMPTY; if (enPassantSquare >= 0) { position.board[enPassantSquare] = SpI.EMPTY; } // Step (8) -> Is the king safe after the displacement? try { return !isAttacked(position, movingPiece === PieceImpl.KING ? to : position.king[position.turn], 1 - position.turn); } // Step (9) -> Reverse the displacement. finally { position.board[from] = fromContent; position.board[to] = toContent; if (enPassantSquare >= 0) { position.board[enPassantSquare] = PieceImpl.PAWN * 2 + 1 - position.turn; } } } /** * Refresh the effective castling flags of the given position if they are set to null * (which means that their states are unknown). */ export function refreshEffectiveCastling(position: PositionImpl) { if (position.effectiveCastling !== null) { return; } // Detect the location of the king and make sure it has royal power. // (no royal power, no castling...) refreshLegalFlagAndKingSquares(position); // Actual computation. position.effectiveCastling = position.variant === GameVariantImpl.CHESS960 ? [ computeEffectiveCastlingForChess960(position, 0), computeEffectiveCastlingForChess960(position, 1) ] : [ computeEffectiveCastlingForRegularChess(position, 0), computeEffectiveCastlingForRegularChess(position, 1) ]; } function computeEffectiveCastlingForRegularChess(position: PositionImpl, color: number) { const rankOffset = 112 * color; if (position.king[color] !== rankOffset + 4 || position.castling[color] === 0) { return 0; } const targetRook = PieceImpl.ROOK * 2 + color; let result = 0; if ((position.castling[color] & 0x01) !== 0 && position.board[rankOffset] === targetRook) { // queen-side castling result |= 0x01; } if ((position.castling[color] & 0x80) !== 0 && position.board[rankOffset + 7] === targetRook) { // king-side castling result |= 0x80; } return result; } function computeEffectiveCastlingForChess960(position: PositionImpl, color: number) { const rankOffset = 112 * color; if (position.king[color] <= rankOffset || position.king[color] >= rankOffset + 7 || position.castling[color] === 0) { // The king must not be in the corners. return 0; } const targetRook = PieceImpl.ROOK * 2 + color; let result = 0; // Queen-side castling. let queenSideRookFile = -1; for (let file = position.king[color] % 16 - 1; file >= 0; --file) { if ((position.castling[color] & 1 << file) === 0 || position.board[rankOffset + file] !== targetRook) { continue; } if (queenSideRookFile < 0) { queenSideRookFile = file; } else { queenSideRookFile = -1; break; } } if (queenSideRookFile >= 0) { result |= 1 << queenSideRookFile; } // King-side castling. let kingSideRookFile = -1; for (let file = position.king[color] % 16 + 1; file < 8; ++file) { if ((position.castling[color] & 1 << file) === 0 || position.board[rankOffset + file] !== targetRook) { continue; } if (kingSideRookFile < 0) { kingSideRookFile = file; } else { kingSideRookFile = -1; break; } } if (kingSideRookFile >= 0) { result |= 1 << kingSideRookFile; } return result; } /** * Refresh the effective en-passant flag of the given position if it is set to null * (which means that its state is unknown). */ export function refreshEffectiveEnPassant(position: PositionImpl) { if (position.effectiveEnPassant !== null) { return; } position.effectiveEnPassant = -1; // If the en-passant flag is unset, so is the effective en-passant flag. if (position.enPassant < 0) { return; } // Geometric condition: for the effective en-passant flag to be set for instance to file E, assuming black is about to play: // - e2 and e3 must be empty, // - there must be a white pawn on e4, // - and there must be at least one black pawn on d4 or f4. const square2 = (6 - position.turn * 5) * 16 + position.enPassant; const square3 = (5 - position.turn * 3) * 16 + position.enPassant; const square4 = (4 - position.turn) * 16 + position.enPassant; const capturingPawn = PieceImpl.PAWN * 2 + position.turn; const capturedPawn = PieceImpl.PAWN * 2 + 1 - position.turn; if (position.board[square2] !== SpI.EMPTY || position.board[square3] !== SpI.EMPTY || position.board[square4] !== capturedPawn) { return; } const hasCapturingPawnBefore = ((square4 - 1) & 0x88) === 0 && position.board[square4 - 1] === capturingPawn; const hasCapturingPawnAfter = ((square4 + 1) & 0x88) === 0 && position.board[square4 + 1] === capturingPawn; if (!hasCapturingPawnBefore && !hasCapturingPawnAfter) { return; } // If en-passant is geometrically valid, ensure that it do not let the king in check. refreshLegalFlagAndKingSquares(position); if ( !(hasCapturingPawnBefore && isKingSafeAfterMove(position, square4 - 1, square3, square4)) && !(hasCapturingPawnAfter && isKingSafeAfterMove(position, square4 + 1, square3, square4)) ) { return; } // At this point, the en-passant flag can be considered as effective. position.effectiveEnPassant = position.enPassant; } export function isEqual(pos1: PositionImpl, pos2: PositionImpl) { if (pos1.turn !== pos2.turn || pos1.variant !== pos2.variant) { return false; } for (let sq = 0; sq < 120; sq += (sq & 0x7) === 7 ? 9 : 1) { if (pos1.board[sq] !== pos2.board[sq]) { return false; } } // No check on `.legal` and `.king` as they are computed attributes. // Ignore `.castling`, compare `.effectiveCastling` instead. refreshEffectiveCastling(pos1); refreshEffectiveCastling(pos2); if ( pos1.effectiveCastling![ColorImpl.WHITE] !== pos2.effectiveCastling![ColorImpl.WHITE] || pos1.effectiveCastling![ColorImpl.BLACK] !== pos2.effectiveCastling![ColorImpl.BLACK] ) { return false; } // Ignore `.enPassant`, compare `.effectiveEnPassant` instead. refreshEffectiveEnPassant(pos1); refreshEffectiveEnPassant(pos2); return pos1.effectiveEnPassant === pos2.effectiveEnPassant; }