react-chess-puzzle-kit 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,7 @@ export interface LineBoardProps {
5
5
  trainSide: LineTrainSide;
6
6
  draggable: boolean;
7
7
  correctMoveSquare?: string | null;
8
+ incorrectMoveSquare?: string | null;
8
9
  lastMoveUci?: string | null;
9
10
  onPieceDrop: (source: string, target: string, piece: string) => boolean;
10
11
  boardWidth: number;
@@ -14,4 +15,4 @@ export interface LineBoardProps {
14
15
  * side's pieces be dragged. Move validation and sequencing live in
15
16
  * {@link LineBoardWithControls}.
16
17
  */
17
- export declare const LineBoard: ({ fen, orientation, trainSide, draggable, correctMoveSquare, lastMoveUci, 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;
@@ -9,6 +9,8 @@ export declare abstract class Position implements Traversable {
9
9
  protected i: number;
10
10
  constructor();
11
11
  getIndex(): number;
12
+ /** UCI of the move that produced the current position. */
13
+ getLastMoveUci(): string | null;
12
14
  next(): boolean;
13
15
  prev(): boolean;
14
16
  isFinished(): boolean;
@@ -21,6 +23,10 @@ export declare abstract class Position implements Traversable {
21
23
  export declare function getCheckSquareFromChess(chess: Chess): string;
22
24
  /** Side to move for the final (user) ply in a puzzle line. */
23
25
  export declare function playerColorForSolution(initialFen: string, moves: string[]): 'white' | 'black';
26
+ /** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
27
+ export declare function playerMoveIndicesInRange(initialFen: string, moves: string[], fromIndex: number, toIndex: number): number[];
28
+ /** Auto-play opponent setup plies until the solver is on move. */
29
+ export declare function advanceToPlayerTurn(position: PuzzlePosition): void;
24
30
  export type PuzzleResumeConfig = {
25
31
  startIndex: number;
26
32
  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, lastMoveUciAtPly, evaluateExpectedMoveDrop, fenAfterUci, boardSquareHighlightColors, analysisBoardHighlightColors } from 'react-chess-core';
3
+ import { useBoardRevision, useCorrectMoveFeedback, useIncorrectMoveFeedback, useMissBoard, HighlightChessboard, DEFAULT_ANSWER_ARROW_COLOR, uciFromDrop, fenAtPlyFromStart, useSolutionLineRecap, lastMoveUciAtPly, ThemeProvider, AnalysisErrorBoundary, AnalysisBoardCore, AnalysisBoardLayout, AnalysisBoard, BoardCompleteCheckOverlay, DEFAULT_ANALYSIS_LAYOUT, AUTO_ADVANCE_ON_COMPLETE_DELAY_MS, 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, }) => {
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, }) => {
65
64
  var _a, _b, _c, _d, _e, _f, _g;
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,33 @@ 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;
185
+ const lastMoveUci = isRecapping
186
+ ? recapBoard.lastMoveUci
187
+ : useRefutation && incorrectActive
188
+ ? missBoard.lastMoveUci
189
+ : ((_e = position === null || position === void 0 ? void 0 : position.getLastMoveUci()) !== null && _e !== void 0 ? _e : null);
164
190
  const missLocked = useRefutation &&
165
191
  incorrectActive &&
166
192
  (missBoard.boardAnimating ||
167
193
  missPhase === 'wrong' ||
168
194
  missPhase === 'refutation');
169
- const arePiecesDraggable = position !== null &&
195
+ const arePiecesDraggable = !isRecapping &&
196
+ position !== null &&
170
197
  !positionLocked &&
171
198
  !missLocked &&
172
- correctMoveSquare === null;
199
+ correctMoveSquare === null &&
200
+ overlayIncorrectSquare === null;
173
201
  const onPieceDrop = (sourceSquare, targetSquare, piece) => {
174
202
  if (!position || positionLocked || position.isSolutionRevealed()) {
175
203
  return false;
@@ -183,6 +211,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
183
211
  if (answerArrowVisible &&
184
212
  !allowRetryOnIncorrect &&
185
213
  !position.isExpectedGuess(sourceSquare, targetSquare)) {
214
+ showIncorrectMove(sourceSquare);
186
215
  position.resetInteractions();
187
216
  snapBoardBack();
188
217
  return false;
@@ -192,6 +221,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
192
221
  });
193
222
  if (!guess.accepted) {
194
223
  attemptMissedRef.current = true;
224
+ showIncorrectMove(useRefutation ? targetSquare : sourceSquare);
195
225
  onFeedback({
196
226
  index: position.getIndex(),
197
227
  guess: { sourceSquare, targetSquare, piece },
@@ -205,8 +235,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
205
235
  missBoard.missSequence.startSequence(setupFen, attemptedUci);
206
236
  }
207
237
  position.resetInteractions();
208
- snapBoardBack();
209
- return false;
238
+ return true;
210
239
  }
211
240
  const revealIncorrectFeedback = () => {
212
241
  if (showAnswerArrowOnIncorrect) {
@@ -272,13 +301,79 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
272
301
  showCorrectMove(targetSquare, finishCorrectFeedback);
273
302
  return true;
274
303
  };
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 }));
304
+ return hasBoard ? (jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: isRecapping ? '' : ((_f = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _f !== void 0 ? _f : ''), hintSquare: isRecapping ? null : ((_g = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _g !== void 0 ? _g : null), incorrectMoveSquare: isRecapping ? null : overlayIncorrectSquare, refutationMoveSquare: isRecapping ? null : refutationMoveSquare, correctMoveSquare: isRecapping ? null : correctMoveSquare, customArrows: customArrows, lastMoveUci: lastMoveUci, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: isRecapping ? recapBoard.animationDuration : 0 }, revision)) : null;
278
305
  };
279
306
 
280
307
  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
308
 
309
+ const inactiveAutoAdvance = {
310
+ active: false,
311
+ secondsRemaining: -1,
312
+ };
313
+ /** Countdown overlay state while waiting to auto-load the next puzzle card. */
314
+ function usePuzzleAutoAdvanceCountdown(enabled, delayMs, onAdvance) {
315
+ const [secondsRemaining, setSecondsRemaining] = useState(-1);
316
+ useEffect(() => {
317
+ if (!enabled || delayMs <= 0) {
318
+ setSecondsRemaining(-1);
319
+ return;
320
+ }
321
+ const startedAt = Date.now();
322
+ const updateCountdown = () => {
323
+ const elapsed = Date.now() - startedAt;
324
+ setSecondsRemaining(Math.max(0, Math.ceil((delayMs - elapsed) / 1000)));
325
+ };
326
+ updateCountdown();
327
+ const intervalId = window.setInterval(updateCountdown, 200);
328
+ const timeoutId = window.setTimeout(() => {
329
+ window.clearInterval(intervalId);
330
+ setSecondsRemaining(-1);
331
+ onAdvance();
332
+ }, delayMs);
333
+ return () => {
334
+ window.clearInterval(intervalId);
335
+ window.clearTimeout(timeoutId);
336
+ setSecondsRemaining(-1);
337
+ };
338
+ }, [delayMs, enabled, onAdvance]);
339
+ if (!enabled || secondsRemaining < 0) {
340
+ return inactiveAutoAdvance;
341
+ }
342
+ return {
343
+ active: true,
344
+ secondsRemaining,
345
+ };
346
+ }
347
+
348
+ /** Pause on the puzzle setup position before the solution recap animates. */
349
+ const PUZZLE_COMPLETION_RECAP_SETUP_MS = 400;
350
+ const usePuzzleCompletionRecap = ({ source, active, onComplete, }) => {
351
+ var _a, _b, _c, _d, _e, _f;
352
+ const startFen = (_a = source === null || source === void 0 ? void 0 : source.startFen) !== null && _a !== void 0 ? _a : '';
353
+ const movesUci = (_b = source === null || source === void 0 ? void 0 : source.movesUci) !== null && _b !== void 0 ? _b : [];
354
+ const startIndex = (_c = source === null || source === void 0 ? void 0 : source.startIndex) !== null && _c !== void 0 ? _c : 0;
355
+ const endIndex = (_d = source === null || source === void 0 ? void 0 : source.endIndex) !== null && _d !== void 0 ? _d : 0;
356
+ const missedIndices = (_e = source === null || source === void 0 ? void 0 : source.missedIndices) !== null && _e !== void 0 ? _e : [];
357
+ const setupUci = (_f = source === null || source === void 0 ? void 0 : source.setupUci) !== null && _f !== void 0 ? _f : null;
358
+ const resolveFen = useCallback((moveIndex, afterMove) => {
359
+ if (!startFen || movesUci.length === 0) {
360
+ return '';
361
+ }
362
+ return fenAtPlyFromStart(startFen, movesUci, afterMove ? moveIndex + 1 : moveIndex);
363
+ }, [movesUci, startFen]);
364
+ return useSolutionLineRecap({
365
+ active: active && source !== null,
366
+ movesUci,
367
+ startIndex,
368
+ endIndex,
369
+ missedIndices,
370
+ segmentStartFen: startFen,
371
+ setupUci,
372
+ onComplete,
373
+ resolveFen,
374
+ });
375
+ };
376
+
282
377
  const isAttemptFinished = (resultStatus) => resultStatus === 'complete' || resultStatus === 'incorrect';
283
378
  /** Library default hint / next / analysis / result controls (unstyled buttons). */
284
379
  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" }))] }));
@@ -422,6 +517,10 @@ class Position {
422
517
  getIndex() {
423
518
  return this.i;
424
519
  }
520
+ /** UCI of the move that produced the current position. */
521
+ getLastMoveUci() {
522
+ return lastMoveUciAtPly(this.moves, this.i);
523
+ }
425
524
  // Common methods shared by all positions
426
525
  next() {
427
526
  if (this.i >= this.moves.length) {
@@ -483,6 +582,29 @@ function playerColorForSolution(initialFen, moves) {
483
582
  }
484
583
  return chess.turn() === 'w' ? 'white' : 'black';
485
584
  }
585
+ /** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
586
+ function playerMoveIndicesInRange(initialFen, moves, fromIndex, toIndex) {
587
+ const playerColor = playerColorForSolution(initialFen, moves);
588
+ const chess = new Chess(initialFen);
589
+ const indices = [];
590
+ for (let i = 0; i < toIndex && i < moves.length; i++) {
591
+ const side = chess.turn() === 'w' ? 'white' : 'black';
592
+ if (i >= fromIndex && side === playerColor) {
593
+ indices.push(i);
594
+ }
595
+ applyUciMove(chess, moves[i]);
596
+ }
597
+ return indices;
598
+ }
599
+ /** Auto-play opponent setup plies until the solver is on move. */
600
+ function advanceToPlayerTurn(position) {
601
+ while (!position.isFinished() &&
602
+ position.getSideToMove() !== position.getPlayerColor()) {
603
+ if (!position.next()) {
604
+ break;
605
+ }
606
+ }
607
+ }
486
608
  class PuzzlePosition extends Position {
487
609
  constructor(initialFEN, moves, resumeConfig) {
488
610
  super();
@@ -727,19 +849,37 @@ class GamePosition extends Position {
727
849
  }
728
850
  }
729
851
 
730
- /** Apply the opponent setup ply immediately so the board does not flash on load. */
852
+ /** Apply opponent setup plies immediately so the board does not flash on load. */
731
853
  const puzzlePositionFromFetch = (fen, moves, resume) => {
732
854
  const newPosition = new PuzzlePosition(fen, moves, resume);
733
855
  if (!resume && moves.length > 1) {
734
856
  newPosition.next();
857
+ advanceToPlayerTurn(newPosition);
858
+ }
859
+ else if (resume &&
860
+ newPosition.getSideToMove() !== newPosition.getPlayerColor()) {
861
+ advanceToPlayerTurn(newPosition);
735
862
  }
736
863
  return newPosition;
737
864
  };
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
865
  const SOLUTION_STEP_MS = 500;
741
866
  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, }) => {
867
+ const uniqueIndices = (indices) => [...new Set(indices)];
868
+ const buildCompletionRecapSource = (position, missedIndices) => {
869
+ var _a, _b;
870
+ const movesUci = position.getSolutionMoves();
871
+ const initialFen = position.getInitialFen();
872
+ const startIndex = (_a = playerMoveIndicesInRange(initialFen, movesUci, 0, movesUci.length)[0]) !== null && _a !== void 0 ? _a : 0;
873
+ return {
874
+ startFen: initialFen,
875
+ movesUci,
876
+ startIndex,
877
+ endIndex: movesUci.length,
878
+ missedIndices,
879
+ setupUci: startIndex > 0 ? (_b = movesUci[startIndex - 1]) !== null && _b !== void 0 ? _b : null : null,
880
+ };
881
+ };
882
+ 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
883
  var _a, _b, _c, _d;
744
884
  const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
745
885
  const stackControlsBelow = useStackPuzzleControlsBelow();
@@ -755,6 +895,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
755
895
  const [puzzleComplete, setPuzzleComplete] = useState(false);
756
896
  const [completedAfterMiss, setCompletedAfterMiss] = useState(false);
757
897
  const [missFeedback, setMissFeedback] = useState(null);
898
+ const [missedMoveIndices, setMissedMoveIndices] = useState([]);
899
+ const [completionCheckVisible, setCompletionCheckVisible] = useState(false);
900
+ const [completionRecapActive, setCompletionRecapActive] = useState(false);
901
+ const [completionRecapDone, setCompletionRecapDone] = useState(false);
902
+ const completionFlowStartedRef = useRef(false);
758
903
  const [, setInteractionNum] = useState(0);
759
904
  const solutionAnimationRef = useRef({ cancelled: false, timeoutIds: [] });
760
905
  const resumeAnimationRef = useRef({ cancelled: false, timeoutIds: [] });
@@ -781,6 +926,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
781
926
  setPuzzleComplete(false);
782
927
  setCompletedAfterMiss(false);
783
928
  setMissFeedback(null);
929
+ setMissedMoveIndices([]);
930
+ setCompletionCheckVisible(false);
931
+ setCompletionRecapActive(false);
932
+ setCompletionRecapDone(false);
933
+ completionFlowStartedRef.current = false;
784
934
  onFetch()
785
935
  .then((data) => {
786
936
  if (cancelled) {
@@ -816,10 +966,16 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
816
966
  feedbackData.isCorrect === false;
817
967
  if (feedbackData.hintRequested) {
818
968
  setHintUsed(true);
969
+ setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
819
970
  }
820
971
  if (incorrectThisFeedback) {
821
972
  setHasIncorrectAttempt(true);
822
973
  }
974
+ if (feedbackData.isCorrect === false &&
975
+ !feedbackData.isFinished &&
976
+ !feedbackData.solutionShown) {
977
+ setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
978
+ }
823
979
  if (feedbackData.isFinished) {
824
980
  setPuzzleComplete(true);
825
981
  setCompletedAfterMiss((prev) => prev ||
@@ -983,6 +1139,10 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
983
1139
  position.recordSolutionShown();
984
1140
  position.setSolutionRevealed(true);
985
1141
  position.wantsHint(false);
1142
+ setMissedMoveIndices((prev) => uniqueIndices([
1143
+ ...prev,
1144
+ ...playerMoveIndicesInRange(position.getInitialFen(), position.getSolutionMoves(), position.getIndex(), position.getSolutionMoves().length),
1145
+ ]));
986
1146
  handleFeedback({
987
1147
  index: position.getIndex(),
988
1148
  solutionShown: true,
@@ -995,30 +1155,46 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
995
1155
  setPuzzleNum((prevPuzzleNum) => prevPuzzleNum + 1);
996
1156
  }, []);
997
1157
  const resultStatus = getResultStatus();
1158
+ const completionRecapSource = useMemo(() => position && showCompletionRecap
1159
+ ? buildCompletionRecapSource(position, missedMoveIndices)
1160
+ : null, [position, showCompletionRecap, missedMoveIndices]);
1161
+ const handleCompletionRecapDone = useCallback(() => {
1162
+ setCompletionRecapActive(false);
1163
+ setCompletionRecapDone(true);
1164
+ }, []);
1165
+ const completionRecap = usePuzzleCompletionRecap({
1166
+ source: completionRecapSource,
1167
+ active: completionRecapActive,
1168
+ onComplete: handleCompletionRecapDone,
1169
+ });
1170
+ const isCompletionRecapping = showCompletionRecap && (completionRecapActive || completionRecap.active);
998
1171
  useEffect(() => {
999
- if (!autoAdvanceOnComplete) {
1000
- return;
1001
- }
1002
- if (resultStatus !== 'complete') {
1172
+ if (!showCompletionRecap ||
1173
+ resultStatus !== 'complete' ||
1174
+ loadingNextPuzzle ||
1175
+ completionFlowStartedRef.current) {
1003
1176
  return;
1004
1177
  }
1005
- if (hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) {
1178
+ completionFlowStartedRef.current = true;
1179
+ setCompletionCheckVisible(true);
1180
+ }, [loadingNextPuzzle, resultStatus, showCompletionRecap]);
1181
+ useEffect(() => {
1182
+ if (!completionCheckVisible) {
1006
1183
  return;
1007
1184
  }
1008
- const timer = setTimeout(() => {
1009
- handleNextPuzzle();
1010
- }, AUTO_ADVANCE_ON_COMPLETE_DELAY_MS);
1185
+ const timer = window.setTimeout(() => {
1186
+ setCompletionCheckVisible(false);
1187
+ setCompletionRecapActive(true);
1188
+ }, PUZZLE_COMPLETION_RECAP_SETUP_MS);
1011
1189
  return () => {
1012
- clearTimeout(timer);
1190
+ window.clearTimeout(timer);
1013
1191
  };
1014
- }, [
1015
- autoAdvanceOnComplete,
1016
- autoAdvanceOnCompleteAfterIncorrect,
1017
- resultStatus,
1018
- hasIncorrectAttempt,
1019
- handleNextPuzzle,
1020
- puzzleNum,
1021
- ]);
1192
+ }, [completionCheckVisible]);
1193
+ const shouldAutoAdvance = autoAdvanceOnComplete &&
1194
+ resultStatus === 'complete' &&
1195
+ !(hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) &&
1196
+ (!showCompletionRecap || completionRecapDone);
1197
+ const autoAdvance = usePuzzleAutoAdvanceCountdown(shouldAutoAdvance, autoAdvanceOnCompleteDelayMs, handleNextPuzzle);
1022
1198
  const controlState = {
1023
1199
  canShowHint: position !== null &&
1024
1200
  !position.isFinished() &&
@@ -1033,7 +1209,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1033
1209
  const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
1034
1210
  renderAnalysisContainer &&
1035
1211
  (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({
1212
+ 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 ||
1213
+ completionCheckVisible ||
1214
+ isCompletionRecapping, onMissFeedbackChange: setMissFeedback, recapBoard: isCompletionRecapping
1215
+ ? {
1216
+ fen: completionRecap.fen,
1217
+ lastMoveUci: completionRecap.lastMoveUci,
1218
+ customArrows: completionRecap.customArrows,
1219
+ animationDuration: completionRecap.animationDuration,
1220
+ }
1221
+ : null }) }), completionCheckVisible && (jsx(BoardCompleteCheckOverlay, { variant: hasIncorrectAttempt || completedAfterMiss || hintUsed
1222
+ ? 'partial'
1223
+ : 'success' }))] }), renderBoardCaption && (jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
1037
1224
  sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
1038
1225
  playerColor: position
1039
1226
  ? position.getPlayerColor()
@@ -1049,7 +1236,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1049
1236
  }) }))] }), jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
1050
1237
  visible: analysis.canOpen,
1051
1238
  openAnalysis: analysis.openAnalysis,
1052
- }, controlState), renderBoardFeedback && resultStatus === 'complete' && (jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
1239
+ }, controlState, autoAdvance), renderBoardFeedback && resultStatus === 'complete' && (jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
1053
1240
  resultStatus,
1054
1241
  cleanSolve: !hasIncorrectAttempt,
1055
1242
  refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
@@ -1063,7 +1250,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1063
1250
  * side's pieces be dragged. Move validation and sequencing live in
1064
1251
  * {@link LineBoardWithControls}.
1065
1252
  */
1066
- const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, lastMoveUci = 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), lastMoveUci: lastMoveUci, autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }) }));
1253
+ 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
1254
 
