react-chess-puzzle-kit 1.0.4 → 1.0.5
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/dist/features/board/LineBoard.d.ts +2 -1
- package/dist/features/board/PuzzleBoardWithControls.d.ts +8 -2
- package/dist/features/board/PuzzlePlaySurface.d.ts +10 -1
- package/dist/features/board/usePuzzleAutoAdvanceCountdown.d.ts +6 -0
- package/dist/features/board/usePuzzleCompletionRecap.d.ts +17 -0
- package/dist/features/position/Position.d.ts +4 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +231 -47
- package/dist/index.js +231 -45
- package/package.json +3 -3
|
@@ -5,6 +5,7 @@ export interface LineBoardProps {
|
|
|
5
5
|
trainSide: LineTrainSide;
|
|
6
6
|
draggable: boolean;
|
|
7
7
|
correctMoveSquare?: string | null;
|
|
8
|
+
incorrectMoveSquare?: string | null;
|
|
8
9
|
lastMoveUci?: string | null;
|
|
9
10
|
onPieceDrop: (source: string, target: string, piece: string) => boolean;
|
|
10
11
|
boardWidth: number;
|
|
@@ -14,4 +15,4 @@ export interface LineBoardProps {
|
|
|
14
15
|
* side's pieces be dragged. Move validation and sequencing live in
|
|
15
16
|
* {@link LineBoardWithControls}.
|
|
16
17
|
*/
|
|
17
|
-
export declare const LineBoard: ({ fen, orientation, trainSide, draggable, correctMoveSquare, lastMoveUci, onPieceDrop, boardWidth, }: LineBoardProps) => import("react").JSX.Element;
|
|
18
|
+
export declare const LineBoard: ({ fen, orientation, trainSide, draggable, correctMoveSquare, incorrectMoveSquare, lastMoveUci, onPieceDrop, boardWidth, }: LineBoardProps) => import("react").JSX.Element;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { PuzzleResultStatus } from '../analysis';
|
|
3
3
|
import { AnalysisContainerRenderProps, AnalysisControls, AnalysisEngineOptions, AnalysisLayoutConfig, AnalysisMainRenderProps, AnalysisSidebarRenderProps, EngineEvaluationRenderProps, type BoardThemeId } from 'react-chess-core';
|
|
4
|
+
import { type PuzzleAutoAdvanceState } from './usePuzzleAutoAdvanceCountdown';
|
|
4
5
|
import { type PuzzleControlState } from './defaults/DefaultPuzzleControls';
|
|
5
6
|
import { type PuzzleMissFeedback } from './PuzzlePlaySurface';
|
|
6
7
|
export type { PuzzleMoveRecord } from '../position/moveHistory';
|
|
@@ -10,6 +11,7 @@ export { DEFAULT_ANALYSIS_LAYOUT } from 'react-chess-core';
|
|
|
10
11
|
export { DEFAULT_PUZZLE_BOARD_WIDTH } from './puzzleBoardLayout';
|
|
11
12
|
export { DefaultPuzzleControls, defaultRenderControls, } from './defaults/DefaultPuzzleControls';
|
|
12
13
|
export type { PuzzleControlState, PuzzleControlsRenderProps, } from './defaults/DefaultPuzzleControls';
|
|
14
|
+
export type { PuzzleAutoAdvanceState } from './usePuzzleAutoAdvanceCountdown';
|
|
13
15
|
export type BoardCaptionRenderProps = {
|
|
14
16
|
/** null while the puzzle position is loading */
|
|
15
17
|
sideToMove: 'white' | 'black' | null;
|
|
@@ -76,7 +78,7 @@ export interface PuzzleBoardWithControlsProps {
|
|
|
76
78
|
}) => void;
|
|
77
79
|
};
|
|
78
80
|
/** Omit to use {@link defaultRenderControls} / {@link DefaultPuzzleControls}. */
|
|
79
|
-
renderControls?: (showHint: () => void, showSolution: () => void, nextPuzzle: () => void, resultStatus: PuzzleResultStatus, analysis: AnalysisControls, controlState: PuzzleControlState) => React.ReactNode;
|
|
81
|
+
renderControls?: (showHint: () => void, showSolution: () => void, nextPuzzle: () => void, resultStatus: PuzzleResultStatus, analysis: AnalysisControls, controlState: PuzzleControlState, autoAdvance?: PuzzleAutoAdvanceState) => React.ReactNode;
|
|
80
82
|
renderAnalysisSidebar?: (props: AnalysisSidebarRenderProps) => React.ReactNode;
|
|
81
83
|
renderAnalysisContainer?: (props: AnalysisContainerRenderProps) => React.ReactNode;
|
|
82
84
|
renderEngineEvaluation?: (props: EngineEvaluationRenderProps) => React.ReactNode;
|
|
@@ -97,6 +99,10 @@ export interface PuzzleBoardWithControlsProps {
|
|
|
97
99
|
autoAdvanceOnComplete?: boolean;
|
|
98
100
|
/** With {@link autoAdvanceOnComplete}, also advance after finishing following a miss or hint. */
|
|
99
101
|
autoAdvanceOnCompleteAfterIncorrect?: boolean;
|
|
102
|
+
/** Delay before auto-loading the next card (defaults to {@link AUTO_ADVANCE_ON_COMPLETE_DELAY_MS}). */
|
|
103
|
+
autoAdvanceOnCompleteDelayMs?: number;
|
|
104
|
+
/** Replay missed solution plies on the board before auto-advancing. */
|
|
105
|
+
showCompletionRecap?: boolean;
|
|
100
106
|
/** After a wrong guess, play the correct move and wait for the user to advance. */
|
|
101
107
|
revealAnswerOnIncorrect?: boolean;
|
|
102
108
|
/** After a wrong guess, show an arrow to the correct square. */
|
|
@@ -111,4 +117,4 @@ export interface PuzzleBoardWithControlsProps {
|
|
|
111
117
|
refutationEngine?: AnalysisEngineOptions;
|
|
112
118
|
answerArrowColor?: string;
|
|
113
119
|
}
|
|
114
|
-
export declare const PuzzleBoardWithControls: ({ theme, boardTheme, apiProxy, renderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth, analysisLayout, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete, autoAdvanceOnCompleteAfterIncorrect, revealAnswerOnIncorrect, showAnswerArrowOnIncorrect, allowRetryOnIncorrect, showRefutationOnIncorrect, autoShowWrongMoves, refutationEngine, answerArrowColor, }: PuzzleBoardWithControlsProps) => React.JSX.Element;
|
|
120
|
+
export declare const PuzzleBoardWithControls: ({ theme, boardTheme, apiProxy, renderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth, analysisLayout, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete, autoAdvanceOnCompleteAfterIncorrect, autoAdvanceOnCompleteDelayMs, showCompletionRecap, revealAnswerOnIncorrect, showAnswerArrowOnIncorrect, allowRetryOnIncorrect, showRefutationOnIncorrect, autoShowWrongMoves, refutationEngine, answerArrowColor, }: PuzzleBoardWithControlsProps) => React.JSX.Element;
|
|
@@ -6,6 +6,13 @@ export type PuzzleMissFeedback = {
|
|
|
6
6
|
/** True while the board shows the correct-move answer arrow. */
|
|
7
7
|
answerArrowVisible: boolean;
|
|
8
8
|
};
|
|
9
|
+
/** Board state driven by the post-completion solution recap animation. */
|
|
10
|
+
export type PuzzleRecapBoardState = {
|
|
11
|
+
fen: string;
|
|
12
|
+
lastMoveUci: string | null;
|
|
13
|
+
customArrows: [string, string, string][];
|
|
14
|
+
animationDuration: number;
|
|
15
|
+
};
|
|
9
16
|
export interface PuzzlePlaySurfaceProps {
|
|
10
17
|
position: PuzzlePosition | null;
|
|
11
18
|
onFeedback: (feedbackData: {
|
|
@@ -41,9 +48,11 @@ export interface PuzzlePlaySurfaceProps {
|
|
|
41
48
|
positionLocked?: boolean;
|
|
42
49
|
/** Fired when refutation miss feedback changes (for host UI). */
|
|
43
50
|
onMissFeedbackChange?: (feedback: PuzzleMissFeedback | null) => void;
|
|
51
|
+
/** When set, replaces the live puzzle position with the completion recap board. */
|
|
52
|
+
recapBoard?: PuzzleRecapBoardState | null;
|
|
44
53
|
}
|
|
45
54
|
/**
|
|
46
55
|
* Single mounted board for puzzle play. Keeps the prior board (and orientation)
|
|
47
56
|
* visible while the next position loads so layout and perspective do not flicker.
|
|
48
57
|
*/
|
|
49
|
-
export declare const PuzzlePlaySurface: ({ position, onFeedback, incInteractionNum, boardWidth, onResumeCorrect, revealAnswerOnIncorrect, showAnswerArrowOnIncorrect, allowRetryOnIncorrect, showRefutationOnIncorrect, autoShowWrongMoves, refutationEngine, answerArrowColor, positionLocked, onMissFeedbackChange, }: PuzzlePlaySurfaceProps) => import("react").JSX.Element;
|
|
58
|
+
export declare const PuzzlePlaySurface: ({ position, onFeedback, incInteractionNum, boardWidth, onResumeCorrect, revealAnswerOnIncorrect, showAnswerArrowOnIncorrect, allowRetryOnIncorrect, showRefutationOnIncorrect, autoShowWrongMoves, refutationEngine, answerArrowColor, positionLocked, onMissFeedbackChange, recapBoard, }: PuzzlePlaySurfaceProps) => import("react").JSX.Element | null;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type PuzzleAutoAdvanceState = {
|
|
2
|
+
active: boolean;
|
|
3
|
+
secondsRemaining: number;
|
|
4
|
+
};
|
|
5
|
+
/** Countdown overlay state while waiting to auto-load the next puzzle card. */
|
|
6
|
+
export declare function usePuzzleAutoAdvanceCountdown(enabled: boolean, delayMs: number, onAdvance: () => void): PuzzleAutoAdvanceState;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type SolutionLineRecapState } from 'react-chess-core';
|
|
2
|
+
/** Pause on the puzzle setup position before the solution recap animates. */
|
|
3
|
+
export declare const PUZZLE_COMPLETION_RECAP_SETUP_MS = 400;
|
|
4
|
+
export type PuzzleCompletionRecapSource = {
|
|
5
|
+
startFen: string;
|
|
6
|
+
movesUci: string[];
|
|
7
|
+
startIndex: number;
|
|
8
|
+
endIndex: number;
|
|
9
|
+
missedIndices: number[];
|
|
10
|
+
setupUci?: string | null;
|
|
11
|
+
};
|
|
12
|
+
export type PuzzleCompletionRecapState = SolutionLineRecapState;
|
|
13
|
+
export declare const usePuzzleCompletionRecap: ({ source, active, onComplete, }: {
|
|
14
|
+
source: PuzzleCompletionRecapSource | null;
|
|
15
|
+
active: boolean;
|
|
16
|
+
onComplete: () => void;
|
|
17
|
+
}) => PuzzleCompletionRecapState;
|
|
@@ -21,6 +21,10 @@ export declare abstract class Position implements Traversable {
|
|
|
21
21
|
export declare function getCheckSquareFromChess(chess: Chess): string;
|
|
22
22
|
/** Side to move for the final (user) ply in a puzzle line. */
|
|
23
23
|
export declare function playerColorForSolution(initialFen: string, moves: string[]): 'white' | 'black';
|
|
24
|
+
/** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
|
|
25
|
+
export declare function playerMoveIndicesInRange(initialFen: string, moves: string[], fromIndex: number, toIndex: number): number[];
|
|
26
|
+
/** Auto-play opponent setup plies until the solver is on move. */
|
|
27
|
+
export declare function advanceToPlayerTurn(position: PuzzlePosition): void;
|
|
24
28
|
export type PuzzleResumeConfig = {
|
|
25
29
|
startIndex: number;
|
|
26
30
|
quizAtIndices: number[];
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './features/board/LineBoardWithControls';
|
|
|
10
10
|
export { DefaultPuzzleControls, defaultRenderControls, } from './features/board/defaults/DefaultPuzzleControls';
|
|
11
11
|
export { DefaultLineControls, defaultRenderLineControls, } from './features/board/defaults/DefaultLineControls';
|
|
12
12
|
export type { PuzzleControlState, PuzzleControlsRenderProps, } from './features/board/defaults/DefaultPuzzleControls';
|
|
13
|
+
export type { PuzzleAutoAdvanceState } from './features/board/usePuzzleAutoAdvanceCountdown';
|
|
13
14
|
export { DEFAULT_PUZZLE_BOARD_WIDTH, PUZZLE_CONTROLS_BESIDE_RESERVE_PX, PUZZLE_CONTROLS_STACK_BREAKPOINT_PX, } from './features/board/puzzleBoardLayout';
|
|
14
15
|
export * from './features/position/moveHistory';
|
|
15
16
|
export * from './features/position/Position';
|
package/dist/index.esm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|
2
2
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
-
import { useBoardRevision, useCorrectMoveFeedback,
|
|
3
|
+
import { useBoardRevision, useCorrectMoveFeedback, useIncorrectMoveFeedback, useMissBoard, HighlightChessboard, DEFAULT_ANSWER_ARROW_COLOR, uciFromDrop, fenAtPlyFromStart, useSolutionLineRecap, ThemeProvider, AnalysisErrorBoundary, AnalysisBoardCore, AnalysisBoardLayout, AnalysisBoard, BoardCompleteCheckOverlay, DEFAULT_ANALYSIS_LAYOUT, AUTO_ADVANCE_ON_COMPLETE_DELAY_MS, lastMoveUciAtPly, evaluateExpectedMoveDrop, fenAfterUci, boardSquareHighlightColors, analysisBoardHighlightColors } from 'react-chess-core';
|
|
4
4
|
export { DEFAULT_ANALYSIS_LAYOUT } from 'react-chess-core';
|
|
5
5
|
import { Chess } from 'chess.js';
|
|
6
6
|
|
|
@@ -56,18 +56,18 @@ const usePuzzleAnalysis = (position, resultStatus, puzzleNum) => {
|
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
const EMPTY_BOARD_FEN = '8/8/8/8/8/8/8/8 w - - 0 1';
|
|
59
|
-
const DEFAULT_ANSWER_ARROW_COLOR = '#42a5f5';
|
|
60
59
|
/**
|
|
61
60
|
* Single mounted board for puzzle play. Keeps the prior board (and orientation)
|
|
62
61
|
* visible while the next position loads so layout and perspective do not flicker.
|
|
63
62
|
*/
|
|
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
|
|
63
|
+
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, recapBoard = null, }) => {
|
|
64
|
+
var _a, _b, _c, _d, _e, _f;
|
|
66
65
|
const [showAnswerArrow, setShowAnswerArrow] = useState(false);
|
|
67
66
|
const [incorrectActive, setIncorrectActive] = useState(false);
|
|
68
67
|
const attemptMissedRef = useRef(false);
|
|
69
68
|
const { revision, bumpRevision } = useBoardRevision();
|
|
70
69
|
const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, } = useCorrectMoveFeedback();
|
|
70
|
+
const { incorrectMoveSquare: transientIncorrectSquare, showIncorrectMove, clearIncorrectMoveFeedback, } = useIncorrectMoveFeedback();
|
|
71
71
|
const boardOrientationRef = useRef('white');
|
|
72
72
|
const boardFenRef = useRef(EMPTY_BOARD_FEN);
|
|
73
73
|
const notifyHost = () => {
|
|
@@ -102,13 +102,25 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
102
102
|
const answerArrowVisible = useRefutation
|
|
103
103
|
? incorrectActive && missPhase === 'answer'
|
|
104
104
|
: showAnswerArrow;
|
|
105
|
+
const overlayIncorrectSquare = transientIncorrectSquare !== null && transientIncorrectSquare !== void 0 ? transientIncorrectSquare : (useRefutation && incorrectActive
|
|
106
|
+
? missBoard.missSequence.display.incorrectMoveSquare
|
|
107
|
+
: null);
|
|
108
|
+
const refutationMoveSquare = useRefutation && incorrectActive
|
|
109
|
+
? missBoard.missSequence.display.refutationMoveSquare
|
|
110
|
+
: null;
|
|
105
111
|
useEffect(() => {
|
|
106
112
|
setShowAnswerArrow(false);
|
|
107
113
|
setIncorrectActive(false);
|
|
108
114
|
attemptMissedRef.current = false;
|
|
109
115
|
clearCorrectMoveFeedback();
|
|
116
|
+
clearIncorrectMoveFeedback();
|
|
110
117
|
onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
|
|
111
|
-
}, [
|
|
118
|
+
}, [
|
|
119
|
+
clearCorrectMoveFeedback,
|
|
120
|
+
clearIncorrectMoveFeedback,
|
|
121
|
+
onMissFeedbackChange,
|
|
122
|
+
position,
|
|
123
|
+
]);
|
|
112
124
|
useEffect(() => {
|
|
113
125
|
var _a, _b;
|
|
114
126
|
if (!onMissFeedbackChange) {
|
|
@@ -140,11 +152,13 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
140
152
|
showAnswerArrow,
|
|
141
153
|
useRefutation,
|
|
142
154
|
]);
|
|
155
|
+
const boardOrientation = position
|
|
156
|
+
? position.getPlayerColor()
|
|
157
|
+
: boardOrientationRef.current;
|
|
143
158
|
if (position) {
|
|
144
|
-
boardOrientationRef.current =
|
|
159
|
+
boardOrientationRef.current = boardOrientation;
|
|
145
160
|
boardFenRef.current = position.fen();
|
|
146
161
|
}
|
|
147
|
-
const boardOrientation = boardOrientationRef.current;
|
|
148
162
|
const boardFen = boardFenRef.current;
|
|
149
163
|
const hasBoard = boardFen !== EMPTY_BOARD_FEN;
|
|
150
164
|
const simpleArrows = useMemo(() => {
|
|
@@ -157,19 +171,28 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
157
171
|
}
|
|
158
172
|
return [[moveUci.slice(0, 2), moveUci.slice(2, 4), answerArrowColor]];
|
|
159
173
|
}, [showAnswerArrow, position, answerArrowColor, useRefutation]);
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
const isRecapping = recapBoard !== null;
|
|
175
|
+
const customArrows = isRecapping
|
|
176
|
+
? recapBoard.customArrows
|
|
177
|
+
: useRefutation && incorrectActive
|
|
178
|
+
? missBoard.customArrows
|
|
179
|
+
: simpleArrows;
|
|
180
|
+
const displayFen = isRecapping
|
|
181
|
+
? recapBoard.fen
|
|
182
|
+
: useRefutation && incorrectActive
|
|
183
|
+
? missBoard.boardPosition
|
|
184
|
+
: boardFen;
|
|
164
185
|
const missLocked = useRefutation &&
|
|
165
186
|
incorrectActive &&
|
|
166
187
|
(missBoard.boardAnimating ||
|
|
167
188
|
missPhase === 'wrong' ||
|
|
168
189
|
missPhase === 'refutation');
|
|
169
|
-
const arePiecesDraggable =
|
|
190
|
+
const arePiecesDraggable = !isRecapping &&
|
|
191
|
+
position !== null &&
|
|
170
192
|
!positionLocked &&
|
|
171
193
|
!missLocked &&
|
|
172
|
-
correctMoveSquare === null
|
|
194
|
+
correctMoveSquare === null &&
|
|
195
|
+
overlayIncorrectSquare === null;
|
|
173
196
|
const onPieceDrop = (sourceSquare, targetSquare, piece) => {
|
|
174
197
|
if (!position || positionLocked || position.isSolutionRevealed()) {
|
|
175
198
|
return false;
|
|
@@ -183,6 +206,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
183
206
|
if (answerArrowVisible &&
|
|
184
207
|
!allowRetryOnIncorrect &&
|
|
185
208
|
!position.isExpectedGuess(sourceSquare, targetSquare)) {
|
|
209
|
+
showIncorrectMove(sourceSquare);
|
|
186
210
|
position.resetInteractions();
|
|
187
211
|
snapBoardBack();
|
|
188
212
|
return false;
|
|
@@ -192,6 +216,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
192
216
|
});
|
|
193
217
|
if (!guess.accepted) {
|
|
194
218
|
attemptMissedRef.current = true;
|
|
219
|
+
showIncorrectMove(useRefutation ? targetSquare : sourceSquare);
|
|
195
220
|
onFeedback({
|
|
196
221
|
index: position.getIndex(),
|
|
197
222
|
guess: { sourceSquare, targetSquare, piece },
|
|
@@ -205,8 +230,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
205
230
|
missBoard.missSequence.startSequence(setupFen, attemptedUci);
|
|
206
231
|
}
|
|
207
232
|
position.resetInteractions();
|
|
208
|
-
|
|
209
|
-
return false;
|
|
233
|
+
return true;
|
|
210
234
|
}
|
|
211
235
|
const revealIncorrectFeedback = () => {
|
|
212
236
|
if (showAnswerArrowOnIncorrect) {
|
|
@@ -272,13 +296,79 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
272
296
|
showCorrectMove(targetSquare, finishCorrectFeedback);
|
|
273
297
|
return true;
|
|
274
298
|
};
|
|
275
|
-
return
|
|
276
|
-
? null
|
|
277
|
-
: ((_g = position === null || position === void 0 ? void 0 : position.getIncorrectMoveSquare()) !== null && _g !== void 0 ? _g : null), correctMoveSquare: correctMoveSquare, customArrows: customArrows, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: 0 }, revision)) : null }));
|
|
299
|
+
return hasBoard ? (jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: isRecapping ? '' : ((_e = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _e !== void 0 ? _e : ''), hintSquare: isRecapping ? null : ((_f = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _f !== void 0 ? _f : null), incorrectMoveSquare: isRecapping ? null : overlayIncorrectSquare, refutationMoveSquare: isRecapping ? null : refutationMoveSquare, correctMoveSquare: isRecapping ? null : correctMoveSquare, customArrows: customArrows, lastMoveUci: isRecapping ? recapBoard.lastMoveUci : null, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: isRecapping ? recapBoard.animationDuration : 0 }, revision)) : null;
|
|
278
300
|
};
|
|
279
301
|
|
|
280
302
|
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 }));
|
|
281
303
|
|
|
304
|
+
const inactiveAutoAdvance = {
|
|
305
|
+
active: false,
|
|
306
|
+
secondsRemaining: -1,
|
|
307
|
+
};
|
|
308
|
+
/** Countdown overlay state while waiting to auto-load the next puzzle card. */
|
|
309
|
+
function usePuzzleAutoAdvanceCountdown(enabled, delayMs, onAdvance) {
|
|
310
|
+
const [secondsRemaining, setSecondsRemaining] = useState(-1);
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (!enabled || delayMs <= 0) {
|
|
313
|
+
setSecondsRemaining(-1);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const startedAt = Date.now();
|
|
317
|
+
const updateCountdown = () => {
|
|
318
|
+
const elapsed = Date.now() - startedAt;
|
|
319
|
+
setSecondsRemaining(Math.max(0, Math.ceil((delayMs - elapsed) / 1000)));
|
|
320
|
+
};
|
|
321
|
+
updateCountdown();
|
|
322
|
+
const intervalId = window.setInterval(updateCountdown, 200);
|
|
323
|
+
const timeoutId = window.setTimeout(() => {
|
|
324
|
+
window.clearInterval(intervalId);
|
|
325
|
+
setSecondsRemaining(-1);
|
|
326
|
+
onAdvance();
|
|
327
|
+
}, delayMs);
|
|
328
|
+
return () => {
|
|
329
|
+
window.clearInterval(intervalId);
|
|
330
|
+
window.clearTimeout(timeoutId);
|
|
331
|
+
setSecondsRemaining(-1);
|
|
332
|
+
};
|
|
333
|
+
}, [delayMs, enabled, onAdvance]);
|
|
334
|
+
if (!enabled || secondsRemaining < 0) {
|
|
335
|
+
return inactiveAutoAdvance;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
active: true,
|
|
339
|
+
secondsRemaining,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Pause on the puzzle setup position before the solution recap animates. */
|
|
344
|
+
const PUZZLE_COMPLETION_RECAP_SETUP_MS = 400;
|
|
345
|
+
const usePuzzleCompletionRecap = ({ source, active, onComplete, }) => {
|
|
346
|
+
var _a, _b, _c, _d, _e, _f;
|
|
347
|
+
const startFen = (_a = source === null || source === void 0 ? void 0 : source.startFen) !== null && _a !== void 0 ? _a : '';
|
|
348
|
+
const movesUci = (_b = source === null || source === void 0 ? void 0 : source.movesUci) !== null && _b !== void 0 ? _b : [];
|
|
349
|
+
const startIndex = (_c = source === null || source === void 0 ? void 0 : source.startIndex) !== null && _c !== void 0 ? _c : 0;
|
|
350
|
+
const endIndex = (_d = source === null || source === void 0 ? void 0 : source.endIndex) !== null && _d !== void 0 ? _d : 0;
|
|
351
|
+
const missedIndices = (_e = source === null || source === void 0 ? void 0 : source.missedIndices) !== null && _e !== void 0 ? _e : [];
|
|
352
|
+
const setupUci = (_f = source === null || source === void 0 ? void 0 : source.setupUci) !== null && _f !== void 0 ? _f : null;
|
|
353
|
+
const resolveFen = useCallback((moveIndex, afterMove) => {
|
|
354
|
+
if (!startFen || movesUci.length === 0) {
|
|
355
|
+
return '';
|
|
356
|
+
}
|
|
357
|
+
return fenAtPlyFromStart(startFen, movesUci, afterMove ? moveIndex + 1 : moveIndex);
|
|
358
|
+
}, [movesUci, startFen]);
|
|
359
|
+
return useSolutionLineRecap({
|
|
360
|
+
active: active && source !== null,
|
|
361
|
+
movesUci,
|
|
362
|
+
startIndex,
|
|
363
|
+
endIndex,
|
|
364
|
+
missedIndices,
|
|
365
|
+
segmentStartFen: startFen,
|
|
366
|
+
setupUci,
|
|
367
|
+
onComplete,
|
|
368
|
+
resolveFen,
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
|
|
282
372
|
const isAttemptFinished = (resultStatus) => resultStatus === 'complete' || resultStatus === 'incorrect';
|
|
283
373
|
/** Library default hint / next / analysis / result controls (unstyled buttons). */
|
|
284
374
|
const DefaultPuzzleControls = ({ showHint, showSolution, nextPuzzle, resultStatus, analysis, controlState, }) => (jsxs("div", { style: rowStyle$1, children: [jsx("button", { type: "button", onClick: showHint, style: buttonStyle, disabled: !controlState.canShowHint, children: "Hint" }), jsx("button", { type: "button", onClick: showSolution, style: buttonStyle, disabled: !controlState.canShowSolution, children: "Show solution" }), jsx("button", { type: "button", onClick: nextPuzzle, style: buttonStyle, children: "Next puzzle" }), analysis.visible && isAttemptFinished(resultStatus) && (jsx("button", { type: "button", onClick: analysis.openAnalysis, style: buttonStyle, children: "Analysis" })), resultStatus === 'complete' && (jsx("span", { style: statusStyle$1, children: "Complete" })), resultStatus === 'incorrect' && (jsx("span", { style: Object.assign(Object.assign({}, statusStyle$1), { color: '#c62828' }), children: "Incorrect" }))] }));
|
|
@@ -483,6 +573,29 @@ function playerColorForSolution(initialFen, moves) {
|
|
|
483
573
|
}
|
|
484
574
|
return chess.turn() === 'w' ? 'white' : 'black';
|
|
485
575
|
}
|
|
576
|
+
/** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
|
|
577
|
+
function playerMoveIndicesInRange(initialFen, moves, fromIndex, toIndex) {
|
|
578
|
+
const playerColor = playerColorForSolution(initialFen, moves);
|
|
579
|
+
const chess = new Chess(initialFen);
|
|
580
|
+
const indices = [];
|
|
581
|
+
for (let i = 0; i < toIndex && i < moves.length; i++) {
|
|
582
|
+
const side = chess.turn() === 'w' ? 'white' : 'black';
|
|
583
|
+
if (i >= fromIndex && side === playerColor) {
|
|
584
|
+
indices.push(i);
|
|
585
|
+
}
|
|
586
|
+
applyUciMove(chess, moves[i]);
|
|
587
|
+
}
|
|
588
|
+
return indices;
|
|
589
|
+
}
|
|
590
|
+
/** Auto-play opponent setup plies until the solver is on move. */
|
|
591
|
+
function advanceToPlayerTurn(position) {
|
|
592
|
+
while (!position.isFinished() &&
|
|
593
|
+
position.getSideToMove() !== position.getPlayerColor()) {
|
|
594
|
+
if (!position.next()) {
|
|
595
|
+
break;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
486
599
|
class PuzzlePosition extends Position {
|
|
487
600
|
constructor(initialFEN, moves, resumeConfig) {
|
|
488
601
|
super();
|
|
@@ -727,19 +840,37 @@ class GamePosition extends Position {
|
|
|
727
840
|
}
|
|
728
841
|
}
|
|
729
842
|
|
|
730
|
-
/** Apply
|
|
843
|
+
/** Apply opponent setup plies immediately so the board does not flash on load. */
|
|
731
844
|
const puzzlePositionFromFetch = (fen, moves, resume) => {
|
|
732
845
|
const newPosition = new PuzzlePosition(fen, moves, resume);
|
|
733
846
|
if (!resume && moves.length > 1) {
|
|
734
847
|
newPosition.next();
|
|
848
|
+
advanceToPlayerTurn(newPosition);
|
|
849
|
+
}
|
|
850
|
+
else if (resume &&
|
|
851
|
+
newPosition.getSideToMove() !== newPosition.getPlayerColor()) {
|
|
852
|
+
advanceToPlayerTurn(newPosition);
|
|
735
853
|
}
|
|
736
854
|
return newPosition;
|
|
737
855
|
};
|
|
738
|
-
/** Brief pause so the user sees a correct result before the next card loads. */
|
|
739
|
-
const AUTO_ADVANCE_ON_COMPLETE_DELAY_MS = 700;
|
|
740
856
|
const SOLUTION_STEP_MS = 500;
|
|
741
857
|
const RESUME_AUTO_STEP_MS = 500;
|
|
742
|
-
const
|
|
858
|
+
const uniqueIndices = (indices) => [...new Set(indices)];
|
|
859
|
+
const buildCompletionRecapSource = (position, missedIndices) => {
|
|
860
|
+
var _a, _b;
|
|
861
|
+
const movesUci = position.getSolutionMoves();
|
|
862
|
+
const initialFen = position.getInitialFen();
|
|
863
|
+
const startIndex = (_a = playerMoveIndicesInRange(initialFen, movesUci, 0, movesUci.length)[0]) !== null && _a !== void 0 ? _a : 0;
|
|
864
|
+
return {
|
|
865
|
+
startFen: initialFen,
|
|
866
|
+
movesUci,
|
|
867
|
+
startIndex,
|
|
868
|
+
endIndex: movesUci.length,
|
|
869
|
+
missedIndices,
|
|
870
|
+
setupUci: startIndex > 0 ? (_b = movesUci[startIndex - 1]) !== null && _b !== void 0 ? _b : null : null,
|
|
871
|
+
};
|
|
872
|
+
};
|
|
873
|
+
const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = DEFAULT_ANALYSIS_LAYOUT, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, autoAdvanceOnCompleteDelayMs = AUTO_ADVANCE_ON_COMPLETE_DELAY_MS, showCompletionRecap = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
|
|
743
874
|
var _a, _b, _c, _d;
|
|
744
875
|
const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
|
|
745
876
|
const stackControlsBelow = useStackPuzzleControlsBelow();
|
|
@@ -755,6 +886,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
755
886
|
const [puzzleComplete, setPuzzleComplete] = useState(false);
|
|
756
887
|
const [completedAfterMiss, setCompletedAfterMiss] = useState(false);
|
|
757
888
|
const [missFeedback, setMissFeedback] = useState(null);
|
|
889
|
+
const [missedMoveIndices, setMissedMoveIndices] = useState([]);
|
|
890
|
+
const [completionCheckVisible, setCompletionCheckVisible] = useState(false);
|
|
891
|
+
const [completionRecapActive, setCompletionRecapActive] = useState(false);
|
|
892
|
+
const [completionRecapDone, setCompletionRecapDone] = useState(false);
|
|
893
|
+
const completionFlowStartedRef = useRef(false);
|
|
758
894
|
const [, setInteractionNum] = useState(0);
|
|
759
895
|
const solutionAnimationRef = useRef({ cancelled: false, timeoutIds: [] });
|
|
760
896
|
const resumeAnimationRef = useRef({ cancelled: false, timeoutIds: [] });
|
|
@@ -781,6 +917,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
781
917
|
setPuzzleComplete(false);
|
|
782
918
|
setCompletedAfterMiss(false);
|
|
783
919
|
setMissFeedback(null);
|
|
920
|
+
setMissedMoveIndices([]);
|
|
921
|
+
setCompletionCheckVisible(false);
|
|
922
|
+
setCompletionRecapActive(false);
|
|
923
|
+
setCompletionRecapDone(false);
|
|
924
|
+
completionFlowStartedRef.current = false;
|
|
784
925
|
onFetch()
|
|
785
926
|
.then((data) => {
|
|
786
927
|
if (cancelled) {
|
|
@@ -816,10 +957,16 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
816
957
|
feedbackData.isCorrect === false;
|
|
817
958
|
if (feedbackData.hintRequested) {
|
|
818
959
|
setHintUsed(true);
|
|
960
|
+
setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
|
|
819
961
|
}
|
|
820
962
|
if (incorrectThisFeedback) {
|
|
821
963
|
setHasIncorrectAttempt(true);
|
|
822
964
|
}
|
|
965
|
+
if (feedbackData.isCorrect === false &&
|
|
966
|
+
!feedbackData.isFinished &&
|
|
967
|
+
!feedbackData.solutionShown) {
|
|
968
|
+
setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
|
|
969
|
+
}
|
|
823
970
|
if (feedbackData.isFinished) {
|
|
824
971
|
setPuzzleComplete(true);
|
|
825
972
|
setCompletedAfterMiss((prev) => prev ||
|
|
@@ -983,6 +1130,10 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
983
1130
|
position.recordSolutionShown();
|
|
984
1131
|
position.setSolutionRevealed(true);
|
|
985
1132
|
position.wantsHint(false);
|
|
1133
|
+
setMissedMoveIndices((prev) => uniqueIndices([
|
|
1134
|
+
...prev,
|
|
1135
|
+
...playerMoveIndicesInRange(position.getInitialFen(), position.getSolutionMoves(), position.getIndex(), position.getSolutionMoves().length),
|
|
1136
|
+
]));
|
|
986
1137
|
handleFeedback({
|
|
987
1138
|
index: position.getIndex(),
|
|
988
1139
|
solutionShown: true,
|
|
@@ -995,30 +1146,46 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
995
1146
|
setPuzzleNum((prevPuzzleNum) => prevPuzzleNum + 1);
|
|
996
1147
|
}, []);
|
|
997
1148
|
const resultStatus = getResultStatus();
|
|
1149
|
+
const completionRecapSource = useMemo(() => position && showCompletionRecap
|
|
1150
|
+
? buildCompletionRecapSource(position, missedMoveIndices)
|
|
1151
|
+
: null, [position, showCompletionRecap, missedMoveIndices]);
|
|
1152
|
+
const handleCompletionRecapDone = useCallback(() => {
|
|
1153
|
+
setCompletionRecapActive(false);
|
|
1154
|
+
setCompletionRecapDone(true);
|
|
1155
|
+
}, []);
|
|
1156
|
+
const completionRecap = usePuzzleCompletionRecap({
|
|
1157
|
+
source: completionRecapSource,
|
|
1158
|
+
active: completionRecapActive,
|
|
1159
|
+
onComplete: handleCompletionRecapDone,
|
|
1160
|
+
});
|
|
1161
|
+
const isCompletionRecapping = showCompletionRecap && (completionRecapActive || completionRecap.active);
|
|
998
1162
|
useEffect(() => {
|
|
999
|
-
if (!
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1163
|
+
if (!showCompletionRecap ||
|
|
1164
|
+
resultStatus !== 'complete' ||
|
|
1165
|
+
loadingNextPuzzle ||
|
|
1166
|
+
completionFlowStartedRef.current) {
|
|
1003
1167
|
return;
|
|
1004
1168
|
}
|
|
1005
|
-
|
|
1169
|
+
completionFlowStartedRef.current = true;
|
|
1170
|
+
setCompletionCheckVisible(true);
|
|
1171
|
+
}, [loadingNextPuzzle, resultStatus, showCompletionRecap]);
|
|
1172
|
+
useEffect(() => {
|
|
1173
|
+
if (!completionCheckVisible) {
|
|
1006
1174
|
return;
|
|
1007
1175
|
}
|
|
1008
|
-
const timer = setTimeout(() => {
|
|
1009
|
-
|
|
1010
|
-
|
|
1176
|
+
const timer = window.setTimeout(() => {
|
|
1177
|
+
setCompletionCheckVisible(false);
|
|
1178
|
+
setCompletionRecapActive(true);
|
|
1179
|
+
}, PUZZLE_COMPLETION_RECAP_SETUP_MS);
|
|
1011
1180
|
return () => {
|
|
1012
|
-
clearTimeout(timer);
|
|
1181
|
+
window.clearTimeout(timer);
|
|
1013
1182
|
};
|
|
1014
|
-
}, [
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
puzzleNum,
|
|
1021
|
-
]);
|
|
1183
|
+
}, [completionCheckVisible]);
|
|
1184
|
+
const shouldAutoAdvance = autoAdvanceOnComplete &&
|
|
1185
|
+
resultStatus === 'complete' &&
|
|
1186
|
+
!(hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) &&
|
|
1187
|
+
(!showCompletionRecap || completionRecapDone);
|
|
1188
|
+
const autoAdvance = usePuzzleAutoAdvanceCountdown(shouldAutoAdvance, autoAdvanceOnCompleteDelayMs, handleNextPuzzle);
|
|
1022
1189
|
const controlState = {
|
|
1023
1190
|
canShowHint: position !== null &&
|
|
1024
1191
|
!position.isFinished() &&
|
|
@@ -1033,7 +1200,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1033
1200
|
const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
|
|
1034
1201
|
renderAnalysisContainer &&
|
|
1035
1202
|
(renderEngineEvaluation || (engine === null || engine === void 0 ? void 0 : engine.enabled) === false));
|
|
1036
|
-
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: resolvedAnalysisBoardWidth, 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: [
|
|
1203
|
+
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: resolvedAnalysisBoardWidth, 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: [jsxs("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 ||
|
|
1204
|
+
completionCheckVisible ||
|
|
1205
|
+
isCompletionRecapping, onMissFeedbackChange: setMissFeedback, recapBoard: isCompletionRecapping
|
|
1206
|
+
? {
|
|
1207
|
+
fen: completionRecap.fen,
|
|
1208
|
+
lastMoveUci: completionRecap.lastMoveUci,
|
|
1209
|
+
customArrows: completionRecap.customArrows,
|
|
1210
|
+
animationDuration: completionRecap.animationDuration,
|
|
1211
|
+
}
|
|
1212
|
+
: null }) }), completionCheckVisible && (jsx(BoardCompleteCheckOverlay, { variant: hasIncorrectAttempt || completedAfterMiss || hintUsed
|
|
1213
|
+
? 'partial'
|
|
1214
|
+
: 'success' }))] }), renderBoardCaption && (jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
|
|
1037
1215
|
sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
|
|
1038
1216
|
playerColor: position
|
|
1039
1217
|
? position.getPlayerColor()
|
|
@@ -1049,7 +1227,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1049
1227
|
}) }))] }), jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
|
|
1050
1228
|
visible: analysis.canOpen,
|
|
1051
1229
|
openAnalysis: analysis.openAnalysis,
|
|
1052
|
-
}, controlState), renderBoardFeedback && resultStatus === 'complete' && (jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
|
|
1230
|
+
}, controlState, autoAdvance), renderBoardFeedback && resultStatus === 'complete' && (jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
|
|
1053
1231
|
resultStatus,
|
|
1054
1232
|
cleanSolve: !hasIncorrectAttempt,
|
|
1055
1233
|
refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
|
|
@@ -1063,7 +1241,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1063
1241
|
* side's pieces be dragged. Move validation and sequencing live in
|
|
1064
1242
|
* {@link LineBoardWithControls}.
|
|
1065
1243
|
*/
|
|
1066
|
-
const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, lastMoveUci = null, onPieceDrop, boardWidth, }) => (jsx(
|
|
1244
|
+
const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, incorrectMoveSquare = null, lastMoveUci = null, onPieceDrop, boardWidth, }) => (jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: incorrectMoveSquare, correctMoveSquare: correctMoveSquare, position: fen, boardOrientation: orientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => piece[0] === trainSide, onPieceDrop: (source, target, piece) => onPieceDrop(source, target, piece), lastMoveUci: lastMoveUci, autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }));
|
|
1067
1245
|
|
|
1068
1246
|
/** Library default line-drill status controls (unstyled). */
|
|
1069
1247
|
const DefaultLineControls = ({ moveNumber, total, finished, isUserTurn, feedback, }) => (jsxs("div", { style: rowStyle, children: [jsx("span", { style: statusStyle, children: finished ? 'Line complete' : `Move ${moveNumber} of ${total}` }), feedback && !finished && (jsx("span", { style: Object.assign(Object.assign({}, statusStyle), { color: feedback.isCorrect ? '#2e7d32' : '#c62828' }), children: feedback.isCorrect
|
|
@@ -1112,6 +1290,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1112
1290
|
const [feedback, setFeedback] = useState(null);
|
|
1113
1291
|
const [displayFen, setDisplayFen] = useState(null);
|
|
1114
1292
|
const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = useCorrectMoveFeedback();
|
|
1293
|
+
const { incorrectMoveSquare, showIncorrectMove, isShowingIncorrectMove, } = useIncorrectMoveFeedback();
|
|
1115
1294
|
const total = line.movesUci.length;
|
|
1116
1295
|
const orientation = boardOrientationForLine(line.trainSide);
|
|
1117
1296
|
const applyMove = useCallback((index) => {
|
|
@@ -1136,7 +1315,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1136
1315
|
}, [line.movesUci]);
|
|
1137
1316
|
// Auto-play opponent moves and detect the end of the line.
|
|
1138
1317
|
useEffect(() => {
|
|
1139
|
-
if (finished || isShowingCorrectMove) {
|
|
1318
|
+
if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
|
|
1140
1319
|
return;
|
|
1141
1320
|
}
|
|
1142
1321
|
if (currentIndex >= total) {
|
|
@@ -1156,6 +1335,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1156
1335
|
applyMove,
|
|
1157
1336
|
opponentMoveDelayMs,
|
|
1158
1337
|
isShowingCorrectMove,
|
|
1338
|
+
isShowingIncorrectMove,
|
|
1159
1339
|
]);
|
|
1160
1340
|
// Emit the completion event exactly once.
|
|
1161
1341
|
useEffect(() => {
|
|
@@ -1167,7 +1347,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1167
1347
|
}, [finished]);
|
|
1168
1348
|
const handleDrop = (source, target, piece) => {
|
|
1169
1349
|
var _a, _b, _c;
|
|
1170
|
-
if (finished || isShowingCorrectMove) {
|
|
1350
|
+
if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
|
|
1171
1351
|
return false;
|
|
1172
1352
|
}
|
|
1173
1353
|
const setupFen = displayFen !== null && displayFen !== void 0 ? displayFen : chessRef.current.fen();
|
|
@@ -1187,7 +1367,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1187
1367
|
setFeedback(moveFeedback);
|
|
1188
1368
|
(_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
|
|
1189
1369
|
if (isCorrect) {
|
|
1190
|
-
const nextFen = fenAfterUci(setupFen, dropResult.uci);
|
|
1370
|
+
const nextFen = fenAfterUci(setupFen, dropResult.attempt.uci);
|
|
1191
1371
|
if (nextFen) {
|
|
1192
1372
|
setDisplayFen(nextFen);
|
|
1193
1373
|
}
|
|
@@ -1198,6 +1378,9 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1198
1378
|
applyMove(index);
|
|
1199
1379
|
});
|
|
1200
1380
|
}
|
|
1381
|
+
else {
|
|
1382
|
+
showIncorrectMove(source);
|
|
1383
|
+
}
|
|
1201
1384
|
return isCorrect;
|
|
1202
1385
|
};
|
|
1203
1386
|
const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
|
|
@@ -1211,13 +1394,14 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1211
1394
|
const moveNumber = Math.min(currentIndex + 1, total);
|
|
1212
1395
|
const isUserTurn = !finished &&
|
|
1213
1396
|
!isShowingCorrectMove &&
|
|
1397
|
+
!isShowingIncorrectMove &&
|
|
1214
1398
|
turnFromFen(boardFen) === line.trainSide &&
|
|
1215
1399
|
currentIndex < total;
|
|
1216
1400
|
const stackControlsBelow = useStackPuzzleControlsBelow();
|
|
1217
1401
|
const controlsPlacement = stackControlsBelow
|
|
1218
1402
|
? 'below'
|
|
1219
1403
|
: 'beside';
|
|
1220
|
-
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: boardFen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, correctMoveSquare: correctMoveSquare, lastMoveUci: lastMoveUci, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
|
|
1404
|
+
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: boardFen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, correctMoveSquare: correctMoveSquare, incorrectMoveSquare: incorrectMoveSquare, lastMoveUci: lastMoveUci, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
|
|
1221
1405
|
trainSide: line.trainSide,
|
|
1222
1406
|
moveNumber,
|
|
1223
1407
|
total,
|
|
@@ -1234,4 +1418,4 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1234
1418
|
/** @deprecated Import {@link boardSquareHighlightColors} and {@link analysisBoardHighlightColors} from `react-chess-core`. */
|
|
1235
1419
|
const squareHighlightColors = Object.assign(Object.assign({}, boardSquareHighlightColors), analysisBoardHighlightColors);
|
|
1236
1420
|
|
|
1237
|
-
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 };
|
|
1421
|
+
export { DEFAULT_PUZZLE_BOARD_WIDTH, DefaultLineControls, DefaultPuzzleControls, GamePosition, LineBoard, LineBoardWithControls, PUZZLE_CONTROLS_BESIDE_RESERVE_PX, PUZZLE_CONTROLS_STACK_BREAKPOINT_PX, Position, PuzzleBoard, PuzzleBoardWithControls, PuzzlePosition, advanceToPlayerTurn, applyUciMove, buildAnalysisContext, defaultRenderControls, defaultRenderLineControls, emptyAnalysisContext, getCheckSquareFromChess, isAnalysisAvailable, playerColorForSolution, playerMoveIndicesInRange, squareHighlightColors, usePuzzleAnalysis };
|
package/dist/index.js
CHANGED
|
@@ -57,18 +57,18 @@ const usePuzzleAnalysis = (position, resultStatus, puzzleNum) => {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const EMPTY_BOARD_FEN = '8/8/8/8/8/8/8/8 w - - 0 1';
|
|
60
|
-
const DEFAULT_ANSWER_ARROW_COLOR = '#42a5f5';
|
|
61
60
|
/**
|
|
62
61
|
* Single mounted board for puzzle play. Keeps the prior board (and orientation)
|
|
63
62
|
* visible while the next position loads so layout and perspective do not flicker.
|
|
64
63
|
*/
|
|
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
|
|
64
|
+
const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth, onResumeCorrect, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect = false, autoShowWrongMoves = true, refutationEngine, answerArrowColor = reactChessCore.DEFAULT_ANSWER_ARROW_COLOR, positionLocked = false, onMissFeedbackChange, recapBoard = null, }) => {
|
|
65
|
+
var _a, _b, _c, _d, _e, _f;
|
|
67
66
|
const [showAnswerArrow, setShowAnswerArrow] = react.useState(false);
|
|
68
67
|
const [incorrectActive, setIncorrectActive] = react.useState(false);
|
|
69
68
|
const attemptMissedRef = react.useRef(false);
|
|
70
69
|
const { revision, bumpRevision } = reactChessCore.useBoardRevision();
|
|
71
70
|
const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, } = reactChessCore.useCorrectMoveFeedback();
|
|
71
|
+
const { incorrectMoveSquare: transientIncorrectSquare, showIncorrectMove, clearIncorrectMoveFeedback, } = reactChessCore.useIncorrectMoveFeedback();
|
|
72
72
|
const boardOrientationRef = react.useRef('white');
|
|
73
73
|
const boardFenRef = react.useRef(EMPTY_BOARD_FEN);
|
|
74
74
|
const notifyHost = () => {
|
|
@@ -103,13 +103,25 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
103
103
|
const answerArrowVisible = useRefutation
|
|
104
104
|
? incorrectActive && missPhase === 'answer'
|
|
105
105
|
: showAnswerArrow;
|
|
106
|
+
const overlayIncorrectSquare = transientIncorrectSquare !== null && transientIncorrectSquare !== void 0 ? transientIncorrectSquare : (useRefutation && incorrectActive
|
|
107
|
+
? missBoard.missSequence.display.incorrectMoveSquare
|
|
108
|
+
: null);
|
|
109
|
+
const refutationMoveSquare = useRefutation && incorrectActive
|
|
110
|
+
? missBoard.missSequence.display.refutationMoveSquare
|
|
111
|
+
: null;
|
|
106
112
|
react.useEffect(() => {
|
|
107
113
|
setShowAnswerArrow(false);
|
|
108
114
|
setIncorrectActive(false);
|
|
109
115
|
attemptMissedRef.current = false;
|
|
110
116
|
clearCorrectMoveFeedback();
|
|
117
|
+
clearIncorrectMoveFeedback();
|
|
111
118
|
onMissFeedbackChange === null || onMissFeedbackChange === void 0 ? void 0 : onMissFeedbackChange(null);
|
|
112
|
-
}, [
|
|
119
|
+
}, [
|
|
120
|
+
clearCorrectMoveFeedback,
|
|
121
|
+
clearIncorrectMoveFeedback,
|
|
122
|
+
onMissFeedbackChange,
|
|
123
|
+
position,
|
|
124
|
+
]);
|
|
113
125
|
react.useEffect(() => {
|
|
114
126
|
var _a, _b;
|
|
115
127
|
if (!onMissFeedbackChange) {
|
|
@@ -141,11 +153,13 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
141
153
|
showAnswerArrow,
|
|
142
154
|
useRefutation,
|
|
143
155
|
]);
|
|
156
|
+
const boardOrientation = position
|
|
157
|
+
? position.getPlayerColor()
|
|
158
|
+
: boardOrientationRef.current;
|
|
144
159
|
if (position) {
|
|
145
|
-
boardOrientationRef.current =
|
|
160
|
+
boardOrientationRef.current = boardOrientation;
|
|
146
161
|
boardFenRef.current = position.fen();
|
|
147
162
|
}
|
|
148
|
-
const boardOrientation = boardOrientationRef.current;
|
|
149
163
|
const boardFen = boardFenRef.current;
|
|
150
164
|
const hasBoard = boardFen !== EMPTY_BOARD_FEN;
|
|
151
165
|
const simpleArrows = react.useMemo(() => {
|
|
@@ -158,19 +172,28 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
158
172
|
}
|
|
159
173
|
return [[moveUci.slice(0, 2), moveUci.slice(2, 4), answerArrowColor]];
|
|
160
174
|
}, [showAnswerArrow, position, answerArrowColor, useRefutation]);
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
175
|
+
const isRecapping = recapBoard !== null;
|
|
176
|
+
const customArrows = isRecapping
|
|
177
|
+
? recapBoard.customArrows
|
|
178
|
+
: useRefutation && incorrectActive
|
|
179
|
+
? missBoard.customArrows
|
|
180
|
+
: simpleArrows;
|
|
181
|
+
const displayFen = isRecapping
|
|
182
|
+
? recapBoard.fen
|
|
183
|
+
: useRefutation && incorrectActive
|
|
184
|
+
? missBoard.boardPosition
|
|
185
|
+
: boardFen;
|
|
165
186
|
const missLocked = useRefutation &&
|
|
166
187
|
incorrectActive &&
|
|
167
188
|
(missBoard.boardAnimating ||
|
|
168
189
|
missPhase === 'wrong' ||
|
|
169
190
|
missPhase === 'refutation');
|
|
170
|
-
const arePiecesDraggable =
|
|
191
|
+
const arePiecesDraggable = !isRecapping &&
|
|
192
|
+
position !== null &&
|
|
171
193
|
!positionLocked &&
|
|
172
194
|
!missLocked &&
|
|
173
|
-
correctMoveSquare === null
|
|
195
|
+
correctMoveSquare === null &&
|
|
196
|
+
overlayIncorrectSquare === null;
|
|
174
197
|
const onPieceDrop = (sourceSquare, targetSquare, piece) => {
|
|
175
198
|
if (!position || positionLocked || position.isSolutionRevealed()) {
|
|
176
199
|
return false;
|
|
@@ -184,6 +207,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
184
207
|
if (answerArrowVisible &&
|
|
185
208
|
!allowRetryOnIncorrect &&
|
|
186
209
|
!position.isExpectedGuess(sourceSquare, targetSquare)) {
|
|
210
|
+
showIncorrectMove(sourceSquare);
|
|
187
211
|
position.resetInteractions();
|
|
188
212
|
snapBoardBack();
|
|
189
213
|
return false;
|
|
@@ -193,6 +217,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
193
217
|
});
|
|
194
218
|
if (!guess.accepted) {
|
|
195
219
|
attemptMissedRef.current = true;
|
|
220
|
+
showIncorrectMove(useRefutation ? targetSquare : sourceSquare);
|
|
196
221
|
onFeedback({
|
|
197
222
|
index: position.getIndex(),
|
|
198
223
|
guess: { sourceSquare, targetSquare, piece },
|
|
@@ -206,8 +231,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
206
231
|
missBoard.missSequence.startSequence(setupFen, attemptedUci);
|
|
207
232
|
}
|
|
208
233
|
position.resetInteractions();
|
|
209
|
-
|
|
210
|
-
return false;
|
|
234
|
+
return true;
|
|
211
235
|
}
|
|
212
236
|
const revealIncorrectFeedback = () => {
|
|
213
237
|
if (showAnswerArrowOnIncorrect) {
|
|
@@ -273,13 +297,79 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
273
297
|
showCorrectMove(targetSquare, finishCorrectFeedback);
|
|
274
298
|
return true;
|
|
275
299
|
};
|
|
276
|
-
return
|
|
277
|
-
? null
|
|
278
|
-
: ((_g = position === null || position === void 0 ? void 0 : position.getIncorrectMoveSquare()) !== null && _g !== void 0 ? _g : null), correctMoveSquare: correctMoveSquare, customArrows: customArrows, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: 0 }, revision)) : null }));
|
|
300
|
+
return hasBoard ? (jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: isRecapping ? '' : ((_e = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _e !== void 0 ? _e : ''), hintSquare: isRecapping ? null : ((_f = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _f !== void 0 ? _f : null), incorrectMoveSquare: isRecapping ? null : overlayIncorrectSquare, refutationMoveSquare: isRecapping ? null : refutationMoveSquare, correctMoveSquare: isRecapping ? null : correctMoveSquare, customArrows: customArrows, lastMoveUci: isRecapping ? recapBoard.lastMoveUci : null, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: isRecapping ? recapBoard.animationDuration : 0 }, revision)) : null;
|
|
279
301
|
};
|
|
280
302
|
|
|
281
303
|
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 }));
|
|
282
304
|
|
|
305
|
+
const inactiveAutoAdvance = {
|
|
306
|
+
active: false,
|
|
307
|
+
secondsRemaining: -1,
|
|
308
|
+
};
|
|
309
|
+
/** Countdown overlay state while waiting to auto-load the next puzzle card. */
|
|
310
|
+
function usePuzzleAutoAdvanceCountdown(enabled, delayMs, onAdvance) {
|
|
311
|
+
const [secondsRemaining, setSecondsRemaining] = react.useState(-1);
|
|
312
|
+
react.useEffect(() => {
|
|
313
|
+
if (!enabled || delayMs <= 0) {
|
|
314
|
+
setSecondsRemaining(-1);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const startedAt = Date.now();
|
|
318
|
+
const updateCountdown = () => {
|
|
319
|
+
const elapsed = Date.now() - startedAt;
|
|
320
|
+
setSecondsRemaining(Math.max(0, Math.ceil((delayMs - elapsed) / 1000)));
|
|
321
|
+
};
|
|
322
|
+
updateCountdown();
|
|
323
|
+
const intervalId = window.setInterval(updateCountdown, 200);
|
|
324
|
+
const timeoutId = window.setTimeout(() => {
|
|
325
|
+
window.clearInterval(intervalId);
|
|
326
|
+
setSecondsRemaining(-1);
|
|
327
|
+
onAdvance();
|
|
328
|
+
}, delayMs);
|
|
329
|
+
return () => {
|
|
330
|
+
window.clearInterval(intervalId);
|
|
331
|
+
window.clearTimeout(timeoutId);
|
|
332
|
+
setSecondsRemaining(-1);
|
|
333
|
+
};
|
|
334
|
+
}, [delayMs, enabled, onAdvance]);
|
|
335
|
+
if (!enabled || secondsRemaining < 0) {
|
|
336
|
+
return inactiveAutoAdvance;
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
active: true,
|
|
340
|
+
secondsRemaining,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Pause on the puzzle setup position before the solution recap animates. */
|
|
345
|
+
const PUZZLE_COMPLETION_RECAP_SETUP_MS = 400;
|
|
346
|
+
const usePuzzleCompletionRecap = ({ source, active, onComplete, }) => {
|
|
347
|
+
var _a, _b, _c, _d, _e, _f;
|
|
348
|
+
const startFen = (_a = source === null || source === void 0 ? void 0 : source.startFen) !== null && _a !== void 0 ? _a : '';
|
|
349
|
+
const movesUci = (_b = source === null || source === void 0 ? void 0 : source.movesUci) !== null && _b !== void 0 ? _b : [];
|
|
350
|
+
const startIndex = (_c = source === null || source === void 0 ? void 0 : source.startIndex) !== null && _c !== void 0 ? _c : 0;
|
|
351
|
+
const endIndex = (_d = source === null || source === void 0 ? void 0 : source.endIndex) !== null && _d !== void 0 ? _d : 0;
|
|
352
|
+
const missedIndices = (_e = source === null || source === void 0 ? void 0 : source.missedIndices) !== null && _e !== void 0 ? _e : [];
|
|
353
|
+
const setupUci = (_f = source === null || source === void 0 ? void 0 : source.setupUci) !== null && _f !== void 0 ? _f : null;
|
|
354
|
+
const resolveFen = react.useCallback((moveIndex, afterMove) => {
|
|
355
|
+
if (!startFen || movesUci.length === 0) {
|
|
356
|
+
return '';
|
|
357
|
+
}
|
|
358
|
+
return reactChessCore.fenAtPlyFromStart(startFen, movesUci, afterMove ? moveIndex + 1 : moveIndex);
|
|
359
|
+
}, [movesUci, startFen]);
|
|
360
|
+
return reactChessCore.useSolutionLineRecap({
|
|
361
|
+
active: active && source !== null,
|
|
362
|
+
movesUci,
|
|
363
|
+
startIndex,
|
|
364
|
+
endIndex,
|
|
365
|
+
missedIndices,
|
|
366
|
+
segmentStartFen: startFen,
|
|
367
|
+
setupUci,
|
|
368
|
+
onComplete,
|
|
369
|
+
resolveFen,
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
|
|
283
373
|
const isAttemptFinished = (resultStatus) => resultStatus === 'complete' || resultStatus === 'incorrect';
|
|
284
374
|
/** Library default hint / next / analysis / result controls (unstyled buttons). */
|
|
285
375
|
const DefaultPuzzleControls = ({ showHint, showSolution, nextPuzzle, resultStatus, analysis, controlState, }) => (jsxRuntime.jsxs("div", { style: rowStyle$1, children: [jsxRuntime.jsx("button", { type: "button", onClick: showHint, style: buttonStyle, disabled: !controlState.canShowHint, children: "Hint" }), jsxRuntime.jsx("button", { type: "button", onClick: showSolution, style: buttonStyle, disabled: !controlState.canShowSolution, children: "Show solution" }), jsxRuntime.jsx("button", { type: "button", onClick: nextPuzzle, style: buttonStyle, children: "Next puzzle" }), analysis.visible && isAttemptFinished(resultStatus) && (jsxRuntime.jsx("button", { type: "button", onClick: analysis.openAnalysis, style: buttonStyle, children: "Analysis" })), resultStatus === 'complete' && (jsxRuntime.jsx("span", { style: statusStyle$1, children: "Complete" })), resultStatus === 'incorrect' && (jsxRuntime.jsx("span", { style: Object.assign(Object.assign({}, statusStyle$1), { color: '#c62828' }), children: "Incorrect" }))] }));
|
|
@@ -484,6 +574,29 @@ function playerColorForSolution(initialFen, moves) {
|
|
|
484
574
|
}
|
|
485
575
|
return chess.turn() === 'w' ? 'white' : 'black';
|
|
486
576
|
}
|
|
577
|
+
/** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
|
|
578
|
+
function playerMoveIndicesInRange(initialFen, moves, fromIndex, toIndex) {
|
|
579
|
+
const playerColor = playerColorForSolution(initialFen, moves);
|
|
580
|
+
const chess = new chess_js.Chess(initialFen);
|
|
581
|
+
const indices = [];
|
|
582
|
+
for (let i = 0; i < toIndex && i < moves.length; i++) {
|
|
583
|
+
const side = chess.turn() === 'w' ? 'white' : 'black';
|
|
584
|
+
if (i >= fromIndex && side === playerColor) {
|
|
585
|
+
indices.push(i);
|
|
586
|
+
}
|
|
587
|
+
applyUciMove(chess, moves[i]);
|
|
588
|
+
}
|
|
589
|
+
return indices;
|
|
590
|
+
}
|
|
591
|
+
/** Auto-play opponent setup plies until the solver is on move. */
|
|
592
|
+
function advanceToPlayerTurn(position) {
|
|
593
|
+
while (!position.isFinished() &&
|
|
594
|
+
position.getSideToMove() !== position.getPlayerColor()) {
|
|
595
|
+
if (!position.next()) {
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
487
600
|
class PuzzlePosition extends Position {
|
|
488
601
|
constructor(initialFEN, moves, resumeConfig) {
|
|
489
602
|
super();
|
|
@@ -728,19 +841,37 @@ class GamePosition extends Position {
|
|
|
728
841
|
}
|
|
729
842
|
}
|
|
730
843
|
|
|
731
|
-
/** Apply
|
|
844
|
+
/** Apply opponent setup plies immediately so the board does not flash on load. */
|
|
732
845
|
const puzzlePositionFromFetch = (fen, moves, resume) => {
|
|
733
846
|
const newPosition = new PuzzlePosition(fen, moves, resume);
|
|
734
847
|
if (!resume && moves.length > 1) {
|
|
735
848
|
newPosition.next();
|
|
849
|
+
advanceToPlayerTurn(newPosition);
|
|
850
|
+
}
|
|
851
|
+
else if (resume &&
|
|
852
|
+
newPosition.getSideToMove() !== newPosition.getPlayerColor()) {
|
|
853
|
+
advanceToPlayerTurn(newPosition);
|
|
736
854
|
}
|
|
737
855
|
return newPosition;
|
|
738
856
|
};
|
|
739
|
-
/** Brief pause so the user sees a correct result before the next card loads. */
|
|
740
|
-
const AUTO_ADVANCE_ON_COMPLETE_DELAY_MS = 700;
|
|
741
857
|
const SOLUTION_STEP_MS = 500;
|
|
742
858
|
const RESUME_AUTO_STEP_MS = 500;
|
|
743
|
-
const
|
|
859
|
+
const uniqueIndices = (indices) => [...new Set(indices)];
|
|
860
|
+
const buildCompletionRecapSource = (position, missedIndices) => {
|
|
861
|
+
var _a, _b;
|
|
862
|
+
const movesUci = position.getSolutionMoves();
|
|
863
|
+
const initialFen = position.getInitialFen();
|
|
864
|
+
const startIndex = (_a = playerMoveIndicesInRange(initialFen, movesUci, 0, movesUci.length)[0]) !== null && _a !== void 0 ? _a : 0;
|
|
865
|
+
return {
|
|
866
|
+
startFen: initialFen,
|
|
867
|
+
movesUci,
|
|
868
|
+
startIndex,
|
|
869
|
+
endIndex: movesUci.length,
|
|
870
|
+
missedIndices,
|
|
871
|
+
setupUci: startIndex > 0 ? (_b = movesUci[startIndex - 1]) !== null && _b !== void 0 ? _b : null : null,
|
|
872
|
+
};
|
|
873
|
+
};
|
|
874
|
+
const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls = defaultRenderControls, renderAnalysisSidebar, renderAnalysisContainer, renderEngineEvaluation, renderBoardCaption, renderBoardFeedback, puzzleBoardWidth = DEFAULT_PUZZLE_BOARD_WIDTH, analysisLayout = reactChessCore.DEFAULT_ANALYSIS_LAYOUT, analysisBoardWidth, renderAnalysisMain, engine, autoAdvanceOnComplete = false, autoAdvanceOnCompleteAfterIncorrect = false, autoAdvanceOnCompleteDelayMs = reactChessCore.AUTO_ADVANCE_ON_COMPLETE_DELAY_MS, showCompletionRecap = false, revealAnswerOnIncorrect = false, showAnswerArrowOnIncorrect = false, allowRetryOnIncorrect = true, showRefutationOnIncorrect, autoShowWrongMoves = true, refutationEngine, answerArrowColor, }) => {
|
|
744
875
|
var _a, _b, _c, _d;
|
|
745
876
|
const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
|
|
746
877
|
const stackControlsBelow = useStackPuzzleControlsBelow();
|
|
@@ -756,6 +887,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
756
887
|
const [puzzleComplete, setPuzzleComplete] = react.useState(false);
|
|
757
888
|
const [completedAfterMiss, setCompletedAfterMiss] = react.useState(false);
|
|
758
889
|
const [missFeedback, setMissFeedback] = react.useState(null);
|
|
890
|
+
const [missedMoveIndices, setMissedMoveIndices] = react.useState([]);
|
|
891
|
+
const [completionCheckVisible, setCompletionCheckVisible] = react.useState(false);
|
|
892
|
+
const [completionRecapActive, setCompletionRecapActive] = react.useState(false);
|
|
893
|
+
const [completionRecapDone, setCompletionRecapDone] = react.useState(false);
|
|
894
|
+
const completionFlowStartedRef = react.useRef(false);
|
|
759
895
|
const [, setInteractionNum] = react.useState(0);
|
|
760
896
|
const solutionAnimationRef = react.useRef({ cancelled: false, timeoutIds: [] });
|
|
761
897
|
const resumeAnimationRef = react.useRef({ cancelled: false, timeoutIds: [] });
|
|
@@ -782,6 +918,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
782
918
|
setPuzzleComplete(false);
|
|
783
919
|
setCompletedAfterMiss(false);
|
|
784
920
|
setMissFeedback(null);
|
|
921
|
+
setMissedMoveIndices([]);
|
|
922
|
+
setCompletionCheckVisible(false);
|
|
923
|
+
setCompletionRecapActive(false);
|
|
924
|
+
setCompletionRecapDone(false);
|
|
925
|
+
completionFlowStartedRef.current = false;
|
|
785
926
|
onFetch()
|
|
786
927
|
.then((data) => {
|
|
787
928
|
if (cancelled) {
|
|
@@ -817,10 +958,16 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
817
958
|
feedbackData.isCorrect === false;
|
|
818
959
|
if (feedbackData.hintRequested) {
|
|
819
960
|
setHintUsed(true);
|
|
961
|
+
setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
|
|
820
962
|
}
|
|
821
963
|
if (incorrectThisFeedback) {
|
|
822
964
|
setHasIncorrectAttempt(true);
|
|
823
965
|
}
|
|
966
|
+
if (feedbackData.isCorrect === false &&
|
|
967
|
+
!feedbackData.isFinished &&
|
|
968
|
+
!feedbackData.solutionShown) {
|
|
969
|
+
setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
|
|
970
|
+
}
|
|
824
971
|
if (feedbackData.isFinished) {
|
|
825
972
|
setPuzzleComplete(true);
|
|
826
973
|
setCompletedAfterMiss((prev) => prev ||
|
|
@@ -984,6 +1131,10 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
984
1131
|
position.recordSolutionShown();
|
|
985
1132
|
position.setSolutionRevealed(true);
|
|
986
1133
|
position.wantsHint(false);
|
|
1134
|
+
setMissedMoveIndices((prev) => uniqueIndices([
|
|
1135
|
+
...prev,
|
|
1136
|
+
...playerMoveIndicesInRange(position.getInitialFen(), position.getSolutionMoves(), position.getIndex(), position.getSolutionMoves().length),
|
|
1137
|
+
]));
|
|
987
1138
|
handleFeedback({
|
|
988
1139
|
index: position.getIndex(),
|
|
989
1140
|
solutionShown: true,
|
|
@@ -996,30 +1147,46 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
996
1147
|
setPuzzleNum((prevPuzzleNum) => prevPuzzleNum + 1);
|
|
997
1148
|
}, []);
|
|
998
1149
|
const resultStatus = getResultStatus();
|
|
1150
|
+
const completionRecapSource = react.useMemo(() => position && showCompletionRecap
|
|
1151
|
+
? buildCompletionRecapSource(position, missedMoveIndices)
|
|
1152
|
+
: null, [position, showCompletionRecap, missedMoveIndices]);
|
|
1153
|
+
const handleCompletionRecapDone = react.useCallback(() => {
|
|
1154
|
+
setCompletionRecapActive(false);
|
|
1155
|
+
setCompletionRecapDone(true);
|
|
1156
|
+
}, []);
|
|
1157
|
+
const completionRecap = usePuzzleCompletionRecap({
|
|
1158
|
+
source: completionRecapSource,
|
|
1159
|
+
active: completionRecapActive,
|
|
1160
|
+
onComplete: handleCompletionRecapDone,
|
|
1161
|
+
});
|
|
1162
|
+
const isCompletionRecapping = showCompletionRecap && (completionRecapActive || completionRecap.active);
|
|
999
1163
|
react.useEffect(() => {
|
|
1000
|
-
if (!
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1164
|
+
if (!showCompletionRecap ||
|
|
1165
|
+
resultStatus !== 'complete' ||
|
|
1166
|
+
loadingNextPuzzle ||
|
|
1167
|
+
completionFlowStartedRef.current) {
|
|
1004
1168
|
return;
|
|
1005
1169
|
}
|
|
1006
|
-
|
|
1170
|
+
completionFlowStartedRef.current = true;
|
|
1171
|
+
setCompletionCheckVisible(true);
|
|
1172
|
+
}, [loadingNextPuzzle, resultStatus, showCompletionRecap]);
|
|
1173
|
+
react.useEffect(() => {
|
|
1174
|
+
if (!completionCheckVisible) {
|
|
1007
1175
|
return;
|
|
1008
1176
|
}
|
|
1009
|
-
const timer = setTimeout(() => {
|
|
1010
|
-
|
|
1011
|
-
|
|
1177
|
+
const timer = window.setTimeout(() => {
|
|
1178
|
+
setCompletionCheckVisible(false);
|
|
1179
|
+
setCompletionRecapActive(true);
|
|
1180
|
+
}, PUZZLE_COMPLETION_RECAP_SETUP_MS);
|
|
1012
1181
|
return () => {
|
|
1013
|
-
clearTimeout(timer);
|
|
1182
|
+
window.clearTimeout(timer);
|
|
1014
1183
|
};
|
|
1015
|
-
}, [
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
puzzleNum,
|
|
1022
|
-
]);
|
|
1184
|
+
}, [completionCheckVisible]);
|
|
1185
|
+
const shouldAutoAdvance = autoAdvanceOnComplete &&
|
|
1186
|
+
resultStatus === 'complete' &&
|
|
1187
|
+
!(hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) &&
|
|
1188
|
+
(!showCompletionRecap || completionRecapDone);
|
|
1189
|
+
const autoAdvance = usePuzzleAutoAdvanceCountdown(shouldAutoAdvance, autoAdvanceOnCompleteDelayMs, handleNextPuzzle);
|
|
1023
1190
|
const controlState = {
|
|
1024
1191
|
canShowHint: position !== null &&
|
|
1025
1192
|
!position.isFinished() &&
|
|
@@ -1034,7 +1201,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1034
1201
|
const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
|
|
1035
1202
|
renderAnalysisContainer &&
|
|
1036
1203
|
(renderEngineEvaluation || (engine === null || engine === void 0 ? void 0 : engine.enabled) === false));
|
|
1037
|
-
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: resolvedAnalysisBoardWidth, 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.
|
|
1204
|
+
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: resolvedAnalysisBoardWidth, 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.jsxs("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 ||
|
|
1205
|
+
completionCheckVisible ||
|
|
1206
|
+
isCompletionRecapping, onMissFeedbackChange: setMissFeedback, recapBoard: isCompletionRecapping
|
|
1207
|
+
? {
|
|
1208
|
+
fen: completionRecap.fen,
|
|
1209
|
+
lastMoveUci: completionRecap.lastMoveUci,
|
|
1210
|
+
customArrows: completionRecap.customArrows,
|
|
1211
|
+
animationDuration: completionRecap.animationDuration,
|
|
1212
|
+
}
|
|
1213
|
+
: null }) }), completionCheckVisible && (jsxRuntime.jsx(reactChessCore.BoardCompleteCheckOverlay, { variant: hasIncorrectAttempt || completedAfterMiss || hintUsed
|
|
1214
|
+
? 'partial'
|
|
1215
|
+
: 'success' }))] }), renderBoardCaption && (jsxRuntime.jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
|
|
1038
1216
|
sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
|
|
1039
1217
|
playerColor: position
|
|
1040
1218
|
? position.getPlayerColor()
|
|
@@ -1050,7 +1228,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1050
1228
|
}) }))] }), jsxRuntime.jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
|
|
1051
1229
|
visible: analysis.canOpen,
|
|
1052
1230
|
openAnalysis: analysis.openAnalysis,
|
|
1053
|
-
}, controlState), renderBoardFeedback && resultStatus === 'complete' && (jsxRuntime.jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
|
|
1231
|
+
}, controlState, autoAdvance), renderBoardFeedback && resultStatus === 'complete' && (jsxRuntime.jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
|
|
1054
1232
|
resultStatus,
|
|
1055
1233
|
cleanSolve: !hasIncorrectAttempt,
|
|
1056
1234
|
refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
|
|
@@ -1064,7 +1242,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1064
1242
|
* side's pieces be dragged. Move validation and sequencing live in
|
|
1065
1243
|
* {@link LineBoardWithControls}.
|
|
1066
1244
|
*/
|
|
1067
|
-
const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, lastMoveUci = null, onPieceDrop, boardWidth, }) => (jsxRuntime.jsx(reactChessCore.
|
|
1245
|
+
const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, incorrectMoveSquare = null, lastMoveUci = null, onPieceDrop, boardWidth, }) => (jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: incorrectMoveSquare, correctMoveSquare: correctMoveSquare, position: fen, boardOrientation: orientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => piece[0] === trainSide, onPieceDrop: (source, target, piece) => onPieceDrop(source, target, piece), lastMoveUci: lastMoveUci, autoPromoteToQueen: true, areArrowsAllowed: false, customBoardStyle: { borderRadius: 4 } }));
|
|
1068
1246
|
|
|
1069
1247
|
/** Library default line-drill status controls (unstyled). */
|
|
1070
1248
|
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
|
|
@@ -1113,6 +1291,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1113
1291
|
const [feedback, setFeedback] = react.useState(null);
|
|
1114
1292
|
const [displayFen, setDisplayFen] = react.useState(null);
|
|
1115
1293
|
const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = reactChessCore.useCorrectMoveFeedback();
|
|
1294
|
+
const { incorrectMoveSquare, showIncorrectMove, isShowingIncorrectMove, } = reactChessCore.useIncorrectMoveFeedback();
|
|
1116
1295
|
const total = line.movesUci.length;
|
|
1117
1296
|
const orientation = boardOrientationForLine(line.trainSide);
|
|
1118
1297
|
const applyMove = react.useCallback((index) => {
|
|
@@ -1137,7 +1316,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1137
1316
|
}, [line.movesUci]);
|
|
1138
1317
|
// Auto-play opponent moves and detect the end of the line.
|
|
1139
1318
|
react.useEffect(() => {
|
|
1140
|
-
if (finished || isShowingCorrectMove) {
|
|
1319
|
+
if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
|
|
1141
1320
|
return;
|
|
1142
1321
|
}
|
|
1143
1322
|
if (currentIndex >= total) {
|
|
@@ -1157,6 +1336,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1157
1336
|
applyMove,
|
|
1158
1337
|
opponentMoveDelayMs,
|
|
1159
1338
|
isShowingCorrectMove,
|
|
1339
|
+
isShowingIncorrectMove,
|
|
1160
1340
|
]);
|
|
1161
1341
|
// Emit the completion event exactly once.
|
|
1162
1342
|
react.useEffect(() => {
|
|
@@ -1168,7 +1348,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1168
1348
|
}, [finished]);
|
|
1169
1349
|
const handleDrop = (source, target, piece) => {
|
|
1170
1350
|
var _a, _b, _c;
|
|
1171
|
-
if (finished || isShowingCorrectMove) {
|
|
1351
|
+
if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
|
|
1172
1352
|
return false;
|
|
1173
1353
|
}
|
|
1174
1354
|
const setupFen = displayFen !== null && displayFen !== void 0 ? displayFen : chessRef.current.fen();
|
|
@@ -1188,7 +1368,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1188
1368
|
setFeedback(moveFeedback);
|
|
1189
1369
|
(_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
|
|
1190
1370
|
if (isCorrect) {
|
|
1191
|
-
const nextFen = reactChessCore.fenAfterUci(setupFen, dropResult.uci);
|
|
1371
|
+
const nextFen = reactChessCore.fenAfterUci(setupFen, dropResult.attempt.uci);
|
|
1192
1372
|
if (nextFen) {
|
|
1193
1373
|
setDisplayFen(nextFen);
|
|
1194
1374
|
}
|
|
@@ -1199,6 +1379,9 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1199
1379
|
applyMove(index);
|
|
1200
1380
|
});
|
|
1201
1381
|
}
|
|
1382
|
+
else {
|
|
1383
|
+
showIncorrectMove(source);
|
|
1384
|
+
}
|
|
1202
1385
|
return isCorrect;
|
|
1203
1386
|
};
|
|
1204
1387
|
const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
|
|
@@ -1212,13 +1395,14 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1212
1395
|
const moveNumber = Math.min(currentIndex + 1, total);
|
|
1213
1396
|
const isUserTurn = !finished &&
|
|
1214
1397
|
!isShowingCorrectMove &&
|
|
1398
|
+
!isShowingIncorrectMove &&
|
|
1215
1399
|
turnFromFen(boardFen) === line.trainSide &&
|
|
1216
1400
|
currentIndex < total;
|
|
1217
1401
|
const stackControlsBelow = useStackPuzzleControlsBelow();
|
|
1218
1402
|
const controlsPlacement = stackControlsBelow
|
|
1219
1403
|
? 'below'
|
|
1220
1404
|
: 'beside';
|
|
1221
|
-
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: boardFen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, correctMoveSquare: correctMoveSquare, lastMoveUci: lastMoveUci, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsxRuntime.jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
|
|
1405
|
+
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: boardFen, orientation: orientation, trainSide: line.trainSide, draggable: isUserTurn, correctMoveSquare: correctMoveSquare, incorrectMoveSquare: incorrectMoveSquare, lastMoveUci: lastMoveUci, onPieceDrop: handleDrop, boardWidth: boardWidth }) }) }), jsxRuntime.jsx("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: renderControls({
|
|
1222
1406
|
trainSide: line.trainSide,
|
|
1223
1407
|
moveNumber,
|
|
1224
1408
|
total,
|
|
@@ -1251,6 +1435,7 @@ exports.Position = Position;
|
|
|
1251
1435
|
exports.PuzzleBoard = PuzzleBoard;
|
|
1252
1436
|
exports.PuzzleBoardWithControls = PuzzleBoardWithControls;
|
|
1253
1437
|
exports.PuzzlePosition = PuzzlePosition;
|
|
1438
|
+
exports.advanceToPlayerTurn = advanceToPlayerTurn;
|
|
1254
1439
|
exports.applyUciMove = applyUciMove;
|
|
1255
1440
|
exports.buildAnalysisContext = buildAnalysisContext;
|
|
1256
1441
|
exports.defaultRenderControls = defaultRenderControls;
|
|
@@ -1259,5 +1444,6 @@ exports.emptyAnalysisContext = emptyAnalysisContext;
|
|
|
1259
1444
|
exports.getCheckSquareFromChess = getCheckSquareFromChess;
|
|
1260
1445
|
exports.isAnalysisAvailable = isAnalysisAvailable;
|
|
1261
1446
|
exports.playerColorForSolution = playerColorForSolution;
|
|
1447
|
+
exports.playerMoveIndicesInRange = playerMoveIndicesInRange;
|
|
1262
1448
|
exports.squareHighlightColors = squareHighlightColors;
|
|
1263
1449
|
exports.usePuzzleAnalysis = usePuzzleAnalysis;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-chess-puzzle-kit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "React chess puzzle kit: play, controls, analysis, and browser Stockfish for endchess.training and other apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Robert Blackwell",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"chess.js": "^1.0.0-beta.8",
|
|
53
53
|
"react": "^18.3.1",
|
|
54
|
-
"react-chess-core": "^0.1.
|
|
54
|
+
"react-chess-core": "^0.1.8",
|
|
55
55
|
"react-chessboard": "^4.7.1"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"chess.js": "^1.0.0-beta.8",
|
|
75
75
|
"jest": "^29.7.0",
|
|
76
76
|
"react": "^18.3.1",
|
|
77
|
-
"react-chess-core": "^0.1.
|
|
77
|
+
"react-chess-core": "^0.1.8",
|
|
78
78
|
"react-chessboard": "^4.7.1",
|
|
79
79
|
"react-dom": "^18.3.1",
|
|
80
80
|
"storybook": "^8.2.9",
|