react-chess-replay-trainer 0.0.1 → 0.0.3

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 CHANGED
@@ -17,6 +17,8 @@ A React component for replaying a chess game move-by-move and drilling it.
17
17
  Depends on `react-chess-core` (board, engine, analysis board), `react-chessboard`,
18
18
  and `chess.js`.
19
19
 
20
+ Used in production at [endchess.com](https://endchess.com).
21
+
20
22
  ## Usage
21
23
 
22
24
  ```tsx
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
- import { type AnalysisEngineOptions, type PlyNavigationRenderProps } from 'react-chess-core';
2
+ import { type AnalysisEngineOptions, type BoardThemeId, type PlyNavigationRenderProps } from 'react-chess-core';
3
3
  import type { ReplayGame, ReplayMiss } from './types';
4
4
  export interface ReplayTrainerProps {
5
5
  gameId: string;
@@ -13,6 +13,7 @@ export interface ReplayTrainerProps {
13
13
  /** Called when the user leaves the trainer. */
14
14
  onExit?: () => void;
15
15
  theme?: 'light' | 'dark';
16
+ boardTheme?: BoardThemeId;
16
17
  boardWidth?: number;
17
18
  /** Side shown at the bottom of the board. Defaults to white. */
18
19
  orientation?: 'white' | 'black';
@@ -28,4 +29,4 @@ export interface ReplayTrainerProps {
28
29
  * next / last / slider) never records anything; once the user hits "Train from
29
30
  * here" each wrong move is reported through {@link ReplayTrainerProps.onMiss}.
30
31
  */
31
- export declare const ReplayTrainer: ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme, boardWidth, orientation, engine, renderPlyNavigation, showPlyScrubber, }: ReplayTrainerProps) => import("react").JSX.Element;
32
+ export declare const ReplayTrainer: ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme, boardTheme, boardWidth, orientation, engine, renderPlyNavigation, showPlyScrubber, }: ReplayTrainerProps) => import("react").JSX.Element;
@@ -1,3 +1,5 @@
1
1
  /** Standard chess starting position; game move lists replay from here. */
2
2
  export declare const REPLAY_START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
3
3
  export declare const DEFAULT_BOARD_WIDTH = 480;
4
+ /** Delay between half-moves during browse autoplay (ms). */
5
+ export declare const REPLAY_AUTOPLAY_STEP_MS = 500;
@@ -39,5 +39,12 @@ export interface ReplayTrainerState {
39
39
  stopTraining: () => void;
40
40
  revealMove: () => void;
41
41
  handleDrop: (source: string, target: string, piece: string) => boolean;
42
+ /** True while browse mode is auto-advancing through the game. */
43
+ autoplayActive: boolean;
44
+ /** Start autoplay from the current ply (exits train mode). No-op at game end. */
45
+ startAutoplay: () => void;
46
+ stopAutoplay: () => void;
47
+ /** Toggle browse autoplay; exits train mode when starting. */
48
+ toggleAutoplay: () => void;
42
49
  }
43
50
  export declare function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, }: UseReplayTrainerOptions): ReplayTrainerState;
@@ -1,7 +1,12 @@
1
1
  export { ReplayTrainer, type ReplayTrainerProps } from './ReplayTrainer';
2
2
  export { useReplayTrainer, type UseReplayTrainerOptions, type ReplayTrainerState, } from './hooks/useReplayTrainer';
3
3
  export type { ReplayGame, ReplayMiss, ReplayMode, ReplayFeedback, ReplaySide, TrainColor, } from './types';
4
- export { REPLAY_START_FEN, DEFAULT_BOARD_WIDTH } from './constants';
5
- export { fenAtPly, findPlyIndexForFen, sideToMove, uciFromDrop, normalizeFen, } from './replayUtils';
4
+ export { REPLAY_START_FEN, DEFAULT_BOARD_WIDTH, REPLAY_AUTOPLAY_STEP_MS, } from './constants';
5
+ export { fenAtPly, findPlyIndexForFen, sideToMove, normalizeFen, } from './replayUtils';
6
+ export { uciFromDrop } from 'react-chess-core';
6
7
  export { buildReplayAnalysisContext } from './buildReplayAnalysisContext';
8
+ export { fenAfterUci, refutationEvalGapCp, refutationFromEvaluation, type RefutationResult as ReplayRefutationResult, } from 'react-chess-core';
9
+ export { refutationEngineOptions as replayRefutationEngineOptions, REFUTATION_EVAL_GAP_CP as REPLAY_REFUTATION_EVAL_GAP_CP, REFUTATION_EVAL_GAP_PAWNS as REPLAY_REFUTATION_EVAL_GAP_PAWNS, } from 'react-chess-core';
10
+ export { getMissDisplay as getReplayMissDisplay, MISS_MOVE_ANIMATION_MS as REPLAY_MISS_MOVE_ANIMATION_MS, MISS_REFUTATION_MAX_WAIT_MS as REPLAY_MISS_REFUTATION_MAX_WAIT_MS, MISS_REFUTATION_PAUSE_MS as REPLAY_MISS_REFUTATION_PAUSE_MS, MISS_WRONG_PAUSE_MS as REPLAY_MISS_WRONG_PAUSE_MS, type MissDisplay as ReplayMissDisplay, type MissSequencePhase, type MissSequenceState, } from 'react-chess-core';
11
+ export { useMissRefutation as useReplayRefutation, useMissSequence as useReplayMissSequence, useMissBoard as useReplayMissBoard, } from 'react-chess-core';
7
12
  export { AnalysisBoard, AnalysisBoardCore, AnalysisErrorBoundary, DEFAULT_ANALYSIS_LAYOUT, DefaultPlyNavigation, PlyNavigation, defaultRenderPlyNavigation, type AnalysisContext, type AnalysisBoardProps, type AnalysisLayoutConfig, type PlyNavigationModel, type PlyNavigationProps, type PlyNavigationRenderProps, } from 'react-chess-core';
@@ -1,5 +1,5 @@
1
1
  import { Chess } from 'chess.js';
2
- import type { ReplaySide } from './types';
2
+ import type { ReplaySide, TrainColor } from './types';
3
3
  /** Compare positions ignoring move clocks (first four FEN fields). */
4
4
  export declare function normalizeFen(fen: string): string;
5
5
  export declare function applyUci(chess: Chess, uci: string): void;
@@ -8,5 +8,4 @@ export declare function fenAtPly(movesUci: string[], ply: number): string;
8
8
  /** Index of the next move to play to reach `targetFen`, or 0 if not found. */
9
9
  export declare function findPlyIndexForFen(movesUci: string[], targetFen: string): number;
10
10
  export declare function sideToMove(fen: string): ReplaySide;
11
- /** Resolve a board drag into a legal UCI string, or null when illegal. */
12
- export declare function uciFromDrop(fen: string, sourceSquare: string, targetSquare: string, piece: string): string | null;
11
+ export declare function isTrainSideToMove(trainColor: TrainColor, side: ReplaySide): boolean;
@@ -9,6 +9,7 @@ export type ReplayGame = {
9
9
  result?: string;
10
10
  timeControl?: string;
11
11
  timeClass?: string;
12
+ date?: string;
12
13
  movesUci: string[];
13
14
  movesSan?: string[];
14
15
  };
package/dist/index.esm.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
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';
3
+ import { createExpectedMoveDropHandler, ThemeProvider, ChessboardDnDProvider, HighlightChessboard, PlyNavigation, AnalysisErrorBoundary, AnalysisBoard } from 'react-chess-core';
4
+ export { AnalysisBoard, AnalysisBoardCore, AnalysisErrorBoundary, DEFAULT_ANALYSIS_LAYOUT, DefaultPlyNavigation, PlyNavigation, MISS_MOVE_ANIMATION_MS as REPLAY_MISS_MOVE_ANIMATION_MS, MISS_REFUTATION_MAX_WAIT_MS as REPLAY_MISS_REFUTATION_MAX_WAIT_MS, MISS_REFUTATION_PAUSE_MS as REPLAY_MISS_REFUTATION_PAUSE_MS, MISS_WRONG_PAUSE_MS as REPLAY_MISS_WRONG_PAUSE_MS, REFUTATION_EVAL_GAP_CP as REPLAY_REFUTATION_EVAL_GAP_CP, REFUTATION_EVAL_GAP_PAWNS as REPLAY_REFUTATION_EVAL_GAP_PAWNS, defaultRenderPlyNavigation, fenAfterUci, getMissDisplay as getReplayMissDisplay, refutationEvalGapCp, refutationFromEvaluation, refutationEngineOptions as replayRefutationEngineOptions, uciFromDrop, useMissBoard as useReplayMissBoard, useMissSequence as useReplayMissSequence, useMissRefutation as useReplayRefutation } from 'react-chess-core';
6
5
  import { Chess } from 'chess.js';
7
6
 
8
7
  /** Standard chess starting position; game move lists replay from here. */
9
8
  const REPLAY_START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
10
9
  const DEFAULT_BOARD_WIDTH = 480;
10
+ /** Delay between half-moves during browse autoplay (ms). */
11
+ const REPLAY_AUTOPLAY_STEP_MS = 500;
11
12
 
12
13
  /** Build a core {@link AnalysisContext} for the current replay browse position. */
13
14
  function buildReplayAnalysisContext(game, plyIndex, boardOrientation) {
@@ -58,18 +59,10 @@ function findPlyIndexForFen(movesUci, targetFen) {
58
59
  function sideToMove(fen) {
59
60
  return fen.trim().split(/\s+/)[1] === 'b' ? 'b' : 'w';
60
61
  }
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 : ''}`;
62
+ function isTrainSideToMove(trainColor, side) {
63
+ return (trainColor === 'both' ||
64
+ (trainColor === 'white' && side === 'w') ||
65
+ (trainColor === 'black' && side === 'b'));
73
66
  }
74
67
 
75
68
  /** Pause (ms) before the opponent's reply is auto-played in single-color drills. */
@@ -83,6 +76,7 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
83
76
  const [plyIndex, setPlyIndex] = useState(0);
84
77
  const [feedback, setFeedback] = useState(null);
85
78
  const [expectedSan, setExpectedSan] = useState(null);
79
+ const [autoplayActive, setAutoplayActive] = useState(false);
86
80
  const [expectedUci, setExpectedUci] = useState(null);
87
81
  const fetchGameRef = useRef(fetchGame);
88
82
  fetchGameRef.current = fetchGame;
@@ -92,6 +86,8 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
92
86
  onCompleteRef.current = onComplete;
93
87
  const recordedRef = useRef(new Set());
94
88
  const completedFiredRef = useRef(false);
89
+ const modeRef = useRef('browse');
90
+ const trainColorRef = useRef('both');
95
91
  useEffect(() => {
96
92
  let cancelled = false;
97
93
  setLoading(true);
@@ -100,10 +96,13 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
100
96
  recordedRef.current = new Set();
101
97
  completedFiredRef.current = false;
102
98
  setMode('browse');
99
+ modeRef.current = 'browse';
103
100
  setTrainColor('both');
101
+ trainColorRef.current = 'both';
104
102
  setFeedback(null);
105
103
  setExpectedSan(null);
106
104
  setExpectedUci(null);
105
+ setAutoplayActive(false);
107
106
  fetchGameRef
108
107
  .current(gameId)
109
108
  .then((loaded) => {
@@ -134,15 +133,19 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
134
133
  const fen = useMemo(() => fenAtPly(movesUci, plyIndex), [movesUci, plyIndex]);
135
134
  const complete = plyIndex >= totalPly && totalPly > 0;
136
135
  const sideToMove$1 = sideToMove(fen);
137
- const isUserTurn = trainColor === 'both' ||
138
- (trainColor === 'white' && sideToMove$1 === 'w') ||
139
- (trainColor === 'black' && sideToMove$1 === 'b');
136
+ const isUserTurn = isTrainSideToMove(trainColor, sideToMove$1);
137
+ modeRef.current = mode;
138
+ trainColorRef.current = trainColor;
140
139
  const clearTransient = useCallback(() => {
141
140
  setFeedback(null);
142
141
  setExpectedSan(null);
143
142
  setExpectedUci(null);
144
143
  }, []);
144
+ const stopAutoplay = useCallback(() => {
145
+ setAutoplayActive(false);
146
+ }, []);
145
147
  const goTo = useCallback((ply) => {
148
+ setAutoplayActive(false);
146
149
  const clamped = Math.max(0, Math.min(ply, totalPly));
147
150
  completedFiredRef.current = clamped >= totalPly ? completedFiredRef.current : false;
148
151
  setPlyIndex(clamped);
@@ -152,12 +155,31 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
152
155
  const goPrev = useCallback(() => goTo(plyIndex - 1), [goTo, plyIndex]);
153
156
  const goNext = useCallback(() => goTo(plyIndex + 1), [goTo, plyIndex]);
154
157
  const goLast = useCallback(() => goTo(totalPly), [goTo, totalPly]);
158
+ const startAutoplay = useCallback(() => {
159
+ if (plyIndex >= totalPly) {
160
+ return;
161
+ }
162
+ setMode('browse');
163
+ clearTransient();
164
+ setAutoplayActive(true);
165
+ }, [plyIndex, totalPly, clearTransient]);
166
+ const toggleAutoplay = useCallback(() => {
167
+ if (autoplayActive) {
168
+ stopAutoplay();
169
+ return;
170
+ }
171
+ startAutoplay();
172
+ }, [autoplayActive, startAutoplay, stopAutoplay]);
155
173
  const startTraining = useCallback((color = 'both') => {
174
+ setAutoplayActive(false);
156
175
  setTrainColor(color);
176
+ trainColorRef.current = color;
177
+ modeRef.current = 'train';
157
178
  setMode('train');
158
179
  clearTransient();
159
180
  }, [clearTransient]);
160
181
  const stopTraining = useCallback(() => {
182
+ modeRef.current = 'browse';
161
183
  setMode('browse');
162
184
  clearTransient();
163
185
  }, [clearTransient]);
@@ -193,29 +215,30 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
193
215
  setFeedback('incorrect');
194
216
  recordMiss(plyIndex);
195
217
  }, [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()) {
218
+ const handleDrop = useCallback((source, target, piece) => createExpectedMoveDropHandler({
219
+ fen,
220
+ expectedUci: movesUci[plyIndex],
221
+ enabled: modeRef.current === 'train' &&
222
+ !complete &&
223
+ isTrainSideToMove(trainColorRef.current, sideToMove$1),
224
+ onCorrect: () => {
207
225
  setFeedback('correct');
208
226
  setExpectedSan(null);
209
227
  setExpectedUci(null);
210
228
  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]);
229
+ },
230
+ onIncorrect: () => {
231
+ var _a, _b;
232
+ const expectedUci = movesUci[plyIndex];
233
+ if (!expectedUci) {
234
+ return;
235
+ }
236
+ setFeedback('incorrect');
237
+ 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);
238
+ setExpectedUci(expectedUci);
239
+ recordMiss(plyIndex);
240
+ },
241
+ })(source, target, piece), [complete, movesUci, plyIndex, fen, game, recordMiss, sideToMove$1]);
219
242
  useEffect(() => {
220
243
  var _a;
221
244
  if (mode === 'train' && complete && !completedFiredRef.current) {
@@ -223,6 +246,21 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
223
246
  (_a = onCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onCompleteRef);
224
247
  }
225
248
  }, [mode, complete]);
249
+ // Browse autoplay: advance one ply at a fixed interval until the game ends.
250
+ useEffect(() => {
251
+ if (!autoplayActive || mode === 'train') {
252
+ return;
253
+ }
254
+ if (plyIndex >= totalPly) {
255
+ setAutoplayActive(false);
256
+ return;
257
+ }
258
+ const id = setTimeout(() => {
259
+ setPlyIndex((p) => (p < totalPly ? p + 1 : p));
260
+ clearTransient();
261
+ }, REPLAY_AUTOPLAY_STEP_MS);
262
+ return () => clearTimeout(id);
263
+ }, [autoplayActive, mode, plyIndex, totalPly, clearTransient]);
226
264
  // In single-color drills, auto-play the opponent's reply once it's their turn
227
265
  // (e.g. after the user guesses correctly, or when training starts mid-game).
228
266
  useEffect(() => {
@@ -261,6 +299,10 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
261
299
  stopTraining,
262
300
  revealMove,
263
301
  handleDrop,
302
+ autoplayActive,
303
+ startAutoplay,
304
+ stopAutoplay,
305
+ toggleAutoplay,
264
306
  };
265
307
  }
266
308
 
@@ -355,7 +397,7 @@ function buttonStyle(colors, variant) {
355
397
  * next / last / slider) never records anything; once the user hits "Train from
356
398
  * here" each wrong move is reported through {@link ReplayTrainerProps.onMiss}.
357
399
  */
358
- const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme = 'dark', boardWidth = DEFAULT_BOARD_WIDTH, orientation = 'white', engine, renderPlyNavigation, showPlyScrubber = true, }) => {
400
+ const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme = 'dark', boardTheme, boardWidth = DEFAULT_BOARD_WIDTH, orientation = 'white', engine, renderPlyNavigation, showPlyScrubber = true, }) => {
359
401
  var _a, _b, _c;
360
402
  const state = useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete });
