react-chess-puzzle-kit 1.0.0 → 1.0.2

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.
Files changed (65) hide show
  1. package/README.md +1 -1
  2. package/dist/features/analysis/analysisContext.d.ts +1 -1
  3. package/dist/features/board/LineBoardWithControls.d.ts +3 -1
  4. package/dist/features/board/PuzzleBoard.d.ts +6 -1
  5. package/dist/features/board/PuzzleBoardWithControls.d.ts +63 -6
  6. package/dist/features/board/PuzzlePlaySurface.d.ts +27 -3
  7. package/dist/features/board/puzzleBoardLayout.d.ts +17 -4
  8. package/dist/features/board/useStackPuzzleControlsBelow.d.ts +2 -0
  9. package/dist/features/position/Position.d.ts +18 -2
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.esm.js +432 -100
  12. package/dist/index.js +432 -98
  13. package/package.json +3 -3
  14. package/dist/features/analysis/AnalysisBoard.d.ts +0 -18
  15. package/dist/features/analysis/AnalysisBoardCore.d.ts +0 -28
  16. package/dist/features/analysis/AnalysisBoardLayout.d.ts +0 -12
  17. package/dist/features/analysis/AnalysisChessboardView.d.ts +0 -5
  18. package/dist/features/analysis/AnalysisPosition.d.ts +0 -53
  19. package/dist/features/analysis/AnalysisTrigger.d.ts +0 -6
  20. package/dist/features/analysis/DefaultAnalysisContainer.d.ts +0 -3
  21. package/dist/features/analysis/DefaultAnalysisSidebar.d.ts +0 -2
  22. package/dist/features/analysis/analysisBoardHighlightColors.d.ts +0 -12
  23. package/dist/features/analysis/analysisLayout.d.ts +0 -5
  24. package/dist/features/analysis/core/AnalysisBoardCore.d.ts +0 -28
  25. package/dist/features/analysis/core/AnalysisChessboardView.d.ts +0 -5
  26. package/dist/features/analysis/core/AnalysisErrorBoundary.d.ts +0 -14
  27. package/dist/features/analysis/core/AnalysisPosition.d.ts +0 -54
  28. package/dist/features/analysis/core/analysisContext.d.ts +0 -13
  29. package/dist/features/analysis/core/analysisLayout.d.ts +0 -5
  30. package/dist/features/analysis/core/analysisLayoutConfig.d.ts +0 -6
  31. package/dist/features/analysis/core/index.d.ts +0 -9
  32. package/dist/features/analysis/core/renderProps.d.ts +0 -39
  33. package/dist/features/analysis/core/useAnalysisBoardModel.d.ts +0 -36
  34. package/dist/features/analysis/core/usePuzzleAnalysis.d.ts +0 -10
  35. package/dist/features/analysis/defaults/AnalysisBoard.d.ts +0 -16
  36. package/dist/features/analysis/defaults/AnalysisBoardLayout.d.ts +0 -14
  37. package/dist/features/analysis/defaults/DefaultAnalysisContainer.d.ts +0 -4
  38. package/dist/features/analysis/defaults/DefaultAnalysisSidebar.d.ts +0 -3
  39. package/dist/features/analysis/defaults/analysisLayout.d.ts +0 -3
  40. package/dist/features/analysis/defaults/analysisModalStyles.d.ts +0 -7
  41. package/dist/features/analysis/defaults/analysisSidebarColors.d.ts +0 -37
  42. package/dist/features/analysis/defaults/analysisSidebarRowStyle.d.ts +0 -8
  43. package/dist/features/analysis/defaults/index.d.ts +0 -8
  44. package/dist/features/analysis/renderProps.d.ts +0 -39
  45. package/dist/features/analysis/useAnalysisBoardModel.d.ts +0 -33
  46. package/dist/features/board/HighlightChessboard.d.ts +0 -2
  47. package/dist/features/board/boardSquareHighlightColors.d.ts +0 -1
  48. package/dist/features/board/chessboardTheme.d.ts +0 -2
  49. package/dist/features/engine/EngineEvaluationPanel.d.ts +0 -8
  50. package/dist/features/engine/StockfishBrowserEngine.d.ts +0 -1
  51. package/dist/features/engine/formatEvaluation.d.ts +0 -1
  52. package/dist/features/engine/index.d.ts +0 -1
  53. package/dist/features/engine/isAnalyzableFen.d.ts +0 -1
  54. package/dist/features/engine/parseUciInfo.d.ts +0 -1
  55. package/dist/features/engine/stockfishUrls.d.ts +0 -1
  56. package/dist/features/engine/types.d.ts +0 -1
  57. package/dist/features/engine/useAnalysisEngine.d.ts +0 -1
  58. package/dist/features/theme/ThemeContext.d.ts +0 -26
  59. package/dist/features/theme/ThemeProvider.d.ts +0 -7
  60. package/dist/features/theme/squareHighlightColors.d.ts +0 -50
  61. package/dist/stories/AnalysisBoard.stories.d.ts +0 -9
  62. package/dist/stories/HighlightChessboard.stories.d.ts +0 -9
  63. package/dist/stories/analysisFixtures.d.ts +0 -4
  64. package/dist/stories/regressions/StockfishAnalysisRegressions.stories.d.ts +0 -7
  65. package/dist/stories/regressions/regressionAnalysisContext.d.ts +0 -5
package/dist/index.esm.js CHANGED
@@ -1,7 +1,6 @@
1
- import { useState, useEffect, useRef, useCallback } from 'react';
1
+ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
- import { ChessboardDnDProvider } from 'react-chessboard';
4
- import { HighlightChessboard, ThemeProvider, AnalysisErrorBoundary, AnalysisBoardCore, AnalysisBoardLayout, AnalysisBoard, DEFAULT_ANALYSIS_LAYOUT, boardSquareHighlightColors, analysisBoardHighlightColors } from 'react-chess-core';
3
+ import { useBoardRevision, useMissBoard, ChessboardDnDProvider, HighlightChessboard, uciFromDrop, ThemeProvider, AnalysisErrorBoundary, AnalysisBoardCore, AnalysisBoardLayout, AnalysisBoard, DEFAULT_ANALYSIS_LAYOUT, evaluateExpectedMoveDrop, boardSquareHighlightColors, analysisBoardHighlightColors } from 'react-chess-core';
5
4
  export { DEFAULT_ANALYSIS_LAYOUT } from 'react-chess-core';
