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 +0 -1
- package/dist/index.d.ts +23 -4
- package/dist/index.js +156 -6
- package/package.json +9 -3
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 {
|
|
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: () =>
|
|
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
|
|
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 =
|
|
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(
|
|
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": "
|
|
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": "
|
|
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
|
-
"
|
|
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",
|