361
403
  const colors = palette(theme);
@@ -370,9 +412,10 @@ const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit
370
412
  if (!state.game) {
371
413
  return;
372
414
  }
415
+ state.stopAutoplay();
373
416
  setAnalysisSnapshot(buildReplayAnalysisContext(state.game, state.plyIndex, boardOrientation));
374
417
  setAnalysisOpen(true);
375
- }, [state.game, state.plyIndex, boardOrientation]);
418
+ }, [state.game, state.plyIndex, boardOrientation, state.stopAutoplay]);
376
419
  const closeAnalysis = useCallback(() => {
377
420
  setAnalysisOpen(false);
378
421
  }, []);
@@ -394,17 +437,17 @@ const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit
394
437
  ]
395
438
  : [];
396
439
  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 }) => {
440
+ return (jsxs(ThemeProvider, { theme: theme, boardTheme: boardTheme, 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
441
  if (state.trainColor === 'white')
399
442
  return piece[0] === 'w';
400
443
  if (state.trainColor === 'black')
401
444
  return piece[0] === 'b';
402
445
  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'
446
+ }, onPieceDrop: (source, target, piece) => state.handleDrop(source, target, piece), customArrows: customArrows, promotionDialogVariant: "modal", areArrowsAllowed: false, customBoardStyle: customBoardStyle }) }), jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4 }, children: [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 }), jsx("button", { type: "button", onClick: state.toggleAutoplay, disabled: !state.autoplayActive && !state.canNext, style: buttonStyle(colors, state.autoplayActive ? 'ghost' : 'primary'), "aria-label": state.autoplayActive ? 'Stop autoplay' : 'Autoplay game', children: state.autoplayActive ? 'Stop' : 'Play' })] }), 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
447
  ? ` · ${state.sideToMove === 'b' ? 'Black' : 'White'} to move`
