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