UNPKG

kokopu

Version:

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

691 lines 30.6 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.Game = void 0; const date_value_1 = require("./date_value"); const exception_1 = require("./exception"); const helper_1 = require("./helper"); const i18n_1 = require("./i18n"); const position_1 = require("./position"); const common_1 = require("./private_game/common"); const node_variation_impl_1 = require("./private_game/node_variation_impl"); const pojo_util_1 = require("./private_game/pojo_util"); const base_types_impl_1 = require("./private_position/base_types_impl"); /** * Chess game, with the move history, the position at each step of the game, the comments and annotations (if any), * the result of the game, and some meta-data such as the name of the players, the date of the game, the name of the tournament, etc... */ class Game { constructor() { this._playerName = [undefined, undefined]; this._playerElo = [undefined, undefined]; this._playerTitle = [undefined, undefined]; this._round = [undefined, undefined, undefined]; this._result = 3 /* GameResultImpl.LINE */; this._moveTreeRoot = new node_variation_impl_1.MoveTreeRoot(); } /** * Clear all the headers (player names, elos, titles, event name, date, etc...). * * The {@link Game.result} header is reseted to its default value. * The initial position and moves are not modified. */ clearHeaders() { this._playerName = [undefined, undefined]; this._playerElo = [undefined, undefined]; this._playerTitle = [undefined, undefined]; this._event = undefined; this._round = [undefined, undefined, undefined]; this._date = undefined; this._site = undefined; this._annotator = undefined; this._eco = undefined; this._opening = undefined; this._openingVariation = undefined; this._openingSubVariation = undefined; this._termination = undefined; this._result = 3 /* GameResultImpl.LINE */; } playerName(color, value) { const colorCode = (0, base_types_impl_1.colorFromString)(color); if (colorCode < 0) { throw new exception_1.IllegalArgument('Game.playerName()'); } if (arguments.length === 1) { return this._playerName[colorCode]; } else { this._playerName[colorCode] = sanitizeStringHeader(value); } } playerElo(color, value) { const colorCode = (0, base_types_impl_1.colorFromString)(color); if (colorCode < 0) { throw new exception_1.IllegalArgument('Game.playerElo()'); } if (arguments.length === 1) { return this._playerElo[colorCode]; } else { value = sanitizeNumberHeader(value); if (value === undefined || (0, common_1.isValidElo)(value)) { this._playerElo[colorCode] = value; } else { throw new exception_1.IllegalArgument('Game.playerElo()'); } } } playerTitle(color, value) { const colorCode = (0, base_types_impl_1.colorFromString)(color); if (colorCode < 0) { throw new exception_1.IllegalArgument('Game.playerTitle()'); } if (arguments.length === 1) { return this._playerTitle[colorCode]; } else { this._playerTitle[colorCode] = sanitizeStringHeader(value); } } event(value) { if (arguments.length === 0) { return this._event; } else { this._event = sanitizeStringHeader(value); } } round(value) { if (arguments.length === 0) { return this._round[0 /* RoundPart.ROUND */]; } else { this._setRoundPart(0 /* RoundPart.ROUND */, value, 'Game.round()'); } } subRound(value) { if (arguments.length === 0) { return this._round[1 /* RoundPart.SUB_ROUND */]; } else { this._setRoundPart(1 /* RoundPart.SUB_ROUND */, value, 'Game.subRound()'); } } subSubRound(value) { if (arguments.length === 0) { return this._round[2 /* RoundPart.SUB_SUB_ROUND */]; } else { this._setRoundPart(2 /* RoundPart.SUB_SUB_ROUND */, value, 'Game.subSubRound()'); } } _setRoundPart(roundPart, value, methodName) { value = sanitizeNumberHeader(value); if (value === undefined || (0, common_1.isValidRound)(value)) { this._round[roundPart] = value; } else { throw new exception_1.IllegalArgument(methodName); } } /** * Get the round, sub-round and sub-sub-round as a human-readable string, the 3 components being separated by dot characters. */ fullRound() { return formatFullRound(this._round[0 /* RoundPart.ROUND */], this._round[1 /* RoundPart.SUB_ROUND */], this._round[2 /* RoundPart.SUB_SUB_ROUND */], '?'); } date(valueOrYear, month, day) { switch (arguments.length) { case 0: return this._date; case 1: if (valueOrYear === undefined || valueOrYear === null) { this._date = undefined; } else if (valueOrYear instanceof date_value_1.DateValue) { this._date = valueOrYear; } else if (valueOrYear instanceof Date) { this._date = new date_value_1.DateValue(valueOrYear); } else if (date_value_1.DateValue.isValid(valueOrYear)) { this._date = new date_value_1.DateValue(valueOrYear); } else { throw new exception_1.IllegalArgument('Game.date()'); } break; default: if (date_value_1.DateValue.isValid(valueOrYear, month, day)) { this._date = new date_value_1.DateValue(valueOrYear, month, day); } else { throw new exception_1.IllegalArgument('Game.date()'); } break; } } /** * Get the date of the game as a standard JavaScript `Date` object. * * If the day of month is undefined for the current game, the returned `Date` object points at the first day of the corresponding month. * If neither the day of month nor the month are undefined for the current game, the returned `Date` object points at the first day of the corresponding year. */ dateAsDate() { return this._date === undefined ? undefined : this._date.toDate(); } /** * Get the date of the game as a human-readable string (e.g. `'November 1955'`, `'September 4, 2021'`). * * @param locales - Locales to use to generate the result. If undefined, the default locale of the execution environment is used. * See [Intl documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl#locale_identification_and_negotiation) * for more details. */ dateAsString(locales) { return this._date === undefined ? undefined : this._date.toHumanReadableString(locales); } site(value) { if (arguments.length === 0) { return this._site; } else { this._site = sanitizeStringHeader(value); } } annotator(value) { if (arguments.length === 0) { return this._annotator; } else { this._annotator = sanitizeStringHeader(value); } } eco(value) { if (arguments.length === 0) { return this._eco; } else { value = sanitizeStringHeader(value); if (value !== undefined && !(0, helper_1.isValidECO)(value)) { throw new exception_1.IllegalArgument('Game.eco()'); } this._eco = value; } } opening(value) { if (arguments.length === 0) { return this._opening; } else { this._opening = sanitizeStringHeader(value); } } openingVariation(value) { if (arguments.length === 0) { return this._openingVariation; } else { this._openingVariation = sanitizeStringHeader(value); } } openingSubVariation(value) { if (arguments.length === 0) { return this._openingSubVariation; } else { this._openingSubVariation = sanitizeStringHeader(value); } } termination(value) { if (arguments.length === 0) { return this._termination; } else { this._termination = sanitizeStringHeader(value); } } result(value) { if (arguments.length === 0) { return (0, base_types_impl_1.resultToString)(this._result); } else { const resultCode = (0, base_types_impl_1.resultFromString)(value); if (resultCode < 0) { throw new exception_1.IllegalArgument('Game.result()'); } this._result = resultCode; } } /** * Get the chess game variant of the game. */ variant() { return this._moveTreeRoot._position.variant(); } initialPosition(initialPosition, fullMoveNumber) { if (arguments.length === 0) { return new position_1.Position(this._moveTreeRoot._position); } else { if (!(initialPosition instanceof position_1.Position)) { throw new exception_1.IllegalArgument('Game.initialPosition()'); } if (arguments.length >= 2) { if (!Number.isInteger(fullMoveNumber)) { throw new exception_1.IllegalArgument('Game.initialPosition()'); } this._moveTreeRoot._fullMoveNumber = fullMoveNumber; } else { this._moveTreeRoot._fullMoveNumber = 1; } this._moveTreeRoot._position = new position_1.Position(initialPosition); this._moveTreeRoot.clearTree(); } } /** * [FEN](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation) representation of the chess position at the beginning of the game. * * The fifty-move clock and full-move number are set according to the move history in the string returned by this method. */ initialFEN() { return this._moveTreeRoot._position.fen({ fiftyMoveClock: 0, fullMoveNumber: this._moveTreeRoot._fullMoveNumber, }); } /** * Full-move number at which the game starts. */ initialFullMoveNumber() { return this._moveTreeRoot._fullMoveNumber; } /** * Chess position at the end of the game. */ finalPosition() { return this._moveTreeRoot.mainVariation().finalPosition(); } /** * [FEN](https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation) representation of the chess position at the end of the game. * * The fifty-move clock and full-move number are set according to the move history in the string returned by this method. */ finalFEN() { return this._moveTreeRoot.mainVariation().finalFEN(); } /** * The main variation of the game. */ mainVariation() { return this._moveTreeRoot.mainVariation(); } /** * Return the nodes corresponding to the moves of the main variation. * * @param withSubVariations - If `true`, the nodes of the sub-variations are also included in the result. */ nodes(withSubVariations = false) { if (!withSubVariations) { return this.mainVariation().nodes(); } const result = []; function processVariation(variation) { for (const currentNode of variation.nodes()) { for (const nextVariation of currentNode.variations()) { processVariation(nextVariation); } result.push(currentNode); } } processVariation(this.mainVariation()); return result; } /** * Number of half-moves in the main variation. * * For instance, after `1.e4 e5 2.Nf3`, the number of half-moves if 3 (2 white moves + 1 black move). */ plyCount() { return this._moveTreeRoot.mainVariation().plyCount(); } /** * Return the node or variation corresponding to the given ID (see {@link Node.id | Node.id} and {@link Variation.id | Variation.id} * to retrieve the ID of a node or variation). * * For the main variation, IDs are built as follows: * - `'start'` is the ID of the main variation, * - `'3w'` is for instance the ID of the node whose {@link Node.fullMoveNumber} is 3 and {@link Node.moveColor} is white * (i.e. the 3rd white move if the game starts from the usual initial position), * - `'end'` is an alias corresponding to the last node in the main variation (or the main variation itself if it is empty). * * For sub-variations, IDs are built as in the following examples: * - `'2b-v0-start'` is the ID of the sub-variation at index 0 on node `'2b'` (in the main variation), * - `'5w-v3-11b'` is the ID of the node whose {@link Node.fullMoveNumber} is 11 and {@link Node.moveColor} is black within * the sub-variation at index 3 on node `'5w'` (in the main variation), * - `'5w-v3-end'` is an alias corresponding to the last node in this sub-variation (or the sub-variation itself if it is empty). * * @param allowAliases - If `true`, search `id` among both IDs and ID aliases. If `false`, search among IDs only. * @returns `undefined` if the given ID does not correspond to an existing {@link Node} and {@link Variation}. */ findById(id, allowAliases = true) { return this._moveTreeRoot.findById(id, allowAliases); } /** * Return the [POJO](https://en.wikipedia.org/wiki/Plain_old_Java_object) representation of the current {@link Game}. * To be used for JSON serialization, deep cloning, etc... */ pojo() { const pojo = {}; function isPlayerPOJOEmpty(game, color) { return game._playerName[color] === undefined && game._playerElo[color] === undefined && game._playerTitle[color] === undefined; } function getPlayerPOJO(game, color) { const playerPOJO = {}; if (game._playerName[color] !== undefined) { playerPOJO.name = game._playerName[color]; } if (game._playerElo[color] !== undefined) { playerPOJO.elo = game._playerElo[color]; } if (game._playerTitle[color] !== undefined) { playerPOJO.title = game._playerTitle[color]; } return playerPOJO; } // Headers if (!isPlayerPOJOEmpty(this, 0 /* ColorImpl.WHITE */)) { pojo.white = getPlayerPOJO(this, 0 /* ColorImpl.WHITE */); } if (!isPlayerPOJOEmpty(this, 1 /* ColorImpl.BLACK */)) { pojo.black = getPlayerPOJO(this, 1 /* ColorImpl.BLACK */); } if (this._event !== undefined) { pojo.event = this._event; } if (this._round[0 /* RoundPart.ROUND */] !== undefined) { pojo.round = this._round[0 /* RoundPart.ROUND */]; } if (this._round[1 /* RoundPart.SUB_ROUND */] !== undefined) { pojo.subRound = this._round[1 /* RoundPart.SUB_ROUND */]; } if (this._round[2 /* RoundPart.SUB_SUB_ROUND */] !== undefined) { pojo.subSubRound = this._round[2 /* RoundPart.SUB_SUB_ROUND */]; } if (this._date !== undefined) { pojo.date = this._date.toString(); } if (this._site !== undefined) { pojo.site = this._site; } if (this._annotator !== undefined) { pojo.annotator = this._annotator; } if (this._eco !== undefined) { pojo.eco = this._eco; } if (this._opening !== undefined) { pojo.opening = this._opening; } if (this._openingVariation !== undefined) { pojo.openingVariation = this._openingVariation; } if (this._openingSubVariation !== undefined) { pojo.openingSubVariation = this._openingSubVariation; } if (this._termination !== undefined) { pojo.termination = this._termination; } if (this._result !== 3 /* GameResultImpl.LINE */) { pojo.result = this.result(); } // Moves this._moveTreeRoot.getPojo(pojo); return pojo; } /** * Decode the [POJO](https://en.wikipedia.org/wiki/Plain_old_Java_object) passed in argument, assuming it follows the schema defined by {@link GamePOJO}. * * @throws {@link exception.InvalidPOJO} if the given object cannot be decoded, either because it does not follow the schema defined by {@link GamePOJO}, * or because it would result in an inconsistent game (e.g. if it contains some invalid moves). */ static fromPOJO(pojo) { if (typeof pojo !== 'object' || pojo === null) { throw new exception_1.InvalidPOJO(pojo, '', i18n_1.i18n.POJO_MUST_BE_AN_OBJECT); } const game = new Game(); const exceptionBuilder = new pojo_util_1.POJOExceptionBuilder(pojo); function processPlayerPOJO(playerPOJO, color) { (0, pojo_util_1.decodeStringField)(playerPOJO, 'name', exceptionBuilder, value => { game._playerName[color] = value; }); (0, pojo_util_1.decodeNumberField)(playerPOJO, 'elo', exceptionBuilder, value => { if (!(0, common_1.isValidElo)(value)) { throw exceptionBuilder.build(i18n_1.i18n.INVALID_ELO_IN_POJO); } game._playerElo[color] = value; }); (0, pojo_util_1.decodeStringField)(playerPOJO, 'title', exceptionBuilder, value => { game._playerTitle[color] = value; }); } function processRoundPart(value, roundPart) { if (!(0, common_1.isValidRound)(value)) { throw exceptionBuilder.build(i18n_1.i18n.INVALID_ROUND_IN_POJO); } game._round[roundPart] = value; } // Headers (0, pojo_util_1.decodeObjectField)(pojo, 'white', exceptionBuilder, value => { processPlayerPOJO(value, 0 /* ColorImpl.WHITE */); }); (0, pojo_util_1.decodeObjectField)(pojo, 'black', exceptionBuilder, value => { processPlayerPOJO(value, 1 /* ColorImpl.BLACK */); }); (0, pojo_util_1.decodeStringField)(pojo, 'event', exceptionBuilder, value => { game._event = value; }); (0, pojo_util_1.decodeNumberField)(pojo, 'round', exceptionBuilder, value => { processRoundPart(value, 0 /* RoundPart.ROUND */); }); (0, pojo_util_1.decodeNumberField)(pojo, 'subRound', exceptionBuilder, value => { processRoundPart(value, 1 /* RoundPart.SUB_ROUND */); }); (0, pojo_util_1.decodeNumberField)(pojo, 'subSubRound', exceptionBuilder, value => { processRoundPart(value, 2 /* RoundPart.SUB_SUB_ROUND */); }); (0, pojo_util_1.decodeStringField)(pojo, 'date', exceptionBuilder, value => { const date = date_value_1.DateValue.fromString(value); if (date === undefined) { throw exceptionBuilder.build(i18n_1.i18n.INVALID_DATE_IN_POJO); } game._date = date; }); (0, pojo_util_1.decodeStringField)(pojo, 'site', exceptionBuilder, value => { game._site = value; }); (0, pojo_util_1.decodeStringField)(pojo, 'annotator', exceptionBuilder, value => { game._annotator = value; }); (0, pojo_util_1.decodeStringField)(pojo, 'eco', exceptionBuilder, value => { if (!(0, helper_1.isValidECO)(value)) { throw exceptionBuilder.build(i18n_1.i18n.INVALID_ECO_CODE_IN_POJO); } game._eco = value; }); (0, pojo_util_1.decodeStringField)(pojo, 'opening', exceptionBuilder, value => { game._opening = value; }); (0, pojo_util_1.decodeStringField)(pojo, 'openingVariation', exceptionBuilder, value => { game._openingVariation = value; }); (0, pojo_util_1.decodeStringField)(pojo, 'openingSubVariation', exceptionBuilder, value => { game._openingSubVariation = value; }); (0, pojo_util_1.decodeStringField)(pojo, 'termination', exceptionBuilder, value => { game._termination = value; }); (0, pojo_util_1.decodeStringField)(pojo, 'result', exceptionBuilder, value => { const resultCode = (0, base_types_impl_1.resultFromString)(value); if (resultCode < 0) { throw exceptionBuilder.build(i18n_1.i18n.INVALID_RESULT_IN_POJO); } game._result = resultCode; }); // Moves game._moveTreeRoot.setPojo(pojo, exceptionBuilder); return game; } /** * Return a human-readable string representing the game. This string is multi-line, * and is intended to be displayed in a fixed-width font (similarly to an ASCII-art picture). */ ascii() { const lines = []; function pushIfDefined(header) { if (header !== undefined) { lines.push(header); } } // Headers pushIfDefined(formatEventAndRound(this._event, this._round[0 /* RoundPart.ROUND */], this._round[1 /* RoundPart.SUB_ROUND */], this._round[2 /* RoundPart.SUB_SUB_ROUND */])); pushIfDefined(formatSimpleHeader('Site', this._site)); pushIfDefined(formatSimpleHeader('Date', this.dateAsString('en-us'))); pushIfDefined(formatPlayer('White', this._playerName[0 /* ColorImpl.WHITE */], this._playerElo[0 /* ColorImpl.WHITE */], this._playerTitle[0 /* ColorImpl.WHITE */])); pushIfDefined(formatPlayer('Black', this._playerName[1 /* ColorImpl.BLACK */], this._playerElo[1 /* ColorImpl.BLACK */], this._playerTitle[1 /* ColorImpl.BLACK */])); pushIfDefined(formatSimpleHeader('Annotator', this._annotator)); pushIfDefined(formatSimpleHeader('ECO', this._eco)); pushIfDefined(formatOpening(this._opening, this._openingVariation, this._openingSubVariation)); pushIfDefined(formatSimpleHeader('Termination', this._termination)); // Variant & initial position const variant = this._moveTreeRoot._position.variant(); if (variant !== 'regular') { lines.push('Variant: ' + variant); } if (!(0, helper_1.variantWithCanonicalStartPosition)(variant) || !position_1.Position.isEqual(this._moveTreeRoot._position, new position_1.Position(variant))) { lines.push(this._moveTreeRoot._position.ascii()); } // Moves & result function isNonEmptyVariation(variation) { return variation.first() !== undefined || variation.nags().length > 0 || variation.tags().length > 0 || variation.comment() !== undefined; } function dumpNode(node, indent, hasSomethingAfter) { // Describe the move const move = indent + node.fullMoveNumber() + (node.moveColor() === 'w' ? '.' : '...') + node.notation(); const moveAnnotations = formatAnnotations(node); lines.push(moveAnnotations.length === 0 ? move : move + ' ' + moveAnnotations.join(' ')); // Print the sub-variations let atLeastOneNonEmptyVariation = false; for (const variation of node.variations()) { if (isNonEmptyVariation(variation)) { lines.push(indent + ' |'); dumpVariation(variation, indent + (hasSomethingAfter ? ' | ' : ' '), indent + ' +- ', false); atLeastOneNonEmptyVariation = true; } } return atLeastOneNonEmptyVariation; } function dumpVariation(variation, indent, indentFirst, hasSomethingAfter) { // Variation annotations const variationAnnotations = formatAnnotations(variation); if (variationAnnotations.length > 0) { lines.push(indentFirst + variationAnnotations.join(' ')); } // List of moves let node = variation.first(); let atLeastOneVariationInPreviousNode = false; let isFirstNode = true; while (node !== undefined) { if (atLeastOneVariationInPreviousNode) { lines.push(indent + ' |'); } const nextNode = node.next(); atLeastOneVariationInPreviousNode = dumpNode(node, isFirstNode && variationAnnotations.length === 0 ? indentFirst : indent, hasSomethingAfter || nextNode !== undefined); isFirstNode = false; node = nextNode; } } dumpVariation(this._moveTreeRoot.mainVariation(), '', '', false); lines.push((0, base_types_impl_1.resultToString)(this._result)); return lines.join('\n'); } } exports.Game = Game; function sanitizeStringHeader(value) { return value === undefined || value === null ? undefined : String(value); } function sanitizeNumberHeader(value) { return value === undefined || value === null ? undefined : Number(value); } function trimCollapseAndMarkEmpty(text) { text = (0, common_1.trimAndCollapseSpaces)(text); return text === '' ? '<empty>' : text; } function formatSimpleHeader(key, header) { return header === undefined ? undefined : `${key}: ${trimCollapseAndMarkEmpty(header)}`; } function formatEventAndRound(event, round, subRound, subSubRound) { if (event === undefined && round === undefined && subRound === undefined && subSubRound === undefined) { return undefined; } let result = event === undefined ? 'Event: <undefined>' : `Event: ${trimCollapseAndMarkEmpty(event)}`; const fullRound = formatFullRound(round, subRound, subSubRound, '*'); if (fullRound !== undefined) { result += ` (${fullRound})`; } return result; } function formatFullRound(round, subRound, subSubRound, undefinedToken) { if (round === undefined && subRound === undefined && subSubRound === undefined) { return undefined; } let result = round === undefined ? undefinedToken : String(round); if (subRound !== undefined || subSubRound !== undefined) { result += '.' + (subRound ?? undefinedToken); } if (subSubRound !== undefined) { result += '.' + subSubRound; } return result; } function formatPlayer(key, playerName, playerElo, playerTitle) { if (playerName === undefined && playerElo === undefined && playerTitle === undefined) { return undefined; } let result = playerName === undefined ? `${key}: <undefined>` : `${key}: ${trimCollapseAndMarkEmpty(playerName)}`; if (playerElo !== undefined && playerTitle !== undefined) { result += ` (${trimCollapseAndMarkEmpty(playerTitle)} ${playerElo})`; } else if (playerElo !== undefined) { result += ` (${playerElo})`; } else if (playerTitle !== undefined) { result += ` (${trimCollapseAndMarkEmpty(playerTitle)})`; } return result; } function formatOpening(opening, openingVariation, openingSubVariation) { if (opening === undefined && openingVariation === undefined && openingSubVariation === undefined) { return undefined; } let result = opening === undefined ? 'Opening: <undefined>' : `Opening: ${trimCollapseAndMarkEmpty(opening)}`; if (openingSubVariation !== undefined) { result += ` (${openingVariation === undefined ? '<undefined>' : trimCollapseAndMarkEmpty(openingVariation)}, ${trimCollapseAndMarkEmpty(openingSubVariation)})`; } else if (openingVariation !== undefined) { result += ` (${trimCollapseAndMarkEmpty(openingVariation)})`; } return result; } function formatAnnotations(node) { const result = []; // NAGs for (const nag of node.nags()) { result.push((0, helper_1.nagSymbol)(nag)); } // Tags for (const tagKey of node.tags()) { result.push(`${tagKey}={${(0, common_1.trimAndCollapseSpaces)(node.tag(tagKey))}}`); } // Comment const comment = node.comment(); if (comment !== undefined) { result.push(trimCollapseAndMarkEmpty(comment)); } return result; } //# sourceMappingURL=game.js.map