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.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,
|
|
25
|
+
const isAnalysisAvailable = (position, _resultStatus) => {
|
|
27
26
|
if (!position) {
|
|
28
27
|
return false;
|
|
29
28
|
}
|
|
30
|
-
return
|
|
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
|
|
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.
|
|
64
|
-
* position loads so
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
92
|
-
}
|
|
217
|
+
snapBoardBack();
|
|
218
|
+
};
|
|
219
|
+
if (showAnswerArrowOnIncorrect && !allowRetryOnIncorrect) {
|
|
220
|
+
revealIncorrectFeedback();
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
setTimeout(revealIncorrectFeedback, 500);
|
|
224
|
+
}
|
|
93
225
|
return false;
|
|
94
226
|
}
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
249
|
+
notifyHost();
|
|
105
250
|
}, 500);
|
|
106
251
|
if (position.isAlternativeCheckmate()) {
|
|
107
|
-
|
|
252
|
+
notifyHost();
|
|
108
253
|
return true;
|
|
109
254
|
}
|
|
110
255
|
position.next();
|
|
111
|
-
|
|
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
|
-
|
|
265
|
+
notifyHost();
|
|
117
266
|
}, 500);
|
|
118
267
|
return true;
|
|
119
268
|
};
|
|
120
|
-
return (jsxRuntime.jsx(
|
|
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
|
-
|
|
132
|
-
alignItems: '
|
|
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
|
-
/**
|
|
155
|
-
const
|
|
156
|
-
|
|
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
|
|
168
|
-
minHeight: PUZZLE_CONTROLS_MIN_HEIGHT,
|
|
169
|
-
flexShrink: 0,
|
|
348
|
+
const puzzleControlsFeedbackStyle = (placement = 'beside') => ({
|
|
170
349
|
display: 'flex',
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
/**
|
|
476
|
-
const
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
536
|
-
clearTimeout(openingMoveTimeoutId);
|
|
537
|
-
}
|
|
802
|
+
clearResumeAnimation();
|
|
538
803
|
};
|
|
539
804
|
}, [puzzleNum]);
|
|
540
805
|
const handleFeedback = (feedbackData) => {
|
|
541
|
-
|
|
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'
|
|
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
|
-
|
|
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:
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
|
834
|
-
const
|
|
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
|
-
(
|
|
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
|
-
|
|
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;
|