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 { getMoveNumber } = require('../helpers');
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
- // Add the next moment if it has a move and is at the same depth
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
- nextMoment?.move &&
47
- !nextNextMoment?.move &&
48
- current.depth >= nextNextMoment.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 next;
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
- // Add fullmove number to the current moment
63
- current.fullmove = Number(current.fen.split(' ')[5]);
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
- // Process moments in order and stop when we leave the current branch
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
- // Skip moments without moves
79
- if (!moment.move) {
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
- const isValidBlackMove =
102
- activeColorAfterCurrent === 'b' &&
103
- activeColorAfterMoment === 'w' &&
104
- fullmove === current.fullmove + 1;
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
- // If the move is valid, add it to the next moments
107
- if (isValidWhiteMove || isValidBlackMove) {
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
@@ -6,6 +6,7 @@ const {
6
6
  getPrevMoment,
7
7
  momentsToPgn,
8
8
  moveTrainer,
9
+ promoteMainline
9
10
  } = require('./functions/extras');
10
11
 
11
12
  module.exports = {
@@ -20,4 +21,5 @@ module.exports = {
20
21
  getPrevMoment,
21
22
  momentsToPgn,
22
23
  moveTrainer,
24
+ promoteMainline
23
25
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chess-moments",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "PGN parser that transforms PGN files into chess \"moments\"",
5
5
  "main": "index.js",
6
6
  "files": [