react-chess-puzzle-kit 1.0.3 → 1.0.5

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.
@@ -5,6 +5,8 @@ export interface LineBoardProps {
5
5
  trainSide: LineTrainSide;
6
6
  draggable: boolean;
7
7
  correctMoveSquare?: string | null;
8
+ incorrectMoveSquare?: string | null;
9
+ lastMoveUci?: string | null;
8
10
  onPieceDrop: (source: string, target: string, piece: string) => boolean;
9
11
  boardWidth: number;
10
12
  }
@@ -13,4 +15,4 @@ export interface LineBoardProps {
13
15
  * side's pieces be dragged. Move validation and sequencing live in
14
16
  * {@link LineBoardWithControls}.
15
17
  */
16
- export declare const LineBoard: ({ fen, orientation, trainSide, draggable, correctMoveSquare, onPieceDrop, boardWidth, }: LineBoardProps) => import("react").JSX.Element;
18
+ export declare const LineBoard: ({ fen, orientation, trainSide, draggable, correctMoveSquare, incorrectMoveSquare, lastMoveUci, onPieceDrop, boardWidth, }: LineBoardProps) => import("react").JSX.Element;
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { PuzzleResultStatus } from '../analysis';
3
3
  import { AnalysisContainerRenderProps, AnalysisControls, AnalysisEngineOptions, AnalysisLayoutConfig, AnalysisMainRenderProps, AnalysisSidebarRenderProps, EngineEvaluationRenderProps, type BoardThemeId } from 'react-chess-core';
4
+ import { type PuzzleAutoAdvanceState } from './usePuzzleAutoAdvanceCountdown';
4
5
  import { type PuzzleControlState } from './defaults/DefaultPuzzleControls';
5
6
  import { type PuzzleMissFeedback } from './PuzzlePlaySurface';
6
7
  export type { PuzzleMoveRecord } from '../position/moveHistory';
@@ -10,6 +11,7 @@ export { DEFAULT_ANALYSIS_LAYOUT } from 'react-chess-core';
10
11
  export { DEFAULT_PUZZLE_BOARD_WIDTH } from './puzzleBoardLayout';
11
12
  export { DefaultPuzzleControls, defaultRenderControls, } from './defaults/DefaultPuzzleControls';
12
13
  export type { PuzzleControlState, PuzzleControlsRenderProps, } from './defaults/DefaultPuzzleControls';
14
+ export type { PuzzleAutoAdvanceState } from './usePuzzleAutoAdvanceCountdown';
13
15
  export type BoardCaptionRenderProps = {
14
16
  /** null while the puzzle position is loading */
15
17
  sideToMove: 'white' | 'black' | null;
@@ -76,7 +78,7 @@ export interface PuzzleBoardWithControlsProps {
76
78
  }) => void;
77
79
  };
78
80
  /** Omit to use {@link defaultRenderControls} / {@link DefaultPuzzleControls}. */
79
- renderControls?: (showHint: () => void, showSolution: () => void, nextPuzzle: () => void, resultStatus: PuzzleResultStatus, analysis: AnalysisControls, controlState: PuzzleControlState) => React.ReactNode;
81
+ renderControls?: (showHint: () => void, showSolution: () => void, nextPuzzle: () => void, resultStatus: PuzzleResultStatus, analysis: AnalysisControls, controlState: PuzzleControlState, autoAdvance?: PuzzleAutoAdvanceState) => React.ReactNode;
80
82
  renderAnalysisSidebar?: (props: AnalysisSidebarRenderProps) => React.ReactNode;
81
83
  renderAnalysisContainer?: (props: AnalysisContainerRenderProps) => React.ReactNode;
82
84
  renderEngineEvaluation?: (props: EngineEvaluationRenderProps) => React.ReactNode;