1068
1255
  /** Library default line-drill status controls (unstyled). */
1069
1256
  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 +1299,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1112
1299
  const [feedback, setFeedback] = useState(null);
1113
1300
  const [displayFen, setDisplayFen] = useState(null);
1114
1301
  const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = useCorrectMoveFeedback();
1302
+ const { incorrectMoveSquare, showIncorrectMove, isShowingIncorrectMove, } = useIncorrectMoveFeedback();
1115
1303
  const total = line.movesUci.length;
1116
1304
  const orientation = boardOrientationForLine(line.trainSide);
1117
1305
  const applyMove = useCallback((index) => {
@@ -1136,7 +1324,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1136
1324
  }, [line.movesUci]);
1137
1325
  // Auto-play opponent moves and detect the end of the line.
1138
1326
  useEffect(() => {
1139
- if (finished || isShowingCorrectMove) {
1327
+ if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
1140
1328
  return;
1141
1329
  }
1142
1330
  if (currentIndex >= total) {
@@ -1156,6 +1344,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1156
1344
  applyMove,
1157
1345
  opponentMoveDelayMs,
1158
1346
  isShowingCorrectMove,
1347
+ isShowingIncorrectMove,
1159
1348
  ]);
1160
1349
  // Emit the completion event exactly once.
1161
1350
  useEffect(() => {
@@ -1167,7 +1356,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1167
1356
  }, [finished]);
1168
1357
  const handleDrop = (source, target, piece) => {
1169
1358
  var _a, _b, _c;
1170
- if (finished || isShowingCorrectMove) {
1359
+ if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
1171
1360
  return false;
1172
1361
  }
1173
1362
  const setupFen = displayFen !== null && displayFen !== void 0 ? displayFen : chessRef.current.fen();
@@ -1187,7 +1376,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1187
1376
  setFeedback(moveFeedback);
1188
1377
  (_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
1189
1378
  if (isCorrect) {
1190
- const nextFen = fenAfterUci(setupFen, dropResult.uci);
1379
+ const nextFen = fenAfterUci(setupFen, dropResult.attempt.uci);
1191
1380
  if (nextFen) {
1192
1381
  setDisplayFen(nextFen);
1193
1382
  }
@@ -1198,6 +1387,9 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1198
1387
  applyMove(index);
1199
1388
  });
1200
1389
  }
1390
+ else {
1391
+ showIncorrectMove(source);
1392
+ }
1201
1393
  return isCorrect;
1202
1394
  };