405
448
  : state.isUserTurn
406
449
  ? ' · Your move'
407
450
  : ' · 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
451
  };
409
452
 
410
- export { DEFAULT_BOARD_WIDTH, REPLAY_START_FEN, ReplayTrainer, buildReplayAnalysisContext, fenAtPly, findPlyIndexForFen, normalizeFen, sideToMove, uciFromDrop, useReplayTrainer };
453
+ export { DEFAULT_BOARD_WIDTH, REPLAY_AUTOPLAY_STEP_MS, REPLAY_START_FEN, ReplayTrainer, buildReplayAnalysisContext, fenAtPly, findPlyIndexForFen, normalizeFen, sideToMove, useReplayTrainer };
package/dist/index.js CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var react = require('react');
5
- var reactChessboard = require('react-chessboard');
6
5
  var reactChessCore = require('react-chess-core');
7
6
  var chess_js = require('chess.js');
8
7
 
9
8
  /** Standard chess starting position; game move lists replay from here. */
10
9
  const REPLAY_START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
11
10
  const DEFAULT_BOARD_WIDTH = 480;
11
+ /** Delay between half-moves during browse autoplay (ms). */
12
+ const REPLAY_AUTOPLAY_STEP_MS = 500;
12
13
 
13
14
  /** Build a core {@link AnalysisContext} for the current replay browse position. */
