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