react-chess-puzzle-kit 1.0.0

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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +331 -0
  3. package/dist/features/analysis/AnalysisBoard.d.ts +18 -0
  4. package/dist/features/analysis/AnalysisBoardCore.d.ts +28 -0
  5. package/dist/features/analysis/AnalysisBoardLayout.d.ts +12 -0
  6. package/dist/features/analysis/AnalysisChessboardView.d.ts +5 -0
  7. package/dist/features/analysis/AnalysisPosition.d.ts +53 -0
  8. package/dist/features/analysis/AnalysisTrigger.d.ts +6 -0
  9. package/dist/features/analysis/DefaultAnalysisContainer.d.ts +3 -0
  10. package/dist/features/analysis/DefaultAnalysisSidebar.d.ts +2 -0
  11. package/dist/features/analysis/analysisBoardHighlightColors.d.ts +12 -0
  12. package/dist/features/analysis/analysisContext.d.ts +8 -0
  13. package/dist/features/analysis/analysisLayout.d.ts +5 -0
  14. package/dist/features/analysis/core/AnalysisBoardCore.d.ts +28 -0
  15. package/dist/features/analysis/core/AnalysisChessboardView.d.ts +5 -0
  16. package/dist/features/analysis/core/AnalysisErrorBoundary.d.ts +14 -0
  17. package/dist/features/analysis/core/AnalysisPosition.d.ts +54 -0
  18. package/dist/features/analysis/core/analysisContext.d.ts +13 -0
  19. package/dist/features/analysis/core/analysisLayout.d.ts +5 -0
  20. package/dist/features/analysis/core/analysisLayoutConfig.d.ts +6 -0
  21. package/dist/features/analysis/core/index.d.ts +9 -0
  22. package/dist/features/analysis/core/renderProps.d.ts +39 -0
  23. package/dist/features/analysis/core/useAnalysisBoardModel.d.ts +36 -0
  24. package/dist/features/analysis/core/usePuzzleAnalysis.d.ts +10 -0
  25. package/dist/features/analysis/defaults/AnalysisBoard.d.ts +16 -0
  26. package/dist/features/analysis/defaults/AnalysisBoardLayout.d.ts +14 -0
  27. package/dist/features/analysis/defaults/DefaultAnalysisContainer.d.ts +4 -0
  28. package/dist/features/analysis/defaults/DefaultAnalysisSidebar.d.ts +3 -0
  29. package/dist/features/analysis/defaults/analysisLayout.d.ts +3 -0
  30. package/dist/features/analysis/defaults/analysisModalStyles.d.ts +7 -0
  31. package/dist/features/analysis/defaults/analysisSidebarColors.d.ts +37 -0
  32. package/dist/features/analysis/defaults/analysisSidebarRowStyle.d.ts +8 -0
  33. package/dist/features/analysis/defaults/index.d.ts +8 -0
  34. package/dist/features/analysis/index.d.ts +3 -0
  35. package/dist/features/analysis/renderProps.d.ts +39 -0
  36. package/dist/features/analysis/useAnalysisBoardModel.d.ts +33 -0
  37. package/dist/features/analysis/usePuzzleAnalysis.d.ts +10 -0
  38. package/dist/features/board/BlankPuzzleBoard.d.ts +5 -0
  39. package/dist/features/board/HighlightChessboard.d.ts +2 -0
  40. package/dist/features/board/LineBoard.d.ts +15 -0
  41. package/dist/features/board/LineBoardWithControls.d.ts +50 -0
  42. package/dist/features/board/PuzzleBoard.d.ts +10 -0
  43. package/dist/features/board/PuzzleBoardWithControls.d.ts +51 -0
  44. package/dist/features/board/PuzzlePlaySurface.d.ts +25 -0
  45. package/dist/features/board/boardSquareHighlightColors.d.ts +1 -0
  46. package/dist/features/board/chessboardTheme.d.ts +2 -0
  47. package/dist/features/board/defaults/DefaultLineControls.d.ts +5 -0
  48. package/dist/features/board/defaults/DefaultPuzzleControls.d.ts +18 -0
  49. package/dist/features/board/defaults/index.d.ts +1 -0
  50. package/dist/features/board/puzzleBoardLayout.d.ts +8 -0
  51. package/dist/features/engine/EngineEvaluationPanel.d.ts +8 -0
  52. package/dist/features/engine/StockfishBrowserEngine.d.ts +1 -0
  53. package/dist/features/engine/formatEvaluation.d.ts +1 -0
  54. package/dist/features/engine/index.d.ts +1 -0
  55. package/dist/features/engine/isAnalyzableFen.d.ts +1 -0
  56. package/dist/features/engine/parseUciInfo.d.ts +1 -0
  57. package/dist/features/engine/stockfishUrls.d.ts +1 -0
  58. package/dist/features/engine/types.d.ts +1 -0
  59. package/dist/features/engine/useAnalysisEngine.d.ts +1 -0
  60. package/dist/features/position/Position.d.ts +67 -0
  61. package/dist/features/position/Position.test.d.ts +1 -0
  62. package/dist/features/position/Traversable.d.ts +4 -0
  63. package/dist/features/position/moveHistory.d.ts +7 -0
  64. package/dist/features/theme/ThemeContext.d.ts +26 -0
  65. package/dist/features/theme/ThemeProvider.d.ts +7 -0
  66. package/dist/features/theme/squareHighlightColors.d.ts +50 -0
  67. package/dist/index.d.ts +26 -0
  68. package/dist/index.esm.js +861 -0
  69. package/dist/index.js +885 -0
  70. package/dist/stories/AnalysisBoard.stories.d.ts +9 -0
  71. package/dist/stories/HighlightChessboard.stories.d.ts +9 -0
  72. package/dist/stories/PuzzleBoard.stories.d.ts +6 -0
  73. package/dist/stories/PuzzleBoardWithControls.stories.d.ts +9 -0
  74. package/dist/stories/analysisFixtures.d.ts +4 -0
  75. package/dist/stories/regressions/PuzzleRegressions.stories.d.ts +7 -0
  76. package/dist/stories/regressions/StockfishAnalysisRegressions.stories.d.ts +7 -0
  77. package/dist/stories/regressions/fixtures.d.ts +18 -0
  78. package/dist/stories/regressions/regressionAnalysisContext.d.ts +5 -0
  79. package/dist/stories/storybookLayout.d.ts +4 -0
  80. package/dist/stories/withChessboardDnD.d.ts +4 -0
  81. package/dist/stories/withDefaultPuzzleControls.d.ts +7 -0
  82. package/dist/stories/withThemeProvider.d.ts +3 -0
  83. package/package.json +87 -0
