UNPKG

kokopu

Version:

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

367 lines 16.3 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.readDatabase = readDatabase; exports.readOneGame = readOneGame; const database_1 = require("../database"); const date_value_1 = require("../date_value"); const exception_1 = require("../exception"); const game_1 = require("../game"); const helper_1 = require("../helper"); const i18n_1 = require("../i18n"); const node_variation_1 = require("../node_variation"); const position_1 = require("../position"); const token_stream_1 = require("./token_stream"); function parseNullableHeader(value) { return value === '?' ? undefined : value; } function parsePositiveIntegerHeader(value) { if (/^\d+$/.test(value)) { const result = Number(value); if (Number.isInteger(result)) { return result; } } return undefined; } function parseRoundHeader(value) { if (/^(\?|\d+)(?:\.(\?|\d+)(?:\.(\?|\d+)(?:\.(?:\?|\d+))*)?)?$/.test(value)) { const round = Number(RegExp.$1); const subRound = RegExp.$2 ? Number(RegExp.$2) : undefined; const subSubRound = RegExp.$3 ? Number(RegExp.$3) : undefined; return { round: Number.isInteger(round) ? round : undefined, subRound: Number.isInteger(subRound) ? subRound : undefined, subSubRound: Number.isInteger(subSubRound) ? subSubRound : undefined, }; } return { round: undefined, subRound: undefined, subSubRound: undefined }; } function parseECOHeader(value) { return (0, helper_1.isValidECO)(value) ? value : undefined; } function parseVariant(value) { value = value.toLowerCase(); if (value === 'regular' || value === 'standard') { return 'regular'; } else if (value === 'fischerandom' || /^chess[ -]?960$/.test(value)) { return 'chess960'; } else if (/^no[ -]king$/.test(value)) { return 'no-king'; } else if (/^white[ -]king[ -]only$/.test(value)) { return 'white-king-only'; } else if (/^black[ -]king[ -]only$/.test(value)) { return 'black-king-only'; } else if (/^anti[ -]?chess/.test(value)) { return 'antichess'; } else if (value === 'horde') { return 'horde'; } else { return undefined; } } function processHeader(stream, game, factory, key, value, valueCharacterIndex, valueLineIndex) { value = value.trim(); switch (key) { case 'White': game.playerName('w', parseNullableHeader(value)); break; case 'Black': game.playerName('b', parseNullableHeader(value)); break; case 'WhiteElo': game.playerElo('w', parsePositiveIntegerHeader(value)); break; case 'BlackElo': game.playerElo('b', parsePositiveIntegerHeader(value)); break; case 'WhiteTitle': game.playerTitle('w', value); break; case 'BlackTitle': game.playerTitle('b', value); break; case 'Event': game.event(parseNullableHeader(value)); break; case 'Round': { const { round, subRound, subSubRound } = parseRoundHeader(value); game.round(round); game.subRound(subRound); game.subSubRound(subSubRound); break; } case 'Date': game.date(date_value_1.DateValue.fromPGNString(value)); break; case 'Site': game.site(parseNullableHeader(value)); break; case 'Annotator': game.annotator(value); break; case 'ECO': game.eco(parseECOHeader(value)); break; case 'Opening': game.opening(value); break; case 'Variation': game.openingVariation(value); break; case 'SubVariation': game.openingSubVariation(value); break; case 'Termination': game.termination(value); break; // The header 'FEN' has a special meaning, in that it is used to define a custom // initial position, that may be different from the usual one. case 'FEN': factory.fen = value; factory.fenTokenCharacterIndex = valueCharacterIndex; factory.fenTokenLineIndex = valueLineIndex; break; // The header 'Variant' indicates that this is not a regular chess game. case 'Variant': factory.variant = parseVariant(value); if (factory.variant === undefined) { throw new exception_1.InvalidPGN(stream.text(), valueCharacterIndex, valueLineIndex, i18n_1.i18n.UNKNOWN_VARIANT, value); } factory.variantTokenCharacterIndex = valueCharacterIndex; factory.variantTokenLineIndex = valueLineIndex; break; } } function initializeInitialPosition(stream, game, factory) { // If a FEN header has been encountered, set-up the initial position with it, taking the optional variant into account. if (factory.fen !== undefined) { try { const position = factory.variant === undefined ? new position_1.Position() : new position_1.Position(factory.variant, 'empty'); const moveCounters = position.fen(factory.fen); game.initialPosition(position, moveCounters.fullMoveNumber); } catch (error) { // istanbul ignore else if (error instanceof exception_1.InvalidFEN) { throw new exception_1.InvalidPGN(stream.text(), factory.fenTokenCharacterIndex, factory.fenTokenLineIndex, i18n_1.i18n.INVALID_FEN_IN_PGN_TEXT, error.message); } else { throw error; } } } // Otherwise, if a variant header has been encountered, but without FEN header... else if (factory.variant !== undefined) { if ((0, helper_1.variantWithCanonicalStartPosition)(factory.variant)) { const position = new position_1.Position(factory.variant, 'start'); game.initialPosition(position, 1); } else { throw new exception_1.InvalidPGN(stream.text(), factory.variantTokenCharacterIndex, factory.variantTokenLineIndex, i18n_1.i18n.VARIANT_WITHOUT_FEN, factory.variant); } } // If neither a variant header nor a FEN header has been encountered, nothing to do (the default initial position as defined in the `Game` object // is the right one). } /** * Parse exactly 1 game from the given stream. */ function doParseGame(stream) { // State variable for syntactic analysis. const game = new game_1.Game(); // the result let endOfGameEncountered = false; let atLeastOneTokenFound = false; let node = null; // current node (or variation) to which the next move should be appended const nodeStack = []; // when starting a variation, its parent node (btw., always a "true" node, not a variation) is stacked here const initialPositionFactory = {}; // Token loop while (!endOfGameEncountered && stream.consumeToken()) { atLeastOneTokenFound = true; // Set-up the root node when the first move-text token is encountered. if (stream.isMoveTextSection() && node === null) { initializeInitialPosition(stream, game, initialPositionFactory); node = game.mainVariation(); } // Token type switch switch (stream.token()) { // Header case 1 /* TokenType.BEGIN_HEADER */: { if (node !== null) { throw new exception_1.InvalidPGN(stream.text(), stream.tokenCharacterIndex(), stream.tokenLineIndex(), i18n_1.i18n.UNEXPECTED_PGN_HEADER); } if (!stream.consumeToken() || stream.token() !== 3 /* TokenType.HEADER_ID */) { throw new exception_1.InvalidPGN(stream.text(), stream.tokenCharacterIndex(), stream.tokenLineIndex(), i18n_1.i18n.MISSING_PGN_HEADER_ID); } const headerId = stream.tokenValue(); if (!stream.consumeToken() || stream.token() !== 4 /* TokenType.HEADER_VALUE */) { throw new exception_1.InvalidPGN(stream.text(), stream.tokenCharacterIndex(), stream.tokenLineIndex(), i18n_1.i18n.MISSING_PGN_HEADER_VALUE); } const headerValue = stream.tokenValue(); const headerValueCharacterIndex = stream.tokenCharacterIndex(); const headerValueLineIndex = stream.tokenLineIndex(); if (!stream.consumeToken() || stream.token() !== 2 /* TokenType.END_HEADER */) { throw new exception_1.InvalidPGN(stream.text(), stream.tokenCharacterIndex(), stream.tokenLineIndex(), i18n_1.i18n.MISSING_END_OF_PGN_HEADER); } processHeader(stream, game, initialPositionFactory, headerId, headerValue, headerValueCharacterIndex, headerValueLineIndex); break; } // Move number case 5 /* TokenType.MOVE_NUMBER */: break; // Move or null-move case 6 /* TokenType.MOVE */: try { node = node.play(stream.tokenValue()); } catch (error) { // istanbul ignore else if (error instanceof exception_1.InvalidNotation) { throw new exception_1.InvalidPGN(stream.text(), stream.tokenCharacterIndex(), stream.tokenLineIndex(), i18n_1.i18n.INVALID_MOVE_IN_PGN_TEXT, error.notation, error.message); } else { throw error; } } break; // NAG case 7 /* TokenType.NAG */: node.addNag(stream.tokenValue()); break; // Comment case 8 /* TokenType.COMMENT */: { const { comment, tags } = stream.tokenValue(); for (const [key, value] of tags) { node.tag(key, value); } if (comment !== undefined) { if (node.comment() === undefined) { const isLongComment = node instanceof node_variation_1.Variation ? stream.emptyLineAfterToken() : stream.emptyLineBeforeToken(); node.comment(comment, isLongComment); } else { // Concatenate the current comment to the previous one, if any. const isLongComment = node.isLongComment(); node.comment(node.comment() + ' ' + comment, isLongComment); } } break; } // Begin of variation case 9 /* TokenType.BEGIN_VARIATION */: if (node instanceof node_variation_1.Variation) { throw new exception_1.InvalidPGN(stream.text(), stream.tokenCharacterIndex(), stream.tokenLineIndex(), i18n_1.i18n.UNEXPECTED_BEGIN_OF_VARIATION); } nodeStack.push(node); node = node.addVariation(stream.emptyLineBeforeToken()); break; // End of variation case 10 /* TokenType.END_VARIATION */: if (nodeStack.length === 0) { throw new exception_1.InvalidPGN(stream.text(), stream.tokenCharacterIndex(), stream.tokenLineIndex(), i18n_1.i18n.UNEXPECTED_END_OF_VARIATION); } node = nodeStack.pop(); break; // End-of-game case 11 /* TokenType.END_OF_GAME */: endOfGameEncountered = true; game.result(stream.tokenValue()); break; // Something unexpected... default: throw new exception_1.InvalidPGN(stream.text(), stream.tokenCharacterIndex(), stream.tokenLineIndex(), i18n_1.i18n.INVALID_PGN_TOKEN); } // switch(stream.token()) } // while(stream.consumeToken()) if (nodeStack.length !== 0) { throw new exception_1.InvalidPGN(stream.text(), stream.tokenCharacterIndex(), stream.tokenLineIndex(), i18n_1.i18n.UNEXPECTED_END_OF_GAME); } else { return { game: game, atLeastOneTokenFound: atLeastOneTokenFound }; } } /** * Implementation of {@link Database} for a PGN reader. */ class PGNDatabaseImpl extends database_1.Database { constructor(pgnString) { super(); this._currentGameIndex = -1; this._text = pgnString; this._gameLocations = []; this._stream = new token_stream_1.TokenStream(pgnString); while (true) { const currentLocation = this._stream.currentLocation(); if (!this._stream.skipGame()) { break; } this._gameLocations.push(currentLocation); } } doGameCount() { return this._gameLocations.length; } doGame(gameIndex) { if (gameIndex >= this._gameLocations.length) { throw new exception_1.InvalidPGN(this._text, -1, -1, i18n_1.i18n.INVALID_GAME_INDEX, gameIndex, this._gameLocations.length); } if (this._currentGameIndex !== gameIndex) { this._stream = new token_stream_1.TokenStream(this._text, this._gameLocations[gameIndex]); } this._currentGameIndex = -1; const { game } = doParseGame(this._stream); this._currentGameIndex = gameIndex + 1; return game; } } /** * Read a PGN string and return a {@link Database} object. */ function readDatabase(pgnString) { return new PGNDatabaseImpl(pgnString); } /** * Read exactly 1 {@link Game} within the given PGN string. */ function readOneGame(pgnString, gameIndex) { const stream = new token_stream_1.TokenStream(pgnString); let gameCounter = 0; while (gameCounter !== gameIndex) { if (!stream.skipGame()) { throw new exception_1.InvalidPGN(pgnString, -1, -1, i18n_1.i18n.INVALID_GAME_INDEX, gameIndex, gameCounter); } ++gameCounter; } const { game, atLeastOneTokenFound } = doParseGame(stream); if (!atLeastOneTokenFound) { throw new exception_1.InvalidPGN(pgnString, -1, -1, i18n_1.i18n.INVALID_GAME_INDEX, gameIndex, gameCounter); } return game; } //# sourceMappingURL=pgn_read_impl.js.map