UNPKG

pgn-manager

Version:

Libraray built on top of chess.js and pgn-parser to load and process PGN files in typescript.

412 lines (411 loc) 15.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FEN_EMPTY_POSITION = exports.FEN_START_POSITION = void 0; const ChessJS = require("chess.js"); const Chess = typeof ChessJS === "function" ? ChessJS : ChessJS.Chess; const pgnParser = require("pgn-parser"); const utils_1 = require("./utils"); exports.FEN_START_POSITION = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; exports.FEN_EMPTY_POSITION = "8/8/8/8/8/8/8/8"; class PGNManager { /** The raw PGN string input */ rawPGN; /** The parsed PGN game object */ game; /** Array of moves in traversal order */ sortedMoves; /** Map of moves to their FEN position strings */ moveFen; /** Map of FEN position string to their move object */ fenMove = new Map(); /** Map of moves to their parent variations (or null for mainline) */ moveParent; /** Map of variations to their parent moves */ ravParent; /** Map of moves to the color of the player who made the move */ moveColor; /** * Creates a new PGNManager instance * @param pgn - The PGN string to parse and manage */ constructor(pgn) { this.rawPGN = pgn; this.game = pgnParser.parse(pgn + " *")[0]; this.sortedMoves = []; this.moveParent = new Map(); this.ravParent = new Map(); this.moveFen = new Map(); this.fenMove = new Map(); this.moveColor = new Map(); this.dfOnGame(this.game); } /** * Initializes the game traversal starting from the initial position * @param game - The parsed PGN game object */ dfOnGame = (game) => { this.sortedMoves = []; var chessGame = new Chess(game.headers?.find((h) => h.name.toUpperCase() === "FEN")?.value || exports.FEN_START_POSITION); for (let move of game.moves) { this.dfsOnGame(move, game, chessGame); } }; /** * Performs depth-first traversal of the game moves and variations * @param move - The current move being processed * @param parent - The parent RAV (variation) containing the move * @param chessGame - The chess instance for the current position */ dfsOnGame = (move, parent, chessGame) => { this.sortedMoves.push(move); this.moveParent.set(move, parent); if (move.ravs) { for (let rav of move.ravs) { this.ravParent.set(rav, move); let newVarChessGame = new Chess(chessGame.fen()); for (let ravMove of rav.moves) { this.dfsOnGame(ravMove, rav, newVarChessGame); } } } // move only after variations are processed if (!chessGame.move(move.move, { sloppy: false }) && !chessGame.move(move.move, { sloppy: true })) console.log("Invalid move: " + move.move); this.moveFen.set(move, chessGame.fen()); this.fenMove.set(chessGame.fen(), move); this.moveColor.set(move, chessGame.turn() === "w" ? "b" : "w"); }; /** * Gets the raw PGN string * @returns The original PGN string */ get pgn() { return this.rawPGN; } /** * Gets the parsed PGN object * @returns The parsed PGN game object */ get parsedPGN() { return this.game; } /** * Gets the game headers * @returns Array of game headers */ get headers() { if (!this.game || !this.game.headers) return []; return this.game.headers; } /** * Gets a move by its number in the sequence * @param moveNumber - The 1-based index of the move * @returns The move object at the specified position */ getMove = (moveNumber) => { let move = this.sortedMoves[moveNumber - 1]; return move; }; /** * Gets the number of a move in the sequence * @param move - The move object * @returns The 1-based index of the move */ getMoveNumber = (move) => { return this.sortedMoves.indexOf(move) + 1; }; /** * Gets the next move in the sequence * @param moveOrId - The current move object or move number * @returns The next move in the sequence * @throws Error if there are no moves in the game */ nextMove = (moveOrId) => { let move; if (typeof moveOrId === "number") { move = this.getMove(moveOrId); } else { move = moveOrId; } if (!move) { if (this.sortedMoves.length == 0) { throw Error("No moves in game"); } return this.sortedMoves[0]; } if (move == this.game.moves[this.game.moves.length - 1] || move === this.sortedMoves[this.sortedMoves.length - 1]) { return move; } let parentRav = this.moveParent.get(move); let tempNextMove = this.getMove(this.getMoveNumber(move) + 1); if (parentRav) { let indexInParent = parentRav.moves.indexOf(move); if (indexInParent == parentRav.moves.length - 1) { let superParent = this.ravParent.get(parentRav); if (superParent) { tempNextMove = this.nextMove(superParent); } } else { tempNextMove = parentRav.moves[indexInParent + 1]; } } return tempNextMove; }; /** * Checks if there is a next move available * @param moveOrId - The current move object or move number * @returns True if there is a next move, false otherwise */ hasNextMove = (moveOrId) => { const move = typeof moveOrId === "number" ? this.getMove(moveOrId) : moveOrId; return !move || this.nextMove(move) !== move; }; /** * Gets the previous move in the sequence * @param moveOrId - The current move object or move number * @returns The previous move or undefined if at the start * @throws Error if there are no moves or if the move parameter is invalid */ previousMove = (moveOrId) => { const move = typeof moveOrId === "number" ? this.getMove(moveOrId) : moveOrId; if (!move) { if (this.sortedMoves.length == 0) { throw Error("No moves in game"); } throw Error("Invalid 'move' parameter while getting previous move"); } let currentMoveNumber = this.getMoveNumber(move); if (currentMoveNumber == 0) { return undefined; } let parentRav = this.moveParent.get(move); let tempPrevMove = this.getMove(currentMoveNumber - 1); if (parentRav) { let indexInParent = parentRav.moves.indexOf(move); if (indexInParent == 0) { let superParent = this.ravParent.get(parentRav); if (superParent) { tempPrevMove = superParent; } } else { tempPrevMove = parentRav.moves[indexInParent - 1]; } } return tempPrevMove; }; /** * Gets the first move in the game * @returns The first move * @throws Error if there are no moves in the game */ getFirstMove = () => { if (this.sortedMoves.length == 0) { throw Error("No moves in game"); } return this.sortedMoves[0]; }; /** * Gets the last move in the game * @returns The last move * @throws Error if there are no moves in the game */ getLastMove = () => { if (this.sortedMoves.length == 0) { throw Error("No moves in game"); } return this.game.moves[this.game.moves.length - 1]; }; /** * Gets the FEN string for a specific move * @param moveOrId - The move object or move number * @returns The FEN string representing the position after the move * @throws Error if the move parameter is invalid */ getMoveFen = (moveOrId) => { const move = typeof moveOrId === "number" ? this.getMove(moveOrId) : moveOrId; if (!move || !this.moveFen.has(move)) { throw Error("Invalid 'move' parameter while getting fen"); } let moveFen = this.moveFen.get(move); return moveFen ? moveFen : exports.FEN_EMPTY_POSITION; }; /** * Gets the parent RAV (variation) for a move * @param moveOrId - The move object or move number * @returns The parent RAV or null if the move is in the main line * @throws Error if the move parameter is invalid */ getParentRav = (moveOrId) => { const move = typeof moveOrId === "number" ? this.getMove(moveOrId) : moveOrId; if (!move || !this.moveParent.has(move)) { throw Error("Invalid 'move' parameter while getting parent rav"); } let parentRav = this.moveParent.get(move); return parentRav ? parentRav : null; }; /** * Gets the color of the player who made the move * @param moveOrId - The move object or move ID number * @returns "w" for white or "b" for black * @throws Error if the move parameter is invalid */ getMoveColor = (moveOrId) => { const move = typeof moveOrId === "number" ? this.getMove(moveOrId) : moveOrId; if (!move || !this.moveColor.has(move)) { throw Error("Invalid 'move' parameter while getting move color"); } return this.moveColor.get(move); }; /*** * Pushes a new move into the game * @param moveId - The ID of the move to push * @param newMove - The move object to add * @param result - The result of the game after this move (default is "*") * @returns The newly created move object * @throws Error if the move parameter is invalid */ pushMove = (moveId, newMove, result = "*") => { let chess; let parentRav; let current = null; // 1) Initialize Chess and parent variation if (moveId === 0) { parentRav = this.parsedPGN; const fenHdr = this.headers.find((h) => h.name.toLowerCase() === "fen"); const startFen = fenHdr ? fenHdr.value : exports.FEN_START_POSITION; chess = new Chess(startFen); } else { current = this.getMove(moveId); if (!current) throw new Error("Invalid moveId while pushing a new move!"); parentRav = this.moveParent.get(current) || this.parsedPGN; chess = new Chess(this.getMoveFen(current)); } // 2) Play the SAN move (strict → sloppy) const played = chess.move(newMove, { sloppy: false }) || chess.move(newMove, { sloppy: true }); if (!played) throw new Error("Invalid move"); const san = chess.history().slice(-1)[0]; const nextToMove = chess.turn(); // 'w' or 'b' // If the FEN already exists, we are trying to add a move that is already played if (this.fenMove.has(chess.fen())) { return this.fenMove.get(chess.fen()); } // 3) Build move object with provisional move_number const provisionalNumber = (() => { if (!current) { return 1; // brand‐new mainline } const currNum = current.move_number || this.previousMove(current)?.move_number; const currColor = this.getMoveColor(current); return currColor === "w" ? currNum : currNum + 1; })(); const moveObj = { move: san, ravs: undefined, move_number: provisionalNumber, comments: [], }; // 4) Insert into structure & flag first‐of‐variation let isFirstOfVariation = false; if (!current) { // brand-new mainline => variation of first move if exists const first = parentRav.moves[0]; if (first) { isFirstOfVariation = true; const newRav = { moves: [moveObj], result }; first.ravs = first.ravs || []; first.ravs.push(newRav); this.moveParent.set(moveObj, newRav); this.ravParent.set(newRav, first); } else { parentRav.moves.push(moveObj); this.moveParent.set(moveObj, parentRav); } } else { // existing‐move branch const lastInRav = parentRav.moves[parentRav.moves.length - 1]; const isContinuation = lastInRav === current; if (!isContinuation) { // new variation off the next move isFirstOfVariation = true; const anchor = this.nextMove(current) || current; anchor.ravs = anchor.ravs || []; const newRav = { moves: [moveObj], result }; anchor.ravs.push(newRav); this.moveParent.set(moveObj, newRav); this.ravParent.set(newRav, anchor); } else { // continuation parentRav.moves.push(moveObj); this.moveParent.set(moveObj, parentRav); } } // 5) Conditionally remove move_number // Keep it only if first‐of‐variation OR new move is White to play if (!(isFirstOfVariation || nextToMove === "b")) { delete moveObj.move_number; } // 6) Final bookkeeping this.moveFen.set(moveObj, chess.fen()); this.fenMove.set(chess.fen(), moveObj); this.moveColor.set(moveObj, nextToMove === "w" ? "b" : "w"); this.rawPGN = (0, utils_1.regeneratePGN)(this.game, this.moveColor); this.dfOnGame(this.game); return moveObj; }; /** * Delete a move and all subsequent moves in its variation from the game * @param moveId - The ID of the move to delete from * @throws Error if the move parameter is invalid */ deleteMove = (moveId) => { const move = this.getMove(moveId); if (!move) { throw Error("Invalid move"); } // delete the move and subsequent moves from the game const parentRav = this.getParentRav(move); const index = parentRav.moves.indexOf(move); const deletedMoves = parentRav.moves.splice(index); // collect all moves and variations that need cleanup const movsToClean = []; const ravsToClean = []; for (const deletedMove of deletedMoves) { movsToClean.push(deletedMove); if (deletedMove.ravs) { for (const rav of deletedMove.ravs) { ravsToClean.push(rav); } } } // Sort moves by their moveId in decreasing order movsToClean.sort((a, b) => this.getMoveNumber(b) - this.getMoveNumber(a)); // clean up all maps at once movsToClean.forEach((move) => { this.moveParent.delete(move); this.fenMove.delete(this.getMoveFen(move)); this.moveFen.delete(move); this.moveColor.delete(move); }); ravsToClean.forEach((rav) => { this.ravParent.delete(rav); }); // update the PGN and rebuild internal state this.rawPGN = (0, utils_1.regeneratePGN)(this.game, this.moveColor); this.dfOnGame(this.game); }; } exports.default = PGNManager;