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