UNPKG

kokopu

Version:

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

661 lines 31.4 kB
"use strict"; /*! * -------------------------------------------------------------------------- * * * * 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/>. * * * * -------------------------------------------------------------------------- */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Position = void 0; const exception_1 = require("./exception"); const helper_1 = require("./helper"); const i18n_1 = require("./i18n"); const attacks_1 = require("./private_position/attacks"); const base_types_impl_1 = require("./private_position/base_types_impl"); const fen_1 = require("./private_position/fen"); const impl_1 = require("./private_position/impl"); const legality_1 = require("./private_position/legality"); const move_descriptor_impl_1 = require("./private_position/move_descriptor_impl"); const move_generation_1 = require("./private_position/move_generation"); const notation_1 = require("./private_position/notation"); const uci_1 = require("./private_position/uci"); /** * Represent a chess position, i.e.: * - the state of a 64-square chessboard, * - who is about to play, * - the castling rights, * - and the file on which *en-passant* is possible, if any. */ class Position { constructor(arg0, arg1) { switch (arguments.length) { // Default constructor case 0: this._impl = (0, impl_1.makeInitial)(0 /* GameVariantImpl.REGULAR_CHESS */); break; // Possible overloads with 1 argument: // - 'start' // - 'empty' // - Position // - GameVariant (valid only for variants with a canonical starting position) // - FEN // - GameVariant:FEN case 1: { if (arg0 === 'start') { this._impl = (0, impl_1.makeInitial)(0 /* GameVariantImpl.REGULAR_CHESS */); } else if (arg0 === 'empty') { this._impl = (0, impl_1.makeEmpty)(0 /* GameVariantImpl.REGULAR_CHESS */); } else if (arg0 instanceof Position) { this._impl = (0, impl_1.makeCopy)(arg0._impl); } else { const variantCode = (0, base_types_impl_1.variantFromString)(arg0); if (variantCode >= 0) { if (!(0, impl_1.hasCanonicalStartPosition)(variantCode)) { throw new exception_1.IllegalArgument('Position()'); } this._impl = (0, impl_1.makeInitial)(variantCode); } else if (typeof arg0 === 'string') { const separatorIndex = arg0.indexOf(':'); if (separatorIndex < 0) { this._impl = (0, fen_1.parseFEN)(0 /* GameVariantImpl.REGULAR_CHESS */, arg0, false).position; } else { const variantPrefix = arg0.substring(0, separatorIndex); const variantPrefixCode = (0, base_types_impl_1.variantFromString)(variantPrefix); if (variantPrefixCode < 0) { throw new exception_1.InvalidFEN(arg0, i18n_1.i18n.INVALID_VARIANT_PREFIX, variantPrefix); } this._impl = (0, fen_1.parseFEN)(variantPrefixCode, arg0.substring(separatorIndex + 1), false).position; } } else { throw new exception_1.IllegalArgument('Position()'); } } break; } // Possible overloads with 2 arguments: // - (GameVariant, 'start') (valid only for variants with a canonical starting position) // - (GameVariant, 'empty') // - (GameVariant, scharnaglCode) (valid only for Chess960) // - (GameVariant, FEN) default: { const variantCode = (0, base_types_impl_1.variantFromString)(arg0); if (variantCode < 0) { throw new exception_1.IllegalArgument('Position()'); } if (arg1 === 'start') { if (!(0, impl_1.hasCanonicalStartPosition)(variantCode)) { throw new exception_1.IllegalArgument('Position()'); } this._impl = (0, impl_1.makeInitial)(variantCode); } else if (arg1 === 'empty') { this._impl = (0, impl_1.makeEmpty)(variantCode); } else if (typeof arg1 === 'number') { if (variantCode !== 1 /* GameVariantImpl.CHESS960 */ || !isValidScharnaglCode(arg1)) { throw new exception_1.IllegalArgument('Position()'); } this._impl = (0, impl_1.make960FromScharnagl)(arg1); } else if (typeof arg1 === 'string') { this._impl = (0, fen_1.parseFEN)(variantCode, arg1, false).position; } else { throw new exception_1.IllegalArgument('Position()'); } break; } } } /** * Set the position to the empty state, for the given chess game variant. */ clear(variant = 'regular') { const variantCode = (0, base_types_impl_1.variantFromString)(variant); if (variantCode < 0) { throw new exception_1.IllegalArgument('Position.clear()'); } this._impl = (0, impl_1.makeEmpty)(variantCode); } /** * Set the position to the starting state (in the regular chess variant). */ reset() { this._impl = (0, impl_1.makeInitial)(0 /* GameVariantImpl.REGULAR_CHESS */); } /** * Set the position to Chess960 starting position corresponding to the given Scharnagl code. * * @param scharnaglCode - Must be between 0 and 959 inclusive (see https://chess960.net/start-positions/ * or https://www.chessprogramming.org/Reinhard_Scharnagl for more details). */ reset960(scharnaglCode) { if (!isValidScharnaglCode(scharnaglCode)) { throw new exception_1.IllegalArgument('Position.reset960()'); } this._impl = (0, impl_1.make960FromScharnagl)(scharnaglCode); } /** * Set the position to the starting state of the antichess variant. */ resetAntichess() { this._impl = (0, impl_1.makeInitial)(5 /* GameVariantImpl.ANTICHESS */); } /** * Set the position to the starting state of the horde chess variant. */ resetHorde() { this._impl = (0, impl_1.makeInitial)(6 /* GameVariantImpl.HORDE */); } /** * Check whether both given objects represent the same chess position (i.e. the same chess variant, same board, * and same turn/castling/en-passant flags). */ static isEqual(pos1, pos2) { return pos1 instanceof Position && pos2 instanceof Position && (0, legality_1.isEqual)(pos1._impl, pos2._impl); } // ------------------------------------------------------------------------- // FEN & ASCII conversion // ------------------------------------------------------------------------- /** * Return a human-readable string representing the position. This string is multi-line, * and is intended to be displayed in a fixed-width font (similarly to an ASCII-art picture). * For instance: * * ``` * const position = new Position(); * console.log(position.ascii({ coordinateVisible: true })); * * // +---+---+---+---+---+---+---+---+ * // 8 | r | n | b | q | k | b | n | r | * // +---+---+---+---+---+---+---+---+ * // 7 | p | p | p | p | p | p | p | p | * // +---+---+---+---+---+---+---+---+ * // 6 | | | | | | | | | * // +---+---+---+---+---+---+---+---+ * // 5 | | | | | | | | | * // +---+---+---+---+---+---+---+---+ * // 4 | | | | | | | | | * // +---+---+---+---+---+---+---+---+ * // 3 | | | | | | | | | * // +---+---+---+---+---+---+---+---+ * // 2 | P | P | P | P | P | P | P | P | * // +---+---+---+---+---+---+---+---+ * // 1 | R | N | B | Q | K | B | N | R | * // +---+---+---+---+---+---+---+---+ * // a b c d e f g h * // w KQkq - * ``` */ ascii(options) { return (0, fen_1.ascii)(this._impl, options ?? {}); } fen(fenOrOptions, strict) { // Getter, without options. if (arguments.length === 0) { return (0, fen_1.getFEN)(this._impl); } // Getter, with options. else if (arguments.length === 1 && typeof fenOrOptions === 'object') { const validate = buildValidator(fenOrOptions, 'Position.fen()'); const fiftyMoveClock = validate('fiftyMoveClock', 0, val => Number.isInteger(val)); const fullMoveNumber = validate('fullMoveNumber', 1, val => Number.isInteger(val)); const withVariant = validate('withVariant', false, val => typeof val === 'boolean'); const regularFENIfPossible = validate('regularFENIfPossible', false, val => typeof val === 'boolean'); return (withVariant ? (0, base_types_impl_1.variantToString)(this._impl.variant) + ':' : '') + (0, fen_1.getFEN)(this._impl, fiftyMoveClock, fullMoveNumber, regularFENIfPossible); } // Setter, without strict option. else if (arguments.length === 1 && typeof fenOrOptions === 'string') { const result = (0, fen_1.parseFEN)(this._impl.variant, fenOrOptions, false); this._impl = result.position; return { fiftyMoveClock: result.fiftyMoveClock, fullMoveNumber: result.fullMoveNumber }; } // Setter, with strict option. else if (arguments.length >= 2 && typeof fenOrOptions === 'string' && typeof strict === 'boolean') { const result = (0, fen_1.parseFEN)(this._impl.variant, fenOrOptions, strict); this._impl = result.position; return { fiftyMoveClock: result.fiftyMoveClock, fullMoveNumber: result.fullMoveNumber }; } // Unsupported overload. else { throw new exception_1.IllegalArgument('Position.fen()'); } } // ------------------------------------------------------------------------- // Accessors // ------------------------------------------------------------------------- /** * Get the chess game variant in use. */ variant() { return (0, base_types_impl_1.variantToString)(this._impl.variant); } square(square, value) { const squareCode = (0, base_types_impl_1.squareFromString)(square); if (squareCode < 0) { throw new exception_1.IllegalArgument('Position.square()'); } if (arguments.length === 1) { const cp = this._impl.board[squareCode]; return cp === -1 /* SpI.EMPTY */ ? '-' : (0, base_types_impl_1.coloredPieceToString)(cp); } else if (value === '-') { this._impl.board[squareCode] = -1 /* SpI.EMPTY */; this._impl.legal = null; this._impl.effectiveCastling = null; this._impl.effectiveEnPassant = null; } else { const cp = (0, base_types_impl_1.coloredPieceFromString)(value); if (cp < 0) { throw new exception_1.IllegalArgument('Position.square()'); } this._impl.board[squareCode] = cp; this._impl.legal = null; this._impl.effectiveCastling = null; this._impl.effectiveEnPassant = null; } } turn(value) { if (arguments.length === 0) { return (0, base_types_impl_1.colorToString)(this._impl.turn); } else { const colorCode = (0, base_types_impl_1.colorFromString)(value); if (colorCode < 0) { throw new exception_1.IllegalArgument('Position.turn()'); } this._impl.turn = colorCode; this._impl.legal = null; this._impl.effectiveEnPassant = null; } } castling(castle, value) { if (!(this._impl.variant === 1 /* GameVariantImpl.CHESS960 */ ? (0, helper_1.isCastle960)(castle) : (0, helper_1.isCastle)(castle))) { throw new exception_1.IllegalArgument('Position.castling()'); } const color = (0, base_types_impl_1.colorFromString)(castle[0]); const file = this._impl.variant === 1 /* GameVariantImpl.CHESS960 */ ? (0, base_types_impl_1.fileFromString)(castle[1]) : castle[1] === 'k' ? 7 : 0; if (arguments.length === 1) { return (this._impl.castling[color] & 1 << file) !== 0; } else if (typeof value === 'boolean') { if (value) { this._impl.castling[color] |= 1 << file; } else { this._impl.castling[color] &= ~(1 << file); } this._impl.effectiveCastling = null; } else { throw new exception_1.IllegalArgument('Position.castling()'); } } /** * Get a validated (aka. effective) castle flag (i.e. whether or not the corresponding castle is allowed or not). * * Compared to {@link Position.castling}, if this method returns `true`, then it is guaranteed that there are a king and a rook on the squares * corresponding to the given castle. * * @param castle - Must be {@link Castle960} if the {@link Position} is configured for Chess960, or {@link Castle} otherwise. */ effectiveCastling(castle) { if (!(this._impl.variant === 1 /* GameVariantImpl.CHESS960 */ ? (0, helper_1.isCastle960)(castle) : (0, helper_1.isCastle)(castle))) { throw new exception_1.IllegalArgument('Position.effectiveCastling()'); } const color = (0, base_types_impl_1.colorFromString)(castle[0]); const file = this._impl.variant === 1 /* GameVariantImpl.CHESS960 */ ? (0, base_types_impl_1.fileFromString)(castle[1]) : castle[1] === 'k' ? 7 : 0; (0, legality_1.refreshEffectiveCastling)(this._impl); return (this._impl.effectiveCastling[color] & 1 << file) !== 0; } enPassant(value) { if (arguments.length === 0) { return this._impl.enPassant < 0 ? '-' : (0, base_types_impl_1.fileToString)(this._impl.enPassant); } else if (value === '-') { this._impl.enPassant = -1; this._impl.effectiveEnPassant = -1; } else { const enPassantCode = (0, base_types_impl_1.fileFromString)(value); if (enPassantCode < 0) { throw new exception_1.IllegalArgument('Position.enPassant()'); } this._impl.enPassant = enPassantCode; this._impl.effectiveEnPassant = null; } } /** * Get the effective *en-passant* flag (i.e. the column on which a *en-passant* capture is possible, if any). * * If {@link Position.enPassant} returns `'-'`, this method returns `'-'`. Otherwise, it returns: * - either the same file that is returned by {@link Position.enPassant} if a *en-passant* capture is allowed on this file, * - or `'-'` otherwise. */ effectiveEnPassant() { (0, legality_1.refreshEffectiveEnPassant)(this._impl); return this._impl.effectiveEnPassant < 0 ? '-' : (0, base_types_impl_1.fileToString)(this._impl.effectiveEnPassant); } // ------------------------------------------------------------------------- // Attacks // ------------------------------------------------------------------------- /** * Check if any piece of the given color attacks the given square. */ isAttacked(square, byWho) { const squareCode = (0, base_types_impl_1.squareFromString)(square); const byWhoCode = (0, base_types_impl_1.colorFromString)(byWho); if (squareCode < 0 || byWhoCode < 0) { throw new exception_1.IllegalArgument('Position.isAttacked()'); } return (0, attacks_1.isAttacked)(this._impl, squareCode, byWhoCode); } /** * Return the squares from which a piece of the given color attacks the given square. */ getAttacks(square, byWho) { const squareCode = (0, base_types_impl_1.squareFromString)(square); const byWhoCode = (0, base_types_impl_1.colorFromString)(byWho); if (squareCode < 0 || byWhoCode < 0) { throw new exception_1.IllegalArgument('Position.getAttacks()'); } return (0, attacks_1.getAttacks)(this._impl, squareCode, byWhoCode).map(base_types_impl_1.squareToString); } // ------------------------------------------------------------------------- // Legality // ------------------------------------------------------------------------- /** * Check whether the current position is legal or not. * * A position is considered to be legal if all the following conditions are met: * * 1. There is exactly one white king and one black king on the board (or more generally, * the number of kings on the board matches what is expected in the game variant of the position). * 2. The player that is not about to play is not in check (this condition is omitted for variants * in which kings have no royal power). * 3. There are no pawn on ranks 1 and 8 (except if the game variant of the position allows it). */ isLegal() { return (0, legality_1.isLegal)(this._impl); } /** * Return the square on which is located the king of the given color. If there is no such king on the board * (or on the contrary, if there are two or more such kings on the board), `false` is returned. * * For non-standard variants, the behavior of this method depends on whether king has royal power in the current variant or not * (i.e. whether it can be put in check or not). For instance: * - in antichess, the king has no royal power, thus `false` is always returned, * - in Chess960, the king has royal power (as in the usual chess rules), thus the method returns the square on which the king is located. */ kingSquare(color) { const colorCode = (0, base_types_impl_1.colorFromString)(color); if (colorCode < 0) { throw new exception_1.IllegalArgument('Position.kingSquare()'); } (0, legality_1.refreshLegalFlagAndKingSquares)(this._impl); const squareCode = this._impl.king[colorCode]; return squareCode < 0 ? false : (0, base_types_impl_1.squareToString)(squareCode); } // ------------------------------------------------------------------------- // Move generation // ------------------------------------------------------------------------- /** * Whether the player that is about to play is in check or not. If the position is not legal (see {@link Position.isLegal}), * the returned value is always `false`. * * For antichess, this method always returns `false`. */ isCheck() { return (0, move_generation_1.isCheck)(this._impl); } /** * Whether the player that is about to play is checkmated or not. If the position is not legal (see {@link Position.isLegal}), * the returned value is always `false`. * * For antichess, this method returns `true` if the player about to play has no remaining piece or pawn, * or if non of his/her remaining pieces can move (i.e. same behavior as {@link Position.isStalemate} for this variant). * * For horde chess, this method returns `true` if black has been checkmated or if white has no remaining piece. */ isCheckmate() { return (0, move_generation_1.isCheckmate)(this._impl); } /** * Whether the player that is about to play is stalemated or not. If the position is not legal (see {@link Position.isLegal}), * the returned value is always `false`. * * For antichess, this method returns `true` if the player about to play has no remaining piece or pawn, * or if non of his/her remaining pieces can move (i.e. same behavior as {@link Position.isCheckmate} for this variant). * * For horde chess, this method returns `true` if black has been stalemated or if white cannot move but has still at least one piece. */ isStalemate() { return (0, move_generation_1.isStalemate)(this._impl); } /** * Whether both players have insufficient material so the game cannot end in checkmate ([dead position rule](https://en.wikipedia.org/wiki/Rules_of_chess#Dead_position)). * If the position is not legal (see {@link Position.isLegal}), the returned value is always `false`. * * By default, the method uses the FIDE rules, i.e. the position is considered as dead if there is no possible checkmate, even if both players * cooperate for that. This is different from the USCF rules, for which the position is considered as dead if no player can force the other into * a checkmate. For instance, king + two knights vs king is NOT considered as dead according to the FIDE rules, whereas it is according to the * USCF rules. * * For antichess and horde chess, this method always returns `false` since it is always possible to end in a checkmate-like situation * (by capturing all the pieces of one player). * * @param uscfRules - `true` to use the USCF rules (forced checkmate), `false` to use the FIDE rules (possible checkmate). */ isDead(uscfRules = false) { return (0, move_generation_1.isDead)(this._impl, uscfRules); } /** * Whether at least one legal move exists in the current position or not. If the position is not legal (see {@link Position.isLegal}), * the returned value is always `false`. */ hasMove() { return (0, move_generation_1.hasMove)(this._impl); } /** * Return the list of all legal moves in the current position. An empty list is returned if the position itself is not legal * (see {@link Position.isLegal}). */ moves() { return (0, move_generation_1.moves)(this._impl); } /** * Check whether a move is legal or not, and return a factory capable the corresponding {@link MoveDescriptor}(-s) if it is legal. * * For castling moves, `to` is supposed to represent: * - for regular chess, the destination square of the king (i.e. c1, g1, c8 or g8), * - for Chess960, the origin square of the rook ("king-take-rook" pattern). * * A code interpreting the result returned by {@link Position.isMoveLegal} would typically look like this: * * ``` * const result = position.isMoveLegal(from, to); * if (!result) { * // The move "from -> to" is not legal. * } * else { * switch (result.status) { * * case 'regular': * // The move "from -> to" is legal, and the corresponding move descriptor * // is `result()`. * break; * * case 'promotion': * // The move "from -> to" is legal, but it corresponds to a promotion, * // so the promoted piece must be specified. The corresponding move descriptors * // are `result('q')`, `result('r')`, `result('b')` and `result('n')`. * break; * * default: * // This case is not supposed to happen. * break; * } * } * ``` */ isMoveLegal(from, to) { const fromCode = (0, base_types_impl_1.squareFromString)(from); const toCode = (0, base_types_impl_1.squareFromString)(to); if (fromCode < 0 || toCode < 0) { throw new exception_1.IllegalArgument('Position.isMoveLegal()'); } const moveInfo = (0, move_generation_1.isMoveLegal)(this._impl, fromCode, toCode); // No legal move. if (!moveInfo) { return false; } switch (moveInfo.type) { case 'promotion': { const result = (promotion) => { const promotionCode = (0, base_types_impl_1.pieceFromString)(promotion); if (promotionCode >= 0) { const moveDescriptor = moveInfo.moveDescriptorFactory(promotionCode); if (moveDescriptor) { return moveDescriptor; } } throw new exception_1.IllegalArgument('Position.isMoveLegal()'); }; result.status = 'promotion'; return result; } case 'regular': { const result = () => moveInfo.moveDescriptor; result.status = 'regular'; return result; } } } play(move) { if (typeof move === 'string') { try { (0, move_generation_1.play)(this._impl, (0, notation_1.parseNotation)(this._impl, move, false, 'standard')); return true; } catch (err) { // istanbul ignore else if (err instanceof exception_1.InvalidNotation) { return false; } else { throw err; } } } else if (move instanceof move_descriptor_impl_1.MoveDescriptorImpl) { (0, move_generation_1.play)(this._impl, move); return true; } else { throw new exception_1.IllegalArgument('Position.play()'); } } /** * Whether a null-move (i.e. switching the player about to play) can be played in the current position or not. * * A null-move is possible if the position is legal and if the current player about to play is not in check. */ isNullMoveLegal() { return (0, move_generation_1.isNullMoveLegal)(this._impl); } /** * Play a null-move on the current position if it is legal. * * @returns `true` if the move has actually been played, `false` otherwise. */ playNullMove() { return (0, move_generation_1.playNullMove)(this._impl); } notation(moveOrDescriptor, strict) { if (arguments.length === 1 && moveOrDescriptor instanceof move_descriptor_impl_1.MoveDescriptorImpl) { return (0, notation_1.getNotation)(this._impl, moveOrDescriptor, 'standard'); } else if (arguments.length === 1 && typeof moveOrDescriptor === 'string') { return (0, notation_1.parseNotation)(this._impl, moveOrDescriptor, false, 'standard'); } else if (arguments.length >= 2 && typeof moveOrDescriptor === 'string' && typeof strict === 'boolean') { return (0, notation_1.parseNotation)(this._impl, moveOrDescriptor, strict, 'standard'); } else { throw new exception_1.IllegalArgument('Position.notation()'); } } figurineNotation(moveOrDescriptor, strict) { if (arguments.length === 1 && moveOrDescriptor instanceof move_descriptor_impl_1.MoveDescriptorImpl) { return (0, notation_1.getNotation)(this._impl, moveOrDescriptor, 'figurine'); } else if (arguments.length === 1 && typeof moveOrDescriptor === 'string') { return (0, notation_1.parseNotation)(this._impl, moveOrDescriptor, false, 'figurine'); } else if (arguments.length >= 2 && typeof moveOrDescriptor === 'string' && typeof strict === 'boolean') { return (0, notation_1.parseNotation)(this._impl, moveOrDescriptor, strict, 'figurine'); } else { throw new exception_1.IllegalArgument('Position.figurineNotation()'); } } uci(moveOrDescriptor, strictOrForceKxR) { if (arguments.length === 1 && moveOrDescriptor instanceof move_descriptor_impl_1.MoveDescriptorImpl) { return (0, uci_1.getUCINotation)(this._impl, moveOrDescriptor, false); } else if (arguments.length >= 2 && moveOrDescriptor instanceof move_descriptor_impl_1.MoveDescriptorImpl && typeof strictOrForceKxR === 'boolean') { return (0, uci_1.getUCINotation)(this._impl, moveOrDescriptor, strictOrForceKxR); } else if (arguments.length === 1 && typeof moveOrDescriptor === 'string') { return (0, uci_1.parseUCINotation)(this._impl, moveOrDescriptor, false); } else if (arguments.length >= 2 && typeof moveOrDescriptor === 'string' && typeof strictOrForceKxR === 'boolean') { return (0, uci_1.parseUCINotation)(this._impl, moveOrDescriptor, strictOrForceKxR); } else { throw new exception_1.IllegalArgument('Position.uci()'); } } } exports.Position = Position; function isValidScharnaglCode(scharnaglCode) { return Number.isInteger(scharnaglCode) && scharnaglCode >= 0 && scharnaglCode < 960; } function buildValidator(options, functionName) { return function (key, defaultValue, validator) { if (options[key] === undefined) { return defaultValue; } else { const value = options[key]; if (!validator(value)) { throw new exception_1.IllegalArgument(functionName); } return value; } }; } //# sourceMappingURL=position.js.map