UNPKG

kokopu

Version:

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

359 lines 15.5 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.ascii = ascii; exports.getFEN = getFEN; exports.parseFEN = parseFEN; const base_types_impl_1 = require("./base_types_impl"); const impl_1 = require("./impl"); const legality_1 = require("./legality"); const exception_1 = require("../exception"); const i18n_1 = require("../i18n"); const FEN_PIECE_SYMBOL = [...'KkQqRrBbNnPp']; const EN_PASSANT_RANK = ['6', '3']; /** * 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). */ function ascii(position, { flipped = false, prefix = '', coordinateVisible = false }) { // Board scanning const lineSeparator = prefix + (coordinateVisible ? ' ' : '') + '+---+---+---+---+---+---+---+---+\n'; const rows = flipped ? [0, 1, 2, 3, 4, 5, 6, 7] : [7, 6, 5, 4, 3, 2, 1, 0]; const columns = flipped ? [7, 6, 5, 4, 3, 2, 1, 0] : [0, 1, 2, 3, 4, 5, 6, 7]; let result = lineSeparator; for (const r of rows) { result += prefix; if (coordinateVisible) { result += (r + 1) + ' '; } for (const c of columns) { const cp = position.board[r * 16 + c]; result += '| ' + (cp === -1 /* SpI.EMPTY */ ? ' ' : FEN_PIECE_SYMBOL[cp]) + ' '; } result += '|\n'; result += lineSeparator; } if (coordinateVisible) { result += prefix + (flipped ? ' h g f e d c b a\n' : ' a b c d e f g h\n'); } // Flags result += prefix + (0, base_types_impl_1.colorToString)(position.turn) + ' ' + castlingToString(position) + ' ' + enPassantToString(position); if (position.variant !== 0 /* GameVariantImpl.REGULAR_CHESS */) { result += ' (' + (0, base_types_impl_1.variantToString)(position.variant) + ')'; } return result; } function getFEN(position, fiftyMoveClock = 0, fullMoveNumber = 1, regularFENIfPossible = false) { let result = ''; // Board scanning for (let r = 7; r >= 0; --r) { let emptyCount = 0; for (let c = 0; c < 8; ++c) { const cp = position.board[r * 16 + c]; if (cp === -1 /* SpI.EMPTY */) { ++emptyCount; } else { if (emptyCount > 0) { result += emptyCount; emptyCount = 0; } result += FEN_PIECE_SYMBOL[cp]; } } if (emptyCount > 0) { result += emptyCount; } if (r > 0) { result += '/'; } } // Flags + additional move counters result += ' ' + (0, base_types_impl_1.colorToString)(position.turn) + ' ' + castlingToString(position, regularFENIfPossible) + ' ' + enPassantToString(position); result += ' ' + fiftyMoveClock + ' ' + fullMoveNumber; return result; } /** * @param regularFENIfPossible - For Chess960, if `true`, format the flags as `KQkq` (regular FEN style) if possible * (instead of `AB...Hab...h` which is used by default, i.e. X-FEN style). * For the other variants, this flag has no effect, as regulary FEN style is always used. */ function castlingToString(position, regularFENIfPossible = false) { (0, legality_1.refreshEffectiveCastling)(position); if (position.variant === 1 /* GameVariantImpl.CHESS960 */) { if (regularFENIfPossible) { const whiteRegularFlags = regularFENCaslingFlagIfPossible(position, 0 /* ColorImpl.WHITE */); const blackRegularFlags = regularFENCaslingFlagIfPossible(position, 1 /* ColorImpl.BLACK */); if (whiteRegularFlags !== false && blackRegularFlags !== false) { return whiteRegularFlags === '' && blackRegularFlags === '' ? '-' : whiteRegularFlags.toUpperCase() + blackRegularFlags; } } let whiteFlags = ''; let blackFlags = ''; for (let file = 0; file < 8; ++file) { if (position.effectiveCastling[0 /* ColorImpl.WHITE */] & 1 << file) { whiteFlags += (0, base_types_impl_1.fileToString)(file); } if (position.effectiveCastling[1 /* ColorImpl.BLACK */] & 1 << file) { blackFlags += (0, base_types_impl_1.fileToString)(file); } } return whiteFlags === '' && blackFlags === '' ? '-' : whiteFlags.toUpperCase() + blackFlags; } else { let result = ''; if (position.effectiveCastling[0 /* ColorImpl.WHITE */] & 0x80) { result += 'K'; } if (position.effectiveCastling[0 /* ColorImpl.WHITE */] & 0x01) { result += 'Q'; } if (position.effectiveCastling[1 /* ColorImpl.BLACK */] & 0x80) { result += 'k'; } if (position.effectiveCastling[1 /* ColorImpl.BLACK */] & 0x01) { result += 'q'; } return result === '' ? '-' : result; } } function regularFENCaslingFlagIfPossible(position, color) { // Decompose the castling flags into: // // +---------------+---+--------------+ // | queenSideMask | 0 | kingSideMask | // +---------------+---+--------------+ // ^ ^ ^ // File a King file File h // const kingFileMask = 1 << (position.king[color] % 16); const kingSideMask = position.effectiveCastling[color] & ~(kingFileMask | (kingFileMask - 1)); const queenSideMask = position.effectiveCastling[color] & (kingFileMask - 1); let fenFlag = ''; const firstSquare = 112 * color; const lastSquare = 112 * color + 7; const targetRook = 2 /* PieceImpl.ROOK */ * 2 + color; // Search for the rooks on king-side. if (kingSideMask !== 0) { let rookFound = false; for (let sq = position.king[color] + 1; sq <= lastSquare; ++sq) { if (position.board[sq] === targetRook) { if (rookFound) { // Ensure there is only 1 rook on the king side. return false; } else { rookFound = true; } } } fenFlag += 'k'; } // Search for the rooks on queen-side. if (queenSideMask !== 0) { let rookFound = false; for (let sq = position.king[color] - 1; sq >= firstSquare; --sq) { if (position.board[sq] === targetRook) { if (rookFound) { // Ensure there is only 1 rook on the queen side. return false; } else { rookFound = true; } } } fenFlag += 'q'; } return fenFlag; } function enPassantToString(position) { (0, legality_1.refreshEffectiveEnPassant)(position); return position.effectiveEnPassant < 0 ? '-' : (0, base_types_impl_1.fileToString)(position.effectiveEnPassant) + EN_PASSANT_RANK[position.turn]; } function parseFEN(variant, fen, strict) { // Trim the input string and split it into 6 fields. const fields = strict ? fen.split(' ') : fen.replace(/^\s+|\s+$/g, '').split(/\s+/); if (fields.length !== 6) { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.WRONG_NUMBER_OF_FEN_FIELDS); } // The first field (that represents the board) is split in 8 sub-fields. const rankFields = fields[0].split('/'); if (rankFields.length !== 8) { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.WRONG_NUMBER_OF_SUBFIELDS_IN_BOARD_FIELD); } // Initialize the position const position = (0, impl_1.makeEmpty)(variant); position.legal = null; position.effectiveCastling = null; position.effectiveEnPassant = null; // Board parsing for (let r = 7; r >= 0; --r) { const rankField = rankFields[7 - r]; let i = 0; let c = 0; while (i < rankField.length && c < 8) { const s = rankField[i]; const cp = FEN_PIECE_SYMBOL.indexOf(s); // The current character is in the range [1-8] -> skip the corresponding number of squares. if (/^[1-8]$/.test(s)) { c += parseInt(s, 10); } // The current character corresponds to a colored piece symbol -> set the current square accordingly. else if (cp >= 0) { position.board[r * 16 + c] = cp; ++c; } // Otherwise -> parsing error. else { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.UNEXPECTED_CHARACTER_IN_BOARD_FIELD, s); } // Increment the character counter. ++i; } // Ensure that the current sub-field deals with all the squares of the current rank. if (i !== rankField.length || c !== 8) { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.UNEXPECTED_END_OF_SUBFIELD_IN_BOARD_FIELD, 8 - r); } } // Turn parsing position.turn = (0, base_types_impl_1.colorFromString)(fields[1]); if (position.turn < 0) { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INVALID_TURN_FIELD); } // Castling rights parsing const castling = variant === 1 /* GameVariantImpl.CHESS960 */ ? castlingFromStringXFEN(fields[2], strict, position.board) : castlingFromStringFEN(fields[2], strict); if (castling === null) { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INVALID_CASTLING_FIELD); } else { position.castling = castling; } // En-passant rights parsing const enPassantField = fields[3]; if (enPassantField !== '-') { if (!/^[a-h][36]$/.test(enPassantField)) { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INVALID_EN_PASSANT_FIELD); } position.enPassant = (0, base_types_impl_1.fileFromString)(enPassantField[0]); if (strict) { if (enPassantField[1] !== EN_PASSANT_RANK[position.turn]) { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.WRONG_RANK_IN_EN_PASSANT_FIELD); } (0, legality_1.refreshEffectiveEnPassant)(position); if (position.enPassant !== position.effectiveEnPassant) { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INEFFECTIVE_EN_PASSANT_FIELD, (0, base_types_impl_1.fileToString)(position.enPassant)); } } } // Move counting flags parsing const moveCountingRegex = strict ? /^(?:0|[1-9][0-9]*)$/ : /^[0-9]+$/; if (!moveCountingRegex.test(fields[4])) { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INVALID_HALF_MOVE_COUNT_FIELD); } if (!moveCountingRegex.test(fields[5])) { throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INVALID_MOVE_NUMBER_FIELD); } return { position: position, fiftyMoveClock: parseInt(fields[4], 10), fullMoveNumber: parseInt(fields[5], 10) }; } function castlingFromStringFEN(castling, strict) { const result = [0, 0]; if (castling === '-') { return result; } if (!(strict ? /^K?Q?k?q?$/ : /^[KQkq]*$/).test(castling)) { return null; } if (castling.indexOf('K') >= 0) { result[0 /* ColorImpl.WHITE */] |= 1 << 7; } if (castling.indexOf('Q') >= 0) { result[0 /* ColorImpl.WHITE */] |= 1 << 0; } if (castling.indexOf('k') >= 0) { result[1 /* ColorImpl.BLACK */] |= 1 << 7; } if (castling.indexOf('q') >= 0) { result[1 /* ColorImpl.BLACK */] |= 1 << 0; } return result; } function castlingFromStringXFEN(castling, strict, board) { const result = [0, 0]; if (castling === '-') { return result; } if (!(strict ? /^[A-H]{0,2}[a-h]{0,2}$/ : /^[A-Ha-h]*|[KQkq]*$/).test(castling)) { return null; } function searchQueenSideRook(color) { const targetRook = 2 /* PieceImpl.ROOK */ * 2 + color; const targetKing = 0 /* PieceImpl.KING */ * 2 + color; for (let sq = 112 * color; sq < 112 * color + 8; ++sq) { if (board[sq] === targetRook) { return sq % 8; } else if (board[sq] === targetKing) { break; } } return 0; } function searchKingSideRook(color) { const targetRook = 2 /* PieceImpl.ROOK */ * 2 + color; const targetKing = 0 /* PieceImpl.KING */ * 2 + color; for (let sq = 112 * color + 7; sq >= 112 * color; --sq) { if (board[sq] === targetRook) { return sq % 8; } else if (board[sq] === targetKing) { break; } } return 7; } if (!strict) { if (castling.indexOf('K') >= 0) { result[0 /* ColorImpl.WHITE */] |= 1 << searchKingSideRook(0 /* ColorImpl.WHITE */); } if (castling.indexOf('Q') >= 0) { result[0 /* ColorImpl.WHITE */] |= 1 << searchQueenSideRook(0 /* ColorImpl.WHITE */); } if (castling.indexOf('k') >= 0) { result[1 /* ColorImpl.BLACK */] |= 1 << searchKingSideRook(1 /* ColorImpl.BLACK */); } if (castling.indexOf('q') >= 0) { result[1 /* ColorImpl.BLACK */] |= 1 << searchQueenSideRook(1 /* ColorImpl.BLACK */); } } for (let file = 0; file < 8; ++file) { const s = (0, base_types_impl_1.fileToString)(file); if (castling.indexOf(s.toUpperCase()) >= 0) { result[0 /* ColorImpl.WHITE */] |= 1 << file; } if (castling.indexOf(s) >= 0) { result[1 /* ColorImpl.BLACK */] |= 1 << file; } } return result; } //# sourceMappingURL=fen.js.map