1203
1395
  const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
@@ -1211,13 +1403,14 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1211
1403
  const moveNumber = Math.min(currentIndex + 1, total);
1212
1404
  const isUserTurn = !finished &&
1213
1405
  !isShowingCorrectMove &&
1406
+ !isShowingIncorrectMove &&
1214
1407
  turnFromFen(boardFen) === line.trainSide &&
1215
1408
  currentIndex < total;
1216
1409
  const stackControlsBelow = useStackPuzzleControlsBelow();
1217
1410
  const controlsPlacement = stackControlsBelow
1218
1411
  ? 'below'
1219
1412
  : 'beside';
1220
- 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, lastMoveUci: lastMoveUci, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1413
+ 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({
1221
1414
  trainSide: line.trainSide,
1222
1415
  moveNumber,
1223
1416
  total,
@@ -1234,4 +1427,4 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1234
1427
  /** @deprecated Import {@link boardSquareHighlightColors} and {@link analysisBoardHighlightColors} from `react-chess-core`. */
1235
1428
  const squareHighlightColors = Object.assign(Object.assign({}, boardSquareHighlightColors), analysisBoardHighlightColors);
1236
1429
 
1237
- 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 };
1430
+ 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, }) => {
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, }) => {
66
65
  var _a, _b, _c, _d, _e, _f, _g;
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,33 @@ 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;
186
+ const lastMoveUci = isRecapping
187
+ ? recapBoard.lastMoveUci
188
+ : useRefutation && incorrectActive
189
+ ? missBoard.lastMoveUci
190
+ : ((_e = position === null || position === void 0 ? void 0 : position.getLastMoveUci()) !== null && _e !== void 0 ? _e : null);
165
191
  const missLocked = useRefutation &&
166
192
  incorrectActive &&
167
193
  (missBoard.boardAnimating ||
168
194
  missPhase === 'wrong' ||
169
195
  missPhase === 'refutation');
170
- const arePiecesDraggable = position !== null &&
196
+ const arePiecesDraggable = !isRecapping &&
197
+ position !== null &&
171
198
  !positionLocked &&
172
199
  !missLocked &&
173
- correctMoveSquare === null;
200
+ correctMoveSquare === null &&
201
+ overlayIncorrectSquare === null;
174
202
  const onPieceDrop = (sourceSquare, targetSquare, piece) => {
175
203
  if (!position || positionLocked || position.isSolutionRevealed()) {
176
204
  return false;
@@ -184,6 +212,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
184
212
  if (answerArrowVisible &&
185
213
  !allowRetryOnIncorrect &&
186
214
  !position.isExpectedGuess(sourceSquare, targetSquare)) {
215
+ showIncorrectMove(sourceSquare);
187
216
  position.resetInteractions();
188
217
  snapBoardBack();
189
218
  return false;
@@ -193,6 +222,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
193
222
  });
194
223
  if (!guess.accepted) {
195
224
  attemptMissedRef.current = true;
225
+ showIncorrectMove(useRefutation ? targetSquare : sourceSquare);
196
226
  onFeedback({
197
227
  index: position.getIndex(),
198
228
  guess: { sourceSquare, targetSquare, piece },
@@ -206,8 +236,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
206
236
  missBoard.missSequence.startSequence(setupFen, attemptedUci);
207
237
  }
208
238
  position.resetInteractions();
209
- snapBoardBack();
210
- return false;
239
+ return true;
211
240
  }
212
241
  const revealIncorrectFeedback = () => {
213
242
  if (showAnswerArrowOnIncorrect) {
@@ -273,13 +302,79 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
273
302
  showCorrectMove(targetSquare, finishCorrectFeedback);
274
303
  return true;
275
304
  };
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 }));
305
+ return hasBoard ? (jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: isRecapping ? '' : ((_f = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _f !== void 0 ? _f : ''), hintSquare: isRecapping ? null : ((_g = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _g !== void 0 ? _g : null), incorrectMoveSquare: isRecapping ? null : overlayIncorrectSquare, refutationMoveSquare: isRecapping ? null : refutationMoveSquare, correctMoveSquare: isRecapping ? null : correctMoveSquare, customArrows: customArrows, lastMoveUci: lastMoveUci, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: isRecapping ? recapBoard.animationDuration : 0 }, revision)) : null;
279
306
  };