14
15
  function buildReplayAnalysisContext(game, plyIndex, boardOrientation) {
@@ -59,18 +60,10 @@ function findPlyIndexForFen(movesUci, targetFen) {
59
60
  function sideToMove(fen) {
60
61
  return fen.trim().split(/\s+/)[1] === 'b' ? 'b' : 'w';
61
62
  }
62
- /** Resolve a board drag into a legal UCI string, or null when illegal. */
63
- function uciFromDrop(fen, sourceSquare, targetSquare, piece) {
64
- var _a, _b;
65
- const chess = new chess_js.Chess(fen);
66
- const pieceType = (_a = piece[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
67
- const legal = chess
68
- .moves({ square: sourceSquare, verbose: true })
69
- .find((move) => move.to === targetSquare &&
70
- (!move.promotion || move.promotion === pieceType));
71
- if (!legal)
72
- return null;
73
- return `${legal.from}${legal.to}${(_b = legal.promotion) !== null && _b !== void 0 ? _b : ''}`;
63
+ function isTrainSideToMove(trainColor, side) {
64
+ return (trainColor === 'both' ||
65
+ (trainColor === 'white' && side === 'w') ||
66
+ (trainColor === 'black' && side === 'b'));
74
67
  }
75
68
 
76
69
  /** Pause (ms) before the opponent's reply is auto-played in single-color drills. */
@@ -84,6 +77,7 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
84
77
  const [plyIndex, setPlyIndex] = react.useState(0);
85
78
  const [feedback, setFeedback] = react.useState(null);
86
79
  const [expectedSan, setExpectedSan] = react.useState(null);
80
+ const [autoplayActive, setAutoplayActive] = react.useState(false);
87
81
  const [expectedUci, setExpectedUci] = react.useState(null);
88
82
  const fetchGameRef = react.useRef(fetchGame);
89
83
  fetchGameRef.current = fetchGame;
@@ -93,6 +87,8 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
93
87
  onCompleteRef.current = onComplete;
94
88
  const recordedRef = react.useRef(new Set());
95
89
  const completedFiredRef = react.useRef(false);
90
+ const modeRef = react.useRef('browse');
91
+ const trainColorRef = react.useRef('both');
96
92
  react.useEffect(() => {
97
93
  let cancelled = false;
98
94
  setLoading(true);
@@ -101,10 +97,13 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
101
97
  recordedRef.current = new Set();
102
98
  completedFiredRef.current = false;
103
99
  setMode('browse');
100
+ modeRef.current = 'browse';
104
101
  setTrainColor('both');
102
+ trainColorRef.current = 'both';
105
103
  setFeedback(null);
106
104
  setExpectedSan(null);
107
105
  setExpectedUci(null);
106
+ setAutoplayActive(false);
108
107
  fetchGameRef
109
108
  .current(gameId)
110
109
  .then((loaded) => {
@@ -135,15 +134,19 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
135
134
  const fen = react.useMemo(() => fenAtPly(movesUci, plyIndex), [movesUci, plyIndex]);
136
135
  const complete = plyIndex >= totalPly && totalPly > 0;
137
136
  const sideToMove$1 = sideToMove(fen);
138
- const isUserTurn = trainColor === 'both' ||
139
- (trainColor === 'white' && sideToMove$1 === 'w') ||
140
- (trainColor === 'black' && sideToMove$1 === 'b');
137
+ const isUserTurn = isTrainSideToMove(trainColor, sideToMove$1);
138
+ modeRef.current = mode;
139
+ trainColorRef.current = trainColor;
141
140
  const clearTransient = react.useCallback(() => {
142
141
  setFeedback(null);
143
142
  setExpectedSan(null);
144
143
  setExpectedUci(null);
145
144
  }, []);
145
+ const stopAutoplay = react.useCallback(() => {
146
+ setAutoplayActive(false);
147
+ }, []);
146
148
  const goTo = react.useCallback((ply) => {
149
+ setAutoplayActive(false);
147
150
  const clamped = Math.max(0, Math.min(ply, totalPly));
148
151
  completedFiredRef.current = clamped >= totalPly ? completedFiredRef.current : false;
149
152
  setPlyIndex(clamped);
@@ -153,12 +156,31 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
153
156
  const goPrev = react.useCallback(() => goTo(plyIndex - 1), [goTo, plyIndex]);
154
157
  const goNext = react.useCallback(() => goTo(plyIndex + 1), [goTo, plyIndex]);
155
158
  const goLast = react.useCallback(() => goTo(totalPly), [goTo, totalPly]);
159
+ const startAutoplay = react.useCallback(() => {
160
+ if (plyIndex >= totalPly) {
161
+ return;
162
+ }
163
+ setMode('browse');
164
+ clearTransient();
165
+ setAutoplayActive(true);
166
+ }, [plyIndex, totalPly, clearTransient]);
167
+ const toggleAutoplay = react.useCallback(() => {
168
+ if (autoplayActive) {
169
+ stopAutoplay();
170
+ return;
171
+ }
172
+ startAutoplay();
173
+ }, [autoplayActive, startAutoplay, stopAutoplay]);
156
174
  const startTraining = react.useCallback((color = 'both') => {
175
+ setAutoplayActive(false);
157
176
  setTrainColor(color);
177
+ trainColorRef.current = color;
178
+ modeRef.current = 'train';
158
179
  setMode('train');
159
180
  clearTransient();
160
181
  }, [clearTransient]);
161
182
  const stopTraining = react.useCallback(() => {
183
+ modeRef.current = 'browse';
162
184
  setMode('browse');
163
185
  clearTransient();
164
186
  }, [clearTransient]);
@@ -194,29 +216,30 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
194
216
  setFeedback('incorrect');
195
217
  recordMiss(plyIndex);
196
218
  }, [complete, game, movesUci, plyIndex, recordMiss, isUserTurn]);
197
- const handleDrop = react.useCallback((source, target, piece) => {
198
- var _a, _b;
199
- if (mode !== 'train' || complete || !isUserTurn)
200
- return false;
201
- const expectedUci = movesUci[plyIndex];
202
- if (!expectedUci)
203
- return false;
204
- const uci = uciFromDrop(fen, source, target, piece);
205
- if (!uci)
206
- return false;
207
- if (uci.toLowerCase() === expectedUci.toLowerCase()) {
219
+ const handleDrop = react.useCallback((source, target, piece) => reactChessCore.createExpectedMoveDropHandler({
220
+ fen,
221
+ expectedUci: movesUci[plyIndex],
222
+ enabled: modeRef.current === 'train' &&
223
+ !complete &&
224
+ isTrainSideToMove(trainColorRef.current, sideToMove$1),
225
+ onCorrect: () => {
208
226
  setFeedback('correct');
209
227
  setExpectedSan(null);
210
228
  setExpectedUci(null);
211
229
  setPlyIndex((p) => p + 1);
212
- return true;
213
- }
214
- setFeedback('incorrect');
215
- 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);
216
- setExpectedUci(expectedUci);
217
- recordMiss(plyIndex);
218
- return false;
219
- }, [mode, complete, isUserTurn, movesUci, plyIndex, fen, game, recordMiss]);
230
+ },
231
+ onIncorrect: () => {
232
+ var _a, _b;
233
+ const expectedUci = movesUci[plyIndex];
234
+ if (!expectedUci) {
235
+ return;
236
+ }
237
+ setFeedback('incorrect');
238
+ 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);
239
+ setExpectedUci(expectedUci);
240
+ recordMiss(plyIndex);
241
+ },
242
+ })(source, target, piece), [complete, movesUci, plyIndex, fen, game, recordMiss, sideToMove$1]);
220
243
  react.useEffect(() => {
221
244
  var _a;
222
245
  if (mode === 'train' && complete && !completedFiredRef.current) {
@@ -224,6 +247,21 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
224
247
  (_a = onCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onCompleteRef);
225
248
  }
226
249
  }, [mode, complete]);
250
+ // Browse autoplay: advance one ply at a fixed interval until the game ends.
251
+ react.useEffect(() => {
252
+ if (!autoplayActive || mode === 'train') {
253
+ return;
254
+ }
255
+ if (plyIndex >= totalPly) {
256
+ setAutoplayActive(false);
257
+ return;
258
+ }
259
+ const id = setTimeout(() => {
260
+ setPlyIndex((p) => (p < totalPly ? p + 1 : p));
261
+ clearTransient();
262
+ }, REPLAY_AUTOPLAY_STEP_MS);
263
+ return () => clearTimeout(id);
264
+ }, [autoplayActive, mode, plyIndex, totalPly, clearTransient]);
227
265
  // In single-color drills, auto-play the opponent's reply once it's their turn
228
266
  // (e.g. after the user guesses correctly, or when training starts mid-game).
229
267
  react.useEffect(() => {
@@ -262,6 +300,10 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
262
300
  stopTraining,
263
301
  revealMove,
264
302
  handleDrop,
303
+ autoplayActive,
304
+ startAutoplay,
305
+ stopAutoplay,
306
+ toggleAutoplay,
265
307
  };
266
308
  }
267
309
 
@@ -356,7 +398,7 @@ function buttonStyle(colors, variant) {
356
398
  * next / last / slider) never records anything; once the user hits "Train from
357
399
  * here" each wrong move is reported through {@link ReplayTrainerProps.onMiss}.
358
400
  */
359
- const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme = 'dark', boardWidth = DEFAULT_BOARD_WIDTH, orientation = 'white', engine, renderPlyNavigation, showPlyScrubber = true, }) => {
401
+ const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme = 'dark', boardTheme, boardWidth = DEFAULT_BOARD_WIDTH, orientation = 'white', engine, renderPlyNavigation, showPlyScrubber = true, }) => {
360
402
  var _a, _b, _c;
361
403
  const state = useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete });
362
404
  const colors = palette(theme);
@@ -371,9 +413,10 @@ const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit
371
413
  if (!state.game) {
372
414
  return;
373
415
  }
416
+ state.stopAutoplay();
374
417
  setAnalysisSnapshot(buildReplayAnalysisContext(state.game, state.plyIndex, boardOrientation));
375
418
  setAnalysisOpen(true);
376
- }, [state.game, state.plyIndex, boardOrientation]);
419
+ }, [state.game, state.plyIndex, boardOrientation, state.stopAutoplay]);
377
420
  const closeAnalysis = react.useCallback(() => {
378
421
  setAnalysisOpen(false);
379
422
  }, []);