@@ -97,6 +99,10 @@ export interface PuzzleBoardWithControlsProps {
97
99
  autoAdvanceOnComplete?: boolean;
98
100
  /** With {@link autoAdvanceOnComplete}, also advance after finishing following a miss or hint. */
99
101
  autoAdvanceOnCompleteAfterIncorrect?: boolean;
102
+ /** Delay before auto-loading the next card (defaults to {@link AUTO_ADVANCE_ON_COMPLETE_DELAY_MS}). */
103
+ autoAdvanceOnCompleteDelayMs?: number;
104
+ /** Replay missed solution plies on the board before auto-advancing. */
105
+ showCompletionRecap?: boolean;
100
106
  /** After a wrong guess, play the correct move and wait for the user to advance. */
101
107
  revealAnswerOnIncorrect?: boolean;
102
108
  /** After a wrong guess, show an arrow to the correct square. */
@@ -111,4 +117,4 @@ export interface PuzzleBoardWithControlsProps {
111
117
  refutationEngine?: AnalysisEngineOptions;
112
118
  answerArrowColor?: string;
113
119
  }
114
- export declare const PuzzleBoardWithControls: ({ theme, boardTheme, apiProxy, renderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth, analysisLayout, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete, autoAdvanceOnCompleteAfterIncorrect, revealAnswerOnIncorrect, showAnswerArrowOnIncorrect, allowRetryOnIncorrect, showRefutationOnIncorrect, autoShowWrongMoves, refutationEngine, answerArrowColor, }: PuzzleBoardWithControlsProps) => React.JSX.Element;
120
+ export declare const PuzzleBoardWithControls: ({ theme, boardTheme, apiProxy, renderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth, analysisLayout, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete, autoAdvanceOnCompleteAfterIncorrect, autoAdvanceOnCompleteDelayMs, showCompletionRecap, revealAnswerOnIncorrect, showAnswerArrowOnIncorrect, allowRetryOnIncorrect, showRefutationOnIncorrect, autoShowWrongMoves, refutationEngine, answerArrowColor, }: PuzzleBoardWithControlsProps) => React.JSX.Element;
@@ -6,6 +6,13 @@ export type PuzzleMissFeedback = {
6
6
  /** True while the board shows the correct-move answer arrow. */
7
7
  answerArrowVisible: boolean;
8
8
  };
9
+ /** Board state driven by the post-completion solution recap animation. */
10
+ export type PuzzleRecapBoardState = {
11
+ fen: string;
12
+ lastMoveUci: string | null;
13
+ customArrows: [string, string, string][];
14
+ animationDuration: number;
15
+ };
9
16
  export interface PuzzlePlaySurfaceProps {
10
17
  position: PuzzlePosition | null;
11
18
  onFeedback: (feedbackData: {
@@ -41,9 +48,11 @@ export interface PuzzlePlaySurfaceProps {
41
48
  positionLocked?: boolean;
42
49
  /** Fired when refutation miss feedback changes (for host UI). */
43
50
  onMissFeedbackChange?: (feedback: PuzzleMissFeedback | null) => void;
51
+ /** When set, replaces the live puzzle position with the completion recap board. */
52
+ recapBoard?: PuzzleRecapBoardState | null;
44
53
  }
45
54
  /**
46
55
  * Single mounted board for puzzle play. Keeps the prior board (and orientation)
47
56
  * visible while the next position loads so layout and perspective do not flicker.
48
57
  */
49
- export declare const PuzzlePlaySurface: ({ position, onFeedback, incInteractionNum, boardWidth, onResumeCorrect, revealAnswerOnIncorrect, showAnswerArrowOnIncorrect, allowRetryOnIncorrect, showRefutationOnIncorrect, autoShowWrongMoves, refutationEngine, answerArrowColor, positionLocked, onMissFeedbackChange, }: PuzzlePlaySurfaceProps) => import("react").JSX.Element;
58
+ export declare const PuzzlePlaySurface: ({ position, onFeedback, incInteractionNum, boardWidth, onResumeCorrect, revealAnswerOnIncorrect, showAnswerArrowOnIncorrect, allowRetryOnIncorrect, showRefutationOnIncorrect, autoShowWrongMoves, refutationEngine, answerArrowColor, positionLocked, onMissFeedbackChange, recapBoard, }: PuzzlePlaySurfaceProps) => import("react").JSX.Element | null;
@@ -0,0 +1,6 @@
1
+ export type PuzzleAutoAdvanceState = {
2
+ active: boolean;
3
+ secondsRemaining: number;
4
+ };
5
+ /** Countdown overlay state while waiting to auto-load the next puzzle card. */
6
+ export declare function usePuzzleAutoAdvanceCountdown(enabled: boolean, delayMs: number, onAdvance: () => void): PuzzleAutoAdvanceState;
@@ -0,0 +1,17 @@
1
+ import { type SolutionLineRecapState } from 'react-chess-core';
2
+ /** Pause on the puzzle setup position before the solution recap animates. */
3
+ export declare const PUZZLE_COMPLETION_RECAP_SETUP_MS = 400;
4
+ export type PuzzleCompletionRecapSource = {
5
+ startFen: string;
6
+ movesUci: string[];
7
+ startIndex: number;
8
+ endIndex: number;
9
+ missedIndices: number[];
10
+ setupUci?: string | null;
11
+ };
12
+ export type PuzzleCompletionRecapState = SolutionLineRecapState;
13
+ export declare const usePuzzleCompletionRecap: ({ source, active, onComplete, }: {
14
+ source: PuzzleCompletionRecapSource | null;
15
+ active: boolean;
16
+ onComplete: () => void;
17
+ }) => PuzzleCompletionRecapState;
@@ -21,6 +21,10 @@ export declare abstract class Position implements Traversable {
21
21
  export declare function getCheckSquareFromChess(chess: Chess): string;
22
22
  /** Side to move for the final (user) ply in a puzzle line. */
23
23
  export declare function playerColorForSolution(initialFen: string, moves: string[]): 'white' | 'black';
24
+ /** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
25
+ export declare function playerMoveIndicesInRange(initialFen: string, moves: string[], fromIndex: number, toIndex: number): number[];
26
+ /** Auto-play opponent setup plies until the solver is on move. */
27
+ export declare function advanceToPlayerTurn(position: PuzzlePosition): void;
24
28
  export type PuzzleResumeConfig = {
25
29
  startIndex: number;
26
30
  quizAtIndices: number[];
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from './features/board/LineBoardWithControls';
10
10
  export { DefaultPuzzleControls, defaultRenderControls, } from './features/board/defaults/DefaultPuzzleControls';
11
11
  export { DefaultLineControls, defaultRenderLineControls, } from './features/board/defaults/DefaultLineControls';
12
12
  export type { PuzzleControlState, PuzzleControlsRenderProps, } from './features/board/defaults/DefaultPuzzleControls';
13
+ export type { PuzzleAutoAdvanceState } from './features/board/usePuzzleAutoAdvanceCountdown';
13
14
  export { DEFAULT_PUZZLE_BOARD_WIDTH, PUZZLE_CONTROLS_BESIDE_RESERVE_PX, PUZZLE_CONTROLS_STACK_BREAKPOINT_PX, } from './features/board/puzzleBoardLayout';
14
15
  export * from './features/position/moveHistory';
15
16
  export * from './features/position/Position';
package/dist/index.esm.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
- import { useBoardRevision, useCorrectMoveFeedback, useMissBoard, ChessboardDnDProvider, HighlightChessboard, uciFromDrop, ThemeProvider, AnalysisErrorBoundary, AnalysisBoardCore, AnalysisBoardLayout, AnalysisBoard, DEFAULT_ANALYSIS_LAYOUT, evaluateExpectedMoveDrop, fenAfterUci, boardSquareHighlightColors, analysisBoardHighlightColors } from 'react-chess-core';
3
+ import { useBoardRevision, useCorrectMoveFeedback, useIncorrectMoveFeedback, useMissBoard, HighlightChessboard, DEFAULT_ANSWER_ARROW_COLOR, uciFromDrop, fenAtPlyFromStart, useSolutionLineRecap, ThemeProvider, AnalysisErrorBoundary, AnalysisBoardCore, AnalysisBoardLayout, AnalysisBoard, BoardCompleteCheckOverlay, DEFAULT_ANALYSIS_LAYOUT, AUTO_ADVANCE_ON_COMPLETE_DELAY_MS, lastMoveUciAtPly, evaluateExpectedMoveDrop, fenAfterUci, boardSquareHighlightColors, analysisBoardHighlightColors } from 'react-chess-core';
4
4
  export { DEFAULT_ANALYSIS_LAYOUT } from 'react-chess-core';
5
5
  import { Chess } from 'chess.js';
6
6
 
@@ -56,18 +56,18 @@ const usePuzzleAnalysis = (position, resultStatus, puzzleNum) => {
56
56
  };
57
57
 
58
58
  const EMPTY_BOARD_FEN = '8/8/8/8/8/8/8/8 w - - 0 1';
59
- const DEFAULT_ANSWER_ARROW_COLOR = '#42a5f5';
60
59
  /**
61
60
  * Single mounted board for puzzle play. Keeps the prior board (and orientation)
62
61
  * visible while the next position loads so layout and perspective do not flicker.
63
62
  */
64
- const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth, onResumeCorrect, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect = false, autoShowWrongMoves = true, refutationEngine, answerArrowColor = DEFAULT_ANSWER_ARROW_COLOR, positionLocked = false, onMissFeedbackChange, }) => {
65
- var _a, _b, _c, _d, _e, _f, _g;
63
+ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth, onResumeCorrect, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect = false, autoShowWrongMoves = true, refutationEngine, answerArrowColor = DEFAULT_ANSWER_ARROW_COLOR, positionLocked = false, onMissFeedbackChange, recapBoard = null, }) => {
64
+ var _a, _b, _c, _d, _e, _f;
66
65
  const [showAnswerArrow, setShowAnswerArrow] = useState(false);
67
66
  const [incorrectActive, setIncorrectActive] = useState(false);
68
67
  const attemptMissedRef = useRef(false);
69
68
  const { revision, bumpRevision } = useBoardRevision();
70
69
  const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, } = useCorrectMoveFeedback();
70
+ const { incorrectMoveSquare: transientIncorrectSquare, showIncorrectMove, clearIncorrectMoveFeedback, } = useIncorrectMoveFeedback();
71
71
  const boardOrientationRef = useRef('white');
72
72
  const boardFenRef = useRef(EMPTY_BOARD_FEN);
73
73
  const notifyHost = () => {
@@ -102,13 +102,25 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
102
102
  const answerArrowVisible = useRefutation
103
103
  ? incorrectActive && missPhase === 'answer'
104
104
  : showAnswerArrow;
105
+ const overlayIncorrectSquare = transientIncorrectSquare !== null && transientIncorrectSquare !== void 0 ? transientIncorrectSquare : (useRefutation && incorrectActive
106
+ ? missBoard.missSequence.display.incorrectMoveSquare
107
+ : null);
108
+ const refutationMoveSquare = useRefutation && incorrectActive
109
+ ? missBoard.missSequence.display.refutationMoveSquare
110
+ : null;
105
111
  useEffect(() => {
106
112
  setShowAnswerArrow(false);
107
113
  setIncorrectActive(false);
108
114
  attemptMissedRef.current = false;
109
115
  clearCorrectMoveFeedback();
116
+ clearIncorrectMoveFeedback();
110
117
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
111
- }, [clearCorrectMoveFeedback, onMissFeedbackChange, position]);
118
+ }, [
119
+ clearCorrectMoveFeedback,
120
+ clearIncorrectMoveFeedback,
121
+ onMissFeedbackChange,
122
+ position,
123
+ ]);
112
124
  useEffect(() => {
113
125
  var _a, _b;
114
126
  if (!onMissFeedbackChange) {
@@ -140,11 +152,13 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
140
152
  showAnswerArrow,
141
153
  useRefutation,
142
154
  ]);
155
+ const boardOrientation = position
156
+ ? position.getPlayerColor()
157
+ : boardOrientationRef.current;
143
158
  if (position) {
144
- boardOrientationRef.current = position.getPlayerColor();
159
+ boardOrientationRef.current = boardOrientation;
145
160
  boardFenRef.current = position.fen();
146
161
  }
147
- const boardOrientation = boardOrientationRef.current;
148
162
  const boardFen = boardFenRef.current;
149
163
  const hasBoard = boardFen !== EMPTY_BOARD_FEN;
150
164
  const simpleArrows = useMemo(() => {
@@ -157,19 +171,28 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
157
171
  }
158
172
  return [[moveUci.slice(0, 2), moveUci.slice(2, 4), answerArrowColor]];
159
173
  }, [showAnswerArrow, position, answerArrowColor, useRefutation]);
160
- const customArrows = useRefutation && incorrectActive
161
- ? missBoard.customArrows
162
- : simpleArrows;
163
- const displayFen = useRefutation && incorrectActive ? missBoard.boardPosition : boardFen;
174
+ const isRecapping = recapBoard !== null;
175
+ const customArrows = isRecapping
176
+ ? recapBoard.customArrows
177
+ : useRefutation && incorrectActive
178
+ ? missBoard.customArrows
179
+ : simpleArrows;
180
+ const displayFen = isRecapping
181
+ ? recapBoard.fen
182
+ : useRefutation && incorrectActive
183
+ ? missBoard.boardPosition
184
+ : boardFen;
164
185
  const missLocked = useRefutation &&
165
186
  incorrectActive &&
166
187
  (missBoard.boardAnimating ||
167
188
  missPhase === 'wrong' ||
168
189
  missPhase === 'refutation');
169
- const arePiecesDraggable = position !== null &&
190
+ const arePiecesDraggable = !isRecapping &&
191
+ position !== null &&
170
192
  !positionLocked &&
171
193
  !missLocked &&
172
- correctMoveSquare === null;
194
+ correctMoveSquare === null &&
195
+ overlayIncorrectSquare === null;
173
196
  const onPieceDrop = (sourceSquare, targetSquare, piece) => {
174
197
  if (!position || positionLocked || position.isSolutionRevealed()) {
175
198
  return false;
@@ -183,6 +206,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
183
206
  if (answerArrowVisible &&
184
207
  !allowRetryOnIncorrect &&
185
208
  !position.isExpectedGuess(sourceSquare, targetSquare)) {
209
+ showIncorrectMove(sourceSquare);
186
210
  position.resetInteractions();
187
211
  snapBoardBack();
188
212
  return false;
@@ -192,6 +216,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
192
216
  });
193
217
  if (!guess.accepted) {
194
218
  attemptMissedRef.current = true;
219
+ showIncorrectMove(useRefutation ? targetSquare : sourceSquare);
195
220
  onFeedback({
196
221
  index: position.getIndex(),
197
222
  guess: { sourceSquare, targetSquare, piece },
@@ -205,8 +230,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
205
230
  missBoard.missSequence.startSequence(setupFen, attemptedUci);
206
231
  }
207
232
  position.resetInteractions();
208
- snapBoardBack();
209
- return false;
233
+ return true;
210
234
  }
211
235
  const revealIncorrectFeedback = () => {
212
236
  if (showAnswerArrowOnIncorrect) {
@@ -272,13 +296,79 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
272
296
  showCorrectMove(targetSquare, finishCorrectFeedback);
273
297
  return true;
274
298
  };
275
- return (jsx(ChessboardDnDProvider, { children: hasBoard ? (jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: (_e = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _e !== void 0 ? _e : '', hintSquare: (_f = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _f !== void 0 ? _f : null, incorrectMoveSquare: showAnswerArrowOnIncorrect
276
- ? null
277
- : ((_g = position === null || position === void 0 ? void 0 : position.getIncorrectMoveSquare()) !== null && _g !== void 0 ? _g : null), correctMoveSquare: correctMoveSquare, customArrows: customArrows, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: 0 }, revision)) : null }));
299
+ return hasBoard ? (jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: isRecapping ? '' : ((_e = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _e !== void 0 ? _e : ''), hintSquare: isRecapping ? null : ((_f = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _f !== void 0 ? _f : null), incorrectMoveSquare: isRecapping ? null : overlayIncorrectSquare, refutationMoveSquare: isRecapping ? null : refutationMoveSquare, correctMoveSquare: isRecapping ? null : correctMoveSquare, customArrows: customArrows, lastMoveUci: isRecapping ? recapBoard.lastMoveUci : null, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: isRecapping ? recapBoard.animationDuration : 0 }, revision)) : null;
278
300
  };
279
301
 
280
302
  const PuzzleBoard = ({ position, onFeedback, incInteractionNum, boardWidth, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, answerArrowColor, }) => (jsx(PuzzlePlaySurface, { position: position, onFeedback: onFeedback, incInteractionNum: incInteractionNum, boardWidth: boardWidth, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, answerArrowColor: answerArrowColor }));
281
303
 
304
+ const inactiveAutoAdvance = {
305
+ active: false,
306
+ secondsRemaining: -1,
307
+ };
308
+ /** Countdown overlay state while waiting to auto-load the next puzzle card. */
309
+ function usePuzzleAutoAdvanceCountdown(enabled, delayMs, onAdvance) {
310
+ const [secondsRemaining, setSecondsRemaining] = useState(-1);
311
+ useEffect(() => {
312
+ if (!enabled || delayMs <= 0) {
313
+ setSecondsRemaining(-1);
314
+ return;
315
+ }
316
+ const startedAt = Date.now();
317
+ const updateCountdown = () => {
318
+ const elapsed = Date.now() - startedAt;
319
+ setSecondsRemaining(Math.max(0, Math.ceil((delayMs - elapsed) / 1000)));
320
+ };
321
+ updateCountdown();
322
+ const intervalId = window.setInterval(updateCountdown, 200);
323
+ const timeoutId = window.setTimeout(() => {
324
+ window.clearInterval(intervalId);
325
+ setSecondsRemaining(-1);
326
+ onAdvance();
327
+ }, delayMs);
328
+ return () => {
329
+ window.clearInterval(intervalId);
330
+ window.clearTimeout(timeoutId);
331
+ setSecondsRemaining(-1);
332
+ };
333
+ }, [delayMs, enabled, onAdvance]);
334
+ if (!enabled || secondsRemaining < 0) {
335
+ return inactiveAutoAdvance;
336
+ }
337
+ return {
338
+ active: true,
339
+ secondsRemaining,
340
+ };
341
+ }
342
+
343
+ /** Pause on the puzzle setup position before the solution recap animates. */
344
+ const PUZZLE_COMPLETION_RECAP_SETUP_MS = 400;
345
+ const usePuzzleCompletionRecap = ({ source, active, onComplete, }) => {
346
+ var _a, _b, _c, _d, _e, _f;
347
+ const startFen = (_a = source === null || source === void 0 ? void 0 : source.startFen) !== null && _a !== void 0 ? _a : '';
348
+ const movesUci = (_b = source === null || source === void 0 ? void 0 : source.movesUci) !== null && _b !== void 0 ? _b : [];
349
+ const startIndex = (_c = source === null || source === void 0 ? void 0 : source.startIndex) !== null && _c !== void 0 ? _c : 0;
350
+ const endIndex = (_d = source === null || source === void 0 ? void 0 : source.endIndex) !== null && _d !== void 0 ? _d : 0;
351
+ const missedIndices = (_e = source === null || source === void 0 ? void 0 : source.missedIndices) !== null && _e !== void 0 ? _e : [];
352
+ const setupUci = (_f = source === null || source === void 0 ? void 0 : source.setupUci) !== null && _f !== void 0 ? _f : null;
353
+ const resolveFen = useCallback((moveIndex, afterMove) => {
354
+ if (!startFen || movesUci.length === 0) {
355
+ return '';
356
+ }
357
+ return fenAtPlyFromStart(startFen, movesUci, afterMove ? moveIndex + 1 : moveIndex);
358
+ }, [movesUci, startFen]);
359
+ return useSolutionLineRecap({
360
+ active: active && source !== null,
361
+ movesUci,
362
+ startIndex,
363
+ endIndex,
364
+ missedIndices,
365
+ segmentStartFen: startFen,
366
+ setupUci,
367
+ onComplete,
368
+ resolveFen,
369
+ });
370
+ };
371
+
282
372
  const isAttemptFinished = (resultStatus) => resultStatus === 'complete' || resultStatus === 'incorrect';
283
373
  /** Library default hint / next / analysis / result controls (unstyled buttons). */
284
374
  const DefaultPuzzleControls = ({ showHint, showSolution, nextPuzzle, resultStatus, analysis, controlState, }) => (jsxs("div", { style: rowStyle$1, children: [jsx("button", { type: "button", onClick: showHint, style: buttonStyle, disabled: !controlState.canShowHint, children: "Hint" }), jsx("button", { type: "button", onClick: showSolution, style: buttonStyle, disabled: !controlState.canShowSolution, children: "Show solution" }), jsx("button", { type: "button", onClick: nextPuzzle, style: buttonStyle, children: "Next puzzle" }), analysis.visible && isAttemptFinished(resultStatus) && (jsx("button", { type: "button", onClick: analysis.openAnalysis, style: buttonStyle, children: "Analysis" })), resultStatus === 'complete' && (jsx("span", { style: statusStyle$1, children: "Complete" })), resultStatus === 'incorrect' && (jsx("span", { style: Object.assign(Object.assign({}, statusStyle$1), { color: '#c62828' }), children: "Incorrect" }))] }));
@@ -483,6 +573,29 @@ function playerColorForSolution(initialFen, moves) {
483
573
  }
484
574
  return chess.turn() === 'w' ? 'white' : 'black';
485
575
  }
576
+ /** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
577
+ function playerMoveIndicesInRange(initialFen, moves, fromIndex, toIndex) {
578
+ const playerColor = playerColorForSolution(initialFen, moves);
579
+ const chess = new Chess(initialFen);
580
+ const indices = [];
581
+ for (let i = 0; i < toIndex && i < moves.length; i++) {
582
+ const side = chess.turn() === 'w' ? 'white' : 'black';
583
+ if (i >= fromIndex && side === playerColor) {
584
+ indices.push(i);
585
+ }
586
+ applyUciMove(chess, moves[i]);
587
+ }
588
+ return indices;
589
+ }
590
+ /** Auto-play opponent setup plies until the solver is on move. */
591
+ function advanceToPlayerTurn(position) {
592
+ while (!position.isFinished() &&
593
+ position.getSideToMove() !== position.getPlayerColor()) {
594
+ if (!position.next()) {
595
+ break;
596
+ }
597
+ }
598
+ }
486
599
  class PuzzlePosition extends Position {
487
600
  constructor(initialFEN, moves, resumeConfig) {
488
601
  super();
@@ -727,19 +840,37 @@ class GamePosition extends Position {
727
840
  }
728
841
  }
729
842
 
730
- /** Apply the opponent setup ply immediately so the board does not flash on load. */
843
+ /** Apply opponent setup plies immediately so the board does not flash on load. */
731
844
  const puzzlePositionFromFetch = (fen, moves, resume) => {
732
845
  const newPosition = new PuzzlePosition(fen, moves, resume);
733
846
  if (!resume && moves.length > 1) {
734
847
  newPosition.next();
848
+ advanceToPlayerTurn(newPosition);
849
+ }
850
+ else if (resume &&
851
+ newPosition.getSideToMove() !== newPosition.getPlayerColor()) {
852
+ advanceToPlayerTurn(newPosition);
735
853
  }
736
854
  return newPosition;
737
855
  };
738
- /** Brief pause so the user sees a correct result before the next card loads. */
739
- const AUTO_ADVANCE_ON_COMPLETE_DELAY_MS = 700;
740
856
  const SOLUTION_STEP_MS = 500;
741
857
  const RESUME_AUTO_STEP_MS = 500;
742
- const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = DEFAULT_ANALYSIS_LAYOUT, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
858
+ const uniqueIndices = (indices) => [...new Set(indices)];
859
+ const buildCompletionRecapSource = (position, missedIndices) => {
860
+ var _a, _b;
861
+ const movesUci = position.getSolutionMoves();
862
+ const initialFen = position.getInitialFen();
863
+ const startIndex = (_a = playerMoveIndicesInRange(initialFen, movesUci, 0, movesUci.length)[0]) !== null && _a !== void 0 ? _a : 0;
864
+ return {
865
+ startFen: initialFen,
866
+ movesUci,
867
+ startIndex,
868
+ endIndex: movesUci.length,
869
+ missedIndices,
870
+ setupUci: startIndex > 0 ? (_b = movesUci[startIndex - 1]) !== null && _b !== void 0 ? _b : null : null,
871
+ };
872
+ };
873
+ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = DEFAULT_ANALYSIS_LAYOUT, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, autoAdvanceOnCompleteDelayMs = AUTO_ADVANCE_ON_COMPLETE_DELAY_MS, showCompletionRecap = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
743
874
  var _a, _b, _c, _d;
744
875
  const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
745
876
  const stackControlsBelow = useStackPuzzleControlsBelow();
@@ -755,6 +886,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
755
886
  const [puzzleComplete, setPuzzleComplete] = useState(false);
756
887
  const [completedAfterMiss, setCompletedAfterMiss] = useState(false);
757
888
  const [missFeedback, setMissFeedback] = useState(null);
889
+ const [missedMoveIndices, setMissedMoveIndices] = useState([]);
890
+ const [completionCheckVisible, setCompletionCheckVisible] = useState(false);
891
+ const [completionRecapActive, setCompletionRecapActive] = useState(false);
892
+ const [completionRecapDone, setCompletionRecapDone] = useState(false);
893
+ const completionFlowStartedRef = useRef(false);
758
894
  const [, setInteractionNum] = useState(0);
759
895
  const solutionAnimationRef = useRef({ cancelled: false, timeoutIds: [] });
760
896
  const resumeAnimationRef = useRef({ cancelled: false, timeoutIds: [] });
@@ -781,6 +917,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
781
917
  setPuzzleComplete(false);
782
918
  setCompletedAfterMiss(false);
783
919
  setMissFeedback(null);
920
+ setMissedMoveIndices([]);
921
+ setCompletionCheckVisible(false);
922
+ setCompletionRecapActive(false);
923
+ setCompletionRecapDone(false);
924
+ completionFlowStartedRef.current = false;
784
925
  onFetch()
785
926
  .then((data) => {
786
927
  if (cancelled) {
@@ -816,10 +957,16 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
816
957
  feedbackData.isCorrect === false;
817
958
  if (feedbackData.hintRequested) {
818
959
  setHintUsed(true);
960
+ setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
819
961
  }
820
962
  if (incorrectThisFeedback) {
821
963
  setHasIncorrectAttempt(true);
822
964
  }
965
+ if (feedbackData.isCorrect === false &&
966
+ !feedbackData.isFinished &&
967
+ !feedbackData.solutionShown) {
968
+ setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
969
+ }
823
970
  if (feedbackData.isFinished) {
824
971
  setPuzzleComplete(true);
825
972
  setCompletedAfterMiss((prev) => prev ||
@@ -983,6 +1130,10 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
983
1130
  position.recordSolutionShown();
984
1131
  position.setSolutionRevealed(true);
985
1132
  position.wantsHint(false);
1133
+ setMissedMoveIndices((prev) => uniqueIndices([
1134
+ ...prev,
1135
+ ...playerMoveIndicesInRange(position.getInitialFen(), position.getSolutionMoves(), position.getIndex(), position.getSolutionMoves().length),
1136
+ ]));
986
1137
  handleFeedback({
987
1138
  index: position.getIndex(),
988
1139
  solutionShown: true,
@@ -995,30 +1146,46 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
995
1146
  setPuzzleNum((prevPuzzleNum) => prevPuzzleNum + 1);
996
1147
  }, []);
997
1148
  const resultStatus = getResultStatus();
1149
+ const completionRecapSource = useMemo(() => position && showCompletionRecap
1150
+ ? buildCompletionRecapSource(position, missedMoveIndices)
1151
+ : null, [position, showCompletionRecap, missedMoveIndices]);
1152
+ const handleCompletionRecapDone = useCallback(() => {
1153
+ setCompletionRecapActive(false);
1154
+ setCompletionRecapDone(true);
1155
+ }, []);
1156
+ const completionRecap = usePuzzleCompletionRecap({
1157
+ source: completionRecapSource,
1158
+ active: completionRecapActive,
1159
+ onComplete: handleCompletionRecapDone,
1160
+ });
1161
+ const isCompletionRecapping = showCompletionRecap && (completionRecapActive || completionRecap.active);
998
1162
  useEffect(() => {
999
- if (!autoAdvanceOnComplete) {
1000
- return;
1001
- }
1002
- if (resultStatus !== 'complete') {
1163
+ if (!showCompletionRecap ||
1164
+ resultStatus !== 'complete' ||
1165
+ loadingNextPuzzle ||
1166
+ completionFlowStartedRef.current) {
1003
1167
  return;
1004
1168
  }
1005
- if (hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) {
1169
+ completionFlowStartedRef.current = true;
1170
+ setCompletionCheckVisible(true);
1171
+ }, [loadingNextPuzzle, resultStatus, showCompletionRecap]);
1172
+ useEffect(() => {
1173
+ if (!completionCheckVisible) {
1006
1174
  return;
1007
1175
  }
1008
- const timer = setTimeout(() => {
1009
- handleNextPuzzle();
1010
- }, AUTO_ADVANCE_ON_COMPLETE_DELAY_MS);
1176
+ const timer = window.setTimeout(() => {
1177
+ setCompletionCheckVisible(false);
1178
+ setCompletionRecapActive(true);
1179
+ }, PUZZLE_COMPLETION_RECAP_SETUP_MS);
1011
1180
  return () => {
1012
- clearTimeout(timer);
1181
+ window.clearTimeout(timer);
1013
1182
  };
1014
- }, [
1015
- autoAdvanceOnComplete,
1016
- autoAdvanceOnCompleteAfterIncorrect,
1017
- resultStatus,
1018
- hasIncorrectAttempt,
1019
- handleNextPuzzle,
1020
- puzzleNum,
1021
- ]);
1183
+ }, [completionCheckVisible]);
1184
+ const shouldAutoAdvance = autoAdvanceOnComplete &&
1185
+ resultStatus === 'complete' &&
1186
+ !(hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) &&
1187
+ (!showCompletionRecap || completionRecapDone);
1188
+ const autoAdvance = usePuzzleAutoAdvanceCountdown(shouldAutoAdvance, autoAdvanceOnCompleteDelayMs, handleNextPuzzle);
1022
1189
  const controlState = {
1023
1190
  canShowHint: position !== null &&
1024
1191
  !position.isFinished() &&
@@ -1033,7 +1200,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1033
1200
  const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
1034
1201
  renderAnalysisContainer &&
1035
1202
  (renderEngineEvaluation || (engine === null || engine === void 0 ? void 0 : engine.enabled) === false));
1036
- return (jsx(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: analysisSnapshot ? (jsx(AnalysisErrorBoundary, { onClose: analysis.closeAnalysis, children: useHostAnalysisUi ? (jsx(AnalysisBoardCore, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, boardWidth: resolvedAnalysisBoardWidth, engine: engine, renderMain: renderAnalysisMain !== null && renderAnalysisMain !== void 0 ? renderAnalysisMain : (({ board, sidebar, model }) => (jsx(AnalysisBoardLayout, { layout: analysisLayout, model: model, board: board, sidebar: sidebar }))), renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation !== null && renderEngineEvaluation !== void 0 ? renderEngineEvaluation : (() => null) })) : (jsx(AnalysisBoard, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, layout: analysisLayout, engine: engine, renderMain: renderAnalysisMain, renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation })) })) : (jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxs("div", { style: puzzleBoardColumnStyle(puzzleBoardWidth, controlsPlacement), children: [jsx("div", { style: puzzleBoardSlotWrapperStyle(), children: jsx("div", { style: puzzleBoardSlotStyle(), children: jsx(PuzzlePlaySurface, { position: position, boardWidth: puzzleBoardWidth, onFeedback: handleFeedback, incInteractionNum: incInteractionNum, onResumeCorrect: runResumeAutoAdvance, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, showRefutationOnIncorrect: refutationOnIncorrect, autoShowWrongMoves: autoShowWrongMoves, refutationEngine: refutationEngine !== null && refutationEngine !== void 0 ? refutationEngine : engine, answerArrowColor: answerArrowColor, positionLocked: loadingNextPuzzle, onMissFeedbackChange: setMissFeedback }) }) }), renderBoardCaption && (jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
1203
+ return (jsx(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: analysisSnapshot ? (jsx(AnalysisErrorBoundary, { onClose: analysis.closeAnalysis, children: useHostAnalysisUi ? (jsx(AnalysisBoardCore, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, boardWidth: resolvedAnalysisBoardWidth, engine: engine, renderMain: renderAnalysisMain !== null && renderAnalysisMain !== void 0 ? renderAnalysisMain : (({ board, sidebar, model }) => (jsx(AnalysisBoardLayout, { layout: analysisLayout, model: model, board: board, sidebar: sidebar }))), renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation !== null && renderEngineEvaluation !== void 0 ? renderEngineEvaluation : (() => null) })) : (jsx(AnalysisBoard, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, layout: analysisLayout, engine: engine, renderMain: renderAnalysisMain, renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation })) })) : (jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxs("div", { style: puzzleBoardColumnStyle(puzzleBoardWidth, controlsPlacement), children: [jsxs("div", { style: puzzleBoardSlotWrapperStyle(), children: [jsx("div", { style: puzzleBoardSlotStyle(), children: jsx(PuzzlePlaySurface, { position: position, boardWidth: puzzleBoardWidth, onFeedback: handleFeedback, incInteractionNum: incInteractionNum, onResumeCorrect: runResumeAutoAdvance, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, showRefutationOnIncorrect: refutationOnIncorrect, autoShowWrongMoves: autoShowWrongMoves, refutationEngine: refutationEngine !== null && refutationEngine !== void 0 ? refutationEngine : engine, answerArrowColor: answerArrowColor, positionLocked: loadingNextPuzzle ||
1204
+ completionCheckVisible ||
1205
+ isCompletionRecapping, onMissFeedbackChange: setMissFeedback, recapBoard: isCompletionRecapping
1206
+ ? {
1207
+ fen: completionRecap.fen,
1208
+ lastMoveUci: completionRecap.lastMoveUci,
1209
+ customArrows: completionRecap.customArrows,
1210
+ animationDuration: completionRecap.animationDuration,
1211
+ }
1212
+ : null }) }), completionCheckVisible && (jsx(BoardCompleteCheckOverlay, { variant: hasIncorrectAttempt || completedAfterMiss || hintUsed
1213
+ ? 'partial'
1214
+ : 'success' }))] }), renderBoardCaption && (jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
1037
1215
  sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
1038
1216
  playerColor: position
1039
1217
  ? position.getPlayerColor()
@@ -1049,7 +1227,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1049
1227
  }) }))] }), jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
1050
1228
  visible: analysis.canOpen,
1051
1229
  openAnalysis: analysis.openAnalysis,
1052
- }, controlState), renderBoardFeedback && resultStatus === 'complete' && (jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
1230
+ }, controlState, autoAdvance), renderBoardFeedback && resultStatus === 'complete' && (jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
1053
1231
  resultStatus,
1054
1232
  cleanSolve: !hasIncorrectAttempt,
1055
1233
  refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
@@ -1063,7 +1241,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1063
1241
  * side's pieces be dragged. Move validation and sequencing live in
1064
1242
  * {@link LineBoardWithControls}.
1065
1243
  */
1066
- const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, onPieceDrop, boardWidth, }) => (jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, correctMoveSquare: correctMoveSquare, position: fen, boardOrientation: orientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => piece[0] === trainSide, onPieceDrop: (source, target, piece) => onPieceDrop(source, target, piece), autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }) }));
1244
+ const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, incorrectMoveSquare = null, lastMoveUci = null, onPieceDrop, boardWidth, }) => (jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: incorrectMoveSquare, correctMoveSquare: correctMoveSquare, position: fen, boardOrientation: orientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => piece[0] === trainSide, onPieceDrop: (source, target, piece) => onPieceDrop(source, target, piece), lastMoveUci: lastMoveUci, autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }));
1067
1245
 
