react-chess-puzzle-kit 1.0.4 → 1.0.6
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 +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +239 -46
- package/dist/index.js +239 -44
- 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;
|
|
@@ -9,6 +9,8 @@ export declare abstract class Position implements Traversable {
|
|
|
9
9
|
protected i: number;
|
|
10
10
|
constructor();
|
|
11
11
|
getIndex(): number;
|
|
12
|
+
/** UCI of the move that produced the current position. */
|
|
13
|
+
getLastMoveUci(): string | null;
|
|
12
14
|
next(): boolean;
|
|
13
15
|
prev(): boolean;
|
|
14
16
|
isFinished(): boolean;
|
|
@@ -21,6 +23,10 @@ export declare abstract class Position implements Traversable {
|
|
|
21
23
|
export declare function getCheckSquareFromChess(chess: Chess): string;
|
|
22
24
|
/** Side to move for the final (user) ply in a puzzle line. */
|
|
23
25
|
export declare function playerColorForSolution(initialFen: string, moves: string[]): 'white' | 'black';
|
|
26
|
+
/** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
|
|
27
|
+
export declare function playerMoveIndicesInRange(initialFen: string, moves: string[], fromIndex: number, toIndex: number): number[];
|
|
28
|
+
/** Auto-play opponent setup plies until the solver is on move. */
|
|
29
|
+
export declare function advanceToPlayerTurn(position: PuzzlePosition): void;
|
|
24
30
|
export type PuzzleResumeConfig = {
|
|
25
31
|
startIndex: number;
|
|
26
32
|
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, lastMoveUciAtPly, ThemeProvider, AnalysisErrorBoundary, AnalysisBoardCore, AnalysisBoardLayout, AnalysisBoard, BoardCompleteCheckOverlay, DEFAULT_ANALYSIS_LAYOUT, AUTO_ADVANCE_ON_COMPLETE_DELAY_MS, 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, }) => {
|
|
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, }) => {
|
|
65
64
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
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,33 @@ 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;
|
|
185
|
+
const lastMoveUci = isRecapping
|
|
186
|
+
? recapBoard.lastMoveUci
|
|
187
|
+
: useRefutation && incorrectActive
|
|
188
|
+
? missBoard.lastMoveUci
|
|
189
|
+
: ((_e = position === null || position === void 0 ? void 0 : position.getLastMoveUci()) !== null && _e !== void 0 ? _e : null);
|
|
164
190
|
const missLocked = useRefutation &&
|
|
165
191
|
incorrectActive &&
|
|
166
192
|
(missBoard.boardAnimating ||
|
|
167
193
|
missPhase === 'wrong' ||
|
|
168
194
|
missPhase === 'refutation');
|
|
169
|
-
const arePiecesDraggable =
|
|
195
|
+
const arePiecesDraggable = !isRecapping &&
|
|
196
|
+
position !== null &&
|
|
170
197
|
!positionLocked &&
|
|
171
198
|
!missLocked &&
|
|
172
|
-
correctMoveSquare === null
|
|
199
|
+
correctMoveSquare === null &&
|
|
200
|
+
overlayIncorrectSquare === null;
|
|
173
201
|
const onPieceDrop = (sourceSquare, targetSquare, piece) => {
|
|
174
202
|
if (!position || positionLocked || position.isSolutionRevealed()) {
|
|
175
203
|
return false;
|
|
@@ -183,6 +211,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
183
211
|
if (answerArrowVisible &&
|
|
184
212
|
!allowRetryOnIncorrect &&
|
|
185
213
|
!position.isExpectedGuess(sourceSquare, targetSquare)) {
|
|
214
|
+
showIncorrectMove(sourceSquare);
|
|
186
215
|
position.resetInteractions();
|
|
187
216
|
snapBoardBack();
|
|
188
217
|
return false;
|
|
@@ -192,6 +221,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
192
221
|
});
|
|
193
222
|
if (!guess.accepted) {
|
|
194
223
|
attemptMissedRef.current = true;
|
|
224
|
+
showIncorrectMove(useRefutation ? targetSquare : sourceSquare);
|
|
195
225
|
onFeedback({
|
|
196
226
|
index: position.getIndex(),
|
|
197
227
|
guess: { sourceSquare, targetSquare, piece },
|
|
@@ -205,8 +235,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
205
235
|
missBoard.missSequence.startSequence(setupFen, attemptedUci);
|
|
206
236
|
}
|
|
207
237
|
position.resetInteractions();
|
|
208
|
-
|
|
209
|
-
return false;
|
|
238
|
+
return true;
|
|
210
239
|
}
|
|
211
240
|
const revealIncorrectFeedback = () => {
|
|
212
241
|
if (showAnswerArrowOnIncorrect) {
|
|
@@ -272,13 +301,79 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
272
301
|
showCorrectMove(targetSquare, finishCorrectFeedback);
|
|
273
302
|
return true;
|
|
274
303
|
};
|
|
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 }));
|
|
304
|
+
return hasBoard ? (jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: isRecapping ? '' : ((_f = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _f !== void 0 ? _f : ''), hintSquare: isRecapping ? null : ((_g = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _g !== void 0 ? _g : null), incorrectMoveSquare: isRecapping ? null : overlayIncorrectSquare, refutationMoveSquare: isRecapping ? null : refutationMoveSquare, correctMoveSquare: isRecapping ? null : correctMoveSquare, customArrows: customArrows, lastMoveUci: lastMoveUci, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: isRecapping ? recapBoard.animationDuration : 0 }, revision)) : null;
|
|
278
305
|
};
|
|
279
306
|
|
|
280
307
|
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
308
|
|
|
309
|
+
const inactiveAutoAdvance = {
|
|
310
|
+
active: false,
|
|
311
|
+
secondsRemaining: -1,
|
|
312
|
+
};
|
|
313
|
+
/** Countdown overlay state while waiting to auto-load the next puzzle card. */
|
|
314
|
+
function usePuzzleAutoAdvanceCountdown(enabled, delayMs, onAdvance) {
|
|
315
|
+
const [secondsRemaining, setSecondsRemaining] = useState(-1);
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
if (!enabled || delayMs <= 0) {
|
|
318
|
+
setSecondsRemaining(-1);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const startedAt = Date.now();
|
|
322
|
+
const updateCountdown = () => {
|
|
323
|
+
const elapsed = Date.now() - startedAt;
|
|
324
|
+
setSecondsRemaining(Math.max(0, Math.ceil((delayMs - elapsed) / 1000)));
|
|
325
|
+
};
|
|
326
|
+
updateCountdown();
|
|
327
|
+
const intervalId = window.setInterval(updateCountdown, 200);
|
|
328
|
+
const timeoutId = window.setTimeout(() => {
|
|
329
|
+
window.clearInterval(intervalId);
|
|
330
|
+
setSecondsRemaining(-1);
|
|
331
|
+
onAdvance();
|
|
332
|
+
}, delayMs);
|
|
333
|
+
return () => {
|
|
334
|
+
window.clearInterval(intervalId);
|
|
335
|
+
window.clearTimeout(timeoutId);
|
|
336
|
+
setSecondsRemaining(-1);
|
|
337
|
+
};
|
|
338
|
+
}, [delayMs, enabled, onAdvance]);
|
|
339
|
+
if (!enabled || secondsRemaining < 0) {
|
|
340
|
+
return inactiveAutoAdvance;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
active: true,
|
|
344
|
+
secondsRemaining,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Pause on the puzzle setup position before the solution recap animates. */
|
|
349
|
+
const PUZZLE_COMPLETION_RECAP_SETUP_MS = 400;
|
|
350
|
+
const usePuzzleCompletionRecap = ({ source, active, onComplete, }) => {
|
|
351
|
+
var _a, _b, _c, _d, _e, _f;
|
|
352
|
+
const startFen = (_a = source === null || source === void 0 ? void 0 : source.startFen) !== null && _a !== void 0 ? _a : '';
|
|
353
|
+
const movesUci = (_b = source === null || source === void 0 ? void 0 : source.movesUci) !== null && _b !== void 0 ? _b : [];
|
|
354
|
+
const startIndex = (_c = source === null || source === void 0 ? void 0 : source.startIndex) !== null && _c !== void 0 ? _c : 0;
|
|
355
|
+
const endIndex = (_d = source === null || source === void 0 ? void 0 : source.endIndex) !== null && _d !== void 0 ? _d : 0;
|
|
356
|
+
const missedIndices = (_e = source === null || source === void 0 ? void 0 : source.missedIndices) !== null && _e !== void 0 ? _e : [];
|
|
357
|
+
const setupUci = (_f = source === null || source === void 0 ? void 0 : source.setupUci) !== null && _f !== void 0 ? _f : null;
|
|
358
|
+
const resolveFen = useCallback((moveIndex, afterMove) => {
|
|
359
|
+
if (!startFen || movesUci.length === 0) {
|
|
360
|
+
return '';
|
|
361
|
+
}
|
|
362
|
+
return fenAtPlyFromStart(startFen, movesUci, afterMove ? moveIndex + 1 : moveIndex);
|
|
363
|
+
}, [movesUci, startFen]);
|
|
364
|
+
return useSolutionLineRecap({
|
|
365
|
+
active: active && source !== null,
|
|
366
|
+
movesUci,
|
|
367
|
+
startIndex,
|
|
368
|
+
endIndex,
|
|
369
|
+
missedIndices,
|
|
370
|
+
segmentStartFen: startFen,
|
|
371
|
+
setupUci,
|
|
372
|
+
onComplete,
|
|
373
|
+
resolveFen,
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
|
|
282
377
|
const isAttemptFinished = (resultStatus) => resultStatus === 'complete' || resultStatus === 'incorrect';
|
|
283
378
|
/** Library default hint / next / analysis / result controls (unstyled buttons). */
|
|
284
379
|
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" }))] }));
|
|
@@ -422,6 +517,10 @@ class Position {
|
|
|
422
517
|
getIndex() {
|
|
423
518
|
return this.i;
|
|
424
519
|
}
|
|
520
|
+
/** UCI of the move that produced the current position. */
|
|
521
|
+
getLastMoveUci() {
|
|
522
|
+
return lastMoveUciAtPly(this.moves, this.i);
|
|
523
|
+
}
|
|
425
524
|
// Common methods shared by all positions
|
|
426
525
|
next() {
|
|
427
526
|
if (this.i >= this.moves.length) {
|
|
@@ -483,6 +582,29 @@ function playerColorForSolution(initialFen, moves) {
|
|
|
483
582
|
}
|
|
484
583
|
return chess.turn() === 'w' ? 'white' : 'black';
|
|
485
584
|
}
|
|
585
|
+
/** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
|
|
586
|
+
function playerMoveIndicesInRange(initialFen, moves, fromIndex, toIndex) {
|
|
587
|
+
const playerColor = playerColorForSolution(initialFen, moves);
|
|
588
|
+
const chess = new Chess(initialFen);
|
|
589
|
+
const indices = [];
|
|
590
|
+
for (let i = 0; i < toIndex && i < moves.length; i++) {
|
|
591
|
+
const side = chess.turn() === 'w' ? 'white' : 'black';
|
|
592
|
+
if (i >= fromIndex && side === playerColor) {
|
|
593
|
+
indices.push(i);
|
|
594
|
+
}
|
|
595
|
+
applyUciMove(chess, moves[i]);
|
|
596
|
+
}
|
|
597
|
+
return indices;
|
|
598
|
+
}
|
|
599
|
+
/** Auto-play opponent setup plies until the solver is on move. */
|
|
600
|
+
function advanceToPlayerTurn(position) {
|
|
601
|
+
while (!position.isFinished() &&
|
|
602
|
+
position.getSideToMove() !== position.getPlayerColor()) {
|
|
603
|
+
if (!position.next()) {
|
|
604
|
+
break;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
486
608
|
class PuzzlePosition extends Position {
|
|
487
609
|
constructor(initialFEN, moves, resumeConfig) {
|
|
488
610
|
super();
|
|
@@ -727,19 +849,37 @@ class GamePosition extends Position {
|
|
|
727
849
|
}
|
|
728
850
|
}
|
|
729
851
|
|
|
730
|
-
/** Apply
|
|
852
|
+
/** Apply opponent setup plies immediately so the board does not flash on load. */
|
|
731
853
|
const puzzlePositionFromFetch = (fen, moves, resume) => {
|
|
732
854
|
const newPosition = new PuzzlePosition(fen, moves, resume);
|
|
733
855
|
if (!resume && moves.length > 1) {
|
|
734
856
|
newPosition.next();
|
|
857
|
+
advanceToPlayerTurn(newPosition);
|
|
858
|
+
}
|
|
859
|
+
else if (resume &&
|
|
860
|
+
newPosition.getSideToMove() !== newPosition.getPlayerColor()) {
|
|
861
|
+
advanceToPlayerTurn(newPosition);
|
|
735
862
|
}
|
|
736
863
|
return newPosition;
|
|
737
864
|
};
|
|
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
865
|
const SOLUTION_STEP_MS = 500;
|
|
741
866
|
const RESUME_AUTO_STEP_MS = 500;
|
|
742
|
-
const
|
|
867
|
+
const uniqueIndices = (indices) => [...new Set(indices)];
|
|
868
|
+
const buildCompletionRecapSource = (position, missedIndices) => {
|
|
869
|
+
var _a, _b;
|
|
870
|
+
const movesUci = position.getSolutionMoves();
|
|
871
|
+
const initialFen = position.getInitialFen();
|
|
872
|
+
const startIndex = (_a = playerMoveIndicesInRange(initialFen, movesUci, 0, movesUci.length)[0]) !== null && _a !== void 0 ? _a : 0;
|
|
873
|
+
return {
|
|
874
|
+
startFen: initialFen,
|
|
875
|
+
movesUci,
|
|
876
|
+
startIndex,
|
|
877
|
+
endIndex: movesUci.length,
|
|
878
|
+
missedIndices,
|
|
879
|
+
setupUci: startIndex > 0 ? (_b = movesUci[startIndex - 1]) !== null && _b !== void 0 ? _b : null : null,
|
|
880
|
+
};
|
|
881
|
+
};
|
|
882
|
+
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
883
|
var _a, _b, _c, _d;
|
|
744
884
|
const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
|
|
745
885
|
const stackControlsBelow = useStackPuzzleControlsBelow();
|
|
@@ -755,6 +895,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
755
895
|
const [puzzleComplete, setPuzzleComplete] = useState(false);
|
|
756
896
|
const [completedAfterMiss, setCompletedAfterMiss] = useState(false);
|
|
757
897
|
const [missFeedback, setMissFeedback] = useState(null);
|
|
898
|
+
const [missedMoveIndices, setMissedMoveIndices] = useState([]);
|
|
899
|
+
const [completionCheckVisible, setCompletionCheckVisible] = useState(false);
|
|
900
|
+
const [completionRecapActive, setCompletionRecapActive] = useState(false);
|
|
901
|
+
const [completionRecapDone, setCompletionRecapDone] = useState(false);
|
|
902
|
+
const completionFlowStartedRef = useRef(false);
|
|
758
903
|
const [, setInteractionNum] = useState(0);
|
|
759
904
|
const solutionAnimationRef = useRef({ cancelled: false, timeoutIds: [] });
|
|
760
905
|
const resumeAnimationRef = useRef({ cancelled: false, timeoutIds: [] });
|
|
@@ -781,6 +926,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
781
926
|
setPuzzleComplete(false);
|
|
782
927
|
setCompletedAfterMiss(false);
|
|
783
928
|
setMissFeedback(null);
|
|
929
|
+
setMissedMoveIndices([]);
|
|
930
|
+
setCompletionCheckVisible(false);
|
|
931
|
+
setCompletionRecapActive(false);
|
|
932
|
+
setCompletionRecapDone(false);
|
|
933
|
+
completionFlowStartedRef.current = false;
|
|
784
934
|
onFetch()
|
|
785
935
|
.then((data) => {
|
|
786
936
|
if (cancelled) {
|
|
@@ -816,10 +966,16 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
816
966
|
feedbackData.isCorrect === false;
|
|
817
967
|
if (feedbackData.hintRequested) {
|
|
818
968
|
setHintUsed(true);
|
|
969
|
+
setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
|
|
819
970
|
}
|
|
820
971
|
if (incorrectThisFeedback) {
|
|
821
972
|
setHasIncorrectAttempt(true);
|
|
822
973
|
}
|
|
974
|
+
if (feedbackData.isCorrect === false &&
|
|
975
|
+
!feedbackData.isFinished &&
|
|
976
|
+
!feedbackData.solutionShown) {
|
|
977
|
+
setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
|
|
978
|
+
}
|
|
823
979
|
if (feedbackData.isFinished) {
|
|
824
980
|
setPuzzleComplete(true);
|
|
825
981
|
setCompletedAfterMiss((prev) => prev ||
|
|
@@ -983,6 +1139,10 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
983
1139
|
position.recordSolutionShown();
|
|
984
1140
|
position.setSolutionRevealed(true);
|
|
985
1141
|
position.wantsHint(false);
|
|
1142
|
+
setMissedMoveIndices((prev) => uniqueIndices([
|
|
1143
|
+
...prev,
|
|
1144
|
+
...playerMoveIndicesInRange(position.getInitialFen(), position.getSolutionMoves(), position.getIndex(), position.getSolutionMoves().length),
|
|
1145
|
+
]));
|
|
986
1146
|
handleFeedback({
|
|
987
1147
|
index: position.getIndex(),
|
|
988
1148
|
solutionShown: true,
|
|
@@ -995,30 +1155,46 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
995
1155
|
setPuzzleNum((prevPuzzleNum) => prevPuzzleNum + 1);
|
|
996
1156
|
}, []);
|
|
997
1157
|
const resultStatus = getResultStatus();
|
|
1158
|
+
const completionRecapSource = useMemo(() => position && showCompletionRecap
|
|
1159
|
+
? buildCompletionRecapSource(position, missedMoveIndices)
|
|
1160
|
+
: null, [position, showCompletionRecap, missedMoveIndices]);
|
|
1161
|
+
const handleCompletionRecapDone = useCallback(() => {
|
|
1162
|
+
setCompletionRecapActive(false);
|
|
1163
|
+
setCompletionRecapDone(true);
|
|
1164
|
+
}, []);
|
|
1165
|
+
const completionRecap = usePuzzleCompletionRecap({
|
|
1166
|
+
source: completionRecapSource,
|
|
1167
|
+
active: completionRecapActive,
|
|
1168
|
+
onComplete: handleCompletionRecapDone,
|
|
1169
|
+
});
|
|
1170
|
+
const isCompletionRecapping = showCompletionRecap && (completionRecapActive || completionRecap.active);
|
|
998
1171
|
useEffect(() => {
|
|
999
|
-
if (!
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1172
|
+
if (!showCompletionRecap ||
|
|
1173
|
+
resultStatus !== 'complete' ||
|
|
1174
|
+
loadingNextPuzzle ||
|
|
1175
|
+
completionFlowStartedRef.current) {
|
|
1003
1176
|
return;
|
|
1004
1177
|
}
|
|
1005
|
-
|
|
1178
|
+
completionFlowStartedRef.current = true;
|
|
1179
|
+
setCompletionCheckVisible(true);
|
|
1180
|
+
}, [loadingNextPuzzle, resultStatus, showCompletionRecap]);
|
|
1181
|
+
useEffect(() => {
|
|
1182
|
+
if (!completionCheckVisible) {
|
|
1006
1183
|
return;
|
|
1007
1184
|
}
|
|
1008
|
-
const timer = setTimeout(() => {
|
|
1009
|
-
|
|
1010
|
-
|
|
1185
|
+
const timer = window.setTimeout(() => {
|
|
1186
|
+
setCompletionCheckVisible(false);
|
|
1187
|
+
setCompletionRecapActive(true);
|
|
1188
|
+
}, PUZZLE_COMPLETION_RECAP_SETUP_MS);
|
|
1011
1189
|
return () => {
|
|
1012
|
-
clearTimeout(timer);
|
|
1190
|
+
window.clearTimeout(timer);
|
|
1013
1191
|
};
|
|
1014
|
-
}, [
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
puzzleNum,
|
|
1021
|
-
]);
|
|
1192
|
+
}, [completionCheckVisible]);
|
|
1193
|
+
const shouldAutoAdvance = autoAdvanceOnComplete &&
|
|
1194
|
+
resultStatus === 'complete' &&
|
|
1195
|
+
!(hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) &&
|
|
1196
|
+
(!showCompletionRecap || completionRecapDone);
|
|
1197
|
+
const autoAdvance = usePuzzleAutoAdvanceCountdown(shouldAutoAdvance, autoAdvanceOnCompleteDelayMs, handleNextPuzzle);
|
|
1022
1198
|
const controlState = {
|
|
1023
1199
|
canShowHint: position !== null &&
|
|
1024
1200
|
!position.isFinished() &&
|
|
@@ -1033,7 +1209,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1033
1209
|
const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
|
|
1034
1210
|
renderAnalysisContainer &&
|
|
1035
1211
|
(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: [
|
|
1212
|
+
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 ||
|
|
1213
|
+
completionCheckVisible ||
|
|
1214
|
+
isCompletionRecapping, onMissFeedbackChange: setMissFeedback, recapBoard: isCompletionRecapping
|
|
1215
|
+
? {
|
|
1216
|
+
fen: completionRecap.fen,
|
|
1217
|
+
lastMoveUci: completionRecap.lastMoveUci,
|
|
1218
|
+
customArrows: completionRecap.customArrows,
|
|
1219
|
+
animationDuration: completionRecap.animationDuration,
|
|
1220
|
+
}
|
|
1221
|
+
: null }) }), completionCheckVisible && (jsx(BoardCompleteCheckOverlay, { variant: hasIncorrectAttempt || completedAfterMiss || hintUsed
|
|
1222
|
+
? 'partial'
|
|
1223
|
+
: 'success' }))] }), renderBoardCaption && (jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
|
|
1037
1224
|
sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
|
|
1038
1225
|
playerColor: position
|
|
1039
1226
|
? position.getPlayerColor()
|
|
@@ -1049,7 +1236,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1049
1236
|
}) }))] }), jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
|
|
1050
1237
|
visible: analysis.canOpen,
|
|
1051
1238
|
openAnalysis: analysis.openAnalysis,
|
|
1052
|
-
}, controlState), renderBoardFeedback && resultStatus === 'complete' && (jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
|
|
1239
|
+
}, controlState, autoAdvance), renderBoardFeedback && resultStatus === 'complete' && (jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
|
|
1053
1240
|
resultStatus,
|
|
1054
1241
|
cleanSolve: !hasIncorrectAttempt,
|
|
1055
1242
|
refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
|
|
@@ -1063,7 +1250,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1063
1250
|
* side's pieces be dragged. Move validation and sequencing live in
|
|
1064
1251
|
* {@link LineBoardWithControls}.
|
|
1065
1252
|
*/
|
|
1066
|
-
const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, lastMoveUci = null, onPieceDrop, boardWidth, }) => (jsx(
|
|
1253
|
+
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
1254
|
|
|
1068
1255
|
/** Library default line-drill status controls (unstyled). */
|
|
1069
1256
|
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 +1299,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1112
1299
|
const [feedback, setFeedback] = useState(null);
|
|
1113
1300
|
const [displayFen, setDisplayFen] = useState(null);
|
|
1114
1301
|
const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = useCorrectMoveFeedback();
|
|
1302
|
+
const { incorrectMoveSquare, showIncorrectMove, isShowingIncorrectMove, } = useIncorrectMoveFeedback();
|
|
1115
1303
|
const total = line.movesUci.length;
|
|
1116
1304
|
const orientation = boardOrientationForLine(line.trainSide);
|
|
1117
1305
|
const applyMove = useCallback((index) => {
|
|
@@ -1136,7 +1324,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1136
1324
|
}, [line.movesUci]);
|
|
1137
1325
|
// Auto-play opponent moves and detect the end of the line.
|
|
1138
1326
|
useEffect(() => {
|
|
1139
|
-
if (finished || isShowingCorrectMove) {
|
|
1327
|
+
if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
|
|
1140
1328
|
return;
|
|
1141
1329
|
}
|
|
1142
1330
|
if (currentIndex >= total) {
|
|
@@ -1156,6 +1344,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1156
1344
|
applyMove,
|
|
1157
1345
|
opponentMoveDelayMs,
|
|
1158
1346
|
isShowingCorrectMove,
|
|
1347
|
+
isShowingIncorrectMove,
|
|
1159
1348
|
]);
|
|
1160
1349
|
// Emit the completion event exactly once.
|
|
1161
1350
|
useEffect(() => {
|
|
@@ -1167,7 +1356,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1167
1356
|
}, [finished]);
|
|
1168
1357
|
const handleDrop = (source, target, piece) => {
|
|
1169
1358
|
var _a, _b, _c;
|
|
1170
|
-
if (finished || isShowingCorrectMove) {
|
|
1359
|
+
if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
|
|
1171
1360
|
return false;
|
|
1172
1361
|
}
|
|
1173
1362
|
const setupFen = displayFen !== null && displayFen !== void 0 ? displayFen : chessRef.current.fen();
|
|
@@ -1187,7 +1376,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1187
1376
|
setFeedback(moveFeedback);
|
|
1188
1377
|
(_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
|
|
1189
1378
|
if (isCorrect) {
|
|
1190
|
-
const nextFen = fenAfterUci(setupFen, dropResult.uci);
|
|
1379
|
+
const nextFen = fenAfterUci(setupFen, dropResult.attempt.uci);
|
|
1191
1380
|
if (nextFen) {
|
|
1192
1381
|
setDisplayFen(nextFen);
|
|
1193
1382
|
}
|
|
@@ -1198,6 +1387,9 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1198
1387
|
applyMove(index);
|
|
1199
1388
|
});
|
|
1200
1389
|
}
|
|
1390
|
+
else {
|
|
1391
|
+
showIncorrectMove(source);
|
|
1392
|
+
}
|
|
1201
1393
|
return isCorrect;
|
|
1202
1394
|
};
|
|
1203
1395
|
const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
|
|
@@ -1211,13 +1403,14 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1211
1403
|
const moveNumber = Math.min(currentIndex + 1, total);
|
|
1212
1404
|
const isUserTurn = !finished &&
|
|
1213
1405
|
!isShowingCorrectMove &&
|
|
1406
|
+
!isShowingIncorrectMove &&
|
|
1214
1407
|
turnFromFen(boardFen) === line.trainSide &&
|
|
1215
1408
|
currentIndex < total;
|
|
1216
1409
|
const stackControlsBelow = useStackPuzzleControlsBelow();
|
|
1217
1410
|
const controlsPlacement = stackControlsBelow
|
|
1218
1411
|
? 'below'
|
|
1219
1412
|
: '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({
|
|
1413
|
+
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
1414
|
trainSide: line.trainSide,
|
|
1222
1415
|
moveNumber,
|
|
1223
1416
|
total,
|
|
@@ -1234,4 +1427,4 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1234
1427
|
/** @deprecated Import {@link boardSquareHighlightColors} and {@link analysisBoardHighlightColors} from `react-chess-core`. */
|
|
1235
1428
|
const squareHighlightColors = Object.assign(Object.assign({}, boardSquareHighlightColors), analysisBoardHighlightColors);
|
|
1236
1429
|
|
|
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 };
|
|
1430
|
+
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, }) => {
|
|
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, }) => {
|
|
66
65
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
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,33 @@ 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;
|
|
186
|
+
const lastMoveUci = isRecapping
|
|
187
|
+
? recapBoard.lastMoveUci
|
|
188
|
+
: useRefutation && incorrectActive
|
|
189
|
+
? missBoard.lastMoveUci
|
|
190
|
+
: ((_e = position === null || position === void 0 ? void 0 : position.getLastMoveUci()) !== null && _e !== void 0 ? _e : null);
|
|
165
191
|
const missLocked = useRefutation &&
|
|
166
192
|
incorrectActive &&
|
|
167
193
|
(missBoard.boardAnimating ||
|
|
168
194
|
missPhase === 'wrong' ||
|
|
169
195
|
missPhase === 'refutation');
|
|
170
|
-
const arePiecesDraggable =
|
|
196
|
+
const arePiecesDraggable = !isRecapping &&
|
|
197
|
+
position !== null &&
|
|
171
198
|
!positionLocked &&
|
|
172
199
|
!missLocked &&
|
|
173
|
-
correctMoveSquare === null
|
|
200
|
+
correctMoveSquare === null &&
|
|
201
|
+
overlayIncorrectSquare === null;
|
|
174
202
|
const onPieceDrop = (sourceSquare, targetSquare, piece) => {
|
|
175
203
|
if (!position || positionLocked || position.isSolutionRevealed()) {
|
|
176
204
|
return false;
|
|
@@ -184,6 +212,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
184
212
|
if (answerArrowVisible &&
|
|
185
213
|
!allowRetryOnIncorrect &&
|
|
186
214
|
!position.isExpectedGuess(sourceSquare, targetSquare)) {
|
|
215
|
+
showIncorrectMove(sourceSquare);
|
|
187
216
|
position.resetInteractions();
|
|
188
217
|
snapBoardBack();
|
|
189
218
|
return false;
|
|
@@ -193,6 +222,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
193
222
|
});
|
|
194
223
|
if (!guess.accepted) {
|
|
195
224
|
attemptMissedRef.current = true;
|
|
225
|
+
showIncorrectMove(useRefutation ? targetSquare : sourceSquare);
|
|
196
226
|
onFeedback({
|
|
197
227
|
index: position.getIndex(),
|
|
198
228
|
guess: { sourceSquare, targetSquare, piece },
|
|
@@ -206,8 +236,7 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
206
236
|
missBoard.missSequence.startSequence(setupFen, attemptedUci);
|
|
207
237
|
}
|
|
208
238
|
position.resetInteractions();
|
|
209
|
-
|
|
210
|
-
return false;
|
|
239
|
+
return true;
|
|
211
240
|
}
|
|
212
241
|
const revealIncorrectFeedback = () => {
|
|
213
242
|
if (showAnswerArrowOnIncorrect) {
|
|
@@ -273,13 +302,79 @@ const PuzzlePlaySurface = ({ position, onFeedback, incInteractionNum, boardWidth
|
|
|
273
302
|
showCorrectMove(targetSquare, finishCorrectFeedback);
|
|
274
303
|
return true;
|
|
275
304
|
};
|
|
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 }));
|
|
305
|
+
return hasBoard ? (jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: isRecapping ? '' : ((_f = position === null || position === void 0 ? void 0 : position.getCheckSquare()) !== null && _f !== void 0 ? _f : ''), hintSquare: isRecapping ? null : ((_g = position === null || position === void 0 ? void 0 : position.getHintSquare()) !== null && _g !== void 0 ? _g : null), incorrectMoveSquare: isRecapping ? null : overlayIncorrectSquare, refutationMoveSquare: isRecapping ? null : refutationMoveSquare, correctMoveSquare: isRecapping ? null : correctMoveSquare, customArrows: customArrows, lastMoveUci: lastMoveUci, onPieceDrop: onPieceDrop, position: displayFen, boardOrientation: boardOrientation, arePiecesDraggable: arePiecesDraggable, areArrowsAllowed: false, promotionDialogVariant: "modal", animationDuration: isRecapping ? recapBoard.animationDuration : 0 }, revision)) : null;
|
|
279
306
|
};
|
|
280
307
|
|
|
281
308
|
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
309
|
|
|
310
|
+
const inactiveAutoAdvance = {
|
|
311
|
+
active: false,
|
|
312
|
+
secondsRemaining: -1,
|
|
313
|
+
};
|
|
314
|
+
/** Countdown overlay state while waiting to auto-load the next puzzle card. */
|
|
315
|
+
function usePuzzleAutoAdvanceCountdown(enabled, delayMs, onAdvance) {
|
|
316
|
+
const [secondsRemaining, setSecondsRemaining] = react.useState(-1);
|
|
317
|
+
react.useEffect(() => {
|
|
318
|
+
if (!enabled || delayMs <= 0) {
|
|
319
|
+
setSecondsRemaining(-1);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const startedAt = Date.now();
|
|
323
|
+
const updateCountdown = () => {
|
|
324
|
+
const elapsed = Date.now() - startedAt;
|
|
325
|
+
setSecondsRemaining(Math.max(0, Math.ceil((delayMs - elapsed) / 1000)));
|
|
326
|
+
};
|
|
327
|
+
updateCountdown();
|
|
328
|
+
const intervalId = window.setInterval(updateCountdown, 200);
|
|
329
|
+
const timeoutId = window.setTimeout(() => {
|
|
330
|
+
window.clearInterval(intervalId);
|
|
331
|
+
setSecondsRemaining(-1);
|
|
332
|
+
onAdvance();
|
|
333
|
+
}, delayMs);
|
|
334
|
+
return () => {
|
|
335
|
+
window.clearInterval(intervalId);
|
|
336
|
+
window.clearTimeout(timeoutId);
|
|
337
|
+
setSecondsRemaining(-1);
|
|
338
|
+
};
|
|
339
|
+
}, [delayMs, enabled, onAdvance]);
|
|
340
|
+
if (!enabled || secondsRemaining < 0) {
|
|
341
|
+
return inactiveAutoAdvance;
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
active: true,
|
|
345
|
+
secondsRemaining,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Pause on the puzzle setup position before the solution recap animates. */
|
|
350
|
+
const PUZZLE_COMPLETION_RECAP_SETUP_MS = 400;
|
|
351
|
+
const usePuzzleCompletionRecap = ({ source, active, onComplete, }) => {
|
|
352
|
+
var _a, _b, _c, _d, _e, _f;
|
|
353
|
+
const startFen = (_a = source === null || source === void 0 ? void 0 : source.startFen) !== null && _a !== void 0 ? _a : '';
|
|
354
|
+
const movesUci = (_b = source === null || source === void 0 ? void 0 : source.movesUci) !== null && _b !== void 0 ? _b : [];
|
|
355
|
+
const startIndex = (_c = source === null || source === void 0 ? void 0 : source.startIndex) !== null && _c !== void 0 ? _c : 0;
|
|
356
|
+
const endIndex = (_d = source === null || source === void 0 ? void 0 : source.endIndex) !== null && _d !== void 0 ? _d : 0;
|
|
357
|
+
const missedIndices = (_e = source === null || source === void 0 ? void 0 : source.missedIndices) !== null && _e !== void 0 ? _e : [];
|
|
358
|
+
const setupUci = (_f = source === null || source === void 0 ? void 0 : source.setupUci) !== null && _f !== void 0 ? _f : null;
|
|
359
|
+
const resolveFen = react.useCallback((moveIndex, afterMove) => {
|
|
360
|
+
if (!startFen || movesUci.length === 0) {
|
|
361
|
+
return '';
|
|
362
|
+
}
|
|
363
|
+
return reactChessCore.fenAtPlyFromStart(startFen, movesUci, afterMove ? moveIndex + 1 : moveIndex);
|
|
364
|
+
}, [movesUci, startFen]);
|
|
365
|
+
return reactChessCore.useSolutionLineRecap({
|
|
366
|
+
active: active && source !== null,
|
|
367
|
+
movesUci,
|
|
368
|
+
startIndex,
|
|
369
|
+
endIndex,
|
|
370
|
+
missedIndices,
|
|
371
|
+
segmentStartFen: startFen,
|
|
372
|
+
setupUci,
|
|
373
|
+
onComplete,
|
|
374
|
+
resolveFen,
|
|
375
|
+
});
|
|
376
|
+
};
|
|
377
|
+
|
|
283
378
|
const isAttemptFinished = (resultStatus) => resultStatus === 'complete' || resultStatus === 'incorrect';
|
|
284
379
|
/** Library default hint / next / analysis / result controls (unstyled buttons). */
|
|
285
380
|
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" }))] }));
|
|
@@ -423,6 +518,10 @@ class Position {
|
|
|
423
518
|
getIndex() {
|
|
424
519
|
return this.i;
|
|
425
520
|
}
|
|
521
|
+
/** UCI of the move that produced the current position. */
|
|
522
|
+
getLastMoveUci() {
|
|
523
|
+
return reactChessCore.lastMoveUciAtPly(this.moves, this.i);
|
|
524
|
+
}
|
|
426
525
|
// Common methods shared by all positions
|
|
427
526
|
next() {
|
|
428
527
|
if (this.i >= this.moves.length) {
|
|
@@ -484,6 +583,29 @@ function playerColorForSolution(initialFen, moves) {
|
|
|
484
583
|
}
|
|
485
584
|
return chess.turn() === 'w' ? 'white' : 'black';
|
|
486
585
|
}
|
|
586
|
+
/** Move indices in `[fromIndex, toIndex)` where the solver is on move. */
|
|
587
|
+
function playerMoveIndicesInRange(initialFen, moves, fromIndex, toIndex) {
|
|
588
|
+
const playerColor = playerColorForSolution(initialFen, moves);
|
|
589
|
+
const chess = new chess_js.Chess(initialFen);
|
|
590
|
+
const indices = [];
|
|
591
|
+
for (let i = 0; i < toIndex && i < moves.length; i++) {
|
|
592
|
+
const side = chess.turn() === 'w' ? 'white' : 'black';
|
|
593
|
+
if (i >= fromIndex && side === playerColor) {
|
|
594
|
+
indices.push(i);
|
|
595
|
+
}
|
|
596
|
+
applyUciMove(chess, moves[i]);
|
|
597
|
+
}
|
|
598
|
+
return indices;
|
|
599
|
+
}
|
|
600
|
+
/** Auto-play opponent setup plies until the solver is on move. */
|
|
601
|
+
function advanceToPlayerTurn(position) {
|
|
602
|
+
while (!position.isFinished() &&
|
|
603
|
+
position.getSideToMove() !== position.getPlayerColor()) {
|
|
604
|
+
if (!position.next()) {
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
487
609
|
class PuzzlePosition extends Position {
|
|
488
610
|
constructor(initialFEN, moves, resumeConfig) {
|
|
489
611
|
super();
|
|
@@ -728,19 +850,37 @@ class GamePosition extends Position {
|
|
|
728
850
|
}
|
|
729
851
|
}
|
|
730
852
|
|
|
731
|
-
/** Apply
|
|
853
|
+
/** Apply opponent setup plies immediately so the board does not flash on load. */
|
|
732
854
|
const puzzlePositionFromFetch = (fen, moves, resume) => {
|
|
733
855
|
const newPosition = new PuzzlePosition(fen, moves, resume);
|
|
734
856
|
if (!resume && moves.length > 1) {
|
|
735
857
|
newPosition.next();
|
|
858
|
+
advanceToPlayerTurn(newPosition);
|
|
859
|
+
}
|
|
860
|
+
else if (resume &&
|
|
861
|
+
newPosition.getSideToMove() !== newPosition.getPlayerColor()) {
|
|
862
|
+
advanceToPlayerTurn(newPosition);
|
|
736
863
|
}
|
|
737
864
|
return newPosition;
|
|
738
865
|
};
|
|
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
866
|
const SOLUTION_STEP_MS = 500;
|
|
742
867
|
const RESUME_AUTO_STEP_MS = 500;
|
|
743
|
-
const
|
|
868
|
+
const uniqueIndices = (indices) => [...new Set(indices)];
|
|
869
|
+
const buildCompletionRecapSource = (position, missedIndices) => {
|
|
870
|
+
var _a, _b;
|
|
871
|
+
const movesUci = position.getSolutionMoves();
|
|
872
|
+
const initialFen = position.getInitialFen();
|
|
873
|
+
const startIndex = (_a = playerMoveIndicesInRange(initialFen, movesUci, 0, movesUci.length)[0]) !== null && _a !== void 0 ? _a : 0;
|
|
874
|
+
return {
|
|
875
|
+
startFen: initialFen,
|
|
876
|
+
movesUci,
|
|
877
|
+
startIndex,
|
|
878
|
+
endIndex: movesUci.length,
|
|
879
|
+
missedIndices,
|
|
880
|
+
setupUci: startIndex > 0 ? (_b = movesUci[startIndex - 1]) !== null && _b !== void 0 ? _b : null : null,
|
|
881
|
+
};
|
|
882
|
+
};
|
|
883
|
+
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
884
|
var _a, _b, _c, _d;
|
|
745
885
|
const refutationOnIncorrect = showRefutationOnIncorrect !== null && showRefutationOnIncorrect !== void 0 ? showRefutationOnIncorrect : showAnswerArrowOnIncorrect;
|
|
746
886
|
const stackControlsBelow = useStackPuzzleControlsBelow();
|
|
@@ -756,6 +896,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
756
896
|
const [puzzleComplete, setPuzzleComplete] = react.useState(false);
|
|
757
897
|
const [completedAfterMiss, setCompletedAfterMiss] = react.useState(false);
|
|
758
898
|
const [missFeedback, setMissFeedback] = react.useState(null);
|
|
899
|
+
const [missedMoveIndices, setMissedMoveIndices] = react.useState([]);
|
|
900
|
+
const [completionCheckVisible, setCompletionCheckVisible] = react.useState(false);
|
|
901
|
+
const [completionRecapActive, setCompletionRecapActive] = react.useState(false);
|
|
902
|
+
const [completionRecapDone, setCompletionRecapDone] = react.useState(false);
|
|
903
|
+
const completionFlowStartedRef = react.useRef(false);
|
|
759
904
|
const [, setInteractionNum] = react.useState(0);
|
|
760
905
|
const solutionAnimationRef = react.useRef({ cancelled: false, timeoutIds: [] });
|
|
761
906
|
const resumeAnimationRef = react.useRef({ cancelled: false, timeoutIds: [] });
|
|
@@ -782,6 +927,11 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
782
927
|
setPuzzleComplete(false);
|
|
783
928
|
setCompletedAfterMiss(false);
|
|
784
929
|
setMissFeedback(null);
|
|
930
|
+
setMissedMoveIndices([]);
|
|
931
|
+
setCompletionCheckVisible(false);
|
|
932
|
+
setCompletionRecapActive(false);
|
|
933
|
+
setCompletionRecapDone(false);
|
|
934
|
+
completionFlowStartedRef.current = false;
|
|
785
935
|
onFetch()
|
|
786
936
|
.then((data) => {
|
|
787
937
|
if (cancelled) {
|
|
@@ -817,10 +967,16 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
817
967
|
feedbackData.isCorrect === false;
|
|
818
968
|
if (feedbackData.hintRequested) {
|
|
819
969
|
setHintUsed(true);
|
|
970
|
+
setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
|
|
820
971
|
}
|
|
821
972
|
if (incorrectThisFeedback) {
|
|
822
973
|
setHasIncorrectAttempt(true);
|
|
823
974
|
}
|
|
975
|
+
if (feedbackData.isCorrect === false &&
|
|
976
|
+
!feedbackData.isFinished &&
|
|
977
|
+
!feedbackData.solutionShown) {
|
|
978
|
+
setMissedMoveIndices((prev) => uniqueIndices([...prev, feedbackData.index]));
|
|
979
|
+
}
|
|
824
980
|
if (feedbackData.isFinished) {
|
|
825
981
|
setPuzzleComplete(true);
|
|
826
982
|
setCompletedAfterMiss((prev) => prev ||
|
|
@@ -984,6 +1140,10 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
984
1140
|
position.recordSolutionShown();
|
|
985
1141
|
position.setSolutionRevealed(true);
|
|
986
1142
|
position.wantsHint(false);
|
|
1143
|
+
setMissedMoveIndices((prev) => uniqueIndices([
|
|
1144
|
+
...prev,
|
|
1145
|
+
...playerMoveIndicesInRange(position.getInitialFen(), position.getSolutionMoves(), position.getIndex(), position.getSolutionMoves().length),
|
|
1146
|
+
]));
|
|
987
1147
|
handleFeedback({
|
|
988
1148
|
index: position.getIndex(),
|
|
989
1149
|
solutionShown: true,
|
|
@@ -996,30 +1156,46 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
996
1156
|
setPuzzleNum((prevPuzzleNum) => prevPuzzleNum + 1);
|
|
997
1157
|
}, []);
|
|
998
1158
|
const resultStatus = getResultStatus();
|
|
1159
|
+
const completionRecapSource = react.useMemo(() => position && showCompletionRecap
|
|
1160
|
+
? buildCompletionRecapSource(position, missedMoveIndices)
|
|
1161
|
+
: null, [position, showCompletionRecap, missedMoveIndices]);
|
|
1162
|
+
const handleCompletionRecapDone = react.useCallback(() => {
|
|
1163
|
+
setCompletionRecapActive(false);
|
|
1164
|
+
setCompletionRecapDone(true);
|
|
1165
|
+
}, []);
|
|
1166
|
+
const completionRecap = usePuzzleCompletionRecap({
|
|
1167
|
+
source: completionRecapSource,
|
|
1168
|
+
active: completionRecapActive,
|
|
1169
|
+
onComplete: handleCompletionRecapDone,
|
|
1170
|
+
});
|
|
1171
|
+
const isCompletionRecapping = showCompletionRecap && (completionRecapActive || completionRecap.active);
|
|
999
1172
|
react.useEffect(() => {
|
|
1000
|
-
if (!
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1173
|
+
if (!showCompletionRecap ||
|
|
1174
|
+
resultStatus !== 'complete' ||
|
|
1175
|
+
loadingNextPuzzle ||
|
|
1176
|
+
completionFlowStartedRef.current) {
|
|
1004
1177
|
return;
|
|
1005
1178
|
}
|
|
1006
|
-
|
|
1179
|
+
completionFlowStartedRef.current = true;
|
|
1180
|
+
setCompletionCheckVisible(true);
|
|
1181
|
+
}, [loadingNextPuzzle, resultStatus, showCompletionRecap]);
|
|
1182
|
+
react.useEffect(() => {
|
|
1183
|
+
if (!completionCheckVisible) {
|
|
1007
1184
|
return;
|
|
1008
1185
|
}
|
|
1009
|
-
const timer = setTimeout(() => {
|
|
1010
|
-
|
|
1011
|
-
|
|
1186
|
+
const timer = window.setTimeout(() => {
|
|
1187
|
+
setCompletionCheckVisible(false);
|
|
1188
|
+
setCompletionRecapActive(true);
|
|
1189
|
+
}, PUZZLE_COMPLETION_RECAP_SETUP_MS);
|
|
1012
1190
|
return () => {
|
|
1013
|
-
clearTimeout(timer);
|
|
1191
|
+
window.clearTimeout(timer);
|
|
1014
1192
|
};
|
|
1015
|
-
}, [
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
puzzleNum,
|
|
1022
|
-
]);
|
|
1193
|
+
}, [completionCheckVisible]);
|
|
1194
|
+
const shouldAutoAdvance = autoAdvanceOnComplete &&
|
|
1195
|
+
resultStatus === 'complete' &&
|
|
1196
|
+
!(hasIncorrectAttempt && !autoAdvanceOnCompleteAfterIncorrect) &&
|
|
1197
|
+
(!showCompletionRecap || completionRecapDone);
|
|
1198
|
+
const autoAdvance = usePuzzleAutoAdvanceCountdown(shouldAutoAdvance, autoAdvanceOnCompleteDelayMs, handleNextPuzzle);
|
|
1023
1199
|
const controlState = {
|
|
1024
1200
|
canShowHint: position !== null &&
|
|
1025
1201
|
!position.isFinished() &&
|
|
@@ -1034,7 +1210,18 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1034
1210
|
const useHostAnalysisUi = Boolean(renderAnalysisSidebar &&
|
|
1035
1211
|
renderAnalysisContainer &&
|
|
1036
1212
|
(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.
|
|
1213
|
+
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 ||
|
|
1214
|
+
completionCheckVisible ||
|
|
1215
|
+
isCompletionRecapping, onMissFeedbackChange: setMissFeedback, recapBoard: isCompletionRecapping
|
|
1216
|
+
? {
|
|
1217
|
+
fen: completionRecap.fen,
|
|
1218
|
+
lastMoveUci: completionRecap.lastMoveUci,
|
|
1219
|
+
customArrows: completionRecap.customArrows,
|
|
1220
|
+
animationDuration: completionRecap.animationDuration,
|
|
1221
|
+
}
|
|
1222
|
+
: null }) }), completionCheckVisible && (jsxRuntime.jsx(reactChessCore.BoardCompleteCheckOverlay, { variant: hasIncorrectAttempt || completedAfterMiss || hintUsed
|
|
1223
|
+
? 'partial'
|
|
1224
|
+
: 'success' }))] }), renderBoardCaption && (jsxRuntime.jsx("div", { style: puzzleBoardCaptionSlotStyle(), children: renderBoardCaption({
|
|
1038
1225
|
sideToMove: (_a = position === null || position === void 0 ? void 0 : position.getSideToMove()) !== null && _a !== void 0 ? _a : null,
|
|
1039
1226
|
playerColor: position
|
|
1040
1227
|
? position.getPlayerColor()
|
|
@@ -1050,7 +1237,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1050
1237
|
}) }))] }), jsxRuntime.jsxs("div", { style: puzzleControlsSlotStyle(controlsPlacement), children: [renderControls(handleHintRequest, handleShowSolution, handleNextPuzzle, resultStatus, {
|
|
1051
1238
|
visible: analysis.canOpen,
|
|
1052
1239
|
openAnalysis: analysis.openAnalysis,
|
|
1053
|
-
}, controlState), renderBoardFeedback && resultStatus === 'complete' && (jsxRuntime.jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
|
|
1240
|
+
}, controlState, autoAdvance), renderBoardFeedback && resultStatus === 'complete' && (jsxRuntime.jsx("div", { style: puzzleControlsFeedbackStyle(controlsPlacement), children: renderBoardFeedback({
|
|
1054
1241
|
resultStatus,
|
|
1055
1242
|
cleanSolve: !hasIncorrectAttempt,
|
|
1056
1243
|
refutationSan: missFeedback === null || missFeedback === void 0 ? void 0 : missFeedback.refutationSan,
|
|
@@ -1064,7 +1251,7 @@ const PuzzleBoardWithControls = ({ theme, boardTheme, apiProxy, renderControls =
|
|
|
1064
1251
|
* side's pieces be dragged. Move validation and sequencing live in
|
|
1065
1252
|
* {@link LineBoardWithControls}.
|
|
1066
1253
|
*/
|
|
1067
|
-
const LineBoard = ({ fen, orientation, trainSide, draggable, correctMoveSquare = null, lastMoveUci = null, onPieceDrop, boardWidth, }) => (jsxRuntime.jsx(reactChessCore.
|
|
1254
|
+
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
1255
|
|
|
1069
1256
|
/** Library default line-drill status controls (unstyled). */
|
|
1070
1257
|
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 +1300,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1113
1300
|
const [feedback, setFeedback] = react.useState(null);
|
|
1114
1301
|
const [displayFen, setDisplayFen] = react.useState(null);
|
|
1115
1302
|
const { correctMoveSquare, showCorrectMove, clearCorrectMoveFeedback, isShowingCorrectMove, } = reactChessCore.useCorrectMoveFeedback();
|
|
1303
|
+
const { incorrectMoveSquare, showIncorrectMove, isShowingIncorrectMove, } = reactChessCore.useIncorrectMoveFeedback();
|
|
1116
1304
|
const total = line.movesUci.length;
|
|
1117
1305
|
const orientation = boardOrientationForLine(line.trainSide);
|
|
1118
1306
|
const applyMove = react.useCallback((index) => {
|
|
@@ -1137,7 +1325,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1137
1325
|
}, [line.movesUci]);
|
|
1138
1326
|
// Auto-play opponent moves and detect the end of the line.
|
|
1139
1327
|
react.useEffect(() => {
|
|
1140
|
-
if (finished || isShowingCorrectMove) {
|
|
1328
|
+
if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
|
|
1141
1329
|
return;
|
|
1142
1330
|
}
|
|
1143
1331
|
if (currentIndex >= total) {
|
|
@@ -1157,6 +1345,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1157
1345
|
applyMove,
|
|
1158
1346
|
opponentMoveDelayMs,
|
|
1159
1347
|
isShowingCorrectMove,
|
|
1348
|
+
isShowingIncorrectMove,
|
|
1160
1349
|
]);
|
|
1161
1350
|
// Emit the completion event exactly once.
|
|
1162
1351
|
react.useEffect(() => {
|
|
@@ -1168,7 +1357,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1168
1357
|
}, [finished]);
|
|
1169
1358
|
const handleDrop = (source, target, piece) => {
|
|
1170
1359
|
var _a, _b, _c;
|
|
1171
|
-
if (finished || isShowingCorrectMove) {
|
|
1360
|
+
if (finished || isShowingCorrectMove || isShowingIncorrectMove) {
|
|
1172
1361
|
return false;
|
|
1173
1362
|
}
|
|
1174
1363
|
const setupFen = displayFen !== null && displayFen !== void 0 ? displayFen : chessRef.current.fen();
|
|
@@ -1188,7 +1377,7 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1188
1377
|
setFeedback(moveFeedback);
|
|
1189
1378
|
(_c = onMoveRef.current) === null || _c === void 0 ? void 0 : _c.call(onMoveRef, moveFeedback);
|
|
1190
1379
|
if (isCorrect) {
|
|
1191
|
-
const nextFen = reactChessCore.fenAfterUci(setupFen, dropResult.uci);
|
|
1380
|
+
const nextFen = reactChessCore.fenAfterUci(setupFen, dropResult.attempt.uci);
|
|
1192
1381
|
if (nextFen) {
|
|
1193
1382
|
setDisplayFen(nextFen);
|
|
1194
1383
|
}
|
|
@@ -1199,6 +1388,9 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1199
1388
|
applyMove(index);
|
|
1200
1389
|
});
|
|
1201
1390
|
}
|
|
1391
|
+
else {
|
|
1392
|
+
showIncorrectMove(source);
|
|
1393
|
+
}
|
|
1202
1394
|
return isCorrect;
|
|
1203
1395
|
};
|
|
1204
1396
|
const boardFen = displayFen !== null && displayFen !== void 0 ? displayFen : fen;
|
|
@@ -1212,13 +1404,14 @@ const LineBoardWithControls = ({ theme, boardTheme, line, onComplete, onMove, re
|
|
|
1212
1404
|
const moveNumber = Math.min(currentIndex + 1, total);
|
|
1213
1405
|
const isUserTurn = !finished &&
|
|
1214
1406
|
!isShowingCorrectMove &&
|
|
1407
|
+
!isShowingIncorrectMove &&
|
|
1215
1408
|
turnFromFen(boardFen) === line.trainSide &&
|
|
1216
1409
|
currentIndex < total;
|
|
1217
1410
|
const stackControlsBelow = useStackPuzzleControlsBelow();
|
|
1218
1411
|
const controlsPlacement = stackControlsBelow
|
|
1219
1412
|
? 'below'
|
|
1220
1413
|
: '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({
|
|
1414
|
+
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
1415
|
trainSide: line.trainSide,
|
|
1223
1416
|
moveNumber,
|
|
1224
1417
|
total,
|
|
@@ -1251,6 +1444,7 @@ exports.Position = Position;
|
|
|
1251
1444
|
exports.PuzzleBoard = PuzzleBoard;
|
|
1252
1445
|
exports.PuzzleBoardWithControls = PuzzleBoardWithControls;
|
|
1253
1446
|
exports.PuzzlePosition = PuzzlePosition;
|
|
1447
|
+
exports.advanceToPlayerTurn = advanceToPlayerTurn;
|
|
1254
1448
|
exports.applyUciMove = applyUciMove;
|
|
1255
1449
|
exports.buildAnalysisContext = buildAnalysisContext;
|
|
1256
1450
|
exports.defaultRenderControls = defaultRenderControls;
|
|
@@ -1259,5 +1453,6 @@ exports.emptyAnalysisContext = emptyAnalysisContext;
|
|
|
1259
1453
|
exports.getCheckSquareFromChess = getCheckSquareFromChess;
|
|
1260
1454
|
exports.isAnalysisAvailable = isAnalysisAvailable;
|
|
1261
1455
|
exports.playerColorForSolution = playerColorForSolution;
|
|
1456
|
+
exports.playerMoveIndicesInRange = playerMoveIndicesInRange;
|
|
1262
1457
|
exports.squareHighlightColors = squareHighlightColors;
|
|
1263
1458
|
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.6",
|
|
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",
|