@@ -395,13 +438,13 @@ const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit
395
438
  ]
396
439
  : [];
397
440
  const draggable = training && !state.complete;
398
- return (jsxRuntime.jsxs(reactChessCore.ThemeProvider, { theme: theme, children: [jsxRuntime.jsxs("div", { style: mainContainerStyle(boardWidth, colors), children: [jsxRuntime.jsxs("div", { style: headerStyle, children: [jsxRuntime.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 && jsxRuntime.jsx("span", { style: subtleTextStyle(colors), children: game.result })] }), jsxRuntime.jsx(reactChessboard.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, position: state.fen, boardOrientation: boardOrientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => {
441
+ return (jsxRuntime.jsxs(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: [jsxRuntime.jsxs("div", { style: mainContainerStyle(boardWidth, colors), children: [jsxRuntime.jsxs("div", { style: headerStyle, children: [jsxRuntime.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 && jsxRuntime.jsx("span", { style: subtleTextStyle(colors), children: game.result })] }), jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, position: state.fen, boardOrientation: boardOrientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => {
399
442
  if (state.trainColor === 'white')
400
443
  return piece[0] === 'w';
401
444
  if (state.trainColor === 'black')
402
445
  return piece[0] === 'b';
403
446
  return piece[0] === state.sideToMove;
404
- }, onPieceDrop: (source, target, piece) => state.handleDrop(source, target, piece), customArrows: customArrows, autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: customBoardStyle }) }), jsxRuntime.jsx(reactChessCore.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 }), jsxRuntime.jsxs("div", { style: statusLineStyle(colors), children: ["Half move ", Math.min(state.plyIndex + (state.complete ? 0 : 1), state.totalPly), " of", ' ', state.totalPly, training && !state.complete && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [` · ${TRAIN_COLOR_LABEL[state.trainColor]}`, state.trainColor === 'both'
447
+ }, onPieceDrop: (source, target, piece) => state.handleDrop(source, target, piece), customArrows: customArrows, promotionDialogVariant: "modal", areArrowsAllowed: false, customBoardStyle: customBoardStyle }) }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4 }, children: [jsxRuntime.jsx(reactChessCore.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 }), jsxRuntime.jsx("button", { type: "button", onClick: state.toggleAutoplay, disabled: !state.autoplayActive && !state.canNext, style: buttonStyle(colors, state.autoplayActive ? 'ghost' : 'primary'), "aria-label": state.autoplayActive ? 'Stop autoplay' : 'Autoplay game', children: state.autoplayActive ? 'Stop' : 'Play' })] }), jsxRuntime.jsxs("div", { style: statusLineStyle(colors), children: ["Half move ", Math.min(state.plyIndex + (state.complete ? 0 : 1), state.totalPly), " of", ' ', state.totalPly, training && !state.complete && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [` · ${TRAIN_COLOR_LABEL[state.trainColor]}`, state.trainColor === 'both'
405
448
  ? ` · ${state.sideToMove === 'b' ? 'Black' : 'White'} to move`
406
449
  : state.isUserTurn
407
450
  ? ' · Your move'
@@ -432,11 +475,72 @@ Object.defineProperty(exports, "PlyNavigation", {
432
475
  enumerable: true,
433
476
  get: function () { return reactChessCore.PlyNavigation; }
434
477
  });
478
+ Object.defineProperty(exports, "REPLAY_MISS_MOVE_ANIMATION_MS", {
479
+ enumerable: true,
480
+ get: function () { return reactChessCore.MISS_MOVE_ANIMATION_MS; }
481
+ });
482
+ Object.defineProperty(exports, "REPLAY_MISS_REFUTATION_MAX_WAIT_MS", {
483
+ enumerable: true,
484
+ get: function () { return reactChessCore.MISS_REFUTATION_MAX_WAIT_MS; }
485
+ });
486
+ Object.defineProperty(exports, "REPLAY_MISS_REFUTATION_PAUSE_MS", {
487
+ enumerable: true,
488
+ get: function () { return reactChessCore.MISS_REFUTATION_PAUSE_MS; }
489
+ });
490
+ Object.defineProperty(exports, "REPLAY_MISS_WRONG_PAUSE_MS", {
491
+ enumerable: true,
492
+ get: function () { return reactChessCore.MISS_WRONG_PAUSE_MS; }
493
+ });
494
+ Object.defineProperty(exports, "REPLAY_REFUTATION_EVAL_GAP_CP", {
495
+ enumerable: true,
496
+ get: function () { return reactChessCore.REFUTATION_EVAL_GAP_CP; }
497
+ });
498
+ Object.defineProperty(exports, "REPLAY_REFUTATION_EVAL_GAP_PAWNS", {
499
+ enumerable: true,
500
+ get: function () { return reactChessCore.REFUTATION_EVAL_GAP_PAWNS; }
501
+ });
435
502
  Object.defineProperty(exports, "defaultRenderPlyNavigation", {
436
503
  enumerable: true,
437
504
  get: function () { return reactChessCore.defaultRenderPlyNavigation; }
438
505
  });
506
+ Object.defineProperty(exports, "fenAfterUci", {
507
+ enumerable: true,
508
+ get: function () { return reactChessCore.fenAfterUci; }
509
+ });
510
+ Object.defineProperty(exports, "getReplayMissDisplay", {
511
+ enumerable: true,
512
+ get: function () { return reactChessCore.getMissDisplay; }
513
+ });
514
+ Object.defineProperty(exports, "refutationEvalGapCp", {
515
+ enumerable: true,
516
+ get: function () { return reactChessCore.refutationEvalGapCp; }
517
+ });
518
+ Object.defineProperty(exports, "refutationFromEvaluation", {
519
+ enumerable: true,
520
+ get: function () { return reactChessCore.refutationFromEvaluation; }
521
+ });
522
+ Object.defineProperty(exports, "replayRefutationEngineOptions", {
523
+ enumerable: true,
524
+ get: function () { return reactChessCore.refutationEngineOptions; }
525
+ });
526
+ Object.defineProperty(exports, "uciFromDrop", {
527
+ enumerable: true,
528
+ get: function () { return reactChessCore.uciFromDrop; }
529
+ });
530
+ Object.defineProperty(exports, "useReplayMissBoard", {
531
+ enumerable: true,
532
+ get: function () { return reactChessCore.useMissBoard; }
533
+ });
534
+ Object.defineProperty(exports, "useReplayMissSequence", {
535
+ enumerable: true,
536
+ get: function () { return reactChessCore.useMissSequence; }
537
+ });
538
+ Object.defineProperty(exports, "useReplayRefutation", {
539
+ enumerable: true,
540
+ get: function () { return reactChessCore.useMissRefutation; }
541
+ });
439
542
  exports.DEFAULT_BOARD_WIDTH = DEFAULT_BOARD_WIDTH;
543
+ exports.REPLAY_AUTOPLAY_STEP_MS = REPLAY_AUTOPLAY_STEP_MS;
440
544
  exports.REPLAY_START_FEN = REPLAY_START_FEN;
441
545
  exports.ReplayTrainer = ReplayTrainer;
442
546
  exports.buildReplayAnalysisContext = buildReplayAnalysisContext;
@@ -444,5 +548,4 @@ exports.fenAtPly = fenAtPly;
444
548
  exports.findPlyIndexForFen = findPlyIndexForFen;
445
549
  exports.normalizeFen = normalizeFen;
446
550
  exports.sideToMove = sideToMove;
447
- exports.uciFromDrop = uciFromDrop;
448
551
  exports.useReplayTrainer = useReplayTrainer;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-chess-replay-trainer",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "React component for browsing a chess game and drilling its moves, reporting misses (uses react-chess-core for board and analysis)",
5
5
  "license": "MIT",
6
6
  "author": "Robert Blackwell",
@@ -24,7 +24,7 @@
24
24
  "peerDependencies": {
25
25
  "chess.js": "^1.0.0-beta.8",
26
26
  "react": "^18.3.1",
27
- "react-chess-core": "^0.1.0",
27
+ "react-chess-core": "^0.1.1",
28
28
  "react-chessboard": "^4.7.1"
29
29
  },
30
30
  "devDependencies": {
@@ -46,7 +46,7 @@
46
46
  "@vitejs/plugin-react": "^4.3.1",
47
47
  "chess.js": "^1.0.0-beta.8",
48
48
  "react": "^18.3.1",
49
- "react-chess-core": "^0.1.0",
49
+ "react-chess-core": "^0.1.1",
50
50
  "react-chessboard": "^4.7.1",
51
51
  "react-dom": "^18.3.1",
52
52
  "storybook": "^8.2.9",
@@ -1,18 +0,0 @@
1
- import React from 'react';
2
- import { AnalysisEngineOptions } from 'react-chess-core';
3
- import { type UseReplayAnalysisBoardArgs } from './hooks/useReplayAnalysisBoard';
4
- import type { ReplayAnalysisContext } from './types';
5
- export declare const DEFAULT_ANALYSIS_BOARD_WIDTH = 400;
6
- export declare const DEFAULT_ANALYSIS_SIDEBAR_WIDTH = 280;
7
- export declare const DEFAULT_ANALYSIS_COLUMN_GAP = 16;
8
- export interface ReplayAnalysisBoardProps extends Omit<UseReplayAnalysisBoardArgs, 'boardWidth'> {
9
- boardWidth?: number;
10
- sidebarWidth?: number;
11
- columnGap?: number;
12
- }
13
- /**
14
- * Full-screen modal analysis board: draggable exploration, move list,
15
- * variations, and Stockfish evaluation via react-chess-core.
16
- */
17
- export declare const ReplayAnalysisBoard: ({ boardWidth, sidebarWidth, columnGap, ...modelArgs }: ReplayAnalysisBoardProps) => React.JSX.Element;
18
- export type { ReplayAnalysisContext, AnalysisEngineOptions };
@@ -1,32 +0,0 @@
1
- import type { ReplayAnalysisContext, ReplayAnalysisHistoryRow, ReplaySolutionMoveDisplay } from './types';
2
- export declare class ReplayAnalysisPosition {
3
- private chess;
4
- private readonly initialFen;
5
- private readonly solutionMoves;
6
- private readonly solutionSans;
7
- private mainPly;
8
- private variation;
9
- private variationCursor;
10
- constructor(context: ReplayAnalysisContext);
11
- private static buildSolutionSans;
12
- private fenAtMainPly;
13
- private rebuildChess;
14
- private findLegalMove;
15
- private uciFromVerboseMove;
16
- private matchesMainMove;
17
- getNavPly(): number;
18
- getMaxNavPly(): number;
19
- getSolutionSans(): ReplaySolutionMoveDisplay[];
20
- getHistoryRows(): ReplayAnalysisHistoryRow[];
21
- isHistoryRowSelected(row: ReplayAnalysisHistoryRow): boolean;
22
- selectHistoryRow(row: ReplayAnalysisHistoryRow): void;
23
- selectMainLine(ply: number): void;
24
- goToNavPly(navPly: number): void;
25
- tryPlayMove(sourceSquare: string, targetSquare: string, piece: string): boolean;
26
- getLastMoveSquares(): {
27
- from: string;
28
- to: string;
29
- } | null;
30
- fen(): string;
31
- getCheckSquare(): string;
32
- }
@@ -1,8 +0,0 @@
1
- import React from 'react';
2
- import { type EngineEvaluation } from 'react-chess-core';
3
- export interface ReplayEngineEvaluationPanelProps {
4
- fen: string;
5
- evaluation: EngineEvaluation;
6
- theme: 'light' | 'dark';
7
- }
8
- export declare const ReplayEngineEvaluationPanel: ({ fen, evaluation, theme, }: ReplayEngineEvaluationPanelProps) => React.JSX.Element;
@@ -1,63 +0,0 @@
1
- export declare const analysisBoardHighlightColors: {
2
- readonly lastMove: {
3
- readonly light: "rgba(253, 216, 53, 0.55)";
4
- readonly dark: "rgba(144, 202, 249, 0.5)";
5
- };
6
- };
7
- export declare const getLastMoveSquareStyles: (from: string, to: string, theme: "light" | "dark") => Record<string, {
8
- backgroundColor: string;
9
- }>;
10
- export declare const analysisSidebarColors: {
11
- readonly activeMove: {
12
- readonly light: "rgba(58, 123, 213, 0.25)";
13
- readonly dark: "rgba(58, 123, 213, 0.35)";
14
- };
15
- readonly mainBand: {
16
- readonly light: readonly ["rgba(0,0,0,0.02)", "rgba(0,0,0,0.05)"];
17
- readonly dark: readonly ["rgba(255,255,255,0.03)", "rgba(255,255,255,0.06)"];
18
- };
19
- readonly variationBand: {
20
- readonly light: readonly ["rgba(0,0,0,0.04)", "rgba(0,0,0,0.07)"];
21
- readonly dark: readonly ["rgba(255,255,255,0.05)", "rgba(255,255,255,0.08)"];
22
- };
23
- };
24
- export declare function getSidebarRowBackground(theme: 'light' | 'dark', row: {
25
- kind: 'start' | 'main' | 'variation';
26
- }, bandCounters: {
27
- main: number;
28
- variation: number;
29
- }): string;
30
- export declare function createSidebarRowBandCounters(): {
31
- main: number;
32
- variation: number;
33
- };
34
- export declare const analysisModalColors: {
35
- readonly light: {
36
- readonly panel: {
37
- readonly backgroundColor: "#fafafa";
38
- readonly color: "#1a1a1a";
39
- };
40
- readonly title: {
41
- readonly color: "#1a1a1a";
42
- };
43
- readonly closeButton: {
44
- readonly backgroundColor: "#eee";
45
- readonly border: "1px solid #ccc";
46
- readonly color: "#333";
47
- };
48
- };
49
- readonly dark: {
50
- readonly panel: {
51
- readonly backgroundColor: "#2a2a2a";
52
- readonly color: "#e8e8e8";
53
- };
54
- readonly title: {
55
- readonly color: "#e8e8e8";
56
- };
57
- readonly closeButton: {
58
- readonly backgroundColor: "#3a3a3a";
59
- readonly border: "1px solid #555";
60
- readonly color: "#e8e8e8";
61
- };
62
- };
63
- };
@@ -1,4 +0,0 @@
1
- import type { ReplayGame } from '../types';
2
- import type { ReplayAnalysisContext } from './types';
3
- /** Build analysis context from the current replay browse position. */
4
- export declare function buildReplayAnalysisContext(game: ReplayGame, plyIndex: number, boardOrientation: 'white' | 'black'): ReplayAnalysisContext;
@@ -1,34 +0,0 @@
1
- import { AnalysisEngineOptions, EngineEvaluation } from 'react-chess-core';
2
- import type { ReplayAnalysisContext, ReplayAnalysisHistoryRow, ReplaySolutionMoveDisplay } from '../types';
3
- export type UseReplayAnalysisBoardArgs = {
4
- analysisContext: ReplayAnalysisContext;
5
- onClose: () => void;
6
- theme: 'light' | 'dark';
7
- boardWidth: number;
8
- engine?: AnalysisEngineOptions;
9
- };
10
- export type ReplayAnalysisBoardModel = {
11
- theme: 'light' | 'dark';
12
- boardWidth: number;
13
- analysisContext: ReplayAnalysisContext;
14
- fen: string;
15
- ply: number;
16
- maxPly: number;
17
- historyRows: ReplayAnalysisHistoryRow[];
18
- solutionSans: ReplaySolutionMoveDisplay[];
19
- boardOrientation: 'white' | 'black';
20
- engineEvaluation: EngineEvaluation;
21
- engineEnabled: boolean;
22
- lastMove: {
23
- from: string;
24
- to: string;
25
- } | null;
26
- checkSquare: string | null;
27
- onSelectPly: (ply: number) => void;
28
- onSelectHistoryRow: (row: ReplayAnalysisHistoryRow) => void;
29
- isHistoryRowSelected: (row: ReplayAnalysisHistoryRow) => boolean;
30
- onPieceDrop: (sourceSquare: string, targetSquare: string, piece: string) => boolean;
31
- onBackdropMouseDown: () => void;
32
- onClose: () => void;
33
- };
34
- export declare function useReplayAnalysisBoard({ analysisContext, onClose, theme, boardWidth, engine, }: UseReplayAnalysisBoardArgs): ReplayAnalysisBoardModel;
@@ -1,6 +0,0 @@
1
- export { ReplayAnalysisBoard, DEFAULT_ANALYSIS_BOARD_WIDTH, DEFAULT_ANALYSIS_SIDEBAR_WIDTH, DEFAULT_ANALYSIS_COLUMN_GAP, type ReplayAnalysisBoardProps, } from './ReplayAnalysisBoard';
2
- export { ReplayEngineEvaluationPanel } from './ReplayEngineEvaluationPanel';
3
- export { buildReplayAnalysisContext } from './buildReplayAnalysisContext';
4
- export { ReplayAnalysisPosition } from './ReplayAnalysisPosition';
5
- export { useReplayAnalysisBoard, type UseReplayAnalysisBoardArgs, type ReplayAnalysisBoardModel, } from './hooks/useReplayAnalysisBoard';
6
- export type { ReplayAnalysisContext, ReplayAnalysisHistoryRow, ReplaySolutionMoveDisplay, } from './types';
@@ -1,3 +0,0 @@
1
- import { Chess } from 'chess.js';
2
- export declare function tryApplyUci(chess: Chess, uci: string): boolean;
3
- export declare function getCheckSquareFromChess(chess: Chess): string;
@@ -1,22 +0,0 @@
1
- /** Context for opening the replay analysis board at a point in the game. */
2
- export type ReplayAnalysisContext = {
3
- initialFen: string;
4
- /** Full game main line in UCI notation. */
5
- solutionMoves: string[];
6
- /** Ply index to open at (0 = start position). */
7
- startPly: number;
8
- boardOrientation: 'white' | 'black';
9
- };
10
- export type ReplayAnalysisHistoryRow = {
11
- key: string;
12
- label: string;
13
- indent: number;
14
- kind: 'start' | 'main' | 'variation';
15
- mainPly: number;
16
- variationIndex: number;
17
- };
18
- export type ReplaySolutionMoveDisplay = {
19
- ply: number;
20
- uci: string;
21
- san: string;
22
- };