react-chess-replay-trainer 0.0.1
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 +44 -0
- package/dist/features/replay/ReplayTrainer.d.ts +31 -0
- package/dist/features/replay/analysis/ReplayAnalysisBoard.d.ts +18 -0
- package/dist/features/replay/analysis/ReplayAnalysisPosition.d.ts +32 -0
- package/dist/features/replay/analysis/ReplayEngineEvaluationPanel.d.ts +8 -0
- package/dist/features/replay/analysis/analysisBoardStyles.d.ts +63 -0
- package/dist/features/replay/analysis/buildReplayAnalysisContext.d.ts +4 -0
- package/dist/features/replay/analysis/hooks/useReplayAnalysisBoard.d.ts +34 -0
- package/dist/features/replay/analysis/index.d.ts +6 -0
- package/dist/features/replay/analysis/replayAnalysisUtils.d.ts +3 -0
- package/dist/features/replay/analysis/types.d.ts +22 -0
- package/dist/features/replay/buildReplayAnalysisContext.d.ts +4 -0
- package/dist/features/replay/constants.d.ts +3 -0
- package/dist/features/replay/hooks/useReplayTrainer.d.ts +43 -0
- package/dist/features/replay/index.d.ts +7 -0
- package/dist/features/replay/replayTrainerStyles.d.ts +28 -0
- package/dist/features/replay/replayUtils.d.ts +12 -0
- package/dist/features/replay/types.d.ts +36 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.esm.js +410 -0
- package/dist/index.js +448 -0
- package/dist/stories/ReplayTrainer.stories.d.ts +11 -0
- package/dist/stories/fixtures/morphyOperaGame.d.ts +4 -0
- package/package.json +56 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
+
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
|
3
|
+
import { ChessboardDnDProvider } from 'react-chessboard';
|
|
4
|
+
import { ThemeProvider, HighlightChessboard, PlyNavigation, AnalysisErrorBoundary, AnalysisBoard } from 'react-chess-core';
|
|
5
|
+
export { AnalysisBoard, AnalysisBoardCore, AnalysisErrorBoundary, DEFAULT_ANALYSIS_LAYOUT, DefaultPlyNavigation, PlyNavigation, defaultRenderPlyNavigation } from 'react-chess-core';
|
|
6
|
+
import { Chess } from 'chess.js';
|
|
7
|
+
|
|
8
|
+
/** Standard chess starting position; game move lists replay from here. */
|
|
9
|
+
const REPLAY_START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
|
10
|
+
const DEFAULT_BOARD_WIDTH = 480;
|
|
11
|
+
|
|
12
|
+
/** Build a core {@link AnalysisContext} for the current replay browse position. */
|
|
13
|
+
function buildReplayAnalysisContext(game, plyIndex, boardOrientation) {
|
|
14
|
+
return {
|
|
15
|
+
initialFen: REPLAY_START_FEN,
|
|
16
|
+
solutionMoves: game.movesUci,
|
|
17
|
+
currentPly: Math.max(0, Math.min(plyIndex, game.movesUci.length)),
|
|
18
|
+
boardOrientation,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Compare positions ignoring move clocks (first four FEN fields). */
|
|
23
|
+
function normalizeFen(fen) {
|
|
24
|
+
return fen.trim().split(/\s+/).slice(0, 4).join(' ');
|
|
25
|
+
}
|
|
26
|
+
function applyUci(chess, uci) {
|
|
27
|
+
const from = uci.slice(0, 2);
|
|
28
|
+
const to = uci.slice(2, 4);
|
|
29
|
+
const promotion = uci.length > 4 ? uci[4] : undefined;
|
|
30
|
+
const move = chess.move({ from, to, promotion });
|
|
31
|
+
if (!move) {
|
|
32
|
+
throw new Error(`Illegal UCI move: ${uci}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** FEN after applying the first `ply` moves from the standard start. */
|
|
36
|
+
function fenAtPly(movesUci, ply) {
|
|
37
|
+
const chess = new Chess(REPLAY_START_FEN);
|
|
38
|
+
for (let i = 0; i < ply && i < movesUci.length; i++) {
|
|
39
|
+
applyUci(chess, movesUci[i]);
|
|
40
|
+
}
|
|
41
|
+
return chess.fen();
|
|
42
|
+
}
|
|
43
|
+
/** Index of the next move to play to reach `targetFen`, or 0 if not found. */
|
|
44
|
+
function findPlyIndexForFen(movesUci, targetFen) {
|
|
45
|
+
const target = normalizeFen(targetFen);
|
|
46
|
+
const chess = new Chess(REPLAY_START_FEN);
|
|
47
|
+
if (normalizeFen(chess.fen()) === target) {
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
for (let i = 0; i < movesUci.length; i++) {
|
|
51
|
+
applyUci(chess, movesUci[i]);
|
|
52
|
+
if (normalizeFen(chess.fen()) === target) {
|
|
53
|
+
return i + 1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
function sideToMove(fen) {
|
|
59
|
+
return fen.trim().split(/\s+/)[1] === 'b' ? 'b' : 'w';
|
|
60
|
+
}
|
|
61
|
+
/** Resolve a board drag into a legal UCI string, or null when illegal. */
|
|
62
|
+
function uciFromDrop(fen, sourceSquare, targetSquare, piece) {
|
|
63
|
+
var _a, _b;
|
|
64
|
+
const chess = new Chess(fen);
|
|
65
|
+
const pieceType = (_a = piece[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
66
|
+
const legal = chess
|
|
67
|
+
.moves({ square: sourceSquare, verbose: true })
|
|
68
|
+
.find((move) => move.to === targetSquare &&
|
|
69
|
+
(!move.promotion || move.promotion === pieceType));
|
|
70
|
+
if (!legal)
|
|
71
|
+
return null;
|
|
72
|
+
return `${legal.from}${legal.to}${(_b = legal.promotion) !== null && _b !== void 0 ? _b : ''}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Pause (ms) before the opponent's reply is auto-played in single-color drills. */
|
|
76
|
+
const OPPONENT_MOVE_DELAY_MS = 350;
|
|
77
|
+
function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, }) {
|
|
78
|
+
const [game, setGame] = useState(null);
|
|
79
|
+
const [loading, setLoading] = useState(true);
|
|
80
|
+
const [error, setError] = useState(null);
|
|
81
|
+
const [mode, setMode] = useState('browse');
|
|
82
|
+
const [trainColor, setTrainColor] = useState('both');
|
|
83
|
+
const [plyIndex, setPlyIndex] = useState(0);
|
|
84
|
+
const [feedback, setFeedback] = useState(null);
|
|
85
|
+
const [expectedSan, setExpectedSan] = useState(null);
|
|
86
|
+
const [expectedUci, setExpectedUci] = useState(null);
|
|
87
|
+
const fetchGameRef = useRef(fetchGame);
|
|
88
|
+
fetchGameRef.current = fetchGame;
|
|
89
|
+
const onMissRef = useRef(onMiss);
|
|
90
|
+
onMissRef.current = onMiss;
|
|
91
|
+
const onCompleteRef = useRef(onComplete);
|
|
92
|
+
onCompleteRef.current = onComplete;
|
|
93
|
+
const recordedRef = useRef(new Set());
|
|
94
|
+
const completedFiredRef = useRef(false);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
let cancelled = false;
|
|
97
|
+
setLoading(true);
|
|
98
|
+
setError(null);
|
|
99
|
+
setGame(null);
|
|
100
|
+
recordedRef.current = new Set();
|
|
101
|
+
completedFiredRef.current = false;
|
|
102
|
+
setMode('browse');
|
|
103
|
+
setTrainColor('both');
|
|
104
|
+
setFeedback(null);
|
|
105
|
+
setExpectedSan(null);
|
|
106
|
+
setExpectedUci(null);
|
|
107
|
+
fetchGameRef
|
|
108
|
+
.current(gameId)
|
|
109
|
+
.then((loaded) => {
|
|
110
|
+
if (cancelled)
|
|
111
|
+
return;
|
|
112
|
+
if (!loaded) {
|
|
113
|
+
setError('Game not found.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
setGame(loaded);
|
|
117
|
+
setPlyIndex(startFen ? findPlyIndexForFen(loaded.movesUci, startFen) : 0);
|
|
118
|
+
})
|
|
119
|
+
.catch((err) => {
|
|
120
|
+
if (cancelled)
|
|
121
|
+
return;
|
|
122
|
+
setError(err instanceof Error ? err.message : 'Failed to load game.');
|
|
123
|
+
})
|
|
124
|
+
.finally(() => {
|
|
125
|
+
if (!cancelled)
|
|
126
|
+
setLoading(false);
|
|
127
|
+
});
|
|
128
|
+
return () => {
|
|
129
|
+
cancelled = true;
|
|
130
|
+
};
|
|
131
|
+
}, [gameId, startFen]);
|
|
132
|
+
const movesUci = useMemo(() => { var _a; return (_a = game === null || game === void 0 ? void 0 : game.movesUci) !== null && _a !== void 0 ? _a : []; }, [game]);
|
|
133
|
+
const totalPly = movesUci.length;
|
|
134
|
+
const fen = useMemo(() => fenAtPly(movesUci, plyIndex), [movesUci, plyIndex]);
|
|
135
|
+
const complete = plyIndex >= totalPly && totalPly > 0;
|
|
136
|
+
const sideToMove$1 = sideToMove(fen);
|
|
137
|
+
const isUserTurn = trainColor === 'both' ||
|
|
138
|
+
(trainColor === 'white' && sideToMove$1 === 'w') ||
|
|
139
|
+
(trainColor === 'black' && sideToMove$1 === 'b');
|
|
140
|
+
const clearTransient = useCallback(() => {
|
|
141
|
+
setFeedback(null);
|
|
142
|
+
setExpectedSan(null);
|
|
143
|
+
setExpectedUci(null);
|
|
144
|
+
}, []);
|
|
145
|
+
const goTo = useCallback((ply) => {
|
|
146
|
+
const clamped = Math.max(0, Math.min(ply, totalPly));
|
|
147
|
+
completedFiredRef.current = clamped >= totalPly ? completedFiredRef.current : false;
|
|
148
|
+
setPlyIndex(clamped);
|
|
149
|
+
clearTransient();
|
|
150
|
+
}, [totalPly, clearTransient]);
|
|
151
|
+
const goFirst = useCallback(() => goTo(0), [goTo]);
|
|
152
|
+
const goPrev = useCallback(() => goTo(plyIndex - 1), [goTo, plyIndex]);
|
|
153
|
+
const goNext = useCallback(() => goTo(plyIndex + 1), [goTo, plyIndex]);
|
|
154
|
+
const goLast = useCallback(() => goTo(totalPly), [goTo, totalPly]);
|
|
155
|
+
const startTraining = useCallback((color = 'both') => {
|
|
156
|
+
setTrainColor(color);
|
|
157
|
+
setMode('train');
|
|
158
|
+
clearTransient();
|
|
159
|
+
}, [clearTransient]);
|
|
160
|
+
const stopTraining = useCallback(() => {
|
|
161
|
+
setMode('browse');
|
|
162
|
+
clearTransient();
|
|
163
|
+
}, [clearTransient]);
|
|
164
|
+
const recordMiss = useCallback((index) => {
|
|
165
|
+
var _a, _b, _c;
|
|
166
|
+
if (recordedRef.current.has(index))
|
|
167
|
+
return;
|
|
168
|
+
recordedRef.current.add(index);
|
|
169
|
+
const expectedUci = movesUci[index];
|
|
170
|
+
if (!expectedUci)
|
|
171
|
+
return;
|
|
172
|
+
const positionFen = fenAtPly(movesUci, index);
|
|
173
|
+
const miss = {
|
|
174
|
+
index,
|
|
175
|
+
fen: positionFen,
|
|
176
|
+
expectedUci,
|
|
177
|
+
expectedSan: (_b = (_a = game === null || game === void 0 ? void 0 : game.movesSan) === null || _a === void 0 ? void 0 : _a[index]) !== null && _b !== void 0 ? _b : expectedUci,
|
|
178
|
+
sideToMove: sideToMove(positionFen),
|
|
179
|
+
setupUci: index > 0 ? movesUci[index - 1] : undefined,
|
|
180
|
+
setupFen: index > 0 ? fenAtPly(movesUci, index - 1) : undefined,
|
|
181
|
+
};
|
|
182
|
+
(_c = onMissRef.current) === null || _c === void 0 ? void 0 : _c.call(onMissRef, miss);
|
|
183
|
+
}, [movesUci, game]);
|
|
184
|
+
const revealMove = useCallback(() => {
|
|
185
|
+
var _a, _b;
|
|
186
|
+
if (complete || !isUserTurn)
|
|
187
|
+
return;
|
|
188
|
+
const uci = movesUci[plyIndex];
|
|
189
|
+
if (!uci)
|
|
190
|
+
return;
|
|
191
|
+
setExpectedUci(uci);
|
|
192
|
+
setExpectedSan((_b = (_a = game === null || game === void 0 ? void 0 : game.movesSan) === null || _a === void 0 ? void 0 : _a[plyIndex]) !== null && _b !== void 0 ? _b : uci);
|
|
193
|
+
setFeedback('incorrect');
|
|
194
|
+
recordMiss(plyIndex);
|
|
195
|
+
}, [complete, game, movesUci, plyIndex, recordMiss, isUserTurn]);
|
|
196
|
+
const handleDrop = useCallback((source, target, piece) => {
|
|
197
|
+
var _a, _b;
|
|
198
|
+
if (mode !== 'train' || complete || !isUserTurn)
|
|
199
|
+
return false;
|
|
200
|
+
const expectedUci = movesUci[plyIndex];
|
|
201
|
+
if (!expectedUci)
|
|
202
|
+
return false;
|
|
203
|
+
const uci = uciFromDrop(fen, source, target, piece);
|
|
204
|
+
if (!uci)
|
|
205
|
+
return false;
|
|
206
|
+
if (uci.toLowerCase() === expectedUci.toLowerCase()) {
|
|
207
|
+
setFeedback('correct');
|
|
208
|
+
setExpectedSan(null);
|
|
209
|
+
setExpectedUci(null);
|
|
210
|
+
setPlyIndex((p) => p + 1);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
setFeedback('incorrect');
|
|
214
|
+
setExpectedSan((_b = (_a = game === null || game === void 0 ? void 0 : game.movesSan) === null || _a === void 0 ? void 0 : _a[plyIndex]) !== null && _b !== void 0 ? _b : expectedUci);
|
|
215
|
+
setExpectedUci(expectedUci);
|
|
216
|
+
recordMiss(plyIndex);
|
|
217
|
+
return false;
|
|
218
|
+
}, [mode, complete, isUserTurn, movesUci, plyIndex, fen, game, recordMiss]);
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
var _a;
|
|
221
|
+
if (mode === 'train' && complete && !completedFiredRef.current) {
|
|
222
|
+
completedFiredRef.current = true;
|
|
223
|
+
(_a = onCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onCompleteRef);
|
|
224
|
+
}
|
|
225
|
+
}, [mode, complete]);
|
|
226
|
+
// In single-color drills, auto-play the opponent's reply once it's their turn
|
|
227
|
+
// (e.g. after the user guesses correctly, or when training starts mid-game).
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (mode !== 'train' || complete || trainColor === 'both' || isUserTurn) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const id = setTimeout(() => {
|
|
233
|
+
setPlyIndex((p) => (p < totalPly ? p + 1 : p));
|
|
234
|
+
clearTransient();
|
|
235
|
+
}, OPPONENT_MOVE_DELAY_MS);
|
|
236
|
+
return () => clearTimeout(id);
|
|
237
|
+
}, [mode, complete, trainColor, isUserTurn, plyIndex, totalPly, clearTransient]);
|
|
238
|
+
return {
|
|
239
|
+
game,
|
|
240
|
+
loading,
|
|
241
|
+
error,
|
|
242
|
+
mode,
|
|
243
|
+
fen,
|
|
244
|
+
plyIndex,
|
|
245
|
+
totalPly,
|
|
246
|
+
complete,
|
|
247
|
+
sideToMove: sideToMove$1,
|
|
248
|
+
trainColor,
|
|
249
|
+
isUserTurn,
|
|
250
|
+
feedback,
|
|
251
|
+
expectedSan,
|
|
252
|
+
expectedUci,
|
|
253
|
+
canPrev: plyIndex > 0,
|
|
254
|
+
canNext: plyIndex < totalPly,
|
|
255
|
+
goFirst,
|
|
256
|
+
goPrev,
|
|
257
|
+
goNext,
|
|
258
|
+
goLast,
|
|
259
|
+
goTo,
|
|
260
|
+
startTraining,
|
|
261
|
+
stopTraining,
|
|
262
|
+
revealMove,
|
|
263
|
+
handleDrop,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const TRAIN_COLOR_LABEL = {
|
|
268
|
+
white: 'Training White',
|
|
269
|
+
black: 'Training Black',
|
|
270
|
+
both: 'Training Both',
|
|
271
|
+
};
|
|
272
|
+
const columnStyle = {
|
|
273
|
+
display: 'flex',
|
|
274
|
+
flexDirection: 'column',
|
|
275
|
+
gap: 8,
|
|
276
|
+
};
|
|
277
|
+
const centerStyle = {
|
|
278
|
+
display: 'flex',
|
|
279
|
+
flexDirection: 'column',
|
|
280
|
+
alignItems: 'center',
|
|
281
|
+
justifyContent: 'center',
|
|
282
|
+
gap: 12,
|
|
283
|
+
minHeight: 200,
|
|
284
|
+
};
|
|
285
|
+
const headerStyle = {
|
|
286
|
+
display: 'flex',
|
|
287
|
+
justifyContent: 'space-between',
|
|
288
|
+
alignItems: 'baseline',
|
|
289
|
+
fontSize: 14,
|
|
290
|
+
};
|
|
291
|
+
const controlsRowStyle = {
|
|
292
|
+
display: 'flex',
|
|
293
|
+
flexWrap: 'wrap',
|
|
294
|
+
gap: 8,
|
|
295
|
+
justifyContent: 'center',
|
|
296
|
+
};
|
|
297
|
+
const playerNameStyle = {
|
|
298
|
+
fontWeight: 600,
|
|
299
|
+
};
|
|
300
|
+
const feedbackContainerStyle = {
|
|
301
|
+
minHeight: 24,
|
|
302
|
+
textAlign: 'center',
|
|
303
|
+
};
|
|
304
|
+
const customBoardStyle = {
|
|
305
|
+
borderRadius: 4,
|
|
306
|
+
};
|
|
307
|
+
function palette(theme) {
|
|
308
|
+
return {
|
|
309
|
+
text: theme === 'dark' ? '#e8e8e8' : '#1a1a1a',
|
|
310
|
+
subtle: theme === 'dark' ? '#9aa0a6' : '#5f6368',
|
|
311
|
+
border: theme === 'dark' ? '#3a3a3a' : '#d0d0d0',
|
|
312
|
+
surface: theme === 'dark' ? '#262626' : '#f5f5f5',
|
|
313
|
+
primary: '#3a7bd5',
|
|
314
|
+
success: '#2e7d32',
|
|
315
|
+
error: '#c62828',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function mainContainerStyle(boardWidth, colors) {
|
|
319
|
+
return Object.assign(Object.assign({}, columnStyle), { width: boardWidth, color: colors.text });
|
|
320
|
+
}
|
|
321
|
+
function centerContainerStyle(boardWidth, color) {
|
|
322
|
+
return Object.assign(Object.assign({}, centerStyle), { width: boardWidth, color });
|
|
323
|
+
}
|
|
324
|
+
function subtleTextStyle(colors) {
|
|
325
|
+
return { color: colors.subtle };
|
|
326
|
+
}
|
|
327
|
+
function statusLineStyle(colors) {
|
|
328
|
+
return { color: colors.subtle, fontSize: 13, textAlign: 'center' };
|
|
329
|
+
}
|
|
330
|
+
function feedbackMessageStyle(colors, tone) {
|
|
331
|
+
return {
|
|
332
|
+
color: tone === 'success' ? colors.success : colors.error,
|
|
333
|
+
fontWeight: 600,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function buttonStyle(colors, variant) {
|
|
337
|
+
const base = {
|
|
338
|
+
padding: variant === 'nav' ? '4px 10px' : '8px 14px',
|
|
339
|
+
borderRadius: 6,
|
|
340
|
+
cursor: 'pointer',
|
|
341
|
+
fontSize: 14,
|
|
342
|
+
fontWeight: 600,
|
|
343
|
+
border: `1px solid ${colors.border}`,
|
|
344
|
+
background: colors.surface,
|
|
345
|
+
color: colors.text,
|
|
346
|
+
};
|
|
347
|
+
if (variant === 'primary') {
|
|
348
|
+
return Object.assign(Object.assign({}, base), { background: colors.primary, borderColor: colors.primary, color: '#fff' });
|
|
349
|
+
}
|
|
350
|
+
return base;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Browse a game freely, then drill it from any point. Browsing (first / prev /
|
|
355
|
+
* next / last / slider) never records anything; once the user hits "Train from
|
|
356
|
+
* here" each wrong move is reported through {@link ReplayTrainerProps.onMiss}.
|
|
357
|
+
*/
|
|
358
|
+
const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme = 'dark', boardWidth = DEFAULT_BOARD_WIDTH, orientation = 'white', engine, renderPlyNavigation, showPlyScrubber = true, }) => {
|
|
359
|
+
var _a, _b, _c;
|
|
360
|
+
const state = useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete });
|
|
361
|
+
const colors = palette(theme);
|
|
362
|
+
const [analysisOpen, setAnalysisOpen] = useState(false);
|
|
363
|
+
const [analysisSnapshot, setAnalysisSnapshot] = useState(null);
|
|
364
|
+
const boardOrientation = state.trainColor === 'white'
|
|
365
|
+
? 'white'
|
|
366
|
+
: state.trainColor === 'black'
|
|
367
|
+
? 'black'
|
|
368
|
+
: orientation;
|
|
369
|
+
const openAnalysis = useCallback(() => {
|
|
370
|
+
if (!state.game) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
setAnalysisSnapshot(buildReplayAnalysisContext(state.game, state.plyIndex, boardOrientation));
|
|
374
|
+
setAnalysisOpen(true);
|
|
375
|
+
}, [state.game, state.plyIndex, boardOrientation]);
|
|
376
|
+
const closeAnalysis = useCallback(() => {
|
|
377
|
+
setAnalysisOpen(false);
|
|
378
|
+
}, []);
|
|
379
|
+
if (state.loading) {
|
|
380
|
+
return (jsx("div", { style: centerContainerStyle(boardWidth, colors.subtle), children: "Loading game\u2026" }));
|
|
381
|
+
}
|
|
382
|
+
if (state.error || !state.game) {
|
|
383
|
+
return (jsxs("div", { style: centerContainerStyle(boardWidth, colors.error), children: [(_a = state.error) !== null && _a !== void 0 ? _a : 'Game unavailable.', onExit && (jsx("button", { type: "button", onClick: onExit, style: buttonStyle(colors, 'ghost'), children: "Back" }))] }));
|
|
384
|
+
}
|
|
385
|
+
const { game } = state;
|
|
386
|
+
const training = state.mode === 'train';
|
|
387
|
+
const customArrows = state.expectedUci && (state.feedback === 'incorrect')
|
|
388
|
+
? [
|
|
389
|
+
[
|
|
390
|
+
state.expectedUci.slice(0, 2),
|
|
391
|
+
state.expectedUci.slice(2, 4),
|
|
392
|
+
colors.primary,
|
|
393
|
+
],
|
|
394
|
+
]
|
|
395
|
+
: [];
|
|
396
|
+
const draggable = training && !state.complete;
|
|
397
|
+
return (jsxs(ThemeProvider, { theme: theme, children: [jsxs("div", { style: mainContainerStyle(boardWidth, colors), children: [jsxs("div", { style: headerStyle, children: [jsxs("span", { style: playerNameStyle, children: [((_b = game.white) !== null && _b !== void 0 ? _b : 'White'), " vs ", ((_c = game.black) !== null && _c !== void 0 ? _c : 'Black')] }), game.result && jsx("span", { style: subtleTextStyle(colors), children: game.result })] }), jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, position: state.fen, boardOrientation: boardOrientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => {
|
|
398
|
+
if (state.trainColor === 'white')
|
|
399
|
+
return piece[0] === 'w';
|
|
400
|
+
if (state.trainColor === 'black')
|
|
401
|
+
return piece[0] === 'b';
|
|
402
|
+
return piece[0] === state.sideToMove;
|
|
403
|
+
}, onPieceDrop: (source, target, piece) => state.handleDrop(source, target, piece), customArrows: customArrows, autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: customBoardStyle }) }), jsx(PlyNavigation, { plyIndex: state.plyIndex, totalPly: state.totalPly, canPrev: state.canPrev, canNext: state.canNext, onGoFirst: state.goFirst, onGoPrev: state.goPrev, onGoNext: state.goNext, onGoLast: state.goLast, onGoTo: state.goTo, theme: theme, showScrubber: showPlyScrubber, renderPlyNavigation: renderPlyNavigation }), jsxs("div", { style: statusLineStyle(colors), children: ["Half move ", Math.min(state.plyIndex + (state.complete ? 0 : 1), state.totalPly), " of", ' ', state.totalPly, training && !state.complete && (jsxs(Fragment, { children: [` · ${TRAIN_COLOR_LABEL[state.trainColor]}`, state.trainColor === 'both'
|
|
404
|
+
? ` · ${state.sideToMove === 'b' ? 'Black' : 'White'} to move`
|
|
405
|
+
: state.isUserTurn
|
|
406
|
+
? ' · Your move'
|
|
407
|
+
: ' · Opponent replying…'] }))] }), jsxs("div", { style: controlsRowStyle, children: [jsx("button", { type: "button", onClick: openAnalysis, style: buttonStyle(colors, 'primary'), children: "Analyze" }), !training && (jsxs(Fragment, { children: [jsx("button", { type: "button", onClick: () => state.startTraining('white'), disabled: state.complete, style: buttonStyle(colors, 'primary'), children: "Train White" }), jsx("button", { type: "button", onClick: () => state.startTraining('black'), disabled: state.complete, style: buttonStyle(colors, 'primary'), children: "Train Black" }), jsx("button", { type: "button", onClick: () => state.startTraining('both'), disabled: state.complete, style: buttonStyle(colors, 'primary'), children: "Train Both" })] })), training && (jsxs(Fragment, { children: [jsx("button", { type: "button", onClick: state.revealMove, disabled: state.complete || !state.isUserTurn, style: buttonStyle(colors, 'ghost'), children: "Show move" }), jsx("button", { type: "button", onClick: state.stopTraining, style: buttonStyle(colors, 'ghost'), children: "Stop drilling" })] })), onExit && (jsx("button", { type: "button", onClick: onExit, style: buttonStyle(colors, 'ghost'), children: "Exit" }))] }), jsxs("div", { style: feedbackContainerStyle, children: [state.complete && training && (jsx("span", { style: feedbackMessageStyle(colors, 'success'), children: "End of game \u2014 drill complete" })), !state.complete && state.feedback === 'correct' && (jsx("span", { style: feedbackMessageStyle(colors, 'success'), children: "Correct" })), !state.complete && state.feedback === 'incorrect' && (jsxs("span", { style: feedbackMessageStyle(colors, 'error'), children: ["Game move was ", state.expectedSan] }))] })] }), analysisOpen && analysisSnapshot && (jsx(AnalysisErrorBoundary, { onClose: closeAnalysis, children: jsx(AnalysisBoard, { analysisContext: analysisSnapshot, onClose: closeAnalysis, theme: theme, engine: engine }) }))] }));
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
export { DEFAULT_BOARD_WIDTH, REPLAY_START_FEN, ReplayTrainer, buildReplayAnalysisContext, fenAtPly, findPlyIndexForFen, normalizeFen, sideToMove, uciFromDrop, useReplayTrainer };
|