280
307
 
281
308
  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
309
 
310
+ const inactiveAutoAdvance = {
311
+ active: false,
312
+ secondsRemaining: -1,
313
+ };
314
+ /** Countdown overlay state while waiting to auto-load the next puzzle card. */
315
+ function usePuzzleAutoAdvanceCountdown(enabled, delayMs, onAdvance) {
316
+ const [secondsRemaining, setSecondsRemaining] = react.useState(-1);
317
+ react.useEffect(() => {
318
+ if (!enabled || delayMs <= 0) {
319
+ setSecondsRemaining(-1);
320
+ return;
321
+ }
322
+ const startedAt = Date.now();
323
+ const updateCountdown = () => {
324
+ const elapsed = Date.now() - startedAt;
325
+ setSecondsRemaining(Math.max(0, Math.ceil((delayMs - elapsed) / 1000)));
326
+ };
327
+ updateCountdown();
328
+ const intervalId = window.setInterval(updateCountdown, 200);
329
+ const timeoutId = window.setTimeout(() => {
330
+ window.clearInterval(intervalId);
331
+ setSecondsRemaining(-1);
332
+ onAdvance();
333
+ }, delayMs);
334
+ return () => {
335
+ window.clearInterval(intervalId);
336
+ window.clearTimeout(timeoutId);
337
+ setSecondsRemaining(-1);
338
+ };
339
+ }, [delayMs, enabled, onAdvance]);
340
+ if (!enabled || secondsRemaining < 0) {
341
+ return inactiveAutoAdvance;
342
+ }
343
+ return {
344
+ active: true,
345
+ secondsRemaining,
346
+ };
347
+ }
348
+
349
+ /** Pause on the puzzle setup position before the solution recap animates. */
350
+ const PUZZLE_COMPLETION_RECAP_SETUP_MS = 400;
351
+ const usePuzzleCompletionRecap = ({ source, active, onComplete, }) => {
352
+ var _a, _b, _c, _d, _e, _f;
353
+ const startFen = (_a = source === null || source === void 0 ? void 0 : source.startFen) !== null && _a !== void 0 ? _a : '';
354
+ const movesUci = (_b = source === null || source === void 0 ? void 0 : source.movesUci) !== null && _b !== void 0 ? _b : [];
355
+ const startIndex = (_c = source === null || source === void 0 ? void 0 : source.startIndex) !== null && _c !== void 0 ? _c : 0;
356
+ const endIndex = (_d = source === null || source === void 0 ? void 0 : source.endIndex) !== null && _d !== void 0 ? _d : 0;
357
+ const missedIndices = (_e = source === null || source === void 0 ? void 0 : source.missedIndices) !== null && _e !== void 0 ? _e : [];
358
+ const setupUci = (_f = source === null || source === void 0 ? void 0 : source.setupUci) !== null && _f !== void 0 ? _f : null;
359
+ const resolveFen = react.useCallback((moveIndex, afterMove) => {
360
+ if (!startFen || movesUci.length === 0) {
361
+ return '';
362
+ }
363
+ return reactChessCore.fenAtPlyFromStart(startFen, movesUci, afterMove ? moveIndex + 1 : moveIndex);
364
+ }, [movesUci, startFen]);
365
+ return reactChessCore.useSolutionLineRecap({
366
+ active: active && source !== null,
367
+ movesUci,
368
+ startIndex,
369
+ endIndex,
370
+ missedIndices,
371
+ segmentStartFen: startFen,
372
+ setupUci,
373
+ onComplete,
374
+ resolveFen,
375
+ });
376
+ };
377
+
283
378
  const isAttemptFinished = (resultStatus) => resultStatus === 'complete' || resultStatus === 'incorrect';
