react-chess-explorer 0.0.4 → 0.0.6
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/dist/features/explorer/hooks/useGameReplayTraining.d.ts +3 -0
- package/dist/features/explorer/hooks/usePositionHistory.d.ts +6 -1
- package/dist/features/explorer/hooks/usePositionReferenceData.d.ts +1 -0
- package/dist/features/explorer/positionUtils.d.ts +1 -0
- package/dist/index.esm.js +59 -19
- package/dist/index.js +58 -18
- package/package.json +1 -1
|
@@ -16,6 +16,9 @@ export declare function useGameReplayTraining({ gameId, startFen, fetchGame, }:
|
|
|
16
16
|
error: string | null;
|
|
17
17
|
feedback: GameReplayFeedback;
|
|
18
18
|
lastExpectedSan: string | null;
|
|
19
|
+
lastMoveUci: any;
|
|
20
|
+
correctMoveSquare: any;
|
|
21
|
+
incorrectMoveSquare: any;
|
|
19
22
|
expectedSan: string | undefined;
|
|
20
23
|
handlePieceDrop: (sourceSquare: string, targetSquare: string, piece: string) => boolean;
|
|
21
24
|
revealMove: () => void;
|
|
@@ -2,6 +2,8 @@ export type PositionHistoryEntry = {
|
|
|
2
2
|
fen: string;
|
|
3
3
|
/** SAN of the move played from the previous entry to reach this FEN. */
|
|
4
4
|
lastSan?: string;
|
|
5
|
+
/** UCI of the move played from the previous entry to reach this FEN. */
|
|
6
|
+
lastUci?: string;
|
|
5
7
|
};
|
|
6
8
|
export declare function initialHistoryState(initialFen: string, initialLineSans?: string[]): {
|
|
7
9
|
history: PositionHistoryEntry[];
|
|
@@ -13,14 +15,17 @@ export declare function usePositionHistory(initialFen: string, initialLineSans?:
|
|
|
13
15
|
lineSans: string[];
|
|
14
16
|
forwardSans: string[];
|
|
15
17
|
currentFen: string;
|
|
16
|
-
|
|
18
|
+
lastMoveUci: string | null;
|
|
19
|
+
pushEntry: (fen: string, lastSan?: string, lastUci?: string) => PositionHistoryEntry;
|
|
17
20
|
pushEntries: (entries: {
|
|
18
21
|
fen: string;
|
|
19
22
|
lastSan: string;
|
|
23
|
+
lastUci?: string;
|
|
20
24
|
}[]) => void;
|
|
21
25
|
replaceLineEntries: (startFen: string, entries: {
|
|
22
26
|
fen: string;
|
|
23
27
|
lastSan: string;
|
|
28
|
+
lastUci?: string;
|
|
24
29
|
}[]) => void;
|
|
25
30
|
goBack: () => PositionHistoryEntry | null;
|
|
26
31
|
goForward: () => PositionHistoryEntry | null;
|
|
@@ -28,6 +28,7 @@ export declare function usePositionReferenceData({ fenProp, onFenChange, initial
|
|
|
28
28
|
canGoForward: boolean;
|
|
29
29
|
forwardSans: string[];
|
|
30
30
|
selectedVariationKey: string | undefined;
|
|
31
|
+
lastMoveUci: string | null;
|
|
31
32
|
setSources: import("react").Dispatch<import("react").SetStateAction<GameSource[]>>;
|
|
32
33
|
setGamesMoveFilterUci: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
|
|
33
34
|
handleMoveSelect: (move: PositionMoveApiDto) => void;
|
|
@@ -10,6 +10,7 @@ export declare function applyBoardMove(fen: string, sourceSquare: string, target
|
|
|
10
10
|
export type LineSansEntry = {
|
|
11
11
|
fen: string;
|
|
12
12
|
lastSan: string;
|
|
13
|
+
lastUci: string;
|
|
13
14
|
};
|
|
14
15
|
/** Play a SAN sequence from a start FEN; returns null if any move is illegal. */
|
|
15
16
|
export declare function applyLineSans(startFen: string, sans: string[]): {
|
package/dist/index.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Chess } from 'chess.js';
|
|
2
2
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
3
|
-
import { ThemeProvider, HighlightChessboard, usePositionKeyboardNav, ChessboardDnDProvider } from 'react-chess-core';
|
|
3
|
+
import { ThemeProvider, HighlightChessboard, usePositionKeyboardNav, ChessboardDnDProvider, useCorrectMoveFeedback, useIncorrectMoveFeedback, lastMoveUciAtPly } from 'react-chess-core';
|
|
4
4
|
import { useEffect, useState, useCallback, useRef, useMemo } from 'react';
|
|
5
5
|
|
|
6
6
|
/** Standard start position — placeholder until game replay is implemented. */
|
|
@@ -39,6 +39,7 @@ function applyBoardMove(fen, sourceSquare, targetSquare, piece) {
|
|
|
39
39
|
}
|
|
40
40
|
/** Play a SAN sequence from a start FEN; returns null if any move is illegal. */
|
|
41
41
|
function applyLineSans(startFen, sans) {
|
|
42
|
+
var _a;
|
|
42
43
|
const chess = new Chess(startFen);
|
|
43
44
|
const entries = [];
|
|
44
45
|
for (const san of sans) {
|
|
@@ -46,9 +47,10 @@ function applyLineSans(startFen, sans) {
|
|
|
46
47
|
const move = chess.move(san);
|
|
47
48
|
if (!move)
|
|
48
49
|
return null;
|
|
49
|
-
|
|
50
|
+
const lastUci = `${move.from}${move.to}${(_a = move.promotion) !== null && _a !== void 0 ? _a : ""}`;
|
|
51
|
+
entries.push({ fen: chess.fen(), lastSan: move.san, lastUci });
|
|
50
52
|
}
|
|
51
|
-
catch (
|
|
53
|
+
catch (_b) {
|
|
52
54
|
return null;
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -523,19 +525,19 @@ function initialHistoryState(initialFen, initialLineSans) {
|
|
|
523
525
|
};
|
|
524
526
|
}
|
|
525
527
|
function usePositionHistory(initialFen, initialLineSans) {
|
|
526
|
-
var _a, _b;
|
|
528
|
+
var _a, _b, _c, _d;
|
|
527
529
|
const [history, setHistory] = useState(() => initialHistoryState(initialFen, initialLineSans).history);
|
|
528
530
|
const [historyIndex, setHistoryIndex] = useState(() => initialHistoryState(initialFen, initialLineSans).historyIndex);
|
|
529
531
|
const canGoBack = historyIndex > 0;
|
|
530
532
|
const canGoForward = historyIndex < history.length - 1;
|
|
531
|
-
const pushEntry = useCallback((fen, lastSan) => {
|
|
533
|
+
const pushEntry = useCallback((fen, lastSan, lastUci) => {
|
|
532
534
|
setHistory((prev) => {
|
|
533
535
|
const trimmed = prev.slice(0, historyIndex + 1);
|
|
534
|
-
const next = [...trimmed, { fen, lastSan }];
|
|
536
|
+
const next = [...trimmed, { fen, lastSan, lastUci }];
|
|
535
537
|
setHistoryIndex(next.length - 1);
|
|
536
538
|
return next;
|
|
537
539
|
});
|
|
538
|
-
return { fen, lastSan };
|
|
540
|
+
return { fen, lastSan, lastUci };
|
|
539
541
|
}, [historyIndex]);
|
|
540
542
|
const goBack = useCallback(() => {
|
|
541
543
|
if (historyIndex <= 0)
|
|
@@ -596,12 +598,14 @@ function usePositionHistory(initialFen, initialLineSans) {
|
|
|
596
598
|
setHistory([{ fen: startFen }, ...entries]);
|
|
597
599
|
setHistoryIndex(entries.length);
|
|
598
600
|
}, []);
|
|
601
|
+
const lastMoveUci = historyIndex > 0 ? (_d = (_c = history[historyIndex]) === null || _c === void 0 ? void 0 : _c.lastUci) !== null && _d !== void 0 ? _d : null : null;
|
|
599
602
|
return {
|
|
600
603
|
canGoBack,
|
|
601
604
|
canGoForward,
|
|
602
605
|
lineSans,
|
|
603
606
|
forwardSans,
|
|
604
607
|
currentFen,
|
|
608
|
+
lastMoveUci,
|
|
605
609
|
pushEntry,
|
|
606
610
|
pushEntries,
|
|
607
611
|
replaceLineEntries,
|
|
@@ -646,7 +650,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
646
650
|
}
|
|
647
651
|
isAnimatingVariationRef.current = false;
|
|
648
652
|
}, []);
|
|
649
|
-
const { canGoBack, canGoForward, lineSans, forwardSans, currentFen, pushEntry, pushEntries, replaceLineEntries, goBack, goForward, goFirst, goLast, resetHistory, } = usePositionHistory(initialFen, initialLineSans);
|
|
653
|
+
const { canGoBack, canGoForward, lineSans, forwardSans, currentFen, pushEntry, pushEntries, replaceLineEntries, goBack, goForward, goFirst, goLast, resetHistory, lastMoveUci: historyLastMoveUci, } = usePositionHistory(initialFen, initialLineSans);
|
|
650
654
|
/** FEN used for explorer API queries — follows move history, not animation frames. */
|
|
651
655
|
const queryFen = currentFen;
|
|
652
656
|
const initialLineKey = useMemo(() => { var _a; return (_a = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.join("|")) !== null && _a !== void 0 ? _a : ""; }, [initialLineSans]);
|
|
@@ -681,6 +685,10 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
681
685
|
}), [sources]);
|
|
682
686
|
const lastAppliedLineKeyRef = useRef(initialLineKey);
|
|
683
687
|
useEffect(() => () => cancelVariationAnimation(), [cancelVariationAnimation]);
|
|
688
|
+
// Hold URL line callbacks until history catches up after an external line change.
|
|
689
|
+
useEffect(() => {
|
|
690
|
+
readyForLineSyncRef.current = false;
|
|
691
|
+
}, [initialLineKey]);
|
|
684
692
|
// Apply URL line changes only when the external line key changes — not when the
|
|
685
693
|
// user clicks moves (internal lineSans updates must not re-sync from stale props).
|
|
686
694
|
useEffect(() => {
|
|
@@ -891,7 +899,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
891
899
|
const nextFen = fenAfterUci(queryFen, move.uci);
|
|
892
900
|
if (!nextFen)
|
|
893
901
|
return;
|
|
894
|
-
pushEntry(nextFen, move.san);
|
|
902
|
+
pushEntry(nextFen, move.san, move.uci);
|
|
895
903
|
setSelectedVariationKey(undefined);
|
|
896
904
|
applyNavigation(nextFen, true);
|
|
897
905
|
}, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
|
|
@@ -905,7 +913,11 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
905
913
|
const nextFen = fenAfterUci(lineFen, line.uciPath[i]);
|
|
906
914
|
if (!nextFen)
|
|
907
915
|
return;
|
|
908
|
-
entries.push({
|
|
916
|
+
entries.push({
|
|
917
|
+
fen: nextFen,
|
|
918
|
+
lastSan: line.moves[i].san,
|
|
919
|
+
lastUci: line.uciPath[i],
|
|
920
|
+
});
|
|
909
921
|
lineFen = nextFen;
|
|
910
922
|
}
|
|
911
923
|
if (entries.length === 0)
|
|
@@ -940,7 +952,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
940
952
|
const result = applyBoardMove(queryFen, sourceSquare, targetSquare, piece);
|
|
941
953
|
if (!result)
|
|
942
954
|
return false;
|
|
943
|
-
pushEntry(result.fen, result.san);
|
|
955
|
+
pushEntry(result.fen, result.san, result.uci);
|
|
944
956
|
setSelectedVariationKey(undefined);
|
|
945
957
|
applyNavigation(result.fen, true);
|
|
946
958
|
return true;
|
|
@@ -1017,6 +1029,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
1017
1029
|
canGoForward,
|
|
1018
1030
|
forwardSans,
|
|
1019
1031
|
selectedVariationKey,
|
|
1032
|
+
lastMoveUci: historyLastMoveUci,
|
|
1020
1033
|
setSources,
|
|
1021
1034
|
setGamesMoveFilterUci,
|
|
1022
1035
|
handleMoveSelect,
|
|
@@ -1115,7 +1128,7 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
|
|
|
1115
1128
|
fetchPositionGames,
|
|
1116
1129
|
fetchPositionVariations,
|
|
1117
1130
|
});
|
|
1118
|
-
const { fen, boardFen, games, sources, lineSans, loading, showPositionLoading, gamesLoading, positionReady, displayMoves, error, lineLabel, canGoBack, canGoForward, forwardSans, selectedVariationKey, setSources, handleMoveSelect, handleLineSelect, handlePieceDrop, handleBack, handleForward, handleFirst, handleLast, } = referenceData;
|
|
1131
|
+
const { fen, boardFen, games, sources, lineSans, loading, showPositionLoading, gamesLoading, positionReady, displayMoves, error, lineLabel, canGoBack, canGoForward, forwardSans, selectedVariationKey, lastMoveUci, setSources, handleMoveSelect, handleLineSelect, handlePieceDrop, handleBack, handleForward, handleFirst, handleLast, } = referenceData;
|
|
1119
1132
|
usePositionKeyboardNav({
|
|
1120
1133
|
enabled: keyboardNav,
|
|
1121
1134
|
canPrev: canGoBack,
|
|
@@ -1179,7 +1192,7 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
|
|
|
1179
1192
|
overflow: fillHeight ? "hidden" : "visible",
|
|
1180
1193
|
boxSizing: "border-box",
|
|
1181
1194
|
};
|
|
1182
|
-
const board = (jsxs(Fragment, { children: [jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, position: boardFen, boardOrientation: boardOrientation, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }, boardOrientation) }), renderBoardNav(boardNavProps)] }));
|
|
1195
|
+
const board = (jsxs(Fragment, { children: [jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, position: boardFen, boardOrientation: boardOrientation, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, lastMoveUci: lastMoveUci, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }, boardOrientation) }), renderBoardNav(boardNavProps)] }));
|
|
1183
1196
|
const referencePanel = (jsx(DefaultReferencePanel, { theme: theme, fillHeight: fillHeight, status: renderStatus({
|
|
1184
1197
|
error,
|
|
1185
1198
|
loading: showPositionLoading,
|
|
@@ -1260,12 +1273,16 @@ function useGameReplayTraining({ gameId, startFen, fetchGame, }) {
|
|
|
1260
1273
|
const [error, setError] = useState(null);
|
|
1261
1274
|
const [feedback, setFeedback] = useState(null);
|
|
1262
1275
|
const [lastExpectedSan, setLastExpectedSan] = useState(null);
|
|
1276
|
+
const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = useCorrectMoveFeedback();
|
|
1277
|
+
const { incorrectMoveSquare, showIncorrectMove, clearIncorrectMoveFeedback, isShowingIncorrectMove, } = useIncorrectMoveFeedback();
|
|
1263
1278
|
useEffect(() => {
|
|
1264
1279
|
let cancelled = false;
|
|
1265
1280
|
setLoading(true);
|
|
1266
1281
|
setError(null);
|
|
1267
1282
|
setFeedback(null);
|
|
1268
1283
|
setLastExpectedSan(null);
|
|
1284
|
+
clearCorrectMoveFeedback();
|
|
1285
|
+
clearIncorrectMoveFeedback();
|
|
1269
1286
|
(() => __awaiter(this, void 0, void 0, function* () {
|
|
1270
1287
|
try {
|
|
1271
1288
|
const loaded = yield fetchGame(gameId);
|
|
@@ -1292,7 +1309,7 @@ function useGameReplayTraining({ gameId, startFen, fetchGame, }) {
|
|
|
1292
1309
|
return () => {
|
|
1293
1310
|
cancelled = true;
|
|
1294
1311
|
};
|
|
1295
|
-
}, [gameId, startFen, fetchGame]);
|
|
1312
|
+
}, [gameId, startFen, fetchGame, clearCorrectMoveFeedback, clearIncorrectMoveFeedback]);
|
|
1296
1313
|
const fen = useMemo(() => {
|
|
1297
1314
|
if (!game)
|
|
1298
1315
|
return startFen;
|
|
@@ -1301,22 +1318,42 @@ function useGameReplayTraining({ gameId, startFen, fetchGame, }) {
|
|
|
1301
1318
|
const complete = game ? plyIndex >= game.movesUci.length : false;
|
|
1302
1319
|
const expectedUci = game === null || game === void 0 ? void 0 : game.movesUci[plyIndex];
|
|
1303
1320
|
const expectedSan = game === null || game === void 0 ? void 0 : game.movesSan[plyIndex];
|
|
1321
|
+
const lastMoveUci = useMemo(() => (game ? lastMoveUciAtPly(game.movesUci, plyIndex) : null), [game, plyIndex]);
|
|
1304
1322
|
const handlePieceDrop = useCallback((sourceSquare, targetSquare, piece) => {
|
|
1305
|
-
if (!game ||
|
|
1323
|
+
if (!game ||
|
|
1324
|
+
complete ||
|
|
1325
|
+
!expectedUci ||
|
|
1326
|
+
isShowingCorrectMove ||
|
|
1327
|
+
isShowingIncorrectMove) {
|
|
1306
1328
|
return false;
|
|
1329
|
+
}
|
|
1307
1330
|
const uci = uciFromDrop(fen, sourceSquare, targetSquare, piece);
|
|
1308
1331
|
if (!uci)
|
|
1309
1332
|
return false;
|
|
1310
1333
|
if (uci === expectedUci) {
|
|
1311
1334
|
setFeedback("correct");
|
|
1312
|
-
setPlyIndex((p) => p + 1);
|
|
1313
1335
|
setLastExpectedSan(null);
|
|
1336
|
+
showCorrectMove(targetSquare, () => {
|
|
1337
|
+
setFeedback(null);
|
|
1338
|
+
setPlyIndex((p) => p + 1);
|
|
1339
|
+
});
|
|
1314
1340
|
return true;
|
|
1315
1341
|
}
|
|
1316
1342
|
setFeedback("incorrect");
|
|
1317
1343
|
setLastExpectedSan(expectedSan !== null && expectedSan !== void 0 ? expectedSan : null);
|
|
1344
|
+
showIncorrectMove(sourceSquare);
|
|
1318
1345
|
return false;
|
|
1319
|
-
}, [
|
|
1346
|
+
}, [
|
|
1347
|
+
game,
|
|
1348
|
+
complete,
|
|
1349
|
+
expectedUci,
|
|
1350
|
+
expectedSan,
|
|
1351
|
+
fen,
|
|
1352
|
+
isShowingCorrectMove,
|
|
1353
|
+
isShowingIncorrectMove,
|
|
1354
|
+
showCorrectMove,
|
|
1355
|
+
showIncorrectMove,
|
|
1356
|
+
]);
|
|
1320
1357
|
const revealMove = useCallback(() => {
|
|
1321
1358
|
if (!game || complete || !expectedSan)
|
|
1322
1359
|
return;
|
|
@@ -1334,6 +1371,9 @@ function useGameReplayTraining({ gameId, startFen, fetchGame, }) {
|
|
|
1334
1371
|
error,
|
|
1335
1372
|
feedback,
|
|
1336
1373
|
lastExpectedSan,
|
|
1374
|
+
lastMoveUci,
|
|
1375
|
+
correctMoveSquare,
|
|
1376
|
+
incorrectMoveSquare,
|
|
1337
1377
|
expectedSan,
|
|
1338
1378
|
handlePieceDrop,
|
|
1339
1379
|
revealMove,
|
|
@@ -1354,7 +1394,7 @@ const defaultButtonStyle = {
|
|
|
1354
1394
|
cursor: "pointer",
|
|
1355
1395
|
};
|
|
1356
1396
|
const GameReplayTrainer = ({ gameId, startFen, fetchGame, onExit, theme = "dark", boardWidth = DEFAULT_REFERENCE_LAYOUT.boardWidth, renderStatus, }) => {
|
|
1357
|
-
const { game, fen, plyIndex, totalPlies, complete, loading, error, feedback, lastExpectedSan, handlePieceDrop, revealMove, } = useGameReplayTraining({ gameId, startFen, fetchGame });
|
|
1397
|
+
const { game, fen, plyIndex, totalPlies, complete, loading, error, feedback, lastExpectedSan, lastMoveUci, correctMoveSquare, incorrectMoveSquare, handlePieceDrop, revealMove, } = useGameReplayTraining({ gameId, startFen, fetchGame });
|
|
1358
1398
|
const status = renderStatus === null || renderStatus === void 0 ? void 0 : renderStatus({
|
|
1359
1399
|
loading,
|
|
1360
1400
|
error,
|
|
@@ -1374,7 +1414,7 @@ const GameReplayTrainer = ({ gameId, startFen, fetchGame, onExit, theme = "dark"
|
|
|
1374
1414
|
!error &&
|
|
1375
1415
|
!complete &&
|
|
1376
1416
|
feedback === "incorrect" &&
|
|
1377
|
-
lastExpectedSan && (jsxs("span", { style: { color: "#ef6c00" }, children: ["Expected ", lastExpectedSan] })), !loading && !error && !complete && feedback === null && (jsxs("span", { children: ["Guess move ", plyIndex + 1, " of ", totalPlies] }))] })), jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, position: fen, checkSquare: "", hintSquare: null, incorrectMoveSquare:
|
|
1417
|
+
lastExpectedSan && (jsxs("span", { style: { color: "#ef6c00" }, children: ["Expected ", lastExpectedSan] })), !loading && !error && !complete && feedback === null && (jsxs("span", { children: ["Guess move ", plyIndex + 1, " of ", totalPlies] }))] })), jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, position: fen, checkSquare: "", hintSquare: null, incorrectMoveSquare: incorrectMoveSquare, correctMoveSquare: correctMoveSquare, lastMoveUci: lastMoveUci, arePiecesDraggable: !complete && !loading && !error && !correctMoveSquare && !incorrectMoveSquare, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }) }), !complete && !loading && !error && (jsx("button", { type: "button", style: defaultButtonStyle, onClick: revealMove, children: "Show move" }))] }) }));
|
|
1378
1418
|
};
|
|
1379
1419
|
|
|
1380
1420
|
const mockPosition = {
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,7 @@ function applyBoardMove(fen, sourceSquare, targetSquare, piece) {
|
|
|
41
41
|
}
|
|
42
42
|
/** Play a SAN sequence from a start FEN; returns null if any move is illegal. */
|
|
43
43
|
function applyLineSans(startFen, sans) {
|
|
44
|
+
var _a;
|
|
44
45
|
const chess = new chess_js.Chess(startFen);
|
|
45
46
|
const entries = [];
|
|
46
47
|
for (const san of sans) {
|
|
@@ -48,9 +49,10 @@ function applyLineSans(startFen, sans) {
|
|
|
48
49
|
const move = chess.move(san);
|
|
49
50
|
if (!move)
|
|
50
51
|
return null;
|
|
51
|
-
|
|
52
|
+
const lastUci = `${move.from}${move.to}${(_a = move.promotion) !== null && _a !== void 0 ? _a : ""}`;
|
|
53
|
+
entries.push({ fen: chess.fen(), lastSan: move.san, lastUci });
|
|
52
54
|
}
|
|
53
|
-
catch (
|
|
55
|
+
catch (_b) {
|
|
54
56
|
return null;
|
|
55
57
|
}
|
|
56
58
|
}
|
|
@@ -525,19 +527,19 @@ function initialHistoryState(initialFen, initialLineSans) {
|
|
|
525
527
|
};
|
|
526
528
|
}
|
|
527
529
|
function usePositionHistory(initialFen, initialLineSans) {
|
|
528
|
-
var _a, _b;
|
|
530
|
+
var _a, _b, _c, _d;
|
|
529
531
|
const [history, setHistory] = react.useState(() => initialHistoryState(initialFen, initialLineSans).history);
|
|
530
532
|
const [historyIndex, setHistoryIndex] = react.useState(() => initialHistoryState(initialFen, initialLineSans).historyIndex);
|
|
531
533
|
const canGoBack = historyIndex > 0;
|
|
532
534
|
const canGoForward = historyIndex < history.length - 1;
|
|
533
|
-
const pushEntry = react.useCallback((fen, lastSan) => {
|
|
535
|
+
const pushEntry = react.useCallback((fen, lastSan, lastUci) => {
|
|
534
536
|
setHistory((prev) => {
|
|
535
537
|
const trimmed = prev.slice(0, historyIndex + 1);
|
|
536
|
-
const next = [...trimmed, { fen, lastSan }];
|
|
538
|
+
const next = [...trimmed, { fen, lastSan, lastUci }];
|
|
537
539
|
setHistoryIndex(next.length - 1);
|
|
538
540
|
return next;
|
|
539
541
|
});
|
|
540
|
-
return { fen, lastSan };
|
|
542
|
+
return { fen, lastSan, lastUci };
|
|
541
543
|
}, [historyIndex]);
|
|
542
544
|
const goBack = react.useCallback(() => {
|
|
543
545
|
if (historyIndex <= 0)
|
|
@@ -598,12 +600,14 @@ function usePositionHistory(initialFen, initialLineSans) {
|
|
|
598
600
|
setHistory([{ fen: startFen }, ...entries]);
|
|
599
601
|
setHistoryIndex(entries.length);
|
|
600
602
|
}, []);
|
|
603
|
+
const lastMoveUci = historyIndex > 0 ? (_d = (_c = history[historyIndex]) === null || _c === void 0 ? void 0 : _c.lastUci) !== null && _d !== void 0 ? _d : null : null;
|
|
601
604
|
return {
|
|
602
605
|
canGoBack,
|
|
603
606
|
canGoForward,
|
|
604
607
|
lineSans,
|
|
605
608
|
forwardSans,
|
|
606
609
|
currentFen,
|
|
610
|
+
lastMoveUci,
|
|
607
611
|
pushEntry,
|
|
608
612
|
pushEntries,
|
|
609
613
|
replaceLineEntries,
|
|
@@ -648,7 +652,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
648
652
|
}
|
|
649
653
|
isAnimatingVariationRef.current = false;
|
|
650
654
|
}, []);
|
|
651
|
-
const { canGoBack, canGoForward, lineSans, forwardSans, currentFen, pushEntry, pushEntries, replaceLineEntries, goBack, goForward, goFirst, goLast, resetHistory, } = usePositionHistory(initialFen, initialLineSans);
|
|
655
|
+
const { canGoBack, canGoForward, lineSans, forwardSans, currentFen, pushEntry, pushEntries, replaceLineEntries, goBack, goForward, goFirst, goLast, resetHistory, lastMoveUci: historyLastMoveUci, } = usePositionHistory(initialFen, initialLineSans);
|
|
652
656
|
/** FEN used for explorer API queries — follows move history, not animation frames. */
|
|
653
657
|
const queryFen = currentFen;
|
|
654
658
|
const initialLineKey = react.useMemo(() => { var _a; return (_a = initialLineSans === null || initialLineSans === void 0 ? void 0 : initialLineSans.join("|")) !== null && _a !== void 0 ? _a : ""; }, [initialLineSans]);
|
|
@@ -683,6 +687,10 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
683
687
|
}), [sources]);
|
|
684
688
|
const lastAppliedLineKeyRef = react.useRef(initialLineKey);
|
|
685
689
|
react.useEffect(() => () => cancelVariationAnimation(), [cancelVariationAnimation]);
|
|
690
|
+
// Hold URL line callbacks until history catches up after an external line change.
|
|
691
|
+
react.useEffect(() => {
|
|
692
|
+
readyForLineSyncRef.current = false;
|
|
693
|
+
}, [initialLineKey]);
|
|
686
694
|
// Apply URL line changes only when the external line key changes — not when the
|
|
687
695
|
// user clicks moves (internal lineSans updates must not re-sync from stale props).
|
|
688
696
|
react.useEffect(() => {
|
|
@@ -893,7 +901,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
893
901
|
const nextFen = fenAfterUci(queryFen, move.uci);
|
|
894
902
|
if (!nextFen)
|
|
895
903
|
return;
|
|
896
|
-
pushEntry(nextFen, move.san);
|
|
904
|
+
pushEntry(nextFen, move.san, move.uci);
|
|
897
905
|
setSelectedVariationKey(undefined);
|
|
898
906
|
applyNavigation(nextFen, true);
|
|
899
907
|
}, [queryFen, pushEntry, applyNavigation, cancelVariationAnimation]);
|
|
@@ -907,7 +915,11 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
907
915
|
const nextFen = fenAfterUci(lineFen, line.uciPath[i]);
|
|
908
916
|
if (!nextFen)
|
|
909
917
|
return;
|
|
910
|
-
entries.push({
|
|
918
|
+
entries.push({
|
|
919
|
+
fen: nextFen,
|
|
920
|
+
lastSan: line.moves[i].san,
|
|
921
|
+
lastUci: line.uciPath[i],
|
|
922
|
+
});
|
|
911
923
|
lineFen = nextFen;
|
|
912
924
|
}
|
|
913
925
|
if (entries.length === 0)
|
|
@@ -942,7 +954,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
942
954
|
const result = applyBoardMove(queryFen, sourceSquare, targetSquare, piece);
|
|
943
955
|
if (!result)
|
|
944
956
|
return false;
|
|
945
|
-
pushEntry(result.fen, result.san);
|
|
957
|
+
pushEntry(result.fen, result.san, result.uci);
|
|
946
958
|
setSelectedVariationKey(undefined);
|
|
947
959
|
applyNavigation(result.fen, true);
|
|
948
960
|
return true;
|
|
@@ -1019,6 +1031,7 @@ function usePositionReferenceData({ fenProp, onFenChange, initialLineSans, onLin
|
|
|
1019
1031
|
canGoForward,
|
|
1020
1032
|
forwardSans,
|
|
1021
1033
|
selectedVariationKey,
|
|
1034
|
+
lastMoveUci: historyLastMoveUci,
|
|
1022
1035
|
setSources,
|
|
1023
1036
|
setGamesMoveFilterUci,
|
|
1024
1037
|
handleMoveSelect,
|
|
@@ -1117,7 +1130,7 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
|
|
|
1117
1130
|
fetchPositionGames,
|
|
1118
1131
|
fetchPositionVariations,
|
|
1119
1132
|
});
|
|
1120
|
-
const { fen, boardFen, games, sources, lineSans, loading, showPositionLoading, gamesLoading, positionReady, displayMoves, error, lineLabel, canGoBack, canGoForward, forwardSans, selectedVariationKey, setSources, handleMoveSelect, handleLineSelect, handlePieceDrop, handleBack, handleForward, handleFirst, handleLast, } = referenceData;
|
|
1133
|
+
const { fen, boardFen, games, sources, lineSans, loading, showPositionLoading, gamesLoading, positionReady, displayMoves, error, lineLabel, canGoBack, canGoForward, forwardSans, selectedVariationKey, lastMoveUci, setSources, handleMoveSelect, handleLineSelect, handlePieceDrop, handleBack, handleForward, handleFirst, handleLast, } = referenceData;
|
|
1121
1134
|
reactChessCore.usePositionKeyboardNav({
|
|
1122
1135
|
enabled: keyboardNav,
|
|
1123
1136
|
canPrev: canGoBack,
|
|
@@ -1181,7 +1194,7 @@ const PositionReferenceExplorerCore = ({ fen: fenProp, onFenChange, initialLineS
|
|
|
1181
1194
|
overflow: fillHeight ? "hidden" : "visible",
|
|
1182
1195
|
boxSizing: "border-box",
|
|
1183
1196
|
};
|
|
1184
|
-
const board = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, position: boardFen, boardOrientation: boardOrientation, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }, boardOrientation) }), renderBoardNav(boardNavProps)] }));
|
|
1197
|
+
const board = (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, position: boardFen, boardOrientation: boardOrientation, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, lastMoveUci: lastMoveUci, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }, boardOrientation) }), renderBoardNav(boardNavProps)] }));
|
|
1185
1198
|
const referencePanel = (jsxRuntime.jsx(DefaultReferencePanel, { theme: theme, fillHeight: fillHeight, status: renderStatus({
|
|
1186
1199
|
error,
|
|
1187
1200
|
loading: showPositionLoading,
|
|
@@ -1262,12 +1275,16 @@ function useGameReplayTraining({ gameId, startFen, fetchGame, }) {
|
|
|
1262
1275
|
const [error, setError] = react.useState(null);
|
|
1263
1276
|
const [feedback, setFeedback] = react.useState(null);
|
|
1264
1277
|
const [lastExpectedSan, setLastExpectedSan] = react.useState(null);
|
|
1278
|
+
const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = reactChessCore.useCorrectMoveFeedback();
|
|
1279
|
+
const { incorrectMoveSquare, showIncorrectMove, clearIncorrectMoveFeedback, isShowingIncorrectMove, } = reactChessCore.useIncorrectMoveFeedback();
|
|
1265
1280
|
react.useEffect(() => {
|
|
1266
1281
|
let cancelled = false;
|
|
1267
1282
|
setLoading(true);
|
|
1268
1283
|
setError(null);
|
|
1269
1284
|
setFeedback(null);
|
|
1270
1285
|
setLastExpectedSan(null);
|
|
1286
|
+
clearCorrectMoveFeedback();
|
|
1287
|
+
clearIncorrectMoveFeedback();
|
|
1271
1288
|
(() => __awaiter(this, void 0, void 0, function* () {
|
|
1272
1289
|
try {
|
|
1273
1290
|
const loaded = yield fetchGame(gameId);
|
|
@@ -1294,7 +1311,7 @@ function useGameReplayTraining({ gameId, startFen, fetchGame, }) {
|
|
|
1294
1311
|
return () => {
|
|
1295
1312
|
cancelled = true;
|
|
1296
1313
|
};
|
|
1297
|
-
}, [gameId, startFen, fetchGame]);
|
|
1314
|
+
}, [gameId, startFen, fetchGame, clearCorrectMoveFeedback, clearIncorrectMoveFeedback]);
|
|
1298
1315
|
const fen = react.useMemo(() => {
|
|
1299
1316
|
if (!game)
|
|
1300
1317
|
return startFen;
|
|
@@ -1303,22 +1320,42 @@ function useGameReplayTraining({ gameId, startFen, fetchGame, }) {
|
|
|
1303
1320
|
const complete = game ? plyIndex >= game.movesUci.length : false;
|
|
1304
1321
|
const expectedUci = game === null || game === void 0 ? void 0 : game.movesUci[plyIndex];
|
|
1305
1322
|
const expectedSan = game === null || game === void 0 ? void 0 : game.movesSan[plyIndex];
|
|
1323
|
+
const lastMoveUci = react.useMemo(() => (game ? reactChessCore.lastMoveUciAtPly(game.movesUci, plyIndex) : null), [game, plyIndex]);
|
|
1306
1324
|
const handlePieceDrop = react.useCallback((sourceSquare, targetSquare, piece) => {
|
|
1307
|
-
if (!game ||
|
|
1325
|
+
if (!game ||
|
|
1326
|
+
complete ||
|
|
1327
|
+
!expectedUci ||
|
|
1328
|
+
isShowingCorrectMove ||
|
|
1329
|
+
isShowingIncorrectMove) {
|
|
1308
1330
|
return false;
|
|
1331
|
+
}
|
|
1309
1332
|
const uci = uciFromDrop(fen, sourceSquare, targetSquare, piece);
|
|
1310
1333
|
if (!uci)
|
|
1311
1334
|
return false;
|
|
1312
1335
|
if (uci === expectedUci) {
|
|
1313
1336
|
setFeedback("correct");
|
|
1314
|
-
setPlyIndex((p) => p + 1);
|
|
1315
1337
|
setLastExpectedSan(null);
|
|
1338
|
+
showCorrectMove(targetSquare, () => {
|
|
1339
|
+
setFeedback(null);
|
|
1340
|
+
setPlyIndex((p) => p + 1);
|
|
1341
|
+
});
|
|
1316
1342
|
return true;
|
|
1317
1343
|
}
|
|
1318
1344
|
setFeedback("incorrect");
|
|
1319
1345
|
setLastExpectedSan(expectedSan !== null && expectedSan !== void 0 ? expectedSan : null);
|
|
1346
|
+
showIncorrectMove(sourceSquare);
|
|
1320
1347
|
return false;
|
|
1321
|
-
}, [
|
|
1348
|
+
}, [
|
|
1349
|
+
game,
|
|
1350
|
+
complete,
|
|
1351
|
+
expectedUci,
|
|
1352
|
+
expectedSan,
|
|
1353
|
+
fen,
|
|
1354
|
+
isShowingCorrectMove,
|
|
1355
|
+
isShowingIncorrectMove,
|
|
1356
|
+
showCorrectMove,
|
|
1357
|
+
showIncorrectMove,
|
|
1358
|
+
]);
|
|
1322
1359
|
const revealMove = react.useCallback(() => {
|
|
1323
1360
|
if (!game || complete || !expectedSan)
|
|
1324
1361
|
return;
|
|
@@ -1336,6 +1373,9 @@ function useGameReplayTraining({ gameId, startFen, fetchGame, }) {
|
|
|
1336
1373
|
error,
|
|
1337
1374
|
feedback,
|
|
1338
1375
|
lastExpectedSan,
|
|
1376
|
+
lastMoveUci,
|
|
1377
|
+
correctMoveSquare,
|
|
1378
|
+
incorrectMoveSquare,
|
|
1339
1379
|
expectedSan,
|
|
1340
1380
|
handlePieceDrop,
|
|
1341
1381
|
revealMove,
|
|
@@ -1356,7 +1396,7 @@ const defaultButtonStyle = {
|
|
|
1356
1396
|
cursor: "pointer",
|
|
1357
1397
|
};
|
|
1358
1398
|
const GameReplayTrainer = ({ gameId, startFen, fetchGame, onExit, theme = "dark", boardWidth = DEFAULT_REFERENCE_LAYOUT.boardWidth, renderStatus, }) => {
|
|
1359
|
-
const { game, fen, plyIndex, totalPlies, complete, loading, error, feedback, lastExpectedSan, handlePieceDrop, revealMove, } = useGameReplayTraining({ gameId, startFen, fetchGame });
|
|
1399
|
+
const { game, fen, plyIndex, totalPlies, complete, loading, error, feedback, lastExpectedSan, lastMoveUci, correctMoveSquare, incorrectMoveSquare, handlePieceDrop, revealMove, } = useGameReplayTraining({ gameId, startFen, fetchGame });
|
|
1360
1400
|
const status = renderStatus === null || renderStatus === void 0 ? void 0 : renderStatus({
|
|
1361
1401
|
loading,
|
|
1362
1402
|
error,
|
|
@@ -1376,7 +1416,7 @@ const GameReplayTrainer = ({ gameId, startFen, fetchGame, onExit, theme = "dark"
|
|
|
1376
1416
|
!error &&
|
|
1377
1417
|
!complete &&
|
|
1378
1418
|
feedback === "incorrect" &&
|
|
1379
|
-
lastExpectedSan && (jsxRuntime.jsxs("span", { style: { color: "#ef6c00" }, children: ["Expected ", lastExpectedSan] })), !loading && !error && !complete && feedback === null && (jsxRuntime.jsxs("span", { children: ["Guess move ", plyIndex + 1, " of ", totalPlies] }))] })), jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, position: fen, checkSquare: "", hintSquare: null, incorrectMoveSquare:
|
|
1419
|
+
lastExpectedSan && (jsxRuntime.jsxs("span", { style: { color: "#ef6c00" }, children: ["Expected ", lastExpectedSan] })), !loading && !error && !complete && feedback === null && (jsxRuntime.jsxs("span", { children: ["Guess move ", plyIndex + 1, " of ", totalPlies] }))] })), jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, position: fen, checkSquare: "", hintSquare: null, incorrectMoveSquare: incorrectMoveSquare, correctMoveSquare: correctMoveSquare, lastMoveUci: lastMoveUci, arePiecesDraggable: !complete && !loading && !error && !correctMoveSquare && !incorrectMoveSquare, onPieceDrop: handlePieceDrop, promotionDialogVariant: "modal" }) }), !complete && !loading && !error && (jsxRuntime.jsx("button", { type: "button", style: defaultButtonStyle, onClick: revealMove, children: "Show move" }))] }) }));
|
|
1380
1420
|
};
|
|
1381
1421
|
|
|
1382
1422
|
const mockPosition = {
|
package/package.json
CHANGED