package/dist/index.js ADDED
@@ -0,0 +1,885 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var reactChessboard = require('react-chessboard');
6
+ var reactChessCore = require('react-chess-core');
7
+ var chess_js = require('chess.js');
8
+
9
+ const emptyAnalysisContext = () => ({
10
+ initialFen: '',
11
+ solutionMoves: [],
12
+ currentPly: 0,
13
+ boardOrientation: 'white',
14
+ });
15
+ const buildAnalysisContext = (position) => {
16
+ if (!position) {
17
+ return emptyAnalysisContext();
18
+ }
19
+ return {
20
+ initialFen: position.getInitialFen(),
21
+ solutionMoves: position.getSolutionMoves(),
22
+ currentPly: position.getIndex(),
23
+ boardOrientation: position.getPlayerColor(),
24
+ };
25
+ };
26
+ const isAnalysisAvailable = (position, resultStatus) => {
27
+ if (!position) {
28
+ return false;
29
+ }
30
+ return (buildAnalysisContext(position).solutionMoves.length > 0 &&
31
+ resultStatus !== 'none');
32
+ };
33
+
34
+ const usePuzzleAnalysis = (position, resultStatus, puzzleNum) => {
35
+ const [isOpen, setIsOpen] = react.useState(false);
36
+ const [snapshot, setSnapshot] = react.useState(null);
37
+ const canOpen = isAnalysisAvailable(position, resultStatus);
38
+ react.useEffect(() => {
39
+ setIsOpen(false);
40
+ setSnapshot(null);
41
+ }, [puzzleNum]);
42
+ const openAnalysis = () => {
43
+ if (!canOpen || !position) {
44
+ return;
45
+ }
46
+ setSnapshot(buildAnalysisContext(position));
47
+ setIsOpen(true);
48
+ };
49
+ const closeAnalysis = () => {
50
+ setIsOpen(false);
51
+ };
52
+ return {
53
+ canOpen,
54
+ isOpen,
55
+ snapshot,
56
+ openAnalysis,
57
+ closeAnalysis,
58
+ };
59
+ };
60
+
61
+ const EMPTY_BOARD_FEN = '8/8/8/8/8/8/8/8 w - - 0 1';
62
+ /**
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.
65
+ */
66
+ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth, revealAnswerOnIncorrect = false, }) => {
67
+ var _a, _b, _c, _d, _e;
68
+ const onPieceDrop = (sourceSquare, targetSquare, piece) => {
69
+ if (!position || position.isSolutionRevealed()) {
70
+ return false;
71
+ }
72
+ if (!position.isLegalMove(sourceSquare, targetSquare)) {
73
+ return false;
74
+ }
75
+ const guess = position.tryGuess(sourceSquare, targetSquare, piece);
76
+ if (!guess.accepted) {
77
+ onFeedback({
78
+ index: position.getIndex(),
79
+ guess: { sourceSquare, targetSquare, piece },
80
+ isCorrect: false,
81
+ });
82
+ incInteractionNum();
83
+ setTimeout(() => {
84
+ if (revealAnswerOnIncorrect) {
85
+ position.resetInteractions();
86
+ position.revealCorrectMove();
87
+ }
88
+ else {
89
+ position.resetInteractions();
90
+ }
91
+ incInteractionNum();
92
+ }, 500);
93
+ return false;
94
+ }
95
+ onFeedback({
96
+ index: position.getIndex(),
97
+ guess: { sourceSquare, targetSquare, piece },
98
+ isCorrect: true,
99
+ isFinished: guess.finished,
100
+ });
101
+ incInteractionNum();
102
+ setTimeout(() => {
103
+ position.resetInteractions();
104
+ incInteractionNum();
105
+ }, 500);
106
+ if (position.isAlternativeCheckmate()) {
107
+ incInteractionNum();
108
+ return true;
109
+ }
110
+ position.next();
111
+ incInteractionNum();
112
+ setTimeout(() => {
113
+ if (!position.isFinished()) {
114
+ position.next();
115
+ }
116
+ incInteractionNum();
117
+ }, 500);
118
+ return true;
119
+ };
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" }) }));
121
+ };
122
+
123
+ const PuzzleBoard = ({ position, onFeedback, incInteractionNum, boardWidth, revealAnswerOnIncorrect = false, }) => (jsxRuntime.jsx(PuzzlePlaySurface, { position: position, onFeedback: onFeedback, incInteractionNum: incInteractionNum, boardWidth: boardWidth, revealAnswerOnIncorrect: revealAnswerOnIncorrect }));
124
+
125
+ const isAttemptFinished = (resultStatus) => resultStatus === 'complete' || resultStatus === 'incorrect';
126
+ /** Library default hint / next / analysis / result controls (unstyled buttons). */
127
+ const DefaultPuzzleControls = ({ showHint, showSolution, nextPuzzle, resultStatus, analysis, controlState, }) => (jsxRuntime.jsxs("div", { style: rowStyle$1, children: [jsxRuntime.jsx("button", { type: "button", onClick: showHint, style: buttonStyle, disabled: !controlState.canShowHint, children: "Hint" }), jsxRuntime.jsx("button", { type: "button", onClick: showSolution, style: buttonStyle, disabled: !controlState.canShowSolution, children: "Show solution" }), jsxRuntime.jsx("button", { type: "button", onClick: nextPuzzle, style: buttonStyle, children: "Next puzzle" }), analysis.visible && isAttemptFinished(resultStatus) && (jsxRuntime.jsx("button", { type: "button", onClick: analysis.openAnalysis, style: buttonStyle, children: "Analysis" })), resultStatus === 'complete' && (jsxRuntime.jsx("span", { style: statusStyle$1, children: "Complete" })), resultStatus === 'incorrect' && (jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, statusStyle$1), { color: '#c62828' }), children: "Incorrect" }))] }));
128
+ const defaultRenderControls = (showHint, showSolution, nextPuzzle, resultStatus, analysis, controlState) => (jsxRuntime.jsx(DefaultPuzzleControls, { showHint: showHint, showSolution: showSolution, nextPuzzle: nextPuzzle, resultStatus: resultStatus, analysis: analysis, controlState: controlState }));
129
+ const rowStyle$1 = {
130
+ display: 'flex',
131
+ flexWrap: 'wrap',
132
+ alignItems: 'center',
133
+ gap: 8,
134
+ width: '100%',
135
+ minHeight: 96,
136
+ alignContent: 'flex-start',
137
+ };
138
+ const buttonStyle = {
139
+ cursor: 'pointer',
140
+ padding: '6px 12px',
141
+ fontSize: 14,
142
+ borderRadius: 4,
143
+ border: '1px solid #ccc',
144
+ background: '#f5f5f5',
145
+ };
146
+ const statusStyle$1 = {
147
+ fontSize: 14,
148
+ fontWeight: 600,
149
+ color: '#2e7d32',
150
+ };
151
+
152
+ /** Default pixel width for the live puzzle board (analysis uses {@link DEFAULT_ANALYSIS_LAYOUT}). */
153
+ 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) => ({
157
+ display: 'flex',
158
+ flexDirection: 'column',
159
+ width: boardWidth,
160
+ maxWidth: '100%',
161
+ });
162
+ const puzzleBoardSlotStyle = () => ({
163
+ width: '100%',
164
+ aspectRatio: '1 / 1',
165
+ flexShrink: 0,
166
+ });
167
+ const puzzleControlsSlotStyle = () => ({
168
+ minHeight: PUZZLE_CONTROLS_MIN_HEIGHT,
169
+ flexShrink: 0,
170
+ display: 'flex',
171
+ alignItems: 'flex-start',
172
+ marginTop: 8,
173
+ });
174
+
175
+ /** Apply a UCI move (e.g. `e7e8q`) without throwing. */
176
+ function applyUciMove(chess, uci) {
177
+ if (!uci || uci.length < 4) {
178
+ return false;
179
+ }
180
+ const from = uci.slice(0, 2);
181
+ const to = uci.slice(2, 4);
182
+ const promotion = uci.length > 4 ? uci[4] : undefined;
183
+ try {
184
+ return chess.move({ from, to, promotion }) !== null;
185
+ }
186
+ catch (_a) {
187
+ try {
188
+ chess.move(uci);
189
+ return true;
190
+ }
191
+ catch (_b) {
192
+ return false;
193
+ }
194
+ }
195
+ }
196
+ class Position {
197
+ constructor() {
198
+ this.i = 0;
199
+ this.chess = new chess_js.Chess();
200
+ this.moves = [];
201
+ }
202
+ getIndex() {
203
+ return this.i;
204
+ }
205
+ // Common methods shared by all positions
206
+ next() {
207
+ if (this.i >= this.moves.length) {
208
+ return false;
209
+ }
210
+ const uci = this.moves[this.i];
211
+ if (!applyUciMove(this.chess, uci)) {
212
+ return false;
213
+ }
214
+ this.i++;
215
+ return true;
216
+ }
217
+ prev() {
218
+ if (this.i > 0) {
219
+ this.chess.undo();
220
+ this.i--;
221
+ return true;
222
+ }
223
+ return false;
224
+ }
225
+ isFinished() {
226
+ return this.i >= this.moves.length;
227
+ }
228
+ /** True when a correct guess at the current index completes the puzzle. */
229
+ isCompletedByCorrectGuess() {
230
+ return this.i >= this.moves.length - 2;
231
+ }
232
+ fen() {
233
+ return this.chess.fen();
234
+ }
235
+ getCheckSquare() {
236
+ return getCheckSquareFromChess(this.chess);
237
+ }
238
+ }
239
+ function getCheckSquareFromChess(chess) {
240
+ if (!chess.inCheck()) {
241
+ return '';
242
+ }
243
+ const turn = chess.turn();
244
+ const kingPieceType = 'k';
245
+ const squaresWithPieces = chess.board().flatMap((row, rowIndex) => row.map((piece, colIndex) => ({
246
+ square: String.fromCharCode(97 + colIndex) + (8 - rowIndex),
247
+ piece: piece,
248
+ })));
249
+ const kingSquare = squaresWithPieces
250
+ .filter(({ piece }) => piece && piece.type === kingPieceType && piece.color === turn)
251
+ .map(({ square }) => square)[0];
252
+ return kingSquare !== null && kingSquare !== void 0 ? kingSquare : '';
253
+ }
254
+ /** Side to move for the final (user) ply in a puzzle line. */
255
+ function playerColorForSolution(initialFen, moves) {
256
+ const chess = new chess_js.Chess(initialFen);
257
+ const setupPlies = Math.max(0, moves.length - 1);
258
+ for (let j = 0; j < setupPlies; j++) {
259
+ applyUciMove(chess, moves[j]);
260
+ }
261
+ return chess.turn() === 'w' ? 'white' : 'black';
262
+ }
263
+ class PuzzlePosition extends Position {
264
+ constructor(initialFEN, moves) {
265
+ super();
266
+ this.isCorrect = false;
267
+ this.guessedMove = '';
268
+ this.isHintWanted = false;
269
+ this.solutionRevealed = false;
270
+ this.moveHistory = [];
271
+ this.usedAlternativeCheckmate = false;
272
+ this.tryGuess = (sourceSquare, targetSquare, piece) => {
273
+ var _a;
274
+ const candidates = this.buildCandidateUcis(sourceSquare, targetSquare);
275
+ if (candidates.length === 0) {
276
+ this.isCorrect = false;
277
+ this.guessedMove = `${sourceSquare}${targetSquare}`;
278
+ return { accepted: false, finished: false };
279
+ }
280
+ for (const uci of candidates) {
281
+ if (this.moves[this.i] === uci) {
282
+ this.isCorrect = true;
283
+ this.guessedMove = uci.slice(0, 4);
284
+ return {
285
+ accepted: true,
286
+ finished: this.isCompletedByCorrectGuess(),
287
+ };
288
+ }
289
+ }
290
+ for (const uci of candidates) {
291
+ if (this.isCheckmatingMove(uci) && this.applyCheckmateGuess(uci)) {
292
+ this.isCorrect = true;
293
+ return { accepted: true, finished: true };
294
+ }
295
+ }
296
+ this.isCorrect = false;
297
+ 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
+ });
304
+ return { accepted: false, finished: false };
305
+ };
306
+ this.judgeGuess = (sourceSquare, targetSquare, piece) => this.tryGuess(sourceSquare, targetSquare, piece).accepted;
307
+ this.initialFen = initialFEN;
308
+ this.chess.load(initialFEN);
309
+ this.moves = moves;
310
+ this.playerColor = playerColorForSolution(initialFEN, moves);
311
+ }
312
+ next() {
313
+ if (this.i >= this.moves.length) {
314
+ return false;
315
+ }
316
+ const moveUci = this.moves[this.i];
317
+ const movingColor = this.chess.turn() === 'w' ? 'white' : 'black';
318
+ const result = super.next();
319
+ if (result) {
320
+ const san = this.chess.history().at(-1);
321
+ this.moveHistory.push({
322
+ ply: this.moveHistory.length,
323
+ uci: moveUci,
324
+ san,
325
+ actor: movingColor === this.playerColor ? 'player' : 'opponent',
326
+ isCorrect: true,
327
+ });
328
+ }
329
+ return result;
330
+ }
331
+ getInitialFen() {
332
+ return this.initialFen;
333
+ }
334
+ getSolutionMoves() {
335
+ return [...this.moves];
336
+ }
337
+ getMoveHistory() {
338
+ return [...this.moveHistory];
339
+ }
340
+ recordHint() {
341
+ var _a;
342
+ this.moveHistory.push({
343
+ ply: this.moveHistory.length,
344
+ uci: (_a = this.moves[this.i]) !== null && _a !== void 0 ? _a : '',
345
+ san: 'Hint',
346
+ actor: 'attempt',
347
+ isCorrect: false,
348
+ });
349
+ }
350
+ recordSolutionShown() {
351
+ this.moveHistory.push({
352
+ ply: this.moveHistory.length,
353
+ uci: '',
354
+ san: 'Solution',
355
+ actor: 'attempt',
356
+ isCorrect: false,
357
+ });
358
+ }
359
+ setSolutionRevealed(revealed) {
360
+ this.solutionRevealed = revealed;
361
+ }
362
+ isSolutionRevealed() {
363
+ return this.solutionRevealed;
364
+ }
365
+ /** Reset the board to replay an already-revealed solution walkthrough. */
366
+ replaySolution() {
367
+ this.chess.load(this.initialFen);
368
+ this.i = 0;
369
+ this.isCorrect = false;
370
+ this.guessedMove = '';
371
+ this.isHintWanted = false;
372
+ this.usedAlternativeCheckmate = false;
373
+ this.solutionRevealed = true;
374
+ const solutionIdx = this.moveHistory.findIndex((m) => m.san === 'Solution');
375
+ if (solutionIdx >= 0) {
376
+ this.moveHistory = this.moveHistory.slice(0, solutionIdx + 1);
377
+ }
378
+ }
379
+ /** Chess-legal move from the current puzzle position (ignores solution line). */
380
+ isLegalMove(sourceSquare, targetSquare) {
381
+ if (sourceSquare === targetSquare) {
382
+ return false;
383
+ }
384
+ return this.chess
385
+ .moves({ square: sourceSquare, verbose: true })
386
+ .some((move) => move.to === targetSquare);
387
+ }
388
+ buildCandidateUcis(sourceSquare, targetSquare) {
389
+ const lanMoves = this.chess
390
+ .moves({ square: sourceSquare, verbose: true })
391
+ .filter((move) => move.to === targetSquare)
392
+ .map((move) => move.lan);
393
+ return [...new Set(lanMoves)];
394
+ }
395
+ tryMakeUciMove(chess, uci) {
396
+ var _a;
397
+ if (!applyUciMove(chess, uci)) {
398
+ return null;
399
+ }
400
+ return (_a = chess.history({ verbose: true }).at(-1)) !== null && _a !== void 0 ? _a : null;
401
+ }
402
+ isCheckmatingMove(uci) {
403
+ const chess = new chess_js.Chess(this.chess.fen());
404
+ return (this.tryMakeUciMove(chess, uci) !== null && chess.isCheckmate());
405
+ }
406
+ applyCheckmateGuess(uci) {
407
+ const result = this.tryMakeUciMove(this.chess, uci);
408
+ if (result === null || !this.chess.isCheckmate()) {
409
+ return false;
410
+ }
411
+ this.moveHistory.push({
412
+ ply: this.moveHistory.length,
413
+ uci,
414
+ san: result.san,
415
+ actor: 'player',
416
+ isCorrect: true,
417
+ });
418
+ this.i = this.moves.length;
419
+ this.guessedMove = uci.slice(0, 4);
420
+ this.usedAlternativeCheckmate = true;
421
+ return true;
422
+ }
423
+ resetInteractions() {
424
+ this.guessedMove = '';
425
+ this.isHintWanted = false;
426
+ }
427
+ /** Play the expected move at the current index and lock further input. */
428
+ revealCorrectMove() {
429
+ if (this.solutionRevealed || this.i >= this.moves.length) {
430
+ return false;
431
+ }
432
+ const played = this.next();
433
+ if (played) {
434
+ this.solutionRevealed = true;
435
+ }
436
+ return played;
437
+ }
438
+ /** True when the last correct guess applied an alternative checkmate. */
439
+ isAlternativeCheckmate() {
440
+ return this.usedAlternativeCheckmate;
441
+ }
442
+ wantsHint(wants) {
443
+ this.isHintWanted = wants;
444
+ }
445
+ getHintSquare() {
446
+ if (!this.isHintWanted) {
447
+ return '';
448
+ }
449
+ return this.hint().slice(0, 2);
450
+ }
451
+ hint() {
452
+ return this.moves[this.i];
453
+ }
454
+ getIncorrectMoveSquare() {
455
+ if (this.isCorrect) {
456
+ return '';
457
+ }
458
+ return this.guessedMove.slice(2, 4);
459
+ }
460
+ getPlayerColor() {
461
+ return this.playerColor;
462
+ }
463
+ }
464
+ class GamePosition extends Position {
465
+ constructor(PGN) {
466
+ super();
467
+ console.log(`pgn: ${PGN}`);
468
+ this.chess.loadPgn(PGN);
469
+ this.moves = this.chess.history();
470
+ this.chess.reset();
471
+ // console.log(`pgn: ${PGN}`);
472
+ }
473
+ }
474
+
475
+ /** Delay before auto-playing the opponent's opening move (ms). */
476
+ const OPPONENT_OPENING_MOVE_DELAY_MS = 500;
477
+ /** Brief pause so the user sees a correct result before the next card loads. */
478
+ const AUTO_ADVANCE_ON_COMPLETE_DELAY_MS = 700;
479
+ 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, }) => {
481
+ const { onFetch, onFetchError, onFeedback } = apiProxy;
482
+ const [position, setPosition] = react.useState(null);
483
+ const [puzzleNum, setPuzzleNum] = react.useState(0);
484
+ const [hasIncorrectAttempt, setHasIncorrectAttempt] = react.useState(false);
485
+ const [puzzleComplete, setPuzzleComplete] = react.useState(false);
486
+ const [, setInteractionNum] = react.useState(0);
487
+ const solutionAnimationRef = react.useRef({ cancelled: false, timeoutIds: [] });
488
+ const incInteractionNum = () => {
489
+ setInteractionNum((prev) => prev + 1);
490
+ };
491
+ const clearSolutionAnimation = () => {
492
+ const anim = solutionAnimationRef.current;
493
+ anim.cancelled = true;
494
+ anim.timeoutIds.forEach(clearTimeout);
495
+ solutionAnimationRef.current = { cancelled: false, timeoutIds: [] };
496
+ };
497
+ react.useEffect(() => {
498
+ let cancelled = false;
499
+ let openingMoveTimeoutId;
500
+ setPosition(null);
501
+ setHasIncorrectAttempt(false);
502
+ setPuzzleComplete(false);
503
+ onFetch()
504
+ .then((data) => {
505
+ if (cancelled) {
506
+ return;
507
+ }
508
+ if (!data || !data.fen || !data.moves) {
509
+ console.error('Invalid data fetched:', data);
510
+ return;
511
+ }
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
+ }
526
+ })
527
+ .catch((error) => {
528
+ if (!cancelled) {
529
+ onFetchError === null || onFetchError === void 0 ? void 0 : onFetchError(error);
530
+ }
531
+ });
532
+ return () => {
533
+ cancelled = true;
534
+ clearSolutionAnimation();
535
+ if (openingMoveTimeoutId !== undefined) {
536
+ clearTimeout(openingMoveTimeoutId);
537
+ }
538
+ };
539
+ }, [puzzleNum]);
540
+ const handleFeedback = (feedbackData) => {
541
+ if (feedbackData.hintRequested ||
542
+ feedbackData.solutionShown ||
543
+ feedbackData.isCorrect === false) {
544
+ setHasIncorrectAttempt(true);
545
+ }
546
+ if (feedbackData.isFinished) {
547
+ setPuzzleComplete(true);
548
+ }
549
+ onFeedback(feedbackData);
550
+ };
551
+ const getResultStatus = () => {
552
+ const finished = puzzleComplete || (position !== null && position.isFinished());
553
+ if (finished) {
554
+ return 'complete';
555
+ }
556
+ if (!position) {
557
+ return 'none';
558
+ }
559
+ if (hasIncorrectAttempt) {
560
+ return 'incorrect';
561
+ }
562
+ return 'none';
563
+ };
564
+ const handleHintRequest = () => {
565
+ if (!position) {
566
+ return;
567
+ }
568
+ position.recordHint();
569
+ handleFeedback({ index: position.getIndex(), hintRequested: true });
570
+ position.wantsHint(true);
571
+ incInteractionNum();
572
+ setTimeout(() => {
573
+ position.resetInteractions();
574
+ incInteractionNum();
575
+ }, 500);
576
+ };
577
+ const runSolutionWalkthrough = (pos, emitFinishFeedback) => {
578
+ clearSolutionAnimation();
579
+ const anim = {
580
+ cancelled: false,
581
+ timeoutIds: [],
582
+ };
583
+ solutionAnimationRef.current = anim;
584
+ const schedule = (fn, ms) => {
585
+ const id = setTimeout(() => {
586
+ if (anim.cancelled) {
587
+ return;
588
+ }
589
+ fn();
590
+ }, ms);
591
+ anim.timeoutIds.push(id);
592
+ };
593
+ const finish = () => {
594
+ setPuzzleComplete(true);
595
+ if (emitFinishFeedback) {
596
+ handleFeedback({
597
+ index: pos.getIndex(),
598
+ isFinished: true,
599
+ isCorrect: false,
600
+ });
601
+ }
602
+ incInteractionNum();
603
+ };
604
+ const playNextMove = () => {
605
+ if (pos.isFinished()) {
606
+ return false;
607
+ }
608
+ const played = pos.next();
609
+ if (played) {
610
+ incInteractionNum();
611
+ }
612
+ return played;
613
+ };
614
+ const advance = () => {
615
+ if (anim.cancelled || pos.isFinished()) {
616
+ if (pos.isFinished()) {
617
+ finish();
618
+ }
619
+ return;
620
+ }
621
+ if (!playNextMove()) {
622
+ if (pos.isFinished()) {
623
+ finish();
624
+ }
625
+ return;
626
+ }
627
+ schedule(() => {
628
+ if (anim.cancelled) {
629
+ return;
630
+ }
631
+ playNextMove();
632
+ if (pos.isFinished()) {
633
+ finish();
634
+ return;
635
+ }
636
+ schedule(advance, SOLUTION_STEP_MS);
637
+ }, SOLUTION_STEP_MS);
638
+ };
639
+ advance();
640
+ };
641
+ const handleShowSolution = () => {
642
+ if (!position) {
643
+ return;
644
+ }
645
+ if (position.isSolutionRevealed()) {
646
+ position.replaySolution();
647
+ setPuzzleComplete(false);
648
+ incInteractionNum();
649
+ runSolutionWalkthrough(position, false);
650
+ return;
651
+ }
652
+ if (position.isFinished()) {
653
+ return;
654
+ }
655
+ position.recordSolutionShown();
656
+ position.setSolutionRevealed(true);
657
+ position.wantsHint(false);
658
+ handleFeedback({
659
+ index: position.getIndex(),
660
+ solutionShown: true,
661
+ isCorrect: false,
662
+ });
663
+ incInteractionNum();
664
+ runSolutionWalkthrough(position, true);
665
+ };
666
+ const handleNextPuzzle = react.useCallback(() => {
667
+ setPuzzleNum((prevPuzzleNum) => prevPuzzleNum + 1);
668
+ }, []);
669
+ const resultStatus = getResultStatus();
670
+ react.useEffect(() => {
671
+ if (!autoAdvanceOnComplete) {
672
+ return;
673
+ }
674
+ if (resultStatus !== 'complete' || hasIncorrectAttempt) {
675
+ return;
676
+ }
677
+ const timer = setTimeout(() => {
678
+ handleNextPuzzle();
679
+ }, AUTO_ADVANCE_ON_COMPLETE_DELAY_MS);
680
+ return () => {
681
+ clearTimeout(timer);
682
+ };
683
+ }, [
684
+ autoAdvanceOnComplete,
685
+ resultStatus,
686
+ hasIncorrectAttempt,
687
+ handleNextPuzzle,
688
+ puzzleNum,
689
+ ]);
690
+ const controlState = {
691
+ canShowHint: position !== null &&
692
+ resultStatus === 'none' &&
693
+ !position.isSolutionRevealed(),
694
+ canShowSolution: position !== null &&
695
+ (position.isSolutionRevealed() || !position.isFinished()),
696
+ };
697
+ const analysis = usePuzzleAnalysis(position, resultStatus, puzzleNum);
698
+ const analysisSnapshot = analysis.isOpen && analysis.snapshot ? analysis.snapshot : null;
699
+ const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
700
+ renderAnalysisContainer &&
701
+ (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) })] })) }));
706
+ };
707
+
708
+ /**
709
+ * Board view for line drilling: a plain themed board that only lets the trainer
710
+ * side's pieces be dragged. Move validation and sequencing live in
711
+ * {@link LineBoardWithControls}.
712
+ */
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 } }) }));
714
+
715
+ /** Library default line-drill status controls (unstyled). */
716
+ 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
717
+ ? `Correct: ${feedback.expectedSan}`
718
+ : `Best was ${feedback.expectedSan}` })), isUserTurn && !finished && jsxRuntime.jsx("span", { style: hintStyle, children: "Your move" })] }));
719
+ const defaultRenderLineControls = (props) => (jsxRuntime.jsx(DefaultLineControls, Object.assign({}, props)));
720
+ const rowStyle = {
721
+ display: 'flex',
722
+ flexWrap: 'wrap',
723
+ alignItems: 'center',
724
+ gap: 8,
725
+ width: '100%',
726
+ minHeight: 96,
727
+ alignContent: 'flex-start',
728
+ };
729
+ const statusStyle = {
730
+ fontSize: 14,
731
+ fontWeight: 600,
732
+ };
733
+ const hintStyle = {
734
+ fontSize: 13,
735
+ color: '#9e9e9e',
736
+ };
737
+
738
+ const DEFAULT_OPPONENT_MOVE_DELAY_MS = 450;
739
+ const turnFromFen = (fen) => fen.trim().split(/\s+/)[1] === 'b' ? 'b' : 'w';
740
+ const boardOrientationForLine = (side) => side === 'b' ? 'black' : 'white';
741
+ /**
742
+ * Guided line-drill board: walks a known move sequence, auto-playing the
743
+ * opponent and grading each of the trainer's moves against the line. Mirrors
744
+ * {@link PuzzleBoardWithControls} but for opening/line repetition rather than
745
+ * tactical puzzles. Mount one instance per drill (key it by line) so its
746
+ * internal state resets between lines.
747
+ */
748
+ const LineBoardWithControls = ({ theme, line, onComplete, onMove, renderControls = defaultRenderLineControls, boardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, opponentMoveDelayMs = DEFAULT_OPPONENT_MOVE_DELAY_MS, }) => {
749
+ const chessRef = react.useRef(new chess_js.Chess(line.startFen));
750
+ const perMoveRef = react.useRef([]);
751
+ const completedRef = react.useRef(false);
752
+ const onCompleteRef = react.useRef(onComplete);
753
+ onCompleteRef.current = onComplete;
754
+ const onMoveRef = react.useRef(onMove);
755
+ onMoveRef.current = onMove;
756
+ const [fen, setFen] = react.useState(line.startFen);
757
+ const [currentIndex, setCurrentIndex] = react.useState(0);
758
+ const [finished, setFinished] = react.useState(false);
759
+ const [feedback, setFeedback] = react.useState(null);
760
+ const total = line.movesUci.length;
761
+ const orientation = boardOrientationForLine(line.trainSide);
762
+ const applyMove = react.useCallback((index) => {
763
+ const uci = line.movesUci[index];
764
+ if (!uci) {
765
+ setFinished(true);
766
+ return;
767
+ }
768
+ const from = uci.slice(0, 2);
769
+ const to = uci.slice(2, 4);
770
+ const promotion = uci.length > 4 ? uci.slice(4) : undefined;
771
+ try {
772
+ chessRef.current.move({ from, to, promotion });
773
+ }
774
+ catch (_a) {
775
+ // Stored line should always be legal; bail out gracefully if not.
776
+ setFinished(true);
777
+ return;
778
+ }
779
+ setFen(chessRef.current.fen());
780
+ setCurrentIndex(index + 1);
781
+ }, [line.movesUci]);
782
+ // Auto-play opponent moves and detect the end of the line.
783
+ react.useEffect(() => {
784
+ if (finished) {
785
+ return;
786
+ }
787
+ if (currentIndex >= total) {
788
+ setFinished(true);
789
+ return;
790
+ }
791
+ if (turnFromFen(chessRef.current.fen()) !== line.trainSide) {
792
+ const timer = setTimeout(() => applyMove(currentIndex), opponentMoveDelayMs);
793
+ return () => clearTimeout(timer);
794
+ }
795
+ return undefined;
796
+ }, [
797
+ currentIndex,
798
+ finished,
799
+ total,
800
+ line.trainSide,
801
+ applyMove,
802
+ opponentMoveDelayMs,
803
+ ]);
804
+ // Emit the completion event exactly once.
805
+ react.useEffect(() => {
806
+ if (!finished || completedRef.current) {
807
+ return;
808
+ }
809
+ completedRef.current = true;
810
+ onCompleteRef.current(perMoveRef.current);
811
+ }, [finished]);
812
+ const handleDrop = (source, target) => {
813
+ var _a, _b, _c, _d;
814
+ if (finished) {
815
+ return false;
816
+ }
817
+ if (turnFromFen(chessRef.current.fen()) !== line.trainSide) {
818
+ return false;
819
+ }
820
+ 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) {
831
+ return false;
832
+ }
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;
836
+ perMoveRef.current.push({ index, isCorrect });
837
+ const moveFeedback = { index, isCorrect, expectedSan };
838
+ setFeedback(moveFeedback);
839
+ (_d = onMoveRef.current) === null || _d === void 0 ? void 0 : _d.call(onMoveRef, moveFeedback);
840
+ applyMove(index);
841
+ return isCorrect;
842
+ };
843
+ const moveNumber = Math.min(currentIndex + 1, total);
844
+ 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({
846
+ trainSide: line.trainSide,
847
+ moveNumber,
848
+ total,
849
+ finished,
850
+ isUserTurn,
851
+ feedback,
852
+ }) })] }) }));
853
+ };
854
+
855
+ /**
856
+ * Puzzle-kit public API — puzzle play and post-puzzle analysis glue.
857
+ * Board theme, analysis UI, and Stockfish: import from `react-chess-core`.
858
+ */
859
+ /** @deprecated Import {@link boardSquareHighlightColors} and {@link analysisBoardHighlightColors} from `react-chess-core`. */
860
+ const squareHighlightColors = Object.assign(Object.assign({}, reactChessCore.boardSquareHighlightColors), reactChessCore.analysisBoardHighlightColors);
861
+
862
+ Object.defineProperty(exports, "DEFAULT_ANALYSIS_LAYOUT", {
863
+ enumerable: true,
864
+ get: function () { return reactChessCore.DEFAULT_ANALYSIS_LAYOUT; }
865
+ });
866
+ exports.DEFAULT_PUZZLE_BOARD_WIDTH = DEFAULT_PUZZLE_BOARD_WIDTH;
867
+ exports.DefaultLineControls = DefaultLineControls;
868
+ exports.DefaultPuzzleControls = DefaultPuzzleControls;
869
+ exports.GamePosition = GamePosition;
870
+ exports.LineBoard = LineBoard;
871
+ exports.LineBoardWithControls = LineBoardWithControls;
872
+ exports.Position = Position;
873
+ exports.PuzzleBoard = PuzzleBoard;
874
+ exports.PuzzleBoardWithControls = PuzzleBoardWithControls;
875
+ exports.PuzzlePosition = PuzzlePosition;
876
+ exports.applyUciMove = applyUciMove;
877
+ exports.buildAnalysisContext = buildAnalysisContext;
878
+ exports.defaultRenderControls = defaultRenderControls;
879
+ exports.defaultRenderLineControls = defaultRenderLineControls;
880
+ exports.emptyAnalysisContext = emptyAnalysisContext;
881
+ exports.getCheckSquareFromChess = getCheckSquareFromChess;
882
+ exports.isAnalysisAvailable = isAnalysisAvailable;
883
+ exports.playerColorForSolution = playerColorForSolution;
884
+ exports.squareHighlightColors = squareHighlightColors;
885
+ exports.usePuzzleAnalysis = usePuzzleAnalysis;