react-chess-puzzle-kit 1.0.0 → 1.0.1

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