6
5
  import { Chess } from 'chess.js';
7
6
 
@@ -22,18 +21,17 @@ const buildAnalysisContext = (position) => {
22
21
  boardOrientation: position.getPlayerColor(),
23
22
  };
24
23
  };
25
- const isAnalysisAvailable = (position, resultStatus) => {
24
+ const isAnalysisAvailable = (position, _resultStatus) => {
26
25
  if (!position) {
27
26
  return false;
28
27
  }
29
- return (buildAnalysisContext(position).solutionMoves.length > 0 &&
30
- resultStatus !== 'none');
28
+ return buildAnalysisContext(position).solutionMoves.length > 0;
31
29
  };
32
30
 
33
31
  const usePuzzleAnalysis = (position, resultStatus, puzzleNum) => {
34
32
  const [isOpen, setIsOpen] = useState(false);
35
33
  const [snapshot, setSnapshot] = useState(null);
36
- const canOpen = isAnalysisAvailable(position, resultStatus);
34
+ const canOpen = isAnalysisAvailable(position);
37
35
  useEffect(() => {
38
36
  setIsOpen(false);
39
37
  setSnapshot(null);
@@ -58,68 +56,221 @@ const usePuzzleAnalysis = (position, resultStatus, puzzleNum) => {
58
56
  };
59
57
 
60
58
  const EMPTY_BOARD_FEN = '8/8/8/8/8/8/8/8 w - - 0 1';
59
+ const DEFAULT_ANSWER_ARROW_COLOR = '#42a5f5';
61
60
  /**
62
- * Single mounted board for puzzle play. Shows an empty board while the next
63
- * position loads so the layout and controls do not flicker between cards.
61
+ * Single mounted board for puzzle play. Keeps the prior board (and orientation)
62
+ * visible while the next position loads so layout and perspective do not flicker.
64
63
  */
65
- const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth, revealAnswerOnIncorrect = false, }) => {
66
- var _a, _b, _c, _d, _e;
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;
66
+ const [showAnswerArrow, setShowAnswerArrow] = useState(false);
67
+ const [incorrectActive, setIncorrectActive] = useState(false);
68
+ const attemptMissedRef = useRef(false);
69
+ const { revision, bumpRevision } = useBoardRevision();
70
+ const boardOrientationRef = useRef('white');
71
+ const boardFenRef = useRef(EMPTY_BOARD_FEN);
72
+ const notifyHost = () => {
73
+ incInteractionNum();
74
+ };
75
+ const expectedUci = (_a = position === null || position === void 0 ? void 0 : position.getExpectedMoveUci()) !== null && _a !== void 0 ? _a : null;
76
+ const positionFen = (_b = position === null || position === void 0 ? void 0 : position.fen()) !== null && _b !== void 0 ? _b : boardFenRef.current;
77
+ const useRefutation = showRefutationOnIncorrect && showAnswerArrowOnIncorrect;
78
+ /**
79
+ * Force a chessboard remount after a rejected drop so pieces snap back.
80
+ * Skip when refutation feedback drives `displayFen` — remounting blinks the
81
+ * whole board without helping snap-back.
82
+ */
83
+ const snapBoardBack = () => {
84
+ if (useRefutation) {
85
+ return;
86
+ }
87
+ bumpRevision();
88
+ incInteractionNum();
89
+ };
90
+ const missBoard = useMissBoard({
91
+ feedback: useRefutation && incorrectActive ? 'incorrect' : null,
92
+ expectedUci: expectedUci || null,
93
+ positionFen,
94
+ answerArrowColor,
95
+ autoShowWrongMoves,
96
+ engineOptions: refutationEngine,
97
+ });
98
+ const missPhase = (_c = missBoard.missSequence.sequence) === null || _c === void 0 ? void 0 : _c.phase;
99
+ const answerArrowVisible = useRefutation
100
+ ? incorrectActive && missPhase === 'answer'
101
+ : showAnswerArrow;
102
+ useEffect(() => {
103
+ setShowAnswerArrow(false);
104
+ setIncorrectActive(false);
105
+ attemptMissedRef.current = false;
106
+ onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
107
+ }, [onMissFeedbackChange, position]);
108
+ useEffect(() => {
109
+ var _a, _b;
110
+ if (!onMissFeedbackChange) {
111
+ return;
112
+ }
113
+ if (useRefutation && incorrectActive) {
114
+ onMissFeedbackChange({
115
+ refutationSan: missBoard.refutation.refutationSan,
116
+ phase: (_b = (_a = missBoard.missSequence.sequence) === null || _a === void 0 ? void 0 : _a.phase) !== null && _b !== void 0 ? _b : null,
117
+ answerArrowVisible,
118
+ });
119
+ return;
120
+ }
121
+ if (showAnswerArrow) {
122
+ onMissFeedbackChange({
123
+ refutationSan: null,
124
+ phase: null,
125
+ answerArrowVisible: true,
126
+ });
127
+ return;
128
+ }
129
+ onMissFeedbackChange(null);
130
+ }, [
131
+ answerArrowVisible,
132
+ incorrectActive,
133
+ (_d = missBoard.missSequence.sequence) === null || _d === void 0 ? void 0 : _d.phase,
134
+ missBoard.refutation.refutationSan,
135
+ onMissFeedbackChange,
136
+ showAnswerArrow,
137
+ useRefutation,
138
+ ]);
139
+ if (position) {
140
+ boardOrientationRef.current = position.getPlayerColor();
141
+ boardFenRef.current = position.fen();
142
+ }
143
+ const boardOrientation = boardOrientationRef.current;
144
+ const boardFen = boardFenRef.current;
145
+ const hasBoard = boardFen !== EMPTY_BOARD_FEN;
146
+ const simpleArrows = useMemo(() => {
147
+ if (!showAnswerArrow || !position || useRefutation) {
148
+ return [];
149
+ }
150
+ const moveUci = position.getExpectedMoveUci();
151
+ if (moveUci.length < 4) {
152
+ return [];
153
+ }
154
+ return [[moveUci.slice(0, 2), moveUci.slice(2, 4), answerArrowColor]];
155
+ }, [showAnswerArrow, position, answerArrowColor, useRefutation]);
156
+ const customArrows = useRefutation && incorrectActive
157
+ ? missBoard.customArrows
158
+ : simpleArrows;
159
+ const displayFen = useRefutation && incorrectActive ? missBoard.boardPosition : boardFen;
160
+ const missLocked = useRefutation &&
161
+ incorrectActive &&
162
+ (missBoard.boardAnimating ||
163
+ missPhase === 'wrong' ||
164
+ missPhase === 'refutation');
165
+ const arePiecesDraggable = position !== null && !positionLocked && !missLocked;
67
166
  const onPieceDrop = (sourceSquare, targetSquare, piece) => {
68
- if (!position || position.isSolutionRevealed()) {
167
+ if (!position || positionLocked || position.isSolutionRevealed()) {
168
+ return false;
169
+ }
170
+ if (position.hasResumeConfig() && !position.isQuizIndex()) {
69
171
  return false;
70
172
  }
71
173
  if (!position.isLegalMove(sourceSquare, targetSquare)) {
72
174
  return false;
73
175
  }
74
- const guess = position.tryGuess(sourceSquare, targetSquare, piece);
176
+ if (answerArrowVisible &&
177
+ !allowRetryOnIncorrect &&
178
+ !position.isExpectedGuess(sourceSquare, targetSquare)) {
179
+ position.resetInteractions();
180
+ snapBoardBack();
181
+ return false;
182
+ }
183
+ const guess = position.tryGuess(sourceSquare, targetSquare, piece, {
184
+ recordIfIncorrect: !(answerArrowVisible && !allowRetryOnIncorrect),
185
+ });
75
186
  if (!guess.accepted) {
187
+ attemptMissedRef.current = true;
76
188
  onFeedback({
77
189
  index: position.getIndex(),
78
190
  guess: { sourceSquare, targetSquare, piece },
79
191
  isCorrect: false,
80
192
  });
81
- incInteractionNum();
82
- setTimeout(() => {
83
- if (revealAnswerOnIncorrect) {
193
+ if (useRefutation) {
194
+ const setupFen = position.fen();
195
+ const attemptedUci = uciFromDrop(setupFen, sourceSquare, targetSquare, piece);
196
+ setIncorrectActive(true);
197
+ if (attemptedUci) {
198
+ missBoard.missSequence.startSequence(setupFen, attemptedUci);
199
+ }
200
+ position.resetInteractions();
201
+ snapBoardBack();
202
+ return false;
203
+ }
204
+ const revealIncorrectFeedback = () => {
205
+ if (showAnswerArrowOnIncorrect) {
206
+ position.resetInteractions();
207
+ setShowAnswerArrow(true);
208
+ }
209
+ else if (revealAnswerOnIncorrect) {
84
210
  position.resetInteractions();
85
211
  position.revealCorrectMove();
86
212
  }
87
213
  else {
88
214
  position.resetInteractions();
89
215
  }
90
- incInteractionNum();
91
- }, 500);
216
+ snapBoardBack();
217
+ };
218
+ if (showAnswerArrowOnIncorrect && !allowRetryOnIncorrect) {
219
+ revealIncorrectFeedback();
220
+ }
221
+ else {
222
+ setTimeout(revealIncorrectFeedback, 500);
223
+ }
92
224
  return false;
93
225
  }
94
- onFeedback({
226
+ setShowAnswerArrow(false);
227
+ setIncorrectActive(false);
228
+ missBoard.missSequence.clearSequence();
229
+ onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
230
+ const assistedByAnswerArrow = answerArrowVisible && attemptMissedRef.current;
231
+ const guessPayload = {
95
232
  index: position.getIndex(),
96
233
  guess: { sourceSquare, targetSquare, piece },
97
- isCorrect: true,
98
- isFinished: guess.finished,
99
- });
100
- incInteractionNum();
234
+ };
235
+ if (assistedByAnswerArrow) {
236
+ // Miss feedback for this ply is already saved; dragging along the answer
237
+ // arrow only continues the line — it must not count as a clean solve.
238
+ if (guess.finished) {
239
+ onFeedback(Object.assign(Object.assign({}, guessPayload), { isCorrect: false, isFinished: true }));
240
+ }
241
+ }
242
+ else {
243
+ onFeedback(Object.assign(Object.assign({}, guessPayload), { isCorrect: true, isFinished: guess.finished }));
244
+ }
245
+ notifyHost();
101
246
  setTimeout(() => {
102
247
  position.resetInteractions();
103
- incInteractionNum();
248
+ notifyHost();
104
249
  }, 500);
105
250
  if (position.isAlternativeCheckmate()) {
106
- incInteractionNum();
251
+ notifyHost();
107
252
  return true;
108
253
  }
109
254
  position.next();
110
- incInteractionNum();
255
+ notifyHost();
256
+ if (position.hasResumeConfig()) {
257
+ onResumeCorrect === null || onResumeCorrect === void 0 ? void 0 : onResumeCorrect(position);
258
+ return true;
259
+ }
111
260
  setTimeout(() => {
112
261
  if (!position.isFinished()) {
113
262
  position.next();
114
263
  }
115
- incInteractionNum();
264
+ notifyHost();
116
265
  }, 500);
117
266
  return true;
118
267
  };
119
- return (jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: (_a = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _a !== void 0 ? _a : '', hintSquare: (_b = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _b !== void 0 ? _b : null, incorrectMoveSquare: (_c = position === null || position === void 0 ? void 0 : position.getIncorrectMoveSquare()) !== null && _c !== void 0 ? _c : null, onPieceDrop: onPieceDrop, position: (_d = position === null || position === void 0 ? void 0 : position.fen()) !== null && _d !== void 0 ? _d : EMPTY_BOARD_FEN, boardOrientation: (_e = position === null || position === void 0 ? void 0 : position.getPlayerColor()) !== null && _e !== void 0 ? _e : 'white', arePiecesDraggable: position !== null, promotionDialogVariant: "modal" }) }));
268
+ 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
269
+ ? null
270
+ : ((_g = position === null || position === void 0 ? void 0 : position.getIncorrectMoveSquare()) !== null && _g !== void 0 ? _g : null), customArrows: customArrows, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: 0 }, revision)) : null }));
120
271
  };
