UNPKG

kokopu

Version:

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

451 lines 22.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.getNotation = getNotation; exports.parseNotation = parseNotation; const attacks_1 = require("./attacks"); const base_types_impl_1 = require("./base_types_impl"); const fen_1 = require("./fen"); const impl_1 = require("./impl"); const legality_1 = require("./legality"); const move_descriptor_impl_1 = require("./move_descriptor_impl"); const move_generation_1 = require("./move_generation"); const exception_1 = require("../exception"); const i18n_1 = require("../i18n"); /** * Convert the given move descriptor to standard algebraic notation. */ function getNotation(position, descriptor, pieceStyle) { let result = ''; // Castling move if (descriptor.isCastling()) { result = descriptor._to % 16 === 6 ? 'O-O' : 'O-O-O'; } // Pawn move else if (Math.trunc(descriptor._movingColoredPiece / 2) === 5 /* PieceImpl.PAWN */) { if (descriptor.isCapture()) { result += (0, base_types_impl_1.fileToString)(descriptor._from % 16) + 'x'; } result += (0, base_types_impl_1.squareToString)(descriptor._to); if (descriptor.isPromotion()) { result += '=' + getPieceSymbol(descriptor._finalColoredPiece, pieceStyle); } } // Non-pawn move else { result += getPieceSymbol(descriptor._movingColoredPiece, pieceStyle); result += getDisambiguationSymbol(position, descriptor._from, descriptor._to); if (descriptor.isCapture()) { result += 'x'; } result += (0, base_types_impl_1.squareToString)(descriptor._to); } // Check/checkmate detection and final result. result += getCheckCheckmateSymbol(position, descriptor); return result; } /** * Return a string representing the given chess piece according to the given style. */ function getPieceSymbol(coloredPiece, pieceStyle) { switch (pieceStyle) { case 'figurine': return (0, base_types_impl_1.figurineToString)(coloredPiece); case 'standard': return (0, base_types_impl_1.pieceToString)(Math.trunc(coloredPiece / 2)).toUpperCase(); } } /** * Return the check/checkmate symbol to use for a move. */ function getCheckCheckmateSymbol(position, descriptor) { const nextPosition = (0, impl_1.makeCopy)(position); (0, move_generation_1.play)(nextPosition, descriptor); return (0, move_generation_1.isCheckmate)(nextPosition) ? '#' : (0, move_generation_1.isCheck)(nextPosition) ? '+' : ''; } /** * Return the disambiguation symbol to use for a move from `from` to `to`. */ function getDisambiguationSymbol(position, from, to) { const attackers = (0, attacks_1.getAttacks)(position, to, position.turn).filter(sq => position.board[sq] === position.board[from]); // Disambiguation is not necessary if there less than 2 attackers. if (attackers.length < 2) { return ''; } let foundNotPined = false; let foundOnSameRank = false; let foundOnSameFile = false; const rankFrom = Math.trunc(from / 16); const fileFrom = from % 16; for (const sq of attackers) { if (sq === from || isPinned(position, sq, to)) { continue; } foundNotPined = true; if (rankFrom === Math.trunc(sq / 16)) { foundOnSameRank = true; } if (fileFrom === sq % 16) { foundOnSameFile = true; } } if (foundOnSameFile) { return foundOnSameRank ? (0, base_types_impl_1.squareToString)(from) : (0, base_types_impl_1.rankToString)(rankFrom); } else { return foundNotPined ? (0, base_types_impl_1.fileToString)(fileFrom) : ''; } } /** * Whether the piece on the given square is pinned or not. */ function isPinned(position, sq, aimingAtSq) { const kingSquare = position.king[position.turn]; if (kingSquare < 0) { return false; } const vector = Math.abs(kingSquare - sq); const aimingAtVector = Math.abs(aimingAtSq - sq); const pinnerQueen = 1 /* PieceImpl.QUEEN */ * 2 + 1 - position.turn; const pinnerRook = 2 /* PieceImpl.ROOK */ * 2 + 1 - position.turn; const pinnerBishop = 3 /* PieceImpl.BISHOP */ * 2 + 1 - position.turn; // Potential pinning on file or rank. if (vector < 8) { return aimingAtVector >= 8 && pinningLoockup(position, kingSquare, sq, kingSquare < sq ? 1 : -1, pinnerRook, pinnerQueen); } else if (vector % 16 === 0) { return aimingAtVector % 16 !== 0 && pinningLoockup(position, kingSquare, sq, kingSquare < sq ? 16 : -16, pinnerRook, pinnerQueen); } // Potential pinning on diagonal. else if (vector % 15 === 0) { return aimingAtVector % 15 !== 0 && pinningLoockup(position, kingSquare, sq, kingSquare < sq ? 15 : -15, pinnerBishop, pinnerQueen); } else if (vector % 17 === 0) { return aimingAtVector % 17 !== 0 && pinningLoockup(position, kingSquare, sq, kingSquare < sq ? 17 : -17, pinnerBishop, pinnerQueen); } // No pinning for sure. else { return false; } } function pinningLoockup(position, kingSquare, targetSquare, direction, pinnerColoredPiece1, pinnerColoredPiece2) { for (let sq = kingSquare + direction; sq !== targetSquare; sq += direction) { if (position.board[sq] !== -1 /* SpI.EMPTY */) { return false; } } for (let sq = targetSquare + direction; (sq & 0x88) === 0; sq += direction) { if (position.board[sq] !== -1 /* SpI.EMPTY */) { return position.board[sq] === pinnerColoredPiece1 || position.board[sq] === pinnerColoredPiece2; } } return false; } /** * Parse a move notation for the given position. */ function parseNotation(position, notation, strict, pieceStyle) { // General syntax const m = /^(?:(O-O-O|0-0-0)|(O-O|0-0)|([A-Z\u2654-\u265f])([a-h])?([1-8])?(x)?([a-h][1-8])|(?:([a-h])(x)?)?([a-h][1-8])(?:(=)?([A-Z\u2654-\u265f]))?)([+#])?$/.exec(notation); if (m === null) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_MOVE_NOTATION_SYNTAX); } // Ensure that the position is legal. if (!(0, legality_1.isLegal)(position)) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.ILLEGAL_POSITION); } // CASTLING // m[1] -> O-O-O // m[2] -> O-O // NON-PAWN MOVE // m[3] -> moving piece // m[4] -> file disambiguation // m[5] -> rank disambiguation // m[6] -> x (capture symbol) // m[7] -> to // PAWN MOVE // m[ 8] -> from column (only for captures) // m[ 9] -> x (capture symbol) // m[10] -> to // m[11] -> = (promotion symbol) // m[12] -> promoted piece // OTHER // m[13] -> +/# (check/checkmate symbol) let descriptor = false; // Parse castling moves if (m[1] !== undefined || m[2] !== undefined) { descriptor = parseCastlingNotation(position, notation, strict, m[1], m[2]); } // Non-pawn move else if (m[3] !== undefined) { descriptor = parseNonPawnNotation(position, notation, strict, pieceStyle, m[3], m[4], m[5], m[7]); } // Pawn move else { descriptor = parsePawnMoveNotation(position, notation, strict, pieceStyle, m[8], m[10], m[11], m[12]); } // STRICT MODE if (strict) { const observedIsCapture = m[6] !== undefined || m[9] !== undefined; if (descriptor.isCapture() !== observedIsCapture) { const message = descriptor.isCapture() ? i18n_1.i18n.MISSING_CAPTURE_SYMBOL : i18n_1.i18n.INVALID_CAPTURE_SYMBOL; throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, message); } const expectedCCS = getCheckCheckmateSymbol(position, descriptor); const observedCCS = m[13] ?? ''; if (expectedCCS !== observedCCS) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.WRONG_CHECK_CHECKMATE_SYMBOL, expectedCCS, observedCCS); } } // Final result return descriptor; } /** * Delegate function that computes the move descriptor corresponding to a castling move (corresponding notation: "O-O" or "O-O-O"). */ function parseCastlingNotation(position, notation, strict, queenSideCastlingSymbol, kingSideCastlingSymbol) { const from = position.king[position.turn]; if (from < 0) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.ILLEGAL_NO_KING_CASTLING); } (0, legality_1.refreshEffectiveCastling)(position); const isKingSideCastling = kingSideCastlingSymbol !== undefined; const toFile = getCastlingDestinationFile(position, isKingSideCastling); const descriptor = toFile >= 0 ? (0, move_generation_1.isCastlingMoveLegal)(position, from, toFile + 112 * position.turn) : false; if (!descriptor) { const message = isKingSideCastling ? i18n_1.i18n.ILLEGAL_KING_SIDE_CASTLING : i18n_1.i18n.ILLEGAL_QUEEN_SIDE_CASTLING; throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, message); } // STRICT-MODE -> ensure that upper-case O is used instead of digit 0. if (strict) { const firstChar = (isKingSideCastling ? kingSideCastlingSymbol : queenSideCastlingSymbol).charAt(0); if (firstChar === '0') { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.CASTLING_MOVE_ENCODED_WITH_ZERO); } } return descriptor; } /** * Returns the file of a `to` square to take into account to check whether a castling move is legal or not. */ function getCastlingDestinationFile(position, isKingSideCastling) { if (position.variant === 1 /* GameVariantImpl.CHESS960 */) { if (position.effectiveCastling[position.turn] !== 0) { const castlingKing = 0 /* PieceImpl.KING */ * 2 + position.turn; for (let file = isKingSideCastling ? 7 : 0; position.board[file + 112 * position.turn] !== castlingKing; file += isKingSideCastling ? -1 : 1) { if ((position.effectiveCastling[position.turn] & 1 << file) !== 0) { return file; } } } return -1; } else { return isKingSideCastling ? 6 : 2; } } /** * Delegate function that computes the move descriptor corresponding to a non-pawn move. Corresponding notation, for instance "Ne3xd5": * * - N: piece symbol * - e: file disambiguation * - 3: rank disambiguation * - d5: destination square */ function parseNonPawnNotation(position, notation, strict, pieceStyle, pieceSymbol, fileDisambiguation, rankDisambiguation, destinationSquare) { const movingColoredPiece = parsePieceSymbol(position, notation, pieceSymbol, strict, pieceStyle) * 2 + position.turn; const to = (0, base_types_impl_1.squareFromString)(destinationSquare); const toContent = position.board[to]; // Cannot take your own pieces! if (toContent !== -1 /* SpI.EMPTY */ && toContent % 2 === position.turn) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.TRYING_TO_CAPTURE_YOUR_OWN_PIECES); } // Capture may be mandatory in some variants. if (toContent === -1 /* SpI.EMPTY */ && (0, move_generation_1.isCaptureMandatory)(position)) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.CAPTURE_IS_MANDATORY); } // Find the "from"-square candidates let attackers = (0, attacks_1.getAttacks)(position, to, position.turn).filter(sq => position.board[sq] === movingColoredPiece); // Apply disambiguation if (fileDisambiguation !== undefined) { const fileFrom = (0, base_types_impl_1.fileFromString)(fileDisambiguation); attackers = attackers.filter(sq => sq % 16 === fileFrom); } if (rankDisambiguation !== undefined) { const rankFrom = (0, base_types_impl_1.rankFromString)(rankDisambiguation); attackers = attackers.filter(sq => Math.trunc(sq / 16) === rankFrom); } if (attackers.length === 0) { const message = fileDisambiguation === undefined && rankDisambiguation === undefined ? i18n_1.i18n.NO_PIECE_CAN_MOVE_TO : i18n_1.i18n.NO_PIECE_CAN_MOVE_TO_DISAMBIGUATION; throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, message, pieceSymbol, destinationSquare); } // Compute the move descriptor for each remaining "from"-square candidate let descriptor = false; for (const sq of attackers) { if ((0, legality_1.isKingSafeAfterMove)(position, sq, to)) { if (descriptor) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.REQUIRE_DISAMBIGUATION, pieceSymbol, destinationSquare); } descriptor = move_descriptor_impl_1.MoveDescriptorImpl.make(sq, to, movingColoredPiece, toContent); } } if (!descriptor) { const message = position.turn === 0 /* ColorImpl.WHITE */ ? i18n_1.i18n.NOT_SAFE_FOR_WHITE_KING : i18n_1.i18n.NOT_SAFE_FOR_BLACK_KING; throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, message); } // STRICT-MODE -> check the disambiguation symbol. if (strict) { const expectedDS = getDisambiguationSymbol(position, descriptor._from, to); const observedDS = (fileDisambiguation ?? '') + (rankDisambiguation ?? ''); if (expectedDS !== observedDS) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.WRONG_DISAMBIGUATION_SYMBOL, expectedDS, observedDS); } } return descriptor; } /** * Delegate function that computes the move descriptor corresponding to a pawn move. Corresponding notation, for instance "fxg8=Q": * * - f: origin file * - g8: destination square * - =: promotion symbol * - Q: promoted piece */ function parsePawnMoveNotation(position, notation, strict, pieceStyle, originFile, destinationSquare, promotionSymbol, promotedPiece) { const coloredPawn = 5 /* PieceImpl.PAWN */ * 2 + position.turn; const to = (0, base_types_impl_1.squareFromString)(destinationSquare); const toContent = position.board[to]; const vector = 16 - position.turn * 32; let from = to - vector; let enPassantSquare = -1; if (originFile !== undefined) { // Capturing pawn move // Ensure that `to` is not on the 1st row. if ((from & 0x88) !== 0) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_CAPTURING_PAWN_MOVE); } // Compute the "from"-square. const columnFrom = (0, base_types_impl_1.fileFromString)(originFile); const columnTo = to % 16; if (columnTo - columnFrom === 1) { from -= 1; } else if (columnTo - columnFrom === -1) { from += 1; } else { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_CAPTURING_PAWN_MOVE); } // Check the content of the "from"-square if (position.board[from] !== coloredPawn) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_CAPTURING_PAWN_MOVE); } // Check the content of the "to"-square if (toContent === -1 /* SpI.EMPTY */) { // Look for en-passant captures (0, legality_1.refreshEffectiveEnPassant)(position); if (to !== (5 - position.turn * 3) * 16 + position.effectiveEnPassant) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_CAPTURING_PAWN_MOVE); } enPassantSquare = (4 - position.turn) * 16 + position.effectiveEnPassant; } else if (toContent % 2 === position.turn) { // detecting regular captures throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_CAPTURING_PAWN_MOVE); } } else if ((0, move_generation_1.isCaptureMandatory)(position)) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.CAPTURE_IS_MANDATORY); } else { // Non-capturing pawn move // Ensure that `to` is not on the 1st row. if ((from & 0x88) !== 0) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_NON_CAPTURING_PAWN_MOVE); } // Check the content of the "to"-square if (toContent !== -1 /* SpI.EMPTY */) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_NON_CAPTURING_PAWN_MOVE); } // Check the content of the "from"-square if (position.board[from] === -1 /* SpI.EMPTY */) { // Look for two-square pawn moves from -= vector; const firstSquareOfArea = position.turn * 96; // a1 for white, a7 for black (2-square pawn move is allowed from 1st row at horde chess) if (from < firstSquareOfArea || from >= firstSquareOfArea + 24 || position.board[from] !== coloredPawn) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_NON_CAPTURING_PAWN_MOVE); } } else if (position.board[from] !== coloredPawn) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_NON_CAPTURING_PAWN_MOVE); } } // Ensure that the pawn move do not let a king in check. if (!(0, legality_1.isKingSafeAfterMove)(position, from, to, enPassantSquare)) { const message = position.turn === 0 /* ColorImpl.WHITE */ ? i18n_1.i18n.NOT_SAFE_FOR_WHITE_KING : i18n_1.i18n.NOT_SAFE_FOR_BLACK_KING; throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, message); } // Promotions if (to < 8 || to >= 112) { if (promotedPiece === undefined) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.MISSING_PROMOTION); } const promotion = parsePieceSymbol(position, notation, promotedPiece, strict, pieceStyle); if (promotion === 5 /* PieceImpl.PAWN */ || (promotion === 0 /* PieceImpl.KING */ && position.variant !== 5 /* GameVariantImpl.ANTICHESS */)) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_PROMOTED_PIECE, promotedPiece); } // STRICT MODE -> do not forget the `=` character! if (strict && promotionSymbol === undefined) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.MISSING_PROMOTION_SYMBOL); } return move_descriptor_impl_1.MoveDescriptorImpl.makePromotion(from, to, position.turn, toContent, promotion); } // Non-promotion moves else { if (promotedPiece !== undefined) { // Detect illegal promotion attempts! throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.ILLEGAL_PROMOTION); } return enPassantSquare >= 0 ? move_descriptor_impl_1.MoveDescriptorImpl.makeEnPassant(from, to, enPassantSquare, position.turn) : move_descriptor_impl_1.MoveDescriptorImpl.make(from, to, coloredPawn, toContent); } } /** * Delegate function for piece symbol parsing. */ function parsePieceSymbol(position, notation, pieceSymbol, strict, pieceStyle) { switch (pieceStyle) { case 'figurine': { const coloredPieceCode = (0, base_types_impl_1.figurineFromString)(pieceSymbol); if (coloredPieceCode < 0) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_PIECE_SYMBOL, pieceSymbol); } if (strict && coloredPieceCode % 2 !== position.turn) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_PIECE_SYMBOL_COLOR, pieceSymbol); } return Math.trunc(coloredPieceCode / 2); } case 'standard': { const pieceCode = (0, base_types_impl_1.pieceFromString)(pieceSymbol.toLowerCase()); if (pieceCode < 0) { throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_PIECE_SYMBOL, pieceSymbol); } return pieceCode; } } } //# sourceMappingURL=notation.js.map