284
379
  /** Library default hint / next / analysis / result controls (unstyled buttons). */
285
380
  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" }))] }));
@@ -423,6 +518,10 @@ class Position {
423
518
  getIndex() {
424
519
  return this.i;
425
520
  }
521
+ /** UCI of the move that produced the current position. */
522
+ getLastMoveUci() {
523
+ return reactChessCore.lastMoveUciAtPly(this.moves, this.i);
524
+ }
426
525
  // Common methods shared by all positions
427
526
  next() {
428
527
  if (this.i >= this.moves.length) {
@@ -484,6 +583,29 @@ function playerColorForSolution(initialFen, moves) {
484
583
  }
485
584
  return chess.turn() === 'w' ? 'white' : 'black';
486
585
  }
586
+ /** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
587
+ function playerMoveIndicesInRange(initialFen, moves, fromIndex, toIndex) {
588
+ const playerColor = playerColorForSolution(initialFen, moves);
589
+ const chess = new chess_js.Chess(initialFen);
590
+ const indices = [];
591
+ for (let i = 0; i < toIndex && i < moves.length; i++) {
592
+ const side = chess.turn() === 'w' ? 'white' : 'black';
593
+ if (i >= fromIndex && side === playerColor) {
594
+ indices.push(i);
595
+ }
596
+ applyUciMove(chess, moves[i]);
597
+ }
598
+ return indices;
599
+ }
600
+ /** Auto-play opponent setup plies until the solver is on move. */
601
+ function advanceToPlayerTurn(position) {
602
+ while (!position.isFinished() &&
603
+ position.getSideToMove() !== position.getPlayerColor()) {
604
+ if (!position.next()) {
605
+ break;
606
+ }
607
+ }
608
+ }
487
609
  class PuzzlePosition extends Position {
488
610
  constructor(initialFEN, moves, resumeConfig) {
489
611
  super();
@@ -728,19 +850,37 @@ class GamePosition extends Position {
728
850
  }
729
851
  }
730
852
 
731
- /** Apply the opponent setup ply immediately so the board does not flash on load. */
853
+ /** Apply opponent setup plies immediately so the board does not flash on load. */
732
854
  const puzzlePositionFromFetch = (fen, moves, resume) => {
733
855
  const newPosition = new PuzzlePosition(fen, moves, resume);
734
856
  if (!resume && moves.length > 1) {
735
857
  newPosition.next();
858
+ advanceToPlayerTurn(newPosition);
859
+ }
860
+ else if (resume &&
861
+ newPosition.getSideToMove() !== newPosition.getPlayerColor()) {
862
+ advanceToPlayerTurn(newPosition);
736
863
  }
737
864
  return newPosition;
738
865
  };
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
866
  const SOLUTION_STEP_MS = 500;
742
867
  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, }) => {
868
+ const uniqueIndices = (indices) => [...new Set(indices)];
869
+ const buildCompletionRecapSource = (position, missedIndices) => {
870
+ var _a, _b;
871
+ const movesUci = position.getSolutionMoves();
872
+ const initialFen = position.getInitialFen();
873
+ const startIndex = (_a = playerMoveIndicesInRange(initialFen, movesUci, 0, movesUci.length)[0]) !== null && _a !== void 0 ? _a : 0;
874
+ return {
875
+ startFen: initialFen,
876
+ movesUci,
877
+ startIndex,
878
+ endIndex: movesUci.length,
879
+ missedIndices,
880
+ setupUci: startIndex > 0 ? (_b = movesUci[startIndex - 1]) !== null && _b !== void 0 ? _b : null : null,
881
+ };
882
+ };
883
+ 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
884
  var _a, _b, _c, _d;
745
885
  const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
746
886
  const stackControlsBelow = useStackPuzzleControlsBelow();
@@ -756,6 +896,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
756
896
  const [puzzleComplete, setPuzzleComplete] = react.useState(false);
757
897
  const [completedAfterMiss, setCompletedAfterMiss] = react.useState(false);
758
898
  const [missFeedback, setMissFeedback] = react.useState(null);
899
+ const [missedMoveIndices, setMissedMoveIndices] = react.useState([]);
900
+ const [completionCheckVisible, setCompletionCheckVisible] = react.useState(false);
901
+ const [completionRecapActive, setCompletionRecapActive] = react.useState(false);
902
+ const [completionRecapDone, setCompletionRecapDone] = react.useState(false);
903
+ const completionFlowStartedRef = react.useRef(false);
759
904
  const [, setInteractionNum] = react.useState(0);
760
905
  const solutionAnimationRef = react.useRef({ cancelled: false, timeoutIds: [] });
761
906
  const resumeAnimationRef = react.useRef({ cancelled: false, timeoutIds: [] });
@@ -782,6 +927,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
782
927
  setPuzzleComplete(false);
783
928
  setCompletedAfterMiss(false);
784
929
  setMissFeedback(null);
930
+ setMissedMoveIndices([]);
931
+ setCompletionCheckVisible(false);
932
+ setCompletionRecapActive(false);
933
+ setCompletionRecapDone(false);
934
+ completionFlowStartedRef.current = false;
785
935
  onFetch()
786
936
  .then((data) => {
787
937
  if (cancelled) {
@@ -817,10 +967,16 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
817
967
  feedbackData.isCorrect === false;
818
968
  if (feedbackData.hintRequested) {
819
969
  setHintUsed(true);
970
+ setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
820
971
  }
821
972
  if (incorrectThisFeedback) {
822
973
  setHasIncorrectAttempt(true);
823
974
  }
975
+ if (feedbackData.isCorrect === false &&
976
+ !feedbackData.isFinished &&
977
+ !feedbackData.solutionShown) {
978
+ setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
979
+ }
824
980
  if (feedbackData.isFinished) {
825
981
  setPuzzleComplete(true);
826
982
  setCompletedAfterMiss((prev) => prev ||
@@ -984,6 +1140,10 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
984
1140
  position.recordSolutionShown();
985
1141
  position.setSolutionRevealed(true);
986
1142
  position.wantsHint(false);
1143
+ setMissedMoveIndices((prev) => uniqueIndices([
1144
+ ...prev,
1145
+ ...playerMoveIndicesInRange(position.getInitialFen(), position.getSolutionMoves(), position.getIndex(), position.getSolutionMoves().length),
1146
+ ]));
987
1147
  handleFeedback({
988
1148
  index: position.getIndex(),
989
1149
  solutionShown: true,
@@ -996,30 +1156,46 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
996
1156
  setPuzzleNum((prevPuzzleNum) => prevPuzzleNum + 1);
997
1157
  }, []);
998
1158
  const resultStatus = getResultStatus();
1159
+ const completionRecapSource = react.useMemo(() => position && showCompletionRecap
1160
+ ? buildCompletionRecapSource(position, missedMoveIndices)
1161
+ : null, [position, showCompletionRecap, missedMoveIndices]);
1162
+ const handleCompletionRecapDone = react.useCallback(() => {
1163
+ setCompletionRecapActive(false);
1164
+ setCompletionRecapDone(true);
1165
+ }, []);
1166
+ const completionRecap = usePuzzleCompletionRecap({
1167
+ source: completionRecapSource,
1168
+ active: completionRecapActive,
1169
+ onComplete: handleCompletionRecapDone,
1170
+ });
1171
+ const isCompletionRecapping = showCompletionRecap && (completionRecapActive || completionRecap.active);
999
1172
  react.useEffect(() => {
1000
- if (!autoAdvanceOnComplete) {
1001
- return;
1002
- }
1003
- if (resultStatus !== 'complete') {
1173
+ if (!showCompletionRecap ||
1174
+ resultStatus !== 'complete' ||
1175
+ loadingNextPuzzle ||
1176
+ completionFlowStartedRef.current) {
1004
1177
  return;
1005
1178
  }
1006
- if (hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) {
1179
+ completionFlowStartedRef.current = true;
1180
+ setCompletionCheckVisible(true);
1181
+ }, [loadingNextPuzzle, resultStatus, showCompletionRecap]);
1182
+ react.useEffect(() => {
1183
+ if (!completionCheckVisible) {
1007
1184
  return;
1008
1185
  }
1009
- const timer = setTimeout(() => {
1010
- handleNextPuzzle();
1011
- }, AUTO_ADVANCE_ON_COMPLETE_DELAY_MS);
1186
+ const timer = window.setTimeout(() => {
1187
+ setCompletionCheckVisible(false);
1188
+ setCompletionRecapActive(true);
1189
+ }, PUZZLE_COMPLETION_RECAP_SETUP_MS);
1012
1190
  return () => {
1013
- clearTimeout(timer);
1191
+ window.clearTimeout(timer);
1014
1192
  };
1015
- }, [
1016
- autoAdvanceOnComplete,
1017
- autoAdvanceOnCompleteAfterIncorrect,
1018
- resultStatus,
1019
- hasIncorrectAttempt,
1020
- handleNextPuzzle,
1021
- puzzleNum,
1022
- ]);
1193
+ }, [completionCheckVisible]);
1194
+ const shouldAutoAdvance = autoAdvanceOnComplete &&
1195
+ resultStatus === 'complete' &&
1196
+ !(hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) &&
1197
+ (!showCompletionRecap || completionRecapDone);
1198
+ const autoAdvance = usePuzzleAutoAdvanceCountdown(shouldAutoAdvance, autoAdvanceOnCompleteDelayMs, handleNextPuzzle);
1023
1199
  const controlState = {
1024
1200
  canShowHint: position !== null &&
1025
1201
  !position.isFinished() &&
@@ -1034,7 +1210,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1034
1210
  const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
1035
1211
  renderAnalysisContainer &&
1036
1212
  (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({
1213
+ 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 ||
1214
+ completionCheckVisible ||
1215
+ isCompletionRecapping, onMissFeedbackChange: setMissFeedback, recapBoard: isCompletionRecapping
1216
+ ? {
1217
+ fen: completionRecap.fen,
1218
+ lastMoveUci: completionRecap.lastMoveUci,
1219
+ customArrows: completionRecap.customArrows,
1220
+ animationDuration: completionRecap.animationDuration,
1221
+ }
1222
+ : null }) }), completionCheckVisible && (jsxRuntime.jsx(reactChessCore.BoardCompleteCheckOverlay, { variant: hasIncorrectAttempt || completedAfterMiss || hintUsed
1223
+ ? 'partial'
1224
+ : 'success' }))] }), renderBoardCaption && (jsxRuntime.jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
1038
1225
  sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
1039
1226
  playerColor: position
1040
1227
  ? position.getPlayerColor()
@@ -1050,7 +1237,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1050
1237
  }) }))] }), jsxRuntime.jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