121
272
 
122
- const PuzzleBoard = ({ position, onFeedback, incInteractionNum, boardWidth, revealAnswerOnIncorrect = false, }) => (jsx(PuzzlePlaySurface, { position: position, onFeedback: onFeedback, incInteractionNum: incInteractionNum, boardWidth: boardWidth, revealAnswerOnIncorrect: revealAnswerOnIncorrect }));
273
+ 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 }));
123
274
 
124
275
  const isAttemptFinished = (resultStatus) => resultStatus === 'complete' || resultStatus === 'incorrect';
125
276
  /** Library default hint / next / analysis / result controls (unstyled buttons). */
@@ -127,12 +278,10 @@ const DefaultPuzzleControls = ({ showHint, showSolution, nextPuzzle, resultStatu
127
278
  const defaultRenderControls = (showHint, showSolution, nextPuzzle, resultStatus, analysis, controlState) => (jsx(DefaultPuzzleControls, { showHint: showHint, showSolution: showSolution, nextPuzzle: nextPuzzle, resultStatus: resultStatus, analysis: analysis, controlState: controlState }));
128
279
  const rowStyle$1 = {
129
280
  display: 'flex',
130
- flexWrap: 'wrap',
131
- alignItems: 'center',
281
+ flexDirection: 'column',
282
+ alignItems: 'stretch',
132
283
  gap: 8,
133
284
  width: '100%',
134
- minHeight: 96,
135
- alignContent: 'flex-start',
136
285
  };
137
286
  const buttonStyle = {
138
287
  cursor: 'pointer',
@@ -150,27 +299,92 @@ const statusStyle$1 = {
150
299
 
151
300
  /** Default pixel width for the live puzzle board (analysis uses {@link DEFAULT_ANALYSIS_LAYOUT}). */
152
301
  const DEFAULT_PUZZLE_BOARD_WIDTH = 480;
153
- /** Minimum height reserved below the board so controls do not shift when status changes. */
154
- const PUZZLE_CONTROLS_MIN_HEIGHT = 96;
155
- const puzzlePlayColumnStyle = (boardWidth) => ({
302
+ /** Width reserved for the vertical controls column beside the board. */
303
+ const PUZZLE_CONTROLS_COLUMN_WIDTH = 160;
304
+ /** Gap between the board column and controls column. */
305
+ const PUZZLE_BOARD_CONTROLS_GAP = 16;
306
+ /** Viewports at or below this width stack controls under the board (through tablet landscape). */
307
+ const PUZZLE_CONTROLS_STACK_BREAKPOINT_PX = 1365;
308
+ /** Horizontal space taken by the controls column when it sits beside the board. */
309
+ const PUZZLE_CONTROLS_BESIDE_RESERVE_PX = PUZZLE_CONTROLS_COLUMN_WIDTH + PUZZLE_BOARD_CONTROLS_GAP;
310
+ /** Minimum height reserved below the board so the caption slot does not shift between loads. */
311
+ const PUZZLE_BOARD_CAPTION_MIN_HEIGHT = 24;
312
+ const puzzlePlayRowStyle = (placement = 'beside') => ({
313
+ display: 'flex',
314
+ flexDirection: placement === 'below' ? 'column' : 'row',
315
+ alignItems: placement === 'below' ? 'stretch' : 'flex-start',
316
+ gap: PUZZLE_BOARD_CONTROLS_GAP,
317
+ width: placement === 'below' ? '100%' : 'fit-content',
318
+ maxWidth: '100%',
319
+ });
320
+ const puzzleBoardColumnStyle = (boardWidth, placement = 'beside') => ({
156
321
  display: 'flex',
157
322
  flexDirection: 'column',
158
323
  width: boardWidth,
159
324
  maxWidth: '100%',
325
+ flexShrink: 0,
326
+ alignSelf: placement === 'below' ? 'center' : undefined,
327
+ });
328
+ const puzzleBoardCaptionSlotStyle = () => ({
329
+ width: '100%',
330
+ minHeight: PUZZLE_BOARD_CAPTION_MIN_HEIGHT,
331
+ flexShrink: 0,
332
+ display: 'flex',
333
+ justifyContent: 'center',
334
+ alignItems: 'center',
335
+ marginTop: 4,
336
+ });
337
+ const puzzleBoardSlotWrapperStyle = () => ({
338
+ position: 'relative',
339
+ width: '100%',
340
+ flexShrink: 0,
160
341
  });
161
342
  const puzzleBoardSlotStyle = () => ({
162
343
  width: '100%',
163
344
  aspectRatio: '1 / 1',
164
345
  flexShrink: 0,
165
346
  });
166
- const puzzleControlsSlotStyle = () => ({
167
- minHeight: PUZZLE_CONTROLS_MIN_HEIGHT,
168
- flexShrink: 0,
347
+ const puzzleControlsFeedbackStyle = (placement = 'beside') => ({
169
348
  display: 'flex',
170
- alignItems: 'flex-start',
171
- marginTop: 8,
349
+ flexDirection: 'column',
350
+ alignItems: placement === 'below' ? 'center' : 'flex-end',
351
+ gap: 4,
352
+ marginTop: placement === 'below' ? 0 : 'auto',
353
+ pointerEvents: 'none',
354
+ });
355
+ const puzzleControlsSlotStyle = (placement = 'beside') => ({
356
+ display: 'flex',
357
+ flexDirection: 'column',
358
+ alignItems: 'stretch',
359
+ gap: 8,
360
+ flexShrink: 0,
361
+ minWidth: placement === 'below' ? undefined : PUZZLE_CONTROLS_COLUMN_WIDTH,
362
+ width: placement === 'below' ? '100%' : undefined,
363
+ alignSelf: 'stretch',
172
364
  });
173
365
 
366
+ const stackControlsQuery = `(max-width: ${PUZZLE_CONTROLS_STACK_BREAKPOINT_PX}px)`;
367
+ const readStackControlsBelow = () => {
368
+ if (typeof window === 'undefined') {
369
+ return false;
370
+ }
371
+ return window.matchMedia(stackControlsQuery).matches;
372
+ };
373
+ /** True when hint / next controls should sit below the board instead of beside it. */
374
+ const useStackPuzzleControlsBelow = () => {
375
+ const [stackBelow, setStackBelow] = useState(readStackControlsBelow);
376
+ useEffect(() => {
377
+ const mediaQueryList = window.matchMedia(stackControlsQuery);
378
+ const onChange = (event) => {
379
+ setStackBelow(event.matches);
380
+ };
381
+ mediaQueryList.addEventListener('change', onChange);
382
+ setStackBelow(mediaQueryList.matches);
383
+ return () => mediaQueryList.removeEventListener('change', onChange);
384
+ }, []);
385
+ return stackBelow;
386
+ };
387
+
174
388
  /** Apply a UCI move (e.g. `e7e8q`) without throwing. */
175
389
  function applyUciMove(chess, uci) {
176
390
  if (!uci || uci.length < 4) {
@@ -231,6 +445,9 @@ class Position {
231
445
  fen() {
232
446
  return this.chess.fen();
233
447
  }
448
+ getSideToMove() {
449
+ return this.chess.turn() === 'w' ? 'white' : 'black';
450
+ }
234
451
  getCheckSquare() {
235
452
  return getCheckSquareFromChess(this.chess);
236
453
  }
@@ -260,7 +477,7 @@ function playerColorForSolution(initialFen, moves) {
260
477
  return chess.turn() === 'w' ? 'white' : 'black';
261
478
  }
262
479
  class PuzzlePosition extends Position {
263
- constructor(initialFEN, moves) {
480
+ constructor(initialFEN, moves, resumeConfig) {
264
481
  super();
265
482
  this.isCorrect = false;
266
483
  this.guessedMove = '';
@@ -268,7 +485,7 @@ class PuzzlePosition extends Position {
268
485
  this.solutionRevealed = false;
269
486
  this.moveHistory = [];
270
487
  this.usedAlternativeCheckmate = false;
271
- this.tryGuess = (sourceSquare, targetSquare, piece) => {
488
+ this.tryGuess = (sourceSquare, targetSquare, piece, options) => {
272
489
  var _a;
273
490
  const candidates = this.buildCandidateUcis(sourceSquare, targetSquare);
274
491
  if (candidates.length === 0) {
@@ -294,12 +511,14 @@ class PuzzlePosition extends Position {
294
511
  }
295
512
  this.isCorrect = false;
296
513
  this.guessedMove = `${sourceSquare}${targetSquare}`;
297
- this.moveHistory.push({
298
- ply: this.moveHistory.length,
299
- uci: (_a = candidates[candidates.length - 1]) !== null && _a !== void 0 ? _a : this.guessedMove,
300
- actor: 'attempt',
301
- isCorrect: false,
302
- });
514
+ if ((options === null || options === void 0 ? void 0 : options.recordIfIncorrect) !== false) {
515
+ this.moveHistory.push({
516
+ ply: this.moveHistory.length,
517
+ uci: (_a = candidates[candidates.length - 1]) !== null && _a !== void 0 ? _a : this.guessedMove,
518
+ actor: 'attempt',
519
+ isCorrect: false,
520
+ });
521
+ }
303
522
  return { accepted: false, finished: false };
304
523
  };
305
524
  this.judgeGuess = (sourceSquare, targetSquare, piece) => this.tryGuess(sourceSquare, targetSquare, piece).accepted;
@@ -307,6 +526,12 @@ class PuzzlePosition extends Position {
307
526
  this.chess.load(initialFEN);
308
527
  this.moves = moves;
309
528
  this.playerColor = playerColorForSolution(initialFEN, moves);
529
+ this.resumeConfig = resumeConfig;
530
+ if (resumeConfig) {
531
+ for (let j = 0; j < resumeConfig.startIndex; j++) {
532
+ super.next();
533
+ }
534
+ }
310
535
  }
311
536
  next() {
312
537
  if (this.i >= this.moves.length) {
@@ -419,6 +644,12 @@ class PuzzlePosition extends Position {
419
644
  this.usedAlternativeCheckmate = true;
420
645
  return true;
421
646
  }
647
+ /** True when dragging from/to matches the expected move at the current index. */
648
+ isExpectedGuess(sourceSquare, targetSquare) {
649
+ const candidates = this.buildCandidateUcis(sourceSquare, targetSquare);
650
+ const expectedUci = this.moves[this.i];
651
+ return candidates.some((uci) => uci === expectedUci);
652
+ }
422
653
  resetInteractions() {
423
654
  this.guessedMove = '';
424
655
  this.isHintWanted = false;
@@ -456,9 +687,27 @@ class PuzzlePosition extends Position {
456
687
  }
457
688
  return this.guessedMove.slice(2, 4);
458
689
  }
690
+ /** UCI of the move the player should play at the current index. */
691
+ getExpectedMoveUci() {
692
+ var _a;
693
+ return (_a = this.moves[this.i]) !== null && _a !== void 0 ? _a : '';
694
+ }
459
695
  getPlayerColor() {
460
696
  return this.playerColor;
461
697
  }
698
+ hasResumeConfig() {
699
+ return this.resumeConfig !== undefined;
700
+ }
701
+ getResumeConfig() {
702
+ return this.resumeConfig;
703
+ }
704
+ /** True when the user must find the move at the current index. */
705
+ isQuizIndex() {
706
+ if (!this.resumeConfig) {
707
+ return !this.isFinished();
708
+ }
709
+ return this.resumeConfig.quizAtIndices.includes(this.i);
710
+ }
462
711
  }
463
712
  class GamePosition extends Position {
464
713
  constructor(PGN) {
@@ -471,19 +720,36 @@ class GamePosition extends Position {
471
720
  }
472
721
  }
473
722
 
474
- /** Delay before auto-playing the opponent's opening move (ms). */
475
- const OPPONENT_OPENING_MOVE_DELAY_MS = 500;
723
+ /** Apply the opponent setup ply immediately so the board does not flash on load. */
724
+ const puzzlePositionFromFetch = (fen, moves, resume) => {
725
+ const newPosition = new PuzzlePosition(fen, moves, resume);
726
+ if (!resume && moves.length > 1) {
727
+ newPosition.next();
728
+ }
729
+ return newPosition;
730
+ };
476
731
  /** Brief pause so the user sees a correct result before the next card loads. */
477
732
  const AUTO_ADVANCE_ON_COMPLETE_DELAY_MS = 700;
478
733
  const SOLUTION_STEP_MS = 500;
479
- const PuzzleBoardWithControls = ({ theme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = DEFAULT_ANALYSIS_LAYOUT, renderAnalysisMain, engine, autoAdvanceOnComplete = false, revealAnswerOnIncorrect = false, }) => {
734
+ const RESUME_AUTO_STEP_MS = 500;
735
+ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = DEFAULT_ANALYSIS_LAYOUT, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
736
+ var _a, _b, _c, _d;
737
+ const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
738
+ const stackControlsBelow = useStackPuzzleControlsBelow();
739
+ const controlsPlacement = stackControlsBelow
740
+ ? 'below'
741
+ : 'beside';
480
742
  const { onFetch, onFetchError, onFeedback } = apiProxy;
481
743
  const [position, setPosition] = useState(null);
744
+ const [loadingNextPuzzle, setLoadingNextPuzzle] = useState(true);
482
745
  const [puzzleNum, setPuzzleNum] = useState(0);
483
746
  const [hasIncorrectAttempt, setHasIncorrectAttempt] = useState(false);
484
747
  const [puzzleComplete, setPuzzleComplete] = useState(false);
748
+ const [completedAfterMiss, setCompletedAfterMiss] = useState(false);
749
+ const [missFeedback, setMissFeedback] = useState(null);
485
750
  const [, setInteractionNum] = useState(0);
486
751
  const solutionAnimationRef = useRef({ cancelled: false, timeoutIds: [] });
752
+ const resumeAnimationRef = useRef({ cancelled: false, timeoutIds: [] });
487
753
  const incInteractionNum = () => {
488
754
  setInteractionNum((prev) => prev + 1);
489
755
  };
@@ -493,57 +759,58 @@ const PuzzleBoardWithControls = ({ theme, apiProxy, renderControls = defaultRend
493
759
  anim.timeoutIds.forEach(clearTimeout);
494
760
  solutionAnimationRef.current = { cancelled: false, timeoutIds: [] };
495
761
  };
762
+ const clearResumeAnimation = () => {
763
+ const anim = resumeAnimationRef.current;
764
+ anim.cancelled = true;
765
+ anim.timeoutIds.forEach(clearTimeout);
766
+ resumeAnimationRef.current = { cancelled: false, timeoutIds: [] };
767
+ };
496
768
  useEffect(() => {
497
769
  let cancelled = false;
498
- let openingMoveTimeoutId;
499
- setPosition(null);
770
+ setLoadingNextPuzzle(true);
500
771
  setHasIncorrectAttempt(false);
501
772
  setPuzzleComplete(false);
773
+ setCompletedAfterMiss(false);
774
+ setMissFeedback(null);
502
775
  onFetch()
503
776
  .then((data) => {
504
777
  if (cancelled) {
505
778
  return;
506
779
  }
507
- if (!data || !data.fen || !data.moves) {
780
+ if (!(data === null || data === void 0 ? void 0 : data.fen) || !Array.isArray(data.moves) || data.moves.length === 0) {
508
781
  console.error('Invalid data fetched:', data);
782
+ setLoadingNextPuzzle(false);
509
783
  return;
510
784
  }
511
- const newPosition = new PuzzlePosition(data.fen, data.moves);
512
- setPosition(newPosition);
513
- // Multi-move puzzles lead with an opponent setup ply; single-move lines
514
- // (e.g. a first-ply opening trainer) are already on the player to move.
515
- if (data.moves.length > 1) {
516
- openingMoveTimeoutId = setTimeout(() => {
517
- if (cancelled) {
518
- return;
519
- }
520
- if (newPosition.next()) {
521
- incInteractionNum();
522
- }
523
- }, OPPONENT_OPENING_MOVE_DELAY_MS);
524
- }
785
+ setPosition(puzzlePositionFromFetch(data.fen, data.moves, data.resume));
786
+ requestAnimationFrame(() => {
787
+ if (!cancelled) {
788
+ setLoadingNextPuzzle(false);
789
+ }
790
+ });
525
791
  })
526
792
  .catch((error) => {
527
793
  if (!cancelled) {
794
+ setLoadingNextPuzzle(false);
528
795
  onFetchError === null || onFetchError === void 0 ? void 0 : onFetchError(error);
529
796
  }
530
797
  });
531
798
  return () => {
532
799
  cancelled = true;
533
800
  clearSolutionAnimation();
534
- if (openingMoveTimeoutId !== undefined) {
535
- clearTimeout(openingMoveTimeoutId);
536
- }
801
+ clearResumeAnimation();
537
802
  };
538
803
  }, [puzzleNum]);
539
804
  const handleFeedback = (feedbackData) => {
540
- if (feedbackData.hintRequested ||
805
+ const incorrectThisFeedback = feedbackData.hintRequested ||
541
806
  feedbackData.solutionShown ||
542
- feedbackData.isCorrect === false) {
807
+ feedbackData.isCorrect === false;
808
+ if (incorrectThisFeedback) {
543
809
  setHasIncorrectAttempt(true);
544
810
  }
545
811
  if (feedbackData.isFinished) {
546
812
  setPuzzleComplete(true);
813
+ setCompletedAfterMiss(hasIncorrectAttempt || incorrectThisFeedback);
547
814
  }
548
815
  onFeedback(feedbackData);
549
816
  };
@@ -637,6 +904,53 @@ const PuzzleBoardWithControls = ({ theme, apiProxy, renderControls = defaultRend
637
904
  };
638
905
  advance();
639
906
  };
907
+ const runResumeAutoAdvance = (pos) => {
908
+ clearResumeAnimation();
909
+ const anim = {
910
+ cancelled: false,
911
+ timeoutIds: [],
912
+ };
913
+ resumeAnimationRef.current = anim;
914
+ const schedule = (fn, ms) => {
915
+ const id = setTimeout(() => {
916
+ if (anim.cancelled) {
917
+ return;
918
+ }
919
+ fn();
920
+ }, ms);
921
+ anim.timeoutIds.push(id);
922
+ };
923
+ const finish = () => {
924
+ setPuzzleComplete(true);
925
+ handleFeedback({
926
+ index: pos.getIndex(),
927
+ isCorrect: true,
928
+ isFinished: true,
929
+ });
930
+ incInteractionNum();
931
+ };
932
+ const step = () => {
933
+ if (anim.cancelled) {
934
+ return;
935
+ }
936
+ if (pos.isFinished()) {
937
+ finish();
938
+ return;
939
+ }
940
+ if (pos.isQuizIndex()) {
941
+ return;
942
+ }
943
+ if (!pos.next()) {
944
+ if (pos.isFinished()) {
945
+ finish();
946
+ }
947
+ return;
948
+ }
949
+ incInteractionNum();
950
+ schedule(step, RESUME_AUTO_STEP_MS);
951
+ };
952
+ schedule(step, RESUME_AUTO_STEP_MS);
953
+ };
640
954
  const handleShowSolution = () => {
641
955
  if (!position) {
642
956
  return;
@@ -670,7 +984,10 @@ const PuzzleBoardWithControls = ({ theme, apiProxy, renderControls = defaultRend
670
984
  if (!autoAdvanceOnComplete) {
671
985
  return;
672
986
  }
673
- if (resultStatus !== 'complete' || hasIncorrectAttempt) {
987
+ if (resultStatus !== 'complete') {
988
+ return;
989
+ }
990
+ if (hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) {
674
991
  return;
675
992
  }
676
993
  const timer = setTimeout(() => {
@@ -681,6 +998,7 @@ const PuzzleBoardWithControls = ({ theme, apiProxy, renderControls = defaultRend
681
998
  };
682
999
  }, [
683
1000
  autoAdvanceOnComplete,
1001
+ autoAdvanceOnCompleteAfterIncorrect,
684
1002
  resultStatus,
685
1003
  hasIncorrectAttempt,
686
1004
  handleNextPuzzle,
@@ -688,8 +1006,9 @@ const PuzzleBoardWithControls = ({ theme, apiProxy, renderControls = defaultRend
688
1006
  ]);
689
1007
  const controlState = {
690
1008
  canShowHint: position !== null &&
691
- resultStatus === 'none' &&
692
- !position.isSolutionRevealed(),
1009
+ !position.isFinished() &&
1010
+ !position.isSolutionRevealed() &&
1011
+ !(hasIncorrectAttempt && showAnswerArrowOnIncorrect && !allowRetryOnIncorrect),
693
1012
  canShowSolution: position !== null &&
694
1013
  (position.isSolutionRevealed() || !position.isFinished()),
695
1014
  };
@@ -698,10 +1017,27 @@ const PuzzleBoardWithControls = ({ theme, apiProxy, renderControls = defaultRend
698
1017
  const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
699
1018
  renderAnalysisContainer &&
700
1019
  (renderEngineEvaluation || (engine === null || engine === void 0 ? void 0 : engine.enabled) === false));
701
- return (jsx(ThemeProvider, { theme: theme, children: analysisSnapshot ? (jsx(AnalysisErrorBoundary, { onClose: analysis.closeAnalysis, children: useHostAnalysisUi ? (jsx(AnalysisBoardCore, { analysisContext: analysisSnapshot, onClose: analysis.closeAnalysis, theme: theme, boardWidth: analysisLayout.boardWidth, 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: puzzlePlayColumnStyle(puzzleBoardWidth), children: [jsx("div", { style: puzzleBoardSlotStyle(), children: jsx(PuzzlePlaySurface, { position: position, boardWidth: puzzleBoardWidth, onFeedback: handleFeedback, incInteractionNum: incInteractionNum, revealAnswerOnIncorrect: revealAnswerOnIncorrect }) }), jsx("div", { style: puzzleControlsSlotStyle(), children: renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
702
- visible: analysis.canOpen,
703
- openAnalysis: analysis.openAnalysis,
704
- }, controlState) })] })) }));
1020
+ 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: analysisLayout.boardWidth, 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({
1021
+ sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
1022
+ playerColor: position
1023
+ ? position.getPlayerColor()
1024
+ : null,
1025
+ incorrectAttempt: resultStatus === 'incorrect',
1026
+ complete: resultStatus === 'complete',
1027
+ cleanSolve: !hasIncorrectAttempt,
1028
+ refutationSan: (_b = missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan) !== null && _b !== void 0 ? _b : null,
1029
+ missPhase: (_c = missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.phase) !== null && _c !== void 0 ? _c : null,
1030
+ answerArrowVisible: (_d = missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.answerArrowVisible) !== null && _d !== void 0 ? _d : false,
1031
+ completedAfterMiss,
1032
+ }) }))] }), jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
1033
+ visible: analysis.canOpen,
1034
+ openAnalysis: analysis.openAnalysis,
1035
+ }, controlState), renderBoardFeedback && resultStatus === 'complete' && (jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
1036
+ resultStatus,
1037
+ cleanSolve: !hasIncorrectAttempt,
1038
+ refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
1039
+ missPhase: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.phase,
1040
+ }) }))] })] })) }));
705
1041
  };
706
1042
 
707
1043
  /**
@@ -744,7 +1080,7 @@ const boardOrientationForLine = (side) => side === 'b' ? 'black' : 'white';
744
1080
  * tactical puzzles. Mount one instance per drill (key it by line) so its
745
1081
  * internal state resets between lines.
746
1082
  */
747
- const LineBoardWithControls = ({ theme, line, onComplete, onMove, renderControls = defaultRenderLineControls, boardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, opponentMoveDelayMs = DEFAULT_OPPONENT_MOVE_DELAY_MS, }) => {
1083
+ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, renderControls = defaultRenderLineControls, boardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, opponentMoveDelayMs = DEFAULT_OPPONENT_MOVE_DELAY_MS, }) => {
748
1084
  const chessRef = useRef(new Chess(line.startFen));
749
1085
  const perMoveRef = useRef([]);
750
1086
  const completedRef = useRef(false);
@@ -808,8 +1144,8 @@ const LineBoardWithControls = ({ theme, line, onComplete, onMove, renderControls
808
1144
  completedRef.current = true;
809
1145
  onCompleteRef.current(perMoveRef.current);
810
1146
  }, [finished]);
811
- const handleDrop = (source, target) => {
812
- var _a, _b, _c, _d;
1147
+ const handleDrop = (source, target, piece) => {
1148
+ var _a, _b, _c;
813
1149
  if (finished) {
814
1150
  return false;
815
1151
  }
@@ -817,31 +1153,27 @@ const LineBoardWithControls = ({ theme, line, onComplete, onMove, renderControls
817
1153
  return false;
818
1154
  }
819
1155
  const index = currentIndex;
820
- let userUci;
821
- try {
822
- const probe = new Chess(chessRef.current.fen());
823
- const move = probe.move({ from: source, to: target, promotion: 'q' });
824
- if (!move) {
825
- return false;
826
- }
827
- userUci = `${move.from}${move.to}${(_a = move.promotion) !== null && _a !== void 0 ? _a : ''}`;
828
- }
829
- catch (_e) {
1156
+ const expected = line.movesUci[index];
1157
+ const dropResult = evaluateExpectedMoveDrop(chessRef.current.fen(), source, target, piece, expected, true);
1158
+ if (dropResult.kind === 'illegal' || dropResult.kind === 'ignored') {
830
1159
  return false;
831
1160
  }
832
- const expected = line.movesUci[index];
833
- const isCorrect = userUci.toLowerCase() === expected.toLowerCase();
834
- const expectedSan = (_c = (_b = line.movesSan) === null || _b === void 0 ? void 0 : _b[index]) !== null && _c !== void 0 ? _c : expected;
1161
+ const isCorrect = dropResult.kind === 'correct';
1162
+ const expectedSan = (_b = (_a = line.movesSan) === null || _a === void 0 ? void 0 : _a[index]) !== null && _b !== void 0 ? _b : expected;
835
1163
  perMoveRef.current.push({ index, isCorrect });
836
1164
  const moveFeedback = { index, isCorrect, expectedSan };
837
1165
  setFeedback(moveFeedback);
838
- (_d = onMoveRef.current) === null || _d === void 0 ? void 0 : _d.call(onMoveRef, moveFeedback);
1166
+ (_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
839
1167
  applyMove(index);
840
1168
  return isCorrect;
841
1169
  };
842
1170
  const moveNumber = Math.min(currentIndex + 1, total);
843
1171
  const isUserTurn = !finished && turnFromFen(fen) === line.trainSide && currentIndex < total;
844
- return (jsx(ThemeProvider, { theme: theme, children: jsxs("div", { style: puzzlePlayColumnStyle(boardWidth), children: [jsx("div", { style: puzzleBoardSlotStyle(), children: jsx(LineBoard, { fen: fen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, onPieceDrop: handleDrop, boardWidth: boardWidth }) }), jsx("div", { style: puzzleControlsSlotStyle(), children: renderControls({
1172
+ const stackControlsBelow = useStackPuzzleControlsBelow();
1173
+ const controlsPlacement = stackControlsBelow
1174
+ ? 'below'
1175
+ : 'beside';
1176
+ 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: fen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
845
1177
  trainSide: line.trainSide,
846
1178
  moveNumber,
847
1179
  total,
@@ -858,4 +1190,4 @@ const LineBoardWithControls = ({ theme, line, onComplete, onMove, renderControls
858
1190
  /** @deprecated Import {@link boardSquareHighlightColors} and {@link analysisBoardHighlightColors} from `react-chess-core`. */
859
1191
  const squareHighlightColors = Object.assign(Object.assign({}, boardSquareHighlightColors), analysisBoardHighlightColors);
860
1192
 
861
- export { DEFAULT_PUZZLE_BOARD_WIDTH, DefaultLineControls, DefaultPuzzleControls, GamePosition, LineBoard, LineBoardWithControls, Position, PuzzleBoard, PuzzleBoardWithControls, PuzzlePosition, applyUciMove, buildAnalysisContext, defaultRenderControls, defaultRenderLineControls, emptyAnalysisContext, getCheckSquareFromChess, isAnalysisAvailable, playerColorForSolution, squareHighlightColors, usePuzzleAnalysis };
1193
+ 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 };