chess-moments 1.2.1 → 1.3.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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { isNextFen } = require('../helpers');
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Always returns an array because there can be more than one next moment
|
|
@@ -23,88 +23,41 @@ const getNextMoments = (moments, current) => {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// Last move of a sideline should not have any next moves
|
|
26
|
-
if (
|
|
27
|
-
!nextMoment?.move &&
|
|
28
|
-
current.depth >= nextMoment.depth &&
|
|
29
|
-
getMoveNumber(nextMoment.fen) <= getMoveNumber(current.fen)
|
|
30
|
-
) {
|
|
26
|
+
if (!nextMoment?.move && current.depth >= nextMoment.depth) {
|
|
31
27
|
return [];
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
//
|
|
35
|
-
if (nextMoment.move && current.depth === nextMoment.depth) {
|
|
36
|
-
next.push(nextMoment);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Return early if we have consecutive moves or no next next moment
|
|
40
|
-
if (!nextNextMoment || (nextMoment?.move && nextNextMoment?.move)) {
|
|
41
|
-
return next;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Second to last move of a sideline should not have any next moves
|
|
30
|
+
// Return early if there are two consecutive moves at the same depth or if the next next move does not exist
|
|
45
31
|
if (
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
current.depth
|
|
49
|
-
getMoveNumber(nextNextMoment.fen) <= getMoveNumber(current.fen)
|
|
32
|
+
current.move &&
|
|
33
|
+
current.depth === nextMoment.depth &&
|
|
34
|
+
(!nextNextMoment || current.depth === nextNextMoment.depth)
|
|
50
35
|
) {
|
|
51
|
-
return
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Remove all next moments until depth equals current.depth when next moment does not have a move
|
|
55
|
-
if (!nextMoment?.move) {
|
|
56
|
-
const filteredMoments = moments.filter((moment, index) => {
|
|
57
|
-
return index <= nextIndex || moment.depth === current.depth;
|
|
58
|
-
});
|
|
59
|
-
moments = filteredMoments;
|
|
36
|
+
return [nextMoment];
|
|
60
37
|
}
|
|
61
38
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Active color after current move (who moves next)
|
|
66
|
-
const activeColorAfterCurrent = current.fen.split(' ')[1];
|
|
39
|
+
// Remove all moves before the current moment
|
|
40
|
+
moments = moments.filter((moment) => moment.index > current.index);
|
|
67
41
|
|
|
68
|
-
//
|
|
42
|
+
// Remove all moments after the first moment that have a lower depth than the current moment
|
|
43
|
+
let filteredMoments = [];
|
|
69
44
|
for (const moment of moments) {
|
|
70
|
-
// Skip moments that are at or before the current moment's index
|
|
71
|
-
if (moment.index <= current.index + 1) {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
// Stop if we hit a moment with lower depth (we've left this branch)
|
|
75
45
|
if (moment.depth < current.depth) {
|
|
76
46
|
break;
|
|
77
47
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
// Only consider moments at current depth or one level deeper
|
|
83
|
-
if (moment.depth > current.depth + 1) {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const activeColorAfterMoment = moment.fen.split(' ')[1];
|
|
88
|
-
const fullmove = Number(moment.fen.split(' ')[5]);
|
|
89
|
-
|
|
90
|
-
// Only consider moments within the valid move range
|
|
91
|
-
if (fullmove > current.fullmove + 1) {
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Check if this is a valid next move based on color and fullmove
|
|
96
|
-
const isValidWhiteMove =
|
|
97
|
-
activeColorAfterCurrent === 'w' &&
|
|
98
|
-
activeColorAfterMoment === 'b' &&
|
|
99
|
-
fullmove === current.fullmove;
|
|
48
|
+
filteredMoments.push(moment);
|
|
49
|
+
}
|
|
100
50
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
51
|
+
// If next moment does not have a move, remove all moments with the depth higher than current depth
|
|
52
|
+
if (!nextMoment.move) {
|
|
53
|
+
filteredMoments = filteredMoments.filter(
|
|
54
|
+
(moment) => moment.depth <= current.depth
|
|
55
|
+
);
|
|
56
|
+
}
|
|
105
57
|
|
|
106
|
-
|
|
107
|
-
|
|
58
|
+
// Finally check for next moments from filtered moments
|
|
59
|
+
for (const moment of filteredMoments) {
|
|
60
|
+
if (moment.move && isNextFen(current.fen, moment.fen)) {
|
|
108
61
|
next.push(moment);
|
|
109
62
|
}
|
|
110
63
|
}
|
|
@@ -4,6 +4,7 @@ const getNextMoments = require('./get-next-moments');
|
|
|
4
4
|
const getPrevMoment = require('./get-prev-moment');
|
|
5
5
|
const momentsToPgn = require('./moments-to-pgn');
|
|
6
6
|
const moveTrainer = require('./move-trainer');
|
|
7
|
+
const promoteMainline = require('./promote-mainline');
|
|
7
8
|
|
|
8
9
|
module.exports = {
|
|
9
10
|
addMomentToTree,
|
|
@@ -12,4 +13,5 @@ module.exports = {
|
|
|
12
13
|
getPrevMoment,
|
|
13
14
|
momentsToPgn,
|
|
14
15
|
moveTrainer,
|
|
16
|
+
promoteMainline,
|
|
15
17
|
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const promoteMainline = (moments, current) => {
|
|
2
|
+
if (!current || current.depth === 1) return moments;
|
|
3
|
+
|
|
4
|
+
const currentDepth = current.depth;
|
|
5
|
+
const targetDepth = currentDepth - 1;
|
|
6
|
+
|
|
7
|
+
// Find sideline position marker
|
|
8
|
+
let sidelinePositionIndex = -1;
|
|
9
|
+
for (let i = 0; i < current.index; i++) {
|
|
10
|
+
if (moments[i].depth === currentDepth && !moments[i].move) {
|
|
11
|
+
sidelinePositionIndex = i;
|
|
12
|
+
break;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (sidelinePositionIndex === -1) return moments;
|
|
16
|
+
|
|
17
|
+
// Find end of sideline window (end of a window = next position at the same depth OR first lower depth)
|
|
18
|
+
let sidelineEndIndex = moments.length;
|
|
19
|
+
for (let i = sidelinePositionIndex + 1; i < moments.length; i++) {
|
|
20
|
+
if (
|
|
21
|
+
(moments[i].depth === currentDepth && !moments[i].move) ||
|
|
22
|
+
moments[i].depth < currentDepth
|
|
23
|
+
) {
|
|
24
|
+
sidelineEndIndex = i;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Find mainline move to replace
|
|
30
|
+
let targetMoveIndex = -1;
|
|
31
|
+
for (let i = sidelinePositionIndex - 1; i >= 0; i--) {
|
|
32
|
+
if (moments[i].depth === targetDepth && moments[i].move) {
|
|
33
|
+
targetMoveIndex = i;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (targetMoveIndex === -1) return moments;
|
|
38
|
+
|
|
39
|
+
// Find end of mainline segment to demote
|
|
40
|
+
let targetSegmentEndIndex = moments.length;
|
|
41
|
+
let seenTargetDepthMove = false;
|
|
42
|
+
for (let i = targetMoveIndex + 1; i < moments.length; i++) {
|
|
43
|
+
const moment = moments[i];
|
|
44
|
+
|
|
45
|
+
if (moment.depth === targetDepth && moment.move) {
|
|
46
|
+
seenTargetDepthMove = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (moment.depth < targetDepth) {
|
|
50
|
+
targetSegmentEndIndex = i;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (moment.depth === targetDepth && !moment.move && seenTargetDepthMove) {
|
|
55
|
+
targetSegmentEndIndex = i;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const demotedIndices = new Set([targetMoveIndex]);
|
|
61
|
+
const demotedMoves = [{ ...moments[targetMoveIndex], depth: currentDepth }];
|
|
62
|
+
|
|
63
|
+
for (let i = targetMoveIndex + 1; i < targetSegmentEndIndex; i++) {
|
|
64
|
+
if (moments[i].depth === targetDepth && moments[i].move) {
|
|
65
|
+
demotedIndices.add(i);
|
|
66
|
+
demotedMoves.push({ ...moments[i], depth: currentDepth });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Rebuild the moments array by promoting the current move to the mainline depth,
|
|
71
|
+
// demoting the old mainline sequence into the sideline, preserving order of
|
|
72
|
+
// unaffected moves, and adjusting depths/indices accordingly.
|
|
73
|
+
const result = [];
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < targetMoveIndex; i++) {
|
|
76
|
+
result.push({ ...moments[i] });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
result.push({ ...moments[current.index], depth: targetDepth });
|
|
80
|
+
|
|
81
|
+
for (let i = targetMoveIndex + 1; i < sidelinePositionIndex; i++) {
|
|
82
|
+
if (!demotedIndices.has(i)) {
|
|
83
|
+
result.push({ ...moments[i] });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
result.push({ ...moments[sidelinePositionIndex] });
|
|
88
|
+
for (const move of demotedMoves) {
|
|
89
|
+
result.push(move);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (let i = sidelinePositionIndex + 1; i < sidelineEndIndex; i++) {
|
|
93
|
+
if (i === current.index) continue;
|
|
94
|
+
|
|
95
|
+
const moment = moments[i];
|
|
96
|
+
if (moment.depth === currentDepth && moment.move) {
|
|
97
|
+
result.push({ ...moment, depth: targetDepth });
|
|
98
|
+
} else {
|
|
99
|
+
result.push({ ...moment });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
for (let i = sidelineEndIndex; i < moments.length; i++) {
|
|
104
|
+
if (!demotedIndices.has(i)) {
|
|
105
|
+
result.push({ ...moments[i] });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Reassign indices
|
|
110
|
+
result.forEach((m, i) => {
|
|
111
|
+
m.index = i;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return result;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
module.exports = promoteMainline;
|
|
@@ -2,6 +2,7 @@ const getBrushCode = require('./get-brush-code');
|
|
|
2
2
|
const getBrushColor = require('./get-brush-color');
|
|
3
3
|
const getMoveNumber = require('./get-move-number');
|
|
4
4
|
const insertMomentIntoTree = require('./insert-moment-into-tree');
|
|
5
|
+
const isNextFen = require('./is-next-fen');
|
|
5
6
|
const prepareMoment = require('./prepare-moment');
|
|
6
7
|
|
|
7
8
|
module.exports = {
|
|
@@ -9,5 +10,6 @@ module.exports = {
|
|
|
9
10
|
getBrushColor,
|
|
10
11
|
getMoveNumber,
|
|
11
12
|
insertMomentIntoTree,
|
|
13
|
+
isNextFen,
|
|
12
14
|
prepareMoment,
|
|
13
15
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines if nextFen comes immediately after currentFen in a chess game
|
|
3
|
+
*
|
|
4
|
+
* @param {String} currentFen - The current FEN position
|
|
5
|
+
* @param {String} nextFen - The potential next FEN position
|
|
6
|
+
* @returns {Boolean} - True if nextFen comes immediately after currentFen
|
|
7
|
+
*/
|
|
8
|
+
const isNextFen = (currentFen, nextFen) => {
|
|
9
|
+
try {
|
|
10
|
+
// Parse FEN components
|
|
11
|
+
const currentParts = currentFen.split(' ');
|
|
12
|
+
const nextParts = nextFen.split(' ');
|
|
13
|
+
|
|
14
|
+
// Validate FEN format (should have 6 parts each)
|
|
15
|
+
if (currentParts.length !== 6 || nextParts.length !== 6) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const currentActiveColor = currentParts[1]; // 'w' or 'b' - who moves in current position
|
|
20
|
+
const currentFullmove = Number(currentParts[5]);
|
|
21
|
+
|
|
22
|
+
const nextActiveColor = nextParts[1]; // 'w' or 'b' - who moves in next position
|
|
23
|
+
const nextFullmove = Number(nextParts[5]);
|
|
24
|
+
|
|
25
|
+
// Check if the active color and fullmove number progression is valid
|
|
26
|
+
if (currentActiveColor === 'w') {
|
|
27
|
+
// If white is to move in current position, then after white's move:
|
|
28
|
+
// - Black should be to move in next position
|
|
29
|
+
// - Fullmove number should remain the same
|
|
30
|
+
return nextActiveColor === 'b' && nextFullmove === currentFullmove;
|
|
31
|
+
} else {
|
|
32
|
+
// If black is to move in current position, then after black's move:
|
|
33
|
+
// - White should be to move in next position
|
|
34
|
+
// - Fullmove number should increment by 1
|
|
35
|
+
return nextActiveColor === 'w' && nextFullmove === currentFullmove + 1;
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
module.exports = isNextFen;
|
package/index.js
CHANGED