chess-moments 0.14.0 → 0.14.2
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/functions/extras/get-next-moments.js +65 -0
- package/functions/extras/get-prev-moment.js +66 -0
- package/functions/extras/index.js +11 -0
- package/functions/extras/moments-to-pgn.js +158 -0
- package/functions/{train.js → extras/move-trainer.js} +4 -4
- package/functions/helpers/get-brush-code.js +14 -0
- package/functions/helpers/get-brush-color.js +15 -0
- package/functions/helpers/index.js +7 -0
- package/functions/index.js +0 -2
- package/index.js +3 -2
- package/package.json +2 -2
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Always returns an array because there can be more than one next moment
|
|
3
|
+
*/
|
|
4
|
+
const getNextMoments = (moments, current) => {
|
|
5
|
+
try {
|
|
6
|
+
const next = [];
|
|
7
|
+
const sameDepth = moments[moments.indexOf(current) + 1];
|
|
8
|
+
if (sameDepth.move && sameDepth.depth === current.depth) {
|
|
9
|
+
next.push(sameDepth);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// If it's the first moment without move, add the next moment
|
|
13
|
+
if (current.index === 0 && !current.move) {
|
|
14
|
+
next.push(moments[1]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Split moments after the current index
|
|
18
|
+
const currentIndex = moments.indexOf(current);
|
|
19
|
+
const moves = moments.slice(currentIndex + 1).filter((m) => m.move);
|
|
20
|
+
|
|
21
|
+
// Add fullmove number to the current moment
|
|
22
|
+
current.fullmove = Number(current.fen.split(' ')[5]);
|
|
23
|
+
|
|
24
|
+
// Active color is white
|
|
25
|
+
if (current.fen.split(' ')[1] === 'w') {
|
|
26
|
+
for (const moment of moves) {
|
|
27
|
+
const activeColor = moment.fen.split(' ')[1];
|
|
28
|
+
const fullmove = Number(moment.fen.split(' ')[5]);
|
|
29
|
+
|
|
30
|
+
// Check if the next moment is a valid move for white
|
|
31
|
+
if (
|
|
32
|
+
moment.depth === current.depth + 1 &&
|
|
33
|
+
activeColor === 'b' &&
|
|
34
|
+
fullmove === current.fullmove
|
|
35
|
+
) {
|
|
36
|
+
next.push(moment);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Active color is black
|
|
42
|
+
if (current.fen.split(' ')[1] === 'b') {
|
|
43
|
+
for (const moment of moves) {
|
|
44
|
+
const activeColor = moment.fen.split(' ')[1];
|
|
45
|
+
const fullmove = Number(moment.fen.split(' ')[5]);
|
|
46
|
+
|
|
47
|
+
// Check if the next moment is a valid move for black
|
|
48
|
+
if (
|
|
49
|
+
moment.depth === current.depth + 1 &&
|
|
50
|
+
activeColor === 'w' &&
|
|
51
|
+
fullmove === current.fullmove + 1
|
|
52
|
+
) {
|
|
53
|
+
next.push(moment);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// If no next moment is found, return an empty array
|
|
59
|
+
return next;
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
module.exports = getNextMoments;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Always returns an object because there can be only one previous moment
|
|
3
|
+
*/
|
|
4
|
+
const getPrevMoment = (moments, current) => {
|
|
5
|
+
try {
|
|
6
|
+
const sameDepth = moments[moments.indexOf(current) - 1];
|
|
7
|
+
if (sameDepth.move && sameDepth.depth === current.depth) {
|
|
8
|
+
return sameDepth;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// If it's the first moment with move, return the previous moment (starting position)
|
|
12
|
+
if (current.index === 1 && current.move) {
|
|
13
|
+
return moments[0];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Split moments until the current index
|
|
17
|
+
const currentIndex = moments.indexOf(current);
|
|
18
|
+
const moves = moments.slice(0, currentIndex).filter((m) => m.move);
|
|
19
|
+
|
|
20
|
+
// Add fullmove number to the current moment
|
|
21
|
+
current.fullmove = Number(current.fen.split(' ')[5]);
|
|
22
|
+
|
|
23
|
+
// Active color is white
|
|
24
|
+
if (current.fen.split(' ')[1] === 'w') {
|
|
25
|
+
// Traverse moves in reverse order to optimize search
|
|
26
|
+
for (let i = moves.length - 1; i >= 0; i--) {
|
|
27
|
+
const moment = moves[i];
|
|
28
|
+
const activeColor = moment.fen.split(' ')[1];
|
|
29
|
+
const fullmove = Number(moment.fen.split(' ')[5]);
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
moment.depth === current.depth - 1 &&
|
|
33
|
+
activeColor === 'b' &&
|
|
34
|
+
fullmove === current.fullmove - 1
|
|
35
|
+
) {
|
|
36
|
+
return moment;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Active color is black
|
|
42
|
+
if (current.fen.split(' ')[1] === 'b') {
|
|
43
|
+
// Traverse moves in reverse order to optimize search
|
|
44
|
+
for (let i = moves.length - 1; i >= 0; i--) {
|
|
45
|
+
const moment = moves[i];
|
|
46
|
+
const activeColor = moment.fen.split(' ')[1];
|
|
47
|
+
const fullmove = Number(moment.fen.split(' ')[5]);
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
moment.depth === current.depth - 1 &&
|
|
51
|
+
activeColor === 'w' &&
|
|
52
|
+
fullmove === current.fullmove
|
|
53
|
+
) {
|
|
54
|
+
return moment;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// If no previous moment is found, return an empty object
|
|
60
|
+
return {};
|
|
61
|
+
} catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
module.exports = getPrevMoment;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const getNextMoments = require('./get-next-moments');
|
|
2
|
+
const getPrevMoment = require('./get-prev-moment');
|
|
3
|
+
const momentsToPgn = require('./moments-to-pgn');
|
|
4
|
+
const moveTrainer = require('./move-trainer');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
getNextMoments,
|
|
8
|
+
getPrevMoment,
|
|
9
|
+
momentsToPgn,
|
|
10
|
+
moveTrainer,
|
|
11
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const fen = require('../fen');
|
|
2
|
+
const { getBrushCode } = require('../helpers');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts chess moments back to PGN format
|
|
6
|
+
* @param {Array} moments - Array of chess moment objects
|
|
7
|
+
* @returns {String} PGN string
|
|
8
|
+
*/
|
|
9
|
+
const momentsToPgn = (moments) => {
|
|
10
|
+
if (!Array.isArray(moments) || moments.length === 0) {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let pgn = '';
|
|
15
|
+
let currentDepth = 1;
|
|
16
|
+
let variationStack = [];
|
|
17
|
+
|
|
18
|
+
// Check if we need to add FEN headers for non-standard starting position
|
|
19
|
+
const initialMoment = moments[0];
|
|
20
|
+
if (initialMoment && initialMoment.fen && initialMoment.fen !== fen.initial) {
|
|
21
|
+
pgn += `[SetUp "1"]\n`;
|
|
22
|
+
pgn += `[FEN "${initialMoment.fen}"]\n\n`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < moments.length; i++) {
|
|
26
|
+
const moment = moments[i];
|
|
27
|
+
const depth = moment.depth || 1;
|
|
28
|
+
|
|
29
|
+
// Handle moments without moves (initial position or variation breaks)
|
|
30
|
+
if (!moment.move) {
|
|
31
|
+
// If this is a variation marker (depth > 1) and not the initial position
|
|
32
|
+
if (depth > 1 && i > 0) {
|
|
33
|
+
// Check if we need to start a new variation at the same depth
|
|
34
|
+
if (depth === currentDepth) {
|
|
35
|
+
// Close current variation and start a new one at the same level
|
|
36
|
+
pgn += ') (';
|
|
37
|
+
} else if (depth > currentDepth) {
|
|
38
|
+
// Starting a new variation at a deeper level
|
|
39
|
+
variationStack.push(currentDepth);
|
|
40
|
+
pgn += ' (';
|
|
41
|
+
currentDepth = depth;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Add initial comment if present
|
|
46
|
+
if (moment.comment) {
|
|
47
|
+
pgn += `{${moment.comment}} `;
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Determine who made this move and what move number it is from the FEN
|
|
53
|
+
const fenParts = moment.fen.split(' ');
|
|
54
|
+
const activeColorAfterMove = fenParts[1]; // 'w' or 'b' - who moves next
|
|
55
|
+
const fullmoveNumber = parseInt(fenParts[5]);
|
|
56
|
+
|
|
57
|
+
// If black is to move after this move, then white made this move
|
|
58
|
+
const moveWasByWhite = activeColorAfterMove === 'b';
|
|
59
|
+
// For white moves, the move number is the fullmove number
|
|
60
|
+
// For black moves, the move number is fullmove number - 1 (since fullmove increments after black's move)
|
|
61
|
+
const moveNumber = moveWasByWhite ? fullmoveNumber : fullmoveNumber - 1;
|
|
62
|
+
|
|
63
|
+
// Handle depth changes (variations)
|
|
64
|
+
if (depth > currentDepth) {
|
|
65
|
+
// Starting a new variation
|
|
66
|
+
variationStack.push(currentDepth);
|
|
67
|
+
pgn += ' (';
|
|
68
|
+
currentDepth = depth;
|
|
69
|
+
} else if (depth < currentDepth) {
|
|
70
|
+
// Ending variation(s) - close as many as needed to get to the right depth
|
|
71
|
+
while (currentDepth > depth) {
|
|
72
|
+
variationStack.pop();
|
|
73
|
+
pgn += ')';
|
|
74
|
+
currentDepth--;
|
|
75
|
+
}
|
|
76
|
+
if (pgn.endsWith(')')) {
|
|
77
|
+
pgn += ' ';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Add move number for white moves or when starting a variation
|
|
82
|
+
if (moveWasByWhite || (depth > 1 && pgn.endsWith('('))) {
|
|
83
|
+
pgn += `${moveNumber}.`;
|
|
84
|
+
if (!moveWasByWhite) {
|
|
85
|
+
pgn += '..';
|
|
86
|
+
}
|
|
87
|
+
pgn += ' ';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Add the move
|
|
91
|
+
pgn += moment.move;
|
|
92
|
+
|
|
93
|
+
// Add comment if present
|
|
94
|
+
if (moment.comment) {
|
|
95
|
+
pgn += ` {${moment.comment}}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Add shapes if present
|
|
99
|
+
if (moment.shapes && moment.shapes.length > 0) {
|
|
100
|
+
let shapesComment = '';
|
|
101
|
+
const squareHighlights = moment.shapes.filter(
|
|
102
|
+
(s) => !s.dest || s.dest === ']'
|
|
103
|
+
);
|
|
104
|
+
const arrows = moment.shapes.filter((s) => s.dest && s.dest !== ']');
|
|
105
|
+
|
|
106
|
+
if (squareHighlights.length > 0) {
|
|
107
|
+
const coloredSquares = squareHighlights
|
|
108
|
+
.map((s) => {
|
|
109
|
+
const colorCode = getBrushCode(s.brush);
|
|
110
|
+
return `${colorCode}${s.orig}`;
|
|
111
|
+
})
|
|
112
|
+
.join(',');
|
|
113
|
+
shapesComment += `[%csl ${coloredSquares}]`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (arrows.length > 0) {
|
|
117
|
+
const coloredArrows = arrows
|
|
118
|
+
.map((s) => {
|
|
119
|
+
const colorCode = getBrushCode(s.brush);
|
|
120
|
+
return `${colorCode}${s.orig}${s.dest}`;
|
|
121
|
+
})
|
|
122
|
+
.join(',');
|
|
123
|
+
if (shapesComment) shapesComment += ' ';
|
|
124
|
+
shapesComment += `[%cal ${coloredArrows}]`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (shapesComment) {
|
|
128
|
+
if (moment.comment) {
|
|
129
|
+
// If there's already a comment, add shapes to it
|
|
130
|
+
pgn = pgn.replace(/\{([^}]*)\}$/, `{$1 ${shapesComment}}`);
|
|
131
|
+
} else {
|
|
132
|
+
pgn += ` {${shapesComment}}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Add space after move if not the last move
|
|
138
|
+
if (i < moments.length - 1) {
|
|
139
|
+
const nextMoment = moments[i + 1];
|
|
140
|
+
if (nextMoment && nextMoment.move) {
|
|
141
|
+
pgn += ' ';
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Close any remaining variations
|
|
147
|
+
while (variationStack.length > 0) {
|
|
148
|
+
variationStack.pop();
|
|
149
|
+
pgn += ')';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add default result
|
|
153
|
+
pgn += ' *';
|
|
154
|
+
|
|
155
|
+
return pgn.trim();
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
module.exports = momentsToPgn;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const { flatten } = require('lodash');
|
|
2
|
-
const splitByMoveIndex = require('
|
|
3
|
-
const tree = require('
|
|
2
|
+
const splitByMoveIndex = require('../split-by-move');
|
|
3
|
+
const tree = require('../tree');
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const moveTrainer = (sloppyPgn, fen) => {
|
|
6
6
|
// Tree is an array of array depths
|
|
7
7
|
const moments = tree(sloppyPgn);
|
|
8
8
|
|
|
@@ -52,4 +52,4 @@ const train = (sloppyPgn, fen) => {
|
|
|
52
52
|
return training;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
module.exports =
|
|
55
|
+
module.exports = moveTrainer;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert brush color name to PGN color code
|
|
3
|
+
*/
|
|
4
|
+
const getBrushCode = (brush) => {
|
|
5
|
+
const brushCodes = {
|
|
6
|
+
green: 'G',
|
|
7
|
+
red: 'R',
|
|
8
|
+
blue: 'B',
|
|
9
|
+
yellow: 'Y',
|
|
10
|
+
};
|
|
11
|
+
return brushCodes[brush] || 'G';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = getBrushCode;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert PGN color code to brush color name
|
|
3
|
+
*/
|
|
4
|
+
const getBrushColor = (letter) => {
|
|
5
|
+
const brushes = {
|
|
6
|
+
R: 'red',
|
|
7
|
+
Y: 'yellow',
|
|
8
|
+
G: 'green',
|
|
9
|
+
B: 'blue',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
return brushes[letter] || 'green';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
module.exports = getBrushColor;
|
package/functions/index.js
CHANGED
|
@@ -8,7 +8,6 @@ const prepare = require('./prepare');
|
|
|
8
8
|
const prettify = require('./prettify');
|
|
9
9
|
const shape = require('./shape');
|
|
10
10
|
const split = require('./split');
|
|
11
|
-
const train = require('./train');
|
|
12
11
|
const tree = require('./tree');
|
|
13
12
|
|
|
14
13
|
module.exports = {
|
|
@@ -22,6 +21,5 @@ module.exports = {
|
|
|
22
21
|
prettify,
|
|
23
22
|
shape,
|
|
24
23
|
split,
|
|
25
|
-
train,
|
|
26
24
|
tree,
|
|
27
25
|
};
|
package/index.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
const { fen, pgn, flat,
|
|
1
|
+
const { fen, pgn, flat, tree } = require('./functions');
|
|
2
2
|
const {
|
|
3
3
|
getNextMoments,
|
|
4
4
|
getPrevMoment,
|
|
5
5
|
momentsToPgn,
|
|
6
|
+
moveTrainer,
|
|
6
7
|
} = require('./functions/extras');
|
|
7
8
|
|
|
8
9
|
module.exports = {
|
|
9
10
|
fen,
|
|
10
11
|
pgn,
|
|
11
12
|
flat,
|
|
12
|
-
train,
|
|
13
13
|
tree,
|
|
14
14
|
getNextMoments,
|
|
15
15
|
getPrevMoment,
|
|
16
16
|
momentsToPgn,
|
|
17
|
+
moveTrainer,
|
|
17
18
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chess-moments",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.2",
|
|
4
4
|
"description": "PGN parser that transforms PGN files into chess \"moments\"",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"index.js",
|
|
8
8
|
"chess.js",
|
|
9
|
-
"functions
|
|
9
|
+
"functions/**/*.js"
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "mocha",
|