pgn-manager 1.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -65,7 +65,6 @@ const headers = manager.headers;
65
65
  - `getLastMove()`: Get the last move of the game
66
66
  - `getMoveFen(moveOrMoveId: Move | number)`: Get FEN position after move
67
67
  - `getParentRav(moveOrMoveId: Move | number)`: Get parent variation for move
68
- - `getChessJSInstance(moveOrMoveId: Move | number)`: Get or create a ChessJS instance for the position after the specified move
69
68
  - `getMoveColor(moveOrMoveId: Move | number)`: Gets the color of the player who made the move ("w" for white or "b" for black)
70
69
 
71
70
  ## Examples 🎯
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { ParsedPGN, Move, Rav, Header } from "pgn-parser";
1
+ import { ShortMove } from "chess.js";
2
+ import * as pgnParser from "pgn-parser";
3
+ import type { ParsedPGN, Move, Rav, Header, Result } from "pgn-parser";
2
4
  export declare const FEN_START_POSITION = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
3
5
  export declare const FEN_EMPTY_POSITION = "8/8/8/8/8/8/8/8";
4
6
  declare class PGNManager {
@@ -10,6 +12,8 @@ declare class PGNManager {
10
12
  private sortedMoves;
11
13
  /** Map of moves to their FEN position strings */
12
14
  private moveFen;
15
+ /** Map of FEN position string to their move object */
16
+ private fenMove;
13
17
  /** Map of moves to their parent variations (or null for mainline) */
14
18
  private moveParent;
15
19
  /** Map of variations to their parent moves */
@@ -53,7 +57,7 @@ declare class PGNManager {
53
57
  * @param moveNumber - The 1-based index of the move
54
58
  * @returns The move object at the specified position
55
59
  */
56
- getMove: (moveNumber: number) => Move;
60
+ getMove: (moveNumber: number) => pgnParser.Move;
57
61
  /**
58
62
  * Gets the number of a move in the sequence
59
63
  * @param move - The move object
@@ -85,13 +89,13 @@ declare class PGNManager {
85
89
  * @returns The first move
86
90
  * @throws Error if there are no moves in the game
87
91
  */
88
- getFirstMove: () => Move;
92
+ getFirstMove: () => pgnParser.Move;
89
93
  /**
90
94
  * Gets the last move in the game
91
95
  * @returns The last move
92
96
  * @throws Error if there are no moves in the game
93
97
  */
94
- getLastMove: () => any;
98
+ getLastMove: () => pgnParser.Move;
95
99
  /**
96
100
  * Gets the FEN string for a specific move
97
101
  * @param moveOrId - The move object or move number
@@ -113,5 +117,20 @@ declare class PGNManager {
113
117
  * @throws Error if the move parameter is invalid
114
118
  */
115
119
  getMoveColor: (moveOrId: Move | number) => "w" | "b";
120
+ /***
121
+ * Pushes a new move into the game
122
+ * @param moveId - The ID of the move to push
123
+ * @param newMove - The move object to add
124
+ * @param result - The result of the game after this move (default is "*")
125
+ * @returns The newly created move object
126
+ * @throws Error if the move parameter is invalid
127
+ */
128
+ pushMove: (moveId: number, newMove: ShortMove, result?: Result) => Move;
129
+ /**
130
+ * Delete a move and all subsequent moves in its variation from the game
131
+ * @param moveId - The ID of the move to delete from
132
+ * @throws Error if the move parameter is invalid
133
+ */
134
+ deleteMove: (moveId: number) => void;
116
135
  }
117
136
  export default PGNManager;
package/dist/index.js CHANGED
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FEN_EMPTY_POSITION = exports.FEN_START_POSITION = void 0;
4
4
  const ChessJS = require("chess.js");
5
5
  const Chess = typeof ChessJS === "function" ? ChessJS : ChessJS.Chess;
6
- const pgn_parser_1 = require("pgn-parser");
6
+ const pgnParser = require("pgn-parser");
7
+ const utils_1 = require("./utils");
7
8
  exports.FEN_START_POSITION = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
8
9
  exports.FEN_EMPTY_POSITION = "8/8/8/8/8/8/8/8";
9
10
  class PGNManager {
@@ -15,6 +16,8 @@ class PGNManager {
15
16
  sortedMoves;
16
17
  /** Map of moves to their FEN position strings */
17
18
  moveFen;
19
+ /** Map of FEN position string to their move object */
20
+ fenMove = new Map();
18
21
  /** Map of moves to their parent variations (or null for mainline) */
19
22
  moveParent;
20
23
  /** Map of variations to their parent moves */
@@ -27,11 +30,12 @@ class PGNManager {
27
30
  */
28
31
  constructor(pgn) {
29
32
  this.rawPGN = pgn;
30
- this.game = pgn_parser_1.default.parse(pgn + " *")[0];
33
+ this.game = pgnParser.parse(pgn + " *")[0];
31
34
  this.sortedMoves = [];
32
35
  this.moveParent = new Map();
33
36
  this.ravParent = new Map();
34
37
  this.moveFen = new Map();
38
+ this.fenMove = new Map();
35
39
  this.moveColor = new Map();
36
40
  this.dfOnGame(this.game);
37
41
  }
@@ -41,7 +45,8 @@ class PGNManager {
41
45
  */
42
46
  dfOnGame = (game) => {
43
47
  this.sortedMoves = [];
44
- var chessGame = new Chess(exports.FEN_START_POSITION);
48
+ var chessGame = new Chess(game.headers?.find((h) => h.name.toUpperCase() === "FEN")?.value ||
49
+ exports.FEN_START_POSITION);
45
50
  for (let move of game.moves) {
46
51
  this.dfsOnGame(move, game, chessGame);
47
52
  }
@@ -69,6 +74,7 @@ class PGNManager {
69
74
  !chessGame.move(move.move, { sloppy: true }))
70
75
  console.log("Invalid move: " + move.move);
71
76
  this.moveFen.set(move, chessGame.fen());
77
+ this.fenMove.set(chessGame.fen(), move);
72
78
  this.moveColor.set(move, chessGame.turn() === "w" ? "b" : "w");
73
79
  };
74
80
  /**
@@ -224,7 +230,7 @@ class PGNManager {
224
230
  */
225
231
  getMoveFen = (moveOrId) => {
226
232
  const move = typeof moveOrId === "number" ? this.getMove(moveOrId) : moveOrId;
227
- if (!move) {
233
+ if (!move || !this.moveFen.has(move)) {
228
234
  throw Error("Invalid 'move' parameter while getting fen");
229
235
  }
230
236
  let moveFen = this.moveFen.get(move);
@@ -238,7 +244,7 @@ class PGNManager {
238
244
  */
239
245
  getParentRav = (moveOrId) => {
240
246
  const move = typeof moveOrId === "number" ? this.getMove(moveOrId) : moveOrId;
241
- if (!move) {
247
+ if (!move || !this.moveParent.has(move)) {
242
248
  throw Error("Invalid 'move' parameter while getting parent rav");
243
249
  }
244
250
  let parentRav = this.moveParent.get(move);
@@ -252,10 +258,154 @@ class PGNManager {
252
258
  */
253
259
  getMoveColor = (moveOrId) => {
254
260
  const move = typeof moveOrId === "number" ? this.getMove(moveOrId) : moveOrId;
255
- if (!move) {
261
+ if (!move || !this.moveColor.has(move)) {
256
262
  throw Error("Invalid 'move' parameter while getting move color");
257
263
  }
258
264
  return this.moveColor.get(move);
259
265
  };
266
+ /***
267
+ * Pushes a new move into the game
268
+ * @param moveId - The ID of the move to push
269
+ * @param newMove - The move object to add
270
+ * @param result - The result of the game after this move (default is "*")
271
+ * @returns The newly created move object
272
+ * @throws Error if the move parameter is invalid
273
+ */
274
+ pushMove = (moveId, newMove, result = "*") => {
275
+ let chess;
276
+ let parentRav;
277
+ let current = null;
278
+ // 1) Initialize Chess and parent variation
279
+ if (moveId === 0) {
280
+ parentRav = this.parsedPGN;
281
+ const fenHdr = this.headers.find((h) => h.name.toLowerCase() === "fen");
282
+ const startFen = fenHdr ? fenHdr.value : exports.FEN_START_POSITION;
283
+ chess = new Chess(startFen);
284
+ }
285
+ else {
286
+ current = this.getMove(moveId);
287
+ if (!current)
288
+ throw new Error("Invalid moveId while pushing a new move!");
289
+ parentRav = this.moveParent.get(current) || this.parsedPGN;
290
+ chess = new Chess(this.getMoveFen(current));
291
+ }
292
+ // 2) Play the SAN move (strict → sloppy)
293
+ const played = chess.move(newMove, { sloppy: false }) ||
294
+ chess.move(newMove, { sloppy: true });
295
+ if (!played)
296
+ throw new Error("Invalid move");
297
+ const san = chess.history().slice(-1)[0];
298
+ const nextToMove = chess.turn(); // 'w' or 'b'
299
+ // If the FEN already exists, we are trying to add a move that is already played
300
+ if (this.fenMove.has(chess.fen())) {
301
+ return this.fenMove.get(chess.fen());
302
+ }
303
+ // 3) Build move object with provisional move_number
304
+ const provisionalNumber = (() => {
305
+ if (!current) {
306
+ return 1; // brand‐new mainline
307
+ }
308
+ const currNum = current.move_number || this.previousMove(current)?.move_number;
309
+ const currColor = this.getMoveColor(current);
310
+ return currColor === "w" ? currNum : currNum + 1;
311
+ })();
312
+ const moveObj = {
313
+ move: san,
314
+ ravs: undefined,
315
+ move_number: provisionalNumber,
316
+ comments: [],
317
+ };
318
+ // 4) Insert into structure & flag first‐of‐variation
319
+ let isFirstOfVariation = false;
320
+ if (!current) {
321
+ // brand-new mainline => variation of first move if exists
322
+ const first = parentRav.moves[0];
323
+ if (first) {
324
+ isFirstOfVariation = true;
325
+ const newRav = { moves: [moveObj], result };
326
+ first.ravs = first.ravs || [];
327
+ first.ravs.push(newRav);
328
+ this.moveParent.set(moveObj, newRav);
329
+ this.ravParent.set(newRav, first);
330
+ }
331
+ else {
332
+ parentRav.moves.push(moveObj);
333
+ this.moveParent.set(moveObj, parentRav);
334
+ }
335
+ }
336
+ else {
337
+ // existing‐move branch
338
+ const lastInRav = parentRav.moves[parentRav.moves.length - 1];
339
+ const isContinuation = lastInRav === current;
340
+ if (!isContinuation) {
341
+ // new variation off the next move
342
+ isFirstOfVariation = true;
343
+ const anchor = this.nextMove(current) || current;
344
+ anchor.ravs = anchor.ravs || [];
345
+ const newRav = { moves: [moveObj], result };
346
+ anchor.ravs.push(newRav);
347
+ this.moveParent.set(moveObj, newRav);
348
+ this.ravParent.set(newRav, anchor);
349
+ }
350
+ else {
351
+ // continuation
352
+ parentRav.moves.push(moveObj);
353
+ this.moveParent.set(moveObj, parentRav);
354
+ }
355
+ }
356
+ // 5) Conditionally remove move_number
357
+ // Keep it only if first‐of‐variation OR new move is White to play
358
+ if (!(isFirstOfVariation || nextToMove === "b")) {
359
+ delete moveObj.move_number;
360
+ }
361
+ // 6) Final bookkeeping
362
+ this.moveFen.set(moveObj, chess.fen());
363
+ this.fenMove.set(chess.fen(), moveObj);
364
+ this.moveColor.set(moveObj, nextToMove === "w" ? "b" : "w");
365
+ this.rawPGN = (0, utils_1.regeneratePGN)(this.game, this.moveColor);
366
+ this.dfOnGame(this.game);
367
+ return moveObj;
368
+ };
369
+ /**
370
+ * Delete a move and all subsequent moves in its variation from the game
371
+ * @param moveId - The ID of the move to delete from
372
+ * @throws Error if the move parameter is invalid
373
+ */
374
+ deleteMove = (moveId) => {
375
+ const move = this.getMove(moveId);
376
+ if (!move) {
377
+ throw Error("Invalid move");
378
+ }
379
+ // delete the move and subsequent moves from the game
380
+ const parentRav = this.getParentRav(move);
381
+ const index = parentRav.moves.indexOf(move);
382
+ const deletedMoves = parentRav.moves.splice(index);
383
+ // collect all moves and variations that need cleanup
384
+ const movsToClean = [];
385
+ const ravsToClean = [];
386
+ for (const deletedMove of deletedMoves) {
387
+ movsToClean.push(deletedMove);
388
+ if (deletedMove.ravs) {
389
+ for (const rav of deletedMove.ravs) {
390
+ ravsToClean.push(rav);
391
+ }
392
+ }
393
+ }
394
+ // Sort moves by their moveId in decreasing order
395
+ movsToClean.sort((a, b) => this.getMoveNumber(b) - this.getMoveNumber(a));
396
+ // clean up all maps at once
397
+ movsToClean.forEach((move) => {
398
+ this.moveParent.delete(move);
399
+ this.fenMove.delete(this.getMoveFen(move));
400
+ this.moveFen.delete(move);
401
+ this.moveColor.delete(move);
402
+ });
403
+ ravsToClean.forEach((rav) => {
404
+ this.ravParent.delete(rav);
405
+ });
406
+ // update the PGN and rebuild internal state
407
+ this.rawPGN = (0, utils_1.regeneratePGN)(this.game, this.moveColor);
408
+ this.dfOnGame(this.game);
409
+ };
260
410
  }
261
411
  exports.default = PGNManager;
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "pgn-manager",
3
- "version": "1.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Libraray built on top of chess.js and pgn-parser to load and process PGN files in typescript.",
5
5
  "main": "dist/index",
6
6
  "typings": "dist/index",
7
7
  "scripts": {
8
- "test": "echo \"Error: no test specified\" && exit 1",
8
+ "test": "jest",
9
+ "test:watch": "jest --watch",
10
+ "test:coverage": "jest --coverage",
9
11
  "prepublishOnly": "npm run compile",
10
12
  "compile": "npm run clean && tsc -p .",
11
13
  "watch": "tsc -w -p .",
@@ -32,7 +34,11 @@
32
34
  },
33
35
  "devDependencies": {
34
36
  "@types/chess.js": "^0.11.2",
35
- "typescript": "4.5.2"
37
+ "@types/jest": "^30.0.0",
38
+ "@types/pgn-parser": "^2.1.0",
39
+ "jest": "^30.0.4",
40
+ "ts-jest": "^29.4.0",
41
+ "typescript": "^5.8.3"
36
42
  },
37
43
  "files": [
38
44
  "dist/index.js",