1068
1246
  /** Library default line-drill status controls (unstyled). */
1069
1247
  const DefaultLineControls = ({ moveNumber, total, finished, isUserTurn, feedback, }) => (jsxs("div", { style: rowStyle, children: [jsx("span", { style: statusStyle, children: finished ? 'Line complete' : `Move ${moveNumber} of ${total}` }), feedback && !finished && (jsx("span", { style: Object.assign(Object.assign({}, statusStyle), { color: feedback.isCorrect ? '#2e7d32' : '#c62828' }), children: feedback.isCorrect
@@ -1112,6 +1290,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1112
1290
  const [feedback, setFeedback] = useState(null);
1113
1291
  const [displayFen, setDisplayFen] = useState(null);
1114
1292
  const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = useCorrectMoveFeedback();
1293
+ const { incorrectMoveSquare, showIncorrectMove, isShowingIncorrectMove, } = useIncorrectMoveFeedback();
1115
1294
  const total = line.movesUci.length;
1116
1295
  const orientation = boardOrientationForLine(line.trainSide);
1117
1296
  const applyMove = useCallback((index) => {
@@ -1136,7 +1315,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1136
1315
  }, [line.movesUci]);
1137
1316
  // Auto-play opponent moves and detect the end of the line.
1138
1317
  useEffect(() => {
1139
- if (finished || isShowingCorrectMove) {
1318
+ if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
1140
1319
  return;
1141
1320
  }
1142
1321
  if (currentIndex >= total) {
@@ -1156,6 +1335,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1156
1335
  applyMove,
1157
1336
  opponentMoveDelayMs,
1158
1337
  isShowingCorrectMove,
1338
+ isShowingIncorrectMove,
1159
1339
  ]);
1160
1340
  // Emit the completion event exactly once.
1161
1341
  useEffect(() => {
@@ -1167,7 +1347,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1167
1347
  }, [finished]);
1168
1348
  const handleDrop = (source, target, piece) => {
1169
1349
  var _a, _b, _c;
1170
- if (finished || isShowingCorrectMove) {
1350
+ if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
1171
1351
  return false;
1172
1352
  }
1173
1353
  const setupFen = displayFen !== null && displayFen !== void 0 ? displayFen : chessRef.current.fen();
@@ -1187,7 +1367,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1187
1367
  setFeedback(moveFeedback);
1188
1368
  (_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
1189
1369
  if (isCorrect) {
1190
- const nextFen = fenAfterUci(setupFen, dropResult.uci);
1370
+ const nextFen = fenAfterUci(setupFen, dropResult.attempt.uci);
1191
1371
  if (nextFen) {
1192
1372
  setDisplayFen(nextFen);
1193
1373
  }
@@ -1198,19 +1378,30 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1198
1378
  applyMove(index);
1199
1379
  });
1200
1380
  }
1381
+ else {
1382
+ showIncorrectMove(source);
1383
+ }
1201
1384
  return isCorrect;
1202
1385
  };
1203
1386
  const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
1387
+ const lastMoveUci = useMemo(() => {
1388
+ var _a;
1389
+ if (displayFen) {
1390
+ return (_a = line.movesUci[currentIndex]) !== null && _a !== void 0 ? _a : null;
1391
+ }
1392
+ return lastMoveUciAtPly(line.movesUci, currentIndex);
1393
+ }, [currentIndex, displayFen, line.movesUci]);
1204
1394
  const moveNumber = Math.min(currentIndex + 1, total);
1205
1395
  const isUserTurn = !finished &&
1206
1396
  !isShowingCorrectMove &&
1397
+ !isShowingIncorrectMove &&
1207
1398
  turnFromFen(boardFen) === line.trainSide &&
1208
1399
  currentIndex < total;
1209
1400
  const stackControlsBelow = useStackPuzzleControlsBelow();
1210
1401
  const controlsPlacement = stackControlsBelow
1211
1402
  ? 'below'
1212
1403
  : 'beside';
1213
- return (jsx(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsx("div", { style: puzzleBoardColumnStyle(boardWidth, controlsPlacement), children: jsx("div", { style: puzzleBoardSlotStyle(), children: jsx(LineBoard, { fen: boardFen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, correctMoveSquare: correctMoveSquare, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1404
+ return (jsx(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsx("div", { style: puzzleBoardColumnStyle(boardWidth, controlsPlacement), children: jsx("div", { style: puzzleBoardSlotStyle(), children: jsx(LineBoard, { fen: boardFen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, correctMoveSquare: correctMoveSquare, incorrectMoveSquare: incorrectMoveSquare, lastMoveUci: lastMoveUci, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1214
1405
  trainSide: line.trainSide,
1215
1406
  moveNumber,
1216
1407
  total,
@@ -1227,4 +1418,4 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1227
1418
  /** @deprecated Import {@link boardSquareHighlightColors} and {@link analysisBoardHighlightColors} from `react-chess-core`. */
1228
1419
  const squareHighlightColors = Object.assign(Object.assign({}, boardSquareHighlightColors), analysisBoardHighlightColors);
1229
1420
 
1230
- export { DEFAULT_PUZZLE_BOARD_WIDTH, DefaultLineControls, DefaultPuzzleControls, GamePosition, LineBoard, LineBoardWithControls, PUZZLE_CONTROLS_BESIDE_RESERVE_PX, PUZZLE_CONTROLS_STACK_BREAKPOINT_PX, Position, PuzzleBoard, PuzzleBoardWithControls, PuzzlePosition, applyUciMove, buildAnalysisContext, defaultRenderControls, defaultRenderLineControls, emptyAnalysisContext, getCheckSquareFromChess, isAnalysisAvailable, playerColorForSolution, squareHighlightColors, usePuzzleAnalysis };
1421
+ export { DEFAULT_PUZZLE_BOARD_WIDTH, DefaultLineControls, DefaultPuzzleControls, GamePosition, LineBoard, LineBoardWithControls, PUZZLE_CONTROLS_BESIDE_RESERVE_PX, PUZZLE_CONTROLS_STACK_BREAKPOINT_PX, Position, PuzzleBoard, PuzzleBoardWithControls, PuzzlePosition, advanceToPlayerTurn, applyUciMove, buildAnalysisContext, defaultRenderControls, defaultRenderLineControls, emptyAnalysisContext, getCheckSquareFromChess, isAnalysisAvailable, playerColorForSolution, playerMoveIndicesInRange, squareHighlightColors, usePuzzleAnalysis };
package/dist/index.js CHANGED
@@ -57,18 +57,18 @@ const usePuzzleAnalysis = (position, resultStatus, puzzleNum) => {
57
57
  };
58
58
 
59
59
  const EMPTY_BOARD_FEN = '8/8/8/8/8/8/8/8 w - - 0 1';
60
- const DEFAULT_ANSWER_ARROW_COLOR = '#42a5f5';
61
60
  /**
62
61
  * Single mounted board for puzzle play. Keeps the prior board (and orientation)
63
62
  * visible while the next position loads so layout and perspective do not flicker.
64
63
  */
65
- const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth, onResumeCorrect, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect = false, autoShowWrongMoves = true, refutationEngine, answerArrowColor = DEFAULT_ANSWER_ARROW_COLOR, positionLocked = false, onMissFeedbackChange, }) => {
66
- var _a, _b, _c, _d, _e, _f, _g;
64
+ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth, onResumeCorrect, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect = false, autoShowWrongMoves = true, refutationEngine, answerArrowColor = reactChessCore.DEFAULT_ANSWER_ARROW_COLOR, positionLocked = false, onMissFeedbackChange, recapBoard = null, }) => {
65
+ var _a, _b, _c, _d, _e, _f;
67
66
  const [showAnswerArrow, setShowAnswerArrow] = react.useState(false);
68
67
  const [incorrectActive, setIncorrectActive] = react.useState(false);
69
68
  const attemptMissedRef = react.useRef(false);
70
69
  const { revision, bumpRevision } = reactChessCore.useBoardRevision();
71
70
  const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, } = reactChessCore.useCorrectMoveFeedback();
71
+ const { incorrectMoveSquare: transientIncorrectSquare, showIncorrectMove, clearIncorrectMoveFeedback, } = reactChessCore.useIncorrectMoveFeedback();
72
72
  const boardOrientationRef = react.useRef('white');
73
73
  const boardFenRef = react.useRef(EMPTY_BOARD_FEN);
74
74
  const notifyHost = () => {
@@ -103,13 +103,25 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
103
103
  const answerArrowVisible = useRefutation
104
104
  ? incorrectActive && missPhase === 'answer'
105
105
  : showAnswerArrow;
106
+ const overlayIncorrectSquare = transientIncorrectSquare !== null && transientIncorrectSquare !== void 0 ? transientIncorrectSquare : (useRefutation && incorrectActive
107
+ ? missBoard.missSequence.display.incorrectMoveSquare
108
+ : null);
109
+ const refutationMoveSquare = useRefutation && incorrectActive
110
+ ? missBoard.missSequence.display.refutationMoveSquare
111
+ : null;
106
112
  react.useEffect(() => {
107
113
  setShowAnswerArrow(false);
108
114
  setIncorrectActive(false);
109
115
  attemptMissedRef.current = false;
110
116
  clearCorrectMoveFeedback();
117
+ clearIncorrectMoveFeedback();
111
118
  onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
112
- }, [clearCorrectMoveFeedback, onMissFeedbackChange, position]);
119
+ }, [
120
+ clearCorrectMoveFeedback,
121
+ clearIncorrectMoveFeedback,
122
+ onMissFeedbackChange,
123
+ position,
124
+ ]);
113
125
  react.useEffect(() => {
114
126
  var _a, _b;
115
127
  if (!onMissFeedbackChange) {
@@ -141,11 +153,13 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
141
153
  showAnswerArrow,
142
154
  useRefutation,
143
155
  ]);
156
+ const boardOrientation = position
157
+ ? position.getPlayerColor()
158
+ : boardOrientationRef.current;
144
159
  if (position) {
145
- boardOrientationRef.current = position.getPlayerColor();
160
+ boardOrientationRef.current = boardOrientation;
146
161
  boardFenRef.current = position.fen();
147
162
  }
148
- const boardOrientation = boardOrientationRef.current;
149
163
  const boardFen = boardFenRef.current;
150
164
  const hasBoard = boardFen !== EMPTY_BOARD_FEN;
151
165
  const simpleArrows = react.useMemo(() => {
@@ -158,19 +172,28 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
158
172
  }
159
173
  return [[moveUci.slice(0, 2), moveUci.slice(2, 4), answerArrowColor]];
160
174
  }, [showAnswerArrow, position, answerArrowColor, useRefutation]);
161
- const customArrows = useRefutation && incorrectActive
162
- ? missBoard.customArrows
163
- : simpleArrows;
164
- const displayFen = useRefutation && incorrectActive ? missBoard.boardPosition : boardFen;
175
+ const isRecapping = recapBoard !== null;
176
+ const customArrows = isRecapping
177
+ ? recapBoard.customArrows
178
+ : useRefutation && incorrectActive
179
+ ? missBoard.customArrows
180
+ : simpleArrows;
181
+ const displayFen = isRecapping
182
+ ? recapBoard.fen
183
+ : useRefutation && incorrectActive
184
+ ? missBoard.boardPosition
185
+ : boardFen;
165
186
  const missLocked = useRefutation &&
166
187
  incorrectActive &&
167
188
  (missBoard.boardAnimating ||
168
189
  missPhase === 'wrong' ||
169
190
  missPhase === 'refutation');
170
- const arePiecesDraggable = position !== null &&
191
+ const arePiecesDraggable = !isRecapping &&
192
+ position !== null &&
171
193
  !positionLocked &&
172
194
  !missLocked &&
173
- correctMoveSquare === null;
195
+ correctMoveSquare === null &&
196
+ overlayIncorrectSquare === null;
174
197
  const onPieceDrop = (sourceSquare, targetSquare, piece) => {
175
198
  if (!position || positionLocked || position.isSolutionRevealed()) {
176
199
  return false;
@@ -184,6 +207,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
184
207
  if (answerArrowVisible &&
185
208
  !allowRetryOnIncorrect &&
186
209
  !position.isExpectedGuess(sourceSquare, targetSquare)) {
210
+ showIncorrectMove(sourceSquare);
187
211
  position.resetInteractions();
188
212
  snapBoardBack();
189
213
  return false;
@@ -193,6 +217,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
193
217
  });
194
218
  if (!guess.accepted) {
195
219
  attemptMissedRef.current = true;
220
+ showIncorrectMove(useRefutation ? targetSquare : sourceSquare);
196
221
  onFeedback({
197
222
  index: position.getIndex(),
198
223
  guess: { sourceSquare, targetSquare, piece },
@@ -206,8 +231,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
206
231
  missBoard.missSequence.startSequence(setupFen, attemptedUci);
207
232
  }
208
233
  position.resetInteractions();
209
- snapBoardBack();
210
- return false;
234
+ return true;
211
235
  }
212
236
  const revealIncorrectFeedback = () => {
213
237
  if (showAnswerArrowOnIncorrect) {
@@ -273,13 +297,79 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
273
297
  showCorrectMove(targetSquare, finishCorrectFeedback);
274
298
  return true;
275
299
  };
276
- return (jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: hasBoard ? (jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: (_e = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _e !== void 0 ? _e : '', hintSquare: (_f = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _f !== void 0 ? _f : null, incorrectMoveSquare: showAnswerArrowOnIncorrect
277
- ? null
278
- : ((_g = position === null || position === void 0 ? void 0 : position.getIncorrectMoveSquare()) !== null && _g !== void 0 ? _g : null), correctMoveSquare: correctMoveSquare, customArrows: customArrows, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: 0 }, revision)) : null }));
300
+ return hasBoard ? (jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: isRecapping ? '' : ((_e = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _e !== void 0 ? _e : ''), hintSquare: isRecapping ? null : ((_f = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _f !== void 0 ? _f : null), incorrectMoveSquare: isRecapping ? null : overlayIncorrectSquare, refutationMoveSquare: isRecapping ? null : refutationMoveSquare, correctMoveSquare: isRecapping ? null : correctMoveSquare, customArrows: customArrows, lastMoveUci: isRecapping ? recapBoard.lastMoveUci : null, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: isRecapping ? recapBoard.animationDuration : 0 }, revision)) : null;
279
301
  };
280
302
 
281
303
  const PuzzleBoard = ({ position, onFeedback, incInteractionNum, boardWidth, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, answerArrowColor, }) => (jsxRuntime.jsx(PuzzlePlaySurface, { position: position, onFeedback: onFeedback, incInteractionNum: incInteractionNum, boardWidth: boardWidth, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, answerArrowColor: answerArrowColor }));
282
304
 
305
+ const inactiveAutoAdvance = {
306
+ active: false,
307
+ secondsRemaining: -1,
308
+ };
309
+ /** Countdown overlay state while waiting to auto-load the next puzzle card. */
310
+ function usePuzzleAutoAdvanceCountdown(enabled, delayMs, onAdvance) {
311
+ const [secondsRemaining, setSecondsRemaining] = react.useState(-1);
312
+ react.useEffect(() => {
313
+ if (!enabled || delayMs <= 0) {
314
+ setSecondsRemaining(-1);
315
+ return;
316
+ }
317
+ const startedAt = Date.now();
318
+ const updateCountdown = () => {
319
+ const elapsed = Date.now() - startedAt;
320
+ setSecondsRemaining(Math.max(0, Math.ceil((delayMs - elapsed) / 1000)));
321
+ };
322
+ updateCountdown();
323
+ const intervalId = window.setInterval(updateCountdown, 200);
324
+ const timeoutId = window.setTimeout(() => {
325
+ window.clearInterval(intervalId);
326
+ setSecondsRemaining(-1);
327
+ onAdvance();
328
+ }, delayMs);
329
+ return () => {
330
+ window.clearInterval(intervalId);
331
+ window.clearTimeout(timeoutId);
332
+ setSecondsRemaining(-1);
333
+ };
334
+ }, [delayMs, enabled, onAdvance]);
335
+ if (!enabled || secondsRemaining < 0) {
336
+ return inactiveAutoAdvance;
337
+ }
338
+ return {
339
+ active: true,
340
+ secondsRemaining,
341
+ };
342
+ }
343
+
344
+ /** Pause on the puzzle setup position before the solution recap animates. */
345
+ const PUZZLE_COMPLETION_RECAP_SETUP_MS = 400;
346
+ const usePuzzleCompletionRecap = ({ source, active, onComplete, }) => {
347
+ var _a, _b, _c, _d, _e, _f;
348
+ const startFen = (_a = source === null || source === void 0 ? void 0 : source.startFen) !== null && _a !== void 0 ? _a : '';
349
+ const movesUci = (_b = source === null || source === void 0 ? void 0 : source.movesUci) !== null && _b !== void 0 ? _b : [];
350
+ const startIndex = (_c = source === null || source === void 0 ? void 0 : source.startIndex) !== null && _c !== void 0 ? _c : 0;
351
+ const endIndex = (_d = source === null || source === void 0 ? void 0 : source.endIndex) !== null && _d !== void 0 ? _d : 0;
352
+ const missedIndices = (_e = source === null || source === void 0 ? void 0 : source.missedIndices) !== null && _e !== void 0 ? _e : [];
353
+ const setupUci = (_f = source === null || source === void 0 ? void 0 : source.setupUci) !== null && _f !== void 0 ? _f : null;
354
+ const resolveFen = react.useCallback((moveIndex, afterMove) => {
355
+ if (!startFen || movesUci.length === 0) {
356
+ return '';
357
+ }
358
+ return reactChessCore.fenAtPlyFromStart(startFen, movesUci, afterMove ? moveIndex + 1 : moveIndex);
359
+ }, [movesUci, startFen]);
360
+ return reactChessCore.useSolutionLineRecap({
361
+ active: active && source !== null,
362
+ movesUci,
363
+ startIndex,
364
+ endIndex,
365
+ missedIndices,
366
+ segmentStartFen: startFen,
367
+ setupUci,
368
+ onComplete,
369
+ resolveFen,
370
+ });
371
+ };
372
+
283
373
  const isAttemptFinished = (resultStatus) => resultStatus === 'complete' || resultStatus === 'incorrect';
284
374
  /** Library default hint / next / analysis / result controls (unstyled buttons). */
285
375
  const DefaultPuzzleControls = ({ showHint, showSolution, nextPuzzle, resultStatus, analysis, controlState, }) => (jsxRuntime.jsxs("div", { style: rowStyle$1, children: [jsxRuntime.jsx("button", { type: "button", onClick: showHint, style: buttonStyle, disabled: !controlState.canShowHint, children: "Hint" }), jsxRuntime.jsx("button", { type: "button", onClick: showSolution, style: buttonStyle, disabled: !controlState.canShowSolution, children: "Show solution" }), jsxRuntime.jsx("button", { type: "button", onClick: nextPuzzle, style: buttonStyle, children: "Next puzzle" }), analysis.visible && isAttemptFinished(resultStatus) && (jsxRuntime.jsx("button", { type: "button", onClick: analysis.openAnalysis, style: buttonStyle, children: "Analysis" })), resultStatus === 'complete' && (jsxRuntime.jsx("span", { style: statusStyle$1, children: "Complete" })), resultStatus === 'incorrect' && (jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, statusStyle$1), { color: '#c62828' }), children: "Incorrect" }))] }));
@@ -484,6 +574,29 @@ function playerColorForSolution(initialFen, moves) {
484
574
  }
485
575
  return chess.turn() === 'w' ? 'white' : 'black';
486
576
  }
577
+ /** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
578
+ function playerMoveIndicesInRange(initialFen, moves, fromIndex, toIndex) {
579
+ const playerColor = playerColorForSolution(initialFen, moves);
580
+ const chess = new chess_js.Chess(initialFen);
581
+ const indices = [];
582
+ for (let i = 0; i < toIndex && i < moves.length; i++) {
583
+ const side = chess.turn() === 'w' ? 'white' : 'black';
584
+ if (i >= fromIndex && side === playerColor) {
585
+ indices.push(i);
586
+ }
587
+ applyUciMove(chess, moves[i]);
588
+ }
589
+ return indices;
590
+ }
591
+ /** Auto-play opponent setup plies until the solver is on move. */
592
+ function advanceToPlayerTurn(position) {
593
+ while (!position.isFinished() &&
594
+ position.getSideToMove() !== position.getPlayerColor()) {
595
+ if (!position.next()) {
596
+ break;
597
+ }
598
+ }
599
+ }
487
600
  class PuzzlePosition extends Position {
488
601
  constructor(initialFEN, moves, resumeConfig) {
489
602
  super();
@@ -728,19 +841,37 @@ class GamePosition extends Position {
728
841
  }
729
842
  }
730
843
 
731
- /** Apply the opponent setup ply immediately so the board does not flash on load. */
844
+ /** Apply opponent setup plies immediately so the board does not flash on load. */
732
845
  const puzzlePositionFromFetch = (fen, moves, resume) => {
733
846
  const newPosition = new PuzzlePosition(fen, moves, resume);
734
847
  if (!resume && moves.length > 1) {
735
848
  newPosition.next();
849
+ advanceToPlayerTurn(newPosition);
850
+ }
851
+ else if (resume &&
852
+ newPosition.getSideToMove() !== newPosition.getPlayerColor()) {
853
+ advanceToPlayerTurn(newPosition);
736
854
  }
737
855
  return newPosition;
738
856
  };
739
- /** Brief pause so the user sees a correct result before the next card loads. */
740
- const AUTO_ADVANCE_ON_COMPLETE_DELAY_MS = 700;
741
857
  const SOLUTION_STEP_MS = 500;
742
858
  const RESUME_AUTO_STEP_MS = 500;
743
- const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = reactChessCore.DEFAULT_ANALYSIS_LAYOUT, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
859
+ const uniqueIndices = (indices) => [...new Set(indices)];
860
+ const buildCompletionRecapSource = (position, missedIndices) => {
861
+ var _a, _b;
862
+ const movesUci = position.getSolutionMoves();
863
+ const initialFen = position.getInitialFen();
864
+ const startIndex = (_a = playerMoveIndicesInRange(initialFen, movesUci, 0, movesUci.length)[0]) !== null && _a !== void 0 ? _a : 0;
865
+ return {
866
+ startFen: initialFen,
867
+ movesUci,
868
+ startIndex,
869
+ endIndex: movesUci.length,
870
+ missedIndices,
871
+ setupUci: startIndex > 0 ? (_b = movesUci[startIndex - 1]) !== null && _b !== void 0 ? _b : null : null,
872
+ };
873
+ };
874
+ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = reactChessCore.DEFAULT_ANALYSIS_LAYOUT, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, autoAdvanceOnCompleteDelayMs = reactChessCore.AUTO_ADVANCE_ON_COMPLETE_DELAY_MS, showCompletionRecap = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
744
875
  var _a, _b, _c, _d;
745
876
  const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
746
877
  const stackControlsBelow = useStackPuzzleControlsBelow();
@@ -756,6 +887,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
756
887
  const [puzzleComplete, setPuzzleComplete] = react.useState(false);
757
888
  const [completedAfterMiss, setCompletedAfterMiss] = react.useState(false);
758
889
  const [missFeedback, setMissFeedback] = react.useState(null);
890
+ const [missedMoveIndices, setMissedMoveIndices] = react.useState([]);
891
+ const [completionCheckVisible, setCompletionCheckVisible] = react.useState(false);
892
+ const [completionRecapActive, setCompletionRecapActive] = react.useState(false);
893
+ const [completionRecapDone, setCompletionRecapDone] = react.useState(false);
894
+ const completionFlowStartedRef = react.useRef(false);
759
895
  const [, setInteractionNum] = react.useState(0);
760
896
  const solutionAnimationRef = react.useRef({ cancelled: false, timeoutIds: [] });
761
897
  const resumeAnimationRef = react.useRef({ cancelled: false, timeoutIds: [] });
@@ -782,6 +918,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
782
918
  setPuzzleComplete(false);
783
919
  setCompletedAfterMiss(false);
784
920
  setMissFeedback(null);
921
+ setMissedMoveIndices([]);
922
+ setCompletionCheckVisible(false);
923
+ setCompletionRecapActive(false);
924
+ setCompletionRecapDone(false);
925
+ completionFlowStartedRef.current = false;
785
926
  onFetch()
786
927
  .then((data) => {
787
928
  if (cancelled) {
@@ -817,10 +958,16 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
817
958
  feedbackData.isCorrect === false;
818
959
  if (feedbackData.hintRequested) {
819
960
  setHintUsed(true);
961
+ setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
820
962
  }
821
963
  if (incorrectThisFeedback) {
822
964
  setHasIncorrectAttempt(true);
823
965
  }
966
+ if (feedbackData.isCorrect === false &&
967
+ !feedbackData.isFinished &&
968
+ !feedbackData.solutionShown) {
969
+ setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
970
+ }
824
971
  if (feedbackData.isFinished) {
825
972
  setPuzzleComplete(true);
826
973
  setCompletedAfterMiss((prev) => prev ||
@@ -984,6 +1131,10 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
984
1131
  position.recordSolutionShown();
985
1132
  position.setSolutionRevealed(true);
986
1133
  position.wantsHint(false);
1134
+ setMissedMoveIndices((prev) => uniqueIndices([
1135
+ ...prev,
1136
+ ...playerMoveIndicesInRange(position.getInitialFen(), position.getSolutionMoves(), position.getIndex(), position.getSolutionMoves().length),
1137
+ ]));
987
1138
  handleFeedback({
988
1139
  index: position.getIndex(),
989
1140
  solutionShown: true,
@@ -996,30 +1147,46 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
996
1147
  setPuzzleNum((prevPuzzleNum) => prevPuzzleNum + 1);
997
1148
  }, []);
998
1149
  const resultStatus = getResultStatus();
1150
+ const completionRecapSource = react.useMemo(() => position && showCompletionRecap
1151
+ ? buildCompletionRecapSource(position, missedMoveIndices)
1152
+ : null, [position, showCompletionRecap, missedMoveIndices]);
1153
+ const handleCompletionRecapDone = react.useCallback(() => {
1154
+ setCompletionRecapActive(false);
1155
+ setCompletionRecapDone(true);
1156
+ }, []);
1157
+ const completionRecap = usePuzzleCompletionRecap({
1158
+ source: completionRecapSource,
1159
+ active: completionRecapActive,
1160
+ onComplete: handleCompletionRecapDone,
1161
+ });
1162
+ const isCompletionRecapping = showCompletionRecap && (completionRecapActive || completionRecap.active);
999
1163
  react.useEffect(() => {
1000
- if (!autoAdvanceOnComplete) {
1001
- return;
1002
- }
1003
- if (resultStatus !== 'complete') {
1164
+ if (!showCompletionRecap ||
1165
+ resultStatus !== 'complete' ||
1166
+ loadingNextPuzzle ||
1167
+ completionFlowStartedRef.current) {
1004
1168
  return;
1005
1169
  }
1006
- if (hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) {
1170
+ completionFlowStartedRef.current = true;
1171
+ setCompletionCheckVisible(true);
1172
+ }, [loadingNextPuzzle, resultStatus, showCompletionRecap]);
1173
+ react.useEffect(() => {
1174
+ if (!completionCheckVisible) {
1007
1175
  return;
1008
1176
  }
1009
- const timer = setTimeout(() => {
1010
- handleNextPuzzle();
1011
- }, AUTO_ADVANCE_ON_COMPLETE_DELAY_MS);
1177
+ const timer = window.setTimeout(() => {
1178
+ setCompletionCheckVisible(false);
1179
+ setCompletionRecapActive(true);
1180
+ }, PUZZLE_COMPLETION_RECAP_SETUP_MS);
1012
1181
  return () => {
1013
- clearTimeout(timer);
1182
+ window.clearTimeout(timer);
1014
1183
  };
1015
- }, [
1016
- autoAdvanceOnComplete,
1017
- autoAdvanceOnCompleteAfterIncorrect,
1018
- resultStatus,
1019
- hasIncorrectAttempt,
1020
- handleNextPuzzle,
1021
- puzzleNum,
1022
- ]);
1184
+ }, [completionCheckVisible]);
1185
+ const shouldAutoAdvance = autoAdvanceOnComplete &&
1186
+ resultStatus === 'complete' &&
1187
+ !(hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) &&
1188
+ (!showCompletionRecap || completionRecapDone);
1189
+ const autoAdvance = usePuzzleAutoAdvanceCountdown(shouldAutoAdvance, autoAdvanceOnCompleteDelayMs, handleNextPuzzle);
1023
1190
  const controlState = {
1024
1191
  canShowHint: position !== null &&
1025
1192
  !position.isFinished() &&
@@ -1034,7 +1201,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1034
1201
  const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
1035
1202
  renderAnalysisContainer &&
1036
1203
  (renderEngineEvaluation || (engine === null || engine === void 0 ? void 0 : engine.enabled) === false));
1037
- return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: analysisSnapshot ? (jsxRuntime.jsx(reactChessCore.AnalysisErrorBoundary, { onClose: analysis.closeAnalysis, children: useHostAnalysisUi ? (jsxRuntime.jsx(reactChessCore.AnalysisBoardCore, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, boardWidth: resolvedAnalysisBoardWidth, engine: engine, renderMain: renderAnalysisMain !== null && renderAnalysisMain !== void 0 ? renderAnalysisMain : (({ board, sidebar, model }) => (jsxRuntime.jsx(reactChessCore.AnalysisBoardLayout, { layout: analysisLayout, model: model, board: board, sidebar: sidebar }))), renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation !== null && renderEngineEvaluation !== void 0 ? renderEngineEvaluation : (() => null) })) : (jsxRuntime.jsx(reactChessCore.AnalysisBoard, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, layout: analysisLayout, engine: engine, renderMain: renderAnalysisMain, renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation })) })) : (jsxRuntime.jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxRuntime.jsxs("div", { style: puzzleBoardColumnStyle(puzzleBoardWidth, controlsPlacement), children: [jsxRuntime.jsx("div", { style: puzzleBoardSlotWrapperStyle(), children: jsxRuntime.jsx("div", { style: puzzleBoardSlotStyle(), children: jsxRuntime.jsx(PuzzlePlaySurface, { position: position, boardWidth: puzzleBoardWidth, onFeedback: handleFeedback, incInteractionNum: incInteractionNum, onResumeCorrect: runResumeAutoAdvance, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, showRefutationOnIncorrect: refutationOnIncorrect, autoShowWrongMoves: autoShowWrongMoves, refutationEngine: refutationEngine !== null && refutationEngine !== void 0 ? refutationEngine : engine, answerArrowColor: answerArrowColor, positionLocked: loadingNextPuzzle, onMissFeedbackChange: setMissFeedback }) }) }), renderBoardCaption && (jsxRuntime.jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
1204
+ return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: analysisSnapshot ? (jsxRuntime.jsx(reactChessCore.AnalysisErrorBoundary, { onClose: analysis.closeAnalysis, children: useHostAnalysisUi ? (jsxRuntime.jsx(reactChessCore.AnalysisBoardCore, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, boardWidth: resolvedAnalysisBoardWidth, engine: engine, renderMain: renderAnalysisMain !== null && renderAnalysisMain !== void 0 ? renderAnalysisMain : (({ board, sidebar, model }) => (jsxRuntime.jsx(reactChessCore.AnalysisBoardLayout, { layout: analysisLayout, model: model, board: board, sidebar: sidebar }))), renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation !== null && renderEngineEvaluation !== void 0 ? renderEngineEvaluation : (() => null) })) : (jsxRuntime.jsx(reactChessCore.AnalysisBoard, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, layout: analysisLayout, engine: engine, renderMain: renderAnalysisMain, renderSidebar: renderAnalysisSidebar, renderContainer: renderAnalysisContainer, renderEngineEvaluation: renderEngineEvaluation })) })) : (jsxRuntime.jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxRuntime.jsxs("div", { style: puzzleBoardColumnStyle(puzzleBoardWidth, controlsPlacement), children: [jsxRuntime.jsxs("div", { style: puzzleBoardSlotWrapperStyle(), children: [jsxRuntime.jsx("div", { style: puzzleBoardSlotStyle(), children: jsxRuntime.jsx(PuzzlePlaySurface, { position: position, boardWidth: puzzleBoardWidth, onFeedback: handleFeedback, incInteractionNum: incInteractionNum, onResumeCorrect: runResumeAutoAdvance, revealAnswerOnIncorrect: revealAnswerOnIncorrect, showAnswerArrowOnIncorrect: showAnswerArrowOnIncorrect, allowRetryOnIncorrect: allowRetryOnIncorrect, showRefutationOnIncorrect: refutationOnIncorrect, autoShowWrongMoves: autoShowWrongMoves, refutationEngine: refutationEngine !== null && refutationEngine !== void 0 ? refutationEngine : engine, answerArrowColor: answerArrowColor, positionLocked: loadingNextPuzzle ||
1205
+ completionCheckVisible ||
1206
+ isCompletionRecapping, onMissFeedbackChange: setMissFeedback, recapBoard: isCompletionRecapping
1207
+ ? {
1208
+ fen: completionRecap.fen,
1209
+ lastMoveUci: completionRecap.lastMoveUci,
1210
+ customArrows: completionRecap.customArrows,
1211
+ animationDuration: completionRecap.animationDuration,
1212
+ }
1213
+ : null }) }), completionCheckVisible && (jsxRuntime.jsx(reactChessCore.BoardCompleteCheckOverlay, { variant: hasIncorrectAttempt || completedAfterMiss || hintUsed
1214
+ ? 'partial'
1215
+ : 'success' }))] }), renderBoardCaption && (jsxRuntime.jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
1038
1216
  sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
1039
1217
  playerColor: position
1040
1218
  ? position.getPlayerColor()
@@ -1050,7 +1228,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1050
1228
  }) }))] }), jsxRuntime.jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
1051
1229
  visible: analysis.canOpen,
1052
1230
  openAnalysis: analysis.openAnalysis,
1053
- }, controlState), renderBoardFeedback && resultStatus === 'complete' && (jsxRuntime.jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
1231
+ }, controlState, autoAdvance), renderBoardFeedback && resultStatus === 'complete' && (jsxRuntime.jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
1054
1232
  resultStatus,
1055
1233
  cleanSolve: !hasIncorrectAttempt,
1056
1234
  refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
@@ -1064,7 +1242,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1064
1242
  * side's pieces be dragged. Move validation and sequencing live in
1065
1243
  * {@link LineBoardWithControls}.
1066
1244
  */
1067
- const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, onPieceDrop, boardWidth, }) => (jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, correctMoveSquare: correctMoveSquare, position: fen, boardOrientation: orientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => piece[0] === trainSide, onPieceDrop: (source, target, piece) => onPieceDrop(source, target, piece), autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }) }));
1245
+ const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, incorrectMoveSquare = null, lastMoveUci = null, onPieceDrop, boardWidth, }) => (jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: incorrectMoveSquare, correctMoveSquare: correctMoveSquare, position: fen, boardOrientation: orientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => piece[0] === trainSide, onPieceDrop: (source, target, piece) => onPieceDrop(source, target, piece), lastMoveUci: lastMoveUci, autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }));
1068
1246
 
1069
1247
  /** Library default line-drill status controls (unstyled). */
1070
1248
  const DefaultLineControls = ({ moveNumber, total, finished, isUserTurn, feedback, }) => (jsxRuntime.jsxs("div", { style: rowStyle, children: [jsxRuntime.jsx("span", { style: statusStyle, children: finished ? 'Line complete' : `Move ${moveNumber} of ${total}` }), feedback && !finished && (jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, statusStyle), { color: feedback.isCorrect ? '#2e7d32' : '#c62828' }), children: feedback.isCorrect
@@ -1113,6 +1291,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1113
1291
  const [feedback, setFeedback] = react.useState(null);
1114
1292
  const [displayFen, setDisplayFen] = react.useState(null);
1115
1293
  const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = reactChessCore.useCorrectMoveFeedback();
1294
+ const { incorrectMoveSquare, showIncorrectMove, isShowingIncorrectMove, } = reactChessCore.useIncorrectMoveFeedback();
1116
1295
  const total = line.movesUci.length;
1117
1296
  const orientation = boardOrientationForLine(line.trainSide);
1118
1297
  const applyMove = react.useCallback((index) => {
@@ -1137,7 +1316,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1137
1316
  }, [line.movesUci]);
1138
1317
  // Auto-play opponent moves and detect the end of the line.
1139
1318
  react.useEffect(() => {
1140
- if (finished || isShowingCorrectMove) {
1319
+ if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
1141
1320
  return;
1142
1321
  }
1143
1322
  if (currentIndex >= total) {
@@ -1157,6 +1336,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1157
1336
  applyMove,
1158
1337
  opponentMoveDelayMs,
1159
1338
  isShowingCorrectMove,
1339
+ isShowingIncorrectMove,
1160
1340
  ]);
1161
1341
  // Emit the completion event exactly once.
1162
1342
  react.useEffect(() => {
@@ -1168,7 +1348,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1168
1348
  }, [finished]);
1169
1349
  const handleDrop = (source, target, piece) => {
1170
1350
  var _a, _b, _c;
1171
- if (finished || isShowingCorrectMove) {
1351
+ if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
1172
1352
  return false;
1173
1353
  }
1174
1354
  const setupFen = displayFen !== null && displayFen !== void 0 ? displayFen : chessRef.current.fen();
@@ -1188,7 +1368,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1188
1368
  setFeedback(moveFeedback);
1189
1369
  (_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
1190
1370
  if (isCorrect) {
1191
- const nextFen = reactChessCore.fenAfterUci(setupFen, dropResult.uci);
1371
+ const nextFen = reactChessCore.fenAfterUci(setupFen, dropResult.attempt.uci);
1192
1372
  if (nextFen) {
1193
1373
  setDisplayFen(nextFen);
1194
1374
  }
@@ -1199,19 +1379,30 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1199
1379
  applyMove(index);
1200
1380
  });
1201
1381
  }
1382
+ else {
1383
+ showIncorrectMove(source);
1384
+ }
1202
1385
  return isCorrect;
1203
1386
  };
1204
1387
  const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
1388
+ const lastMoveUci = react.useMemo(() => {
1389
+ var _a;
1390
+ if (displayFen) {
1391
+ return (_a = line.movesUci[currentIndex]) !== null && _a !== void 0 ? _a : null;
1392
+ }
1393
+ return reactChessCore.lastMoveUciAtPly(line.movesUci, currentIndex);
1394
+ }, [currentIndex, displayFen, line.movesUci]);
1205
1395
  const moveNumber = Math.min(currentIndex + 1, total);
1206
1396
  const isUserTurn = !finished &&
1207
1397
  !isShowingCorrectMove &&
1398
+ !isShowingIncorrectMove &&
1208
1399
  turnFromFen(boardFen) === line.trainSide &&
1209
1400
  currentIndex < total;
1210
1401
  const stackControlsBelow = useStackPuzzleControlsBelow();
1211
1402
  const controlsPlacement = stackControlsBelow
1212
1403
  ? 'below'
1213
1404
  : 'beside';
1214
- return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsxRuntime.jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxRuntime.jsx("div", { style: puzzleBoardColumnStyle(boardWidth, controlsPlacement), children: jsxRuntime.jsx("div", { style: puzzleBoardSlotStyle(), children: jsxRuntime.jsx(LineBoard, { fen: boardFen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, correctMoveSquare: correctMoveSquare, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsxRuntime.jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1405
+ return (jsxRuntime.jsx(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: jsxRuntime.jsxs("div", { style: puzzlePlayRowStyle(controlsPlacement), children: [jsxRuntime.jsx("div", { style: puzzleBoardColumnStyle(boardWidth, controlsPlacement), children: jsxRuntime.jsx("div", { style: puzzleBoardSlotStyle(), children: jsxRuntime.jsx(LineBoard, { fen: boardFen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, correctMoveSquare: correctMoveSquare, incorrectMoveSquare: incorrectMoveSquare, lastMoveUci: lastMoveUci, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsxRuntime.jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1215
1406
  trainSide: line.trainSide,
1216
1407
  moveNumber,
1217
1408
  total,
@@ -1244,6 +1435,7 @@ exports.Position = Position;
1244
1435
  exports.PuzzleBoard = PuzzleBoard;
1245
1436
  exports.PuzzleBoardWithControls = PuzzleBoardWithControls;
1246
1437
  exports.PuzzlePosition = PuzzlePosition;
1438
+ exports.advanceToPlayerTurn = advanceToPlayerTurn;
1247
1439
  exports.applyUciMove = applyUciMove;
1248
1440
  exports.buildAnalysisContext = buildAnalysisContext;
1249
1441
  exports.defaultRenderControls = defaultRenderControls;
@@ -1252,5 +1444,6 @@ exports.emptyAnalysisContext = emptyAnalysisContext;
1252
1444
  exports.getCheckSquareFromChess = getCheckSquareFromChess;
1253
1445
  exports.isAnalysisAvailable = isAnalysisAvailable;
1254
1446
  exports.playerColorForSolution = playerColorForSolution;
1447
+ exports.playerMoveIndicesInRange = playerMoveIndicesInRange;
1255
1448
  exports.squareHighlightColors = squareHighlightColors;
1256
1449
  exports.usePuzzleAnalysis = usePuzzleAnalysis;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-chess-puzzle-kit",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "React chess puzzle kit: play, controls, analysis, and browser Stockfish for endchess.training and other apps",
5
5
  "license": "MIT",
6
6
  "author": "Robert Blackwell",
@@ -51,7 +51,7 @@
51
51
  "peerDependencies": {
52
52
  "chess.js": "^1.0.0-beta.8",
53
53
  "react": "^18.3.1",
54
- "react-chess-core": "^0.1.1",
54
+ "react-chess-core": "^0.1.8",
55
55
  "react-chessboard": "^4.7.1"
56
56
  },
57
57
  "devDependencies": {
@@ -74,8 +74,9 @@
74
74
  "chess.js": "^1.0.0-beta.8",
75
75
  "jest": "^29.7.0",
76
76
  "react": "^18.3.1",
77
- "react-chess-core": "^0.1.1",
77
+ "react-chess-core": "^0.1.8",
78
78
  "react-chessboard": "^4.7.1",
79
+ "react-dom": "^18.3.1",
79
80
  "storybook": "^8.2.9",
80
81
  "ts-jest": "^29.2.4",
81
82
  "vite": "^5.4.11",