1051
1238
  visible: analysis.canOpen,
1052
1239
  openAnalysis: analysis.openAnalysis,
1053
- }, controlState), renderBoardFeedback && resultStatus === 'complete' && (jsxRuntime.jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
1240
+ }, controlState, autoAdvance), renderBoardFeedback && resultStatus === 'complete' && (jsxRuntime.jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
1054
1241
  resultStatus,
1055
1242
  cleanSolve: !hasIncorrectAttempt,
1056
1243
  refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
@@ -1064,7 +1251,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
1064
1251
  * side's pieces be dragged. Move validation and sequencing live in
1065
1252
  * {@link LineBoardWithControls}.
1066
1253
  */
1067
- const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, lastMoveUci = 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), lastMoveUci: lastMoveUci, autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }) }));
1254
+ 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
1255
 
1069
1256
  /** Library default line-drill status controls (unstyled). */
1070
1257
  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 +1300,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1113
1300
  const [feedback, setFeedback] = react.useState(null);
1114
1301
  const [displayFen, setDisplayFen] = react.useState(null);
1115
1302
  const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = reactChessCore.useCorrectMoveFeedback();
1303
+ const { incorrectMoveSquare, showIncorrectMove, isShowingIncorrectMove, } = reactChessCore.useIncorrectMoveFeedback();
1116
1304
  const total = line.movesUci.length;
1117
1305
  const orientation = boardOrientationForLine(line.trainSide);
1118
1306
  const applyMove = react.useCallback((index) => {
@@ -1137,7 +1325,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1137
1325
  }, [line.movesUci]);
1138
1326
  // Auto-play opponent moves and detect the end of the line.
1139
1327
  react.useEffect(() => {
1140
- if (finished || isShowingCorrectMove) {
1328
+ if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
1141
1329
  return;
1142
1330
  }
1143
1331
  if (currentIndex >= total) {
@@ -1157,6 +1345,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1157
1345
  applyMove,
1158
1346
  opponentMoveDelayMs,
1159
1347
  isShowingCorrectMove,
1348
+ isShowingIncorrectMove,
1160
1349
  ]);
1161
1350
  // Emit the completion event exactly once.
1162
1351
  react.useEffect(() => {
@@ -1168,7 +1357,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1168
1357
  }, [finished]);
1169
1358
  const handleDrop = (source, target, piece) => {
1170
1359
  var _a, _b, _c;
1171
- if (finished || isShowingCorrectMove) {
1360
+ if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
1172
1361
  return false;
1173
1362
  }
1174
1363
  const setupFen = displayFen !== null && displayFen !== void 0 ? displayFen : chessRef.current.fen();
@@ -1188,7 +1377,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1188
1377
  setFeedback(moveFeedback);
1189
1378
  (_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
1190
1379
  if (isCorrect) {
1191
- const nextFen = reactChessCore.fenAfterUci(setupFen, dropResult.uci);
1380
+ const nextFen = reactChessCore.fenAfterUci(setupFen, dropResult.attempt.uci);
1192
1381
  if (nextFen) {
1193
1382
  setDisplayFen(nextFen);
1194
1383
  }
@@ -1199,6 +1388,9 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1199
1388
  applyMove(index);
1200
1389
  });
1201
1390
  }
1391
+ else {
1392
+ showIncorrectMove(source);
1393
+ }
1202
1394
  return isCorrect;
1203
1395
  };
1204
1396
  const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
@@ -1212,13 +1404,14 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
1212
1404
  const moveNumber = Math.min(currentIndex + 1, total);
1213
1405
  const isUserTurn = !finished &&
1214
1406
  !isShowingCorrectMove &&
1407
+ !isShowingIncorrectMove &&
1215
1408
  turnFromFen(boardFen) === line.trainSide &&
1216
1409
  currentIndex < total;
1217
1410
  const stackControlsBelow = useStackPuzzleControlsBelow();
1218
1411
  const controlsPlacement = stackControlsBelow
1219
1412
  ? 'below'
1220
1413
  : 'beside';
1221
- 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, lastMoveUci: lastMoveUci, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsxRuntime.jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
1414
+ 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({
1222
1415
  trainSide: line.trainSide,
1223
1416
  moveNumber,
1224
1417
  total,
@@ -1251,6 +1444,7 @@ exports.Position = Position;
1251
1444
  exports.PuzzleBoard = PuzzleBoard;
1252
1445
  exports.PuzzleBoardWithControls = PuzzleBoardWithControls;
1253
1446
  exports.PuzzlePosition = PuzzlePosition;
1447
+ exports.advanceToPlayerTurn = advanceToPlayerTurn;
1254
1448
  exports.applyUciMove = applyUciMove;
1255
1449
  exports.buildAnalysisContext = buildAnalysisContext;
1256
1450
  exports.defaultRenderControls = defaultRenderControls;
@@ -1259,5 +1453,6 @@ exports.emptyAnalysisContext = emptyAnalysisContext;
1259
1453
  exports.getCheckSquareFromChess = getCheckSquareFromChess;
1260
1454
  exports.isAnalysisAvailable = isAnalysisAvailable;
1261
1455
  exports.playerColorForSolution = playerColorForSolution;
1456
+ exports.playerMoveIndicesInRange = playerMoveIndicesInRange;
1262
1457
  exports.squareHighlightColors = squareHighlightColors;
1263
1458
  exports.usePuzzleAnalysis = usePuzzleAnalysis;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-chess-puzzle-kit",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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,7 +74,7 @@
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
79
  "react-dom": "^18.3.1",
80
80
  "storybook": "^8.2.9",