react-chess-replay-trainer 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -44
- package/dist/features/replay/ReplayTrainer.d.ts +3 -2
- package/dist/features/replay/constants.d.ts +2 -0
- package/dist/features/replay/hooks/useReplayTrainer.d.ts +7 -0
- package/dist/features/replay/index.d.ts +7 -2
- package/dist/features/replay/miss/replayMissDisplay.d.ts +16 -0
- package/dist/features/replay/miss/useReplayMissBoard.d.ts +28 -0
- package/dist/features/replay/miss/useReplayMissSequence.d.ts +10 -0
- package/dist/features/replay/miss/useReplayRefutation.d.ts +3 -0
- package/dist/features/replay/refutation/replayRefutation.d.ts +19 -0
- package/dist/features/replay/replayUtils.d.ts +2 -3
- package/dist/features/replay/types.d.ts +1 -0
- package/dist/index.esm.js +85 -42
- package/dist/index.js +143 -40
- package/package.json +56 -56
- package/dist/features/replay/analysis/ReplayAnalysisBoard.d.ts +0 -18
- package/dist/features/replay/analysis/ReplayAnalysisPosition.d.ts +0 -32
- package/dist/features/replay/analysis/ReplayEngineEvaluationPanel.d.ts +0 -8
- package/dist/features/replay/analysis/analysisBoardStyles.d.ts +0 -63
- package/dist/features/replay/analysis/buildReplayAnalysisContext.d.ts +0 -4
- package/dist/features/replay/analysis/hooks/useReplayAnalysisBoard.d.ts +0 -34
- package/dist/features/replay/analysis/index.d.ts +0 -6
- package/dist/features/replay/analysis/replayAnalysisUtils.d.ts +0 -3
- package/dist/features/replay/analysis/types.d.ts +0 -22
package/README.md
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
# react-chess-replay-trainer
|
|
2
|
-
|
|
3
|
-
A React component for replaying a chess game move-by-move and drilling it.
|
|
4
|
-
|
|
5
|
-
- **Browse** freely through the game (first / prev / next / last / jump) without
|
|
6
|
-
recording anything, so you can pick the part of the game you want to study.
|
|
7
|
-
- **Train White / Train Black / Train Both** start a drill at the current ply:
|
|
8
|
-
you guess each move, and every mistake is reported via `onMiss` so the host can
|
|
9
|
-
(for example) enroll the missed position into a spaced-repetition deck.
|
|
10
|
-
- With **Train White** or **Train Black** you only guess that side's moves; the
|
|
11
|
-
opponent's reply is played automatically after each correct guess, and the
|
|
12
|
-
board is rotated so the trained side is on the bottom.
|
|
13
|
-
- With **Train Both** you drill every ply for both colors.
|
|
14
|
-
- **Analyze** opens the built-in analysis board (`AnalysisBoard` from
|
|
15
|
-
`react-chess-core`) at the current position.
|
|
16
|
-
|
|
17
|
-
Depends on `react-chess-core` (board, engine, analysis board), `react-chessboard`,
|
|
18
|
-
and `chess.js`.
|
|
19
|
-
|
|
20
|
-
## Usage
|
|
21
|
-
|
|
22
|
-
```tsx
|
|
23
|
-
import { ReplayTrainer } from 'react-chess-replay-trainer';
|
|
24
|
-
|
|
25
|
-
<ReplayTrainer
|
|
26
|
-
gameId={gameId}
|
|
27
|
-
startFen={fenWhereUserWasBrowsing}
|
|
28
|
-
fetchGame={fetchGame}
|
|
29
|
-
onMiss={(miss) => enrollMissedPosition(miss)}
|
|
30
|
-
onExit={() => setTraining(null)}
|
|
31
|
-
theme="dark"
|
|
32
|
-
engine={{ depth: 18, multiPv: 3 }}
|
|
33
|
-
/>;
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
For a custom shell, use `useReplayTrainer` with `AnalysisBoard` from the same package:
|
|
37
|
-
|
|
38
|
-
```tsx
|
|
39
|
-
import {
|
|
40
|
-
useReplayTrainer,
|
|
41
|
-
buildReplayAnalysisContext,
|
|
42
|
-
AnalysisBoard,
|
|
43
|
-
} from 'react-chess-replay-trainer';
|
|
44
|
-
```
|
|
1
|
+
# react-chess-replay-trainer
|
|
2
|
+
|
|
3
|
+
A React component for replaying a chess game move-by-move and drilling it.
|
|
4
|
+
|
|
5
|
+
- **Browse** freely through the game (first / prev / next / last / jump) without
|
|
6
|
+
recording anything, so you can pick the part of the game you want to study.
|
|
7
|
+
- **Train White / Train Black / Train Both** start a drill at the current ply:
|
|
8
|
+
you guess each move, and every mistake is reported via `onMiss` so the host can
|
|
9
|
+
(for example) enroll the missed position into a spaced-repetition deck.
|
|
10
|
+
- With **Train White** or **Train Black** you only guess that side's moves; the
|
|
11
|
+
opponent's reply is played automatically after each correct guess, and the
|
|
12
|
+
board is rotated so the trained side is on the bottom.
|
|
13
|
+
- With **Train Both** you drill every ply for both colors.
|
|
14
|
+
- **Analyze** opens the built-in analysis board (`AnalysisBoard` from
|
|
15
|
+
`react-chess-core`) at the current position.
|
|
16
|
+
|
|
17
|
+
Depends on `react-chess-core` (board, engine, analysis board), `react-chessboard`,
|
|
18
|
+
and `chess.js`.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { ReplayTrainer } from 'react-chess-replay-trainer';
|
|
24
|
+
|
|
25
|
+
<ReplayTrainer
|
|
26
|
+
gameId={gameId}
|
|
27
|
+
startFen={fenWhereUserWasBrowsing}
|
|
28
|
+
fetchGame={fetchGame}
|
|
29
|
+
onMiss={(miss) => enrollMissedPosition(miss)}
|
|
30
|
+
onExit={() => setTraining(null)}
|
|
31
|
+
theme="dark"
|
|
32
|
+
engine={{ depth: 18, multiPv: 3 }}
|
|
33
|
+
/>;
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For a custom shell, use `useReplayTrainer` with `AnalysisBoard` from the same package:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import {
|
|
40
|
+
useReplayTrainer,
|
|
41
|
+
buildReplayAnalysisContext,
|
|
42
|
+
AnalysisBoard,
|
|
43
|
+
} from 'react-chess-replay-trainer';
|
|
44
|
+
```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
import { type AnalysisEngineOptions, type PlyNavigationRenderProps } from 'react-chess-core';
|
|
2
|
+
import { type AnalysisEngineOptions, type BoardThemeId, type PlyNavigationRenderProps } from 'react-chess-core';
|
|
3
3
|
import type { ReplayGame, ReplayMiss } from './types';
|
|
4
4
|
export interface ReplayTrainerProps {
|
|
5
5
|
gameId: string;
|
|
@@ -13,6 +13,7 @@ export interface ReplayTrainerProps {
|
|
|
13
13
|
/** Called when the user leaves the trainer. */
|
|
14
14
|
onExit?: () => void;
|
|
15
15
|
theme?: 'light' | 'dark';
|
|
16
|
+
boardTheme?: BoardThemeId;
|
|
16
17
|
boardWidth?: number;
|
|
17
18
|
/** Side shown at the bottom of the board. Defaults to white. */
|
|
18
19
|
orientation?: 'white' | 'black';
|
|
@@ -28,4 +29,4 @@ export interface ReplayTrainerProps {
|
|
|
28
29
|
* next / last / slider) never records anything; once the user hits "Train from
|
|
29
30
|
* here" each wrong move is reported through {@link ReplayTrainerProps.onMiss}.
|
|
30
31
|
*/
|
|
31
|
-
export declare const ReplayTrainer: ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme, boardWidth, orientation, engine, renderPlyNavigation, showPlyScrubber, }: ReplayTrainerProps) => import("react").JSX.Element;
|
|
32
|
+
export declare const ReplayTrainer: ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme, boardTheme, boardWidth, orientation, engine, renderPlyNavigation, showPlyScrubber, }: ReplayTrainerProps) => import("react").JSX.Element;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
/** Standard chess starting position; game move lists replay from here. */
|
|
2
2
|
export declare const REPLAY_START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
3
3
|
export declare const DEFAULT_BOARD_WIDTH = 480;
|
|
4
|
+
/** Delay between half-moves during browse autoplay (ms). */
|
|
5
|
+
export declare const REPLAY_AUTOPLAY_STEP_MS = 500;
|
|
@@ -39,5 +39,12 @@ export interface ReplayTrainerState {
|
|
|
39
39
|
stopTraining: () => void;
|
|
40
40
|
revealMove: () => void;
|
|
41
41
|
handleDrop: (source: string, target: string, piece: string) => boolean;
|
|
42
|
+
/** True while browse mode is auto-advancing through the game. */
|
|
43
|
+
autoplayActive: boolean;
|
|
44
|
+
/** Start autoplay from the current ply (exits train mode). No-op at game end. */
|
|
45
|
+
startAutoplay: () => void;
|
|
46
|
+
stopAutoplay: () => void;
|
|
47
|
+
/** Toggle browse autoplay; exits train mode when starting. */
|
|
48
|
+
toggleAutoplay: () => void;
|
|
42
49
|
}
|
|
43
50
|
export declare function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, }: UseReplayTrainerOptions): ReplayTrainerState;
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { ReplayTrainer, type ReplayTrainerProps } from './ReplayTrainer';
|
|
2
2
|
export { useReplayTrainer, type UseReplayTrainerOptions, type ReplayTrainerState, } from './hooks/useReplayTrainer';
|
|
3
3
|
export type { ReplayGame, ReplayMiss, ReplayMode, ReplayFeedback, ReplaySide, TrainColor, } from './types';
|
|
4
|
-
export { REPLAY_START_FEN, DEFAULT_BOARD_WIDTH } from './constants';
|
|
5
|
-
export { fenAtPly, findPlyIndexForFen, sideToMove,
|
|
4
|
+
export { REPLAY_START_FEN, DEFAULT_BOARD_WIDTH, REPLAY_AUTOPLAY_STEP_MS, } from './constants';
|
|
5
|
+
export { fenAtPly, findPlyIndexForFen, sideToMove, normalizeFen, } from './replayUtils';
|
|
6
|
+
export { uciFromDrop } from 'react-chess-core';
|
|
6
7
|
export { buildReplayAnalysisContext } from './buildReplayAnalysisContext';
|
|
8
|
+
export { fenAfterUci, refutationEvalGapCp, refutationFromEvaluation, type RefutationResult as ReplayRefutationResult, } from 'react-chess-core';
|
|
9
|
+
export { refutationEngineOptions as replayRefutationEngineOptions, REFUTATION_EVAL_GAP_CP as REPLAY_REFUTATION_EVAL_GAP_CP, REFUTATION_EVAL_GAP_PAWNS as REPLAY_REFUTATION_EVAL_GAP_PAWNS, } from 'react-chess-core';
|
|
10
|
+
export { getMissDisplay as getReplayMissDisplay, MISS_MOVE_ANIMATION_MS as REPLAY_MISS_MOVE_ANIMATION_MS, MISS_REFUTATION_MAX_WAIT_MS as REPLAY_MISS_REFUTATION_MAX_WAIT_MS, MISS_REFUTATION_PAUSE_MS as REPLAY_MISS_REFUTATION_PAUSE_MS, MISS_WRONG_PAUSE_MS as REPLAY_MISS_WRONG_PAUSE_MS, type MissDisplay as ReplayMissDisplay, type MissSequencePhase, type MissSequenceState, } from 'react-chess-core';
|
|
11
|
+
export { useMissRefutation as useReplayRefutation, useMissSequence as useReplayMissSequence, useMissBoard as useReplayMissBoard, } from 'react-chess-core';
|
|
7
12
|
export { AnalysisBoard, AnalysisBoardCore, AnalysisErrorBoundary, DEFAULT_ANALYSIS_LAYOUT, DefaultPlyNavigation, PlyNavigation, defaultRenderPlyNavigation, type AnalysisContext, type AnalysisBoardProps, type AnalysisLayoutConfig, type PlyNavigationModel, type PlyNavigationProps, type PlyNavigationRenderProps, } from 'react-chess-core';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type MissSequencePhase = 'wrong' | 'refutation' | 'answer' | 'retry';
|
|
2
|
+
export type MissSequenceState = {
|
|
3
|
+
setupFen: string;
|
|
4
|
+
attemptedUci: string;
|
|
5
|
+
phase: MissSequencePhase;
|
|
6
|
+
};
|
|
7
|
+
export type ReplayMissDisplay = {
|
|
8
|
+
fen: string | null;
|
|
9
|
+
arrows: [string, string, string][];
|
|
10
|
+
animating: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare const REPLAY_MISS_WRONG_PAUSE_MS = 450;
|
|
13
|
+
export declare const REPLAY_MISS_REFUTATION_PAUSE_MS = 900;
|
|
14
|
+
export declare const REPLAY_MISS_REFUTATION_MAX_WAIT_MS = 4000;
|
|
15
|
+
export declare const REPLAY_MISS_MOVE_ANIMATION_MS = 220;
|
|
16
|
+
export declare function getReplayMissDisplay(sequence: MissSequenceState | null, expectedUci: string | null, refutationUci: string | null, answerArrowColor: string): ReplayMissDisplay;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AnalysisEngineOptions } from 'react-chess-core';
|
|
2
|
+
type ReplayMissFeedback = 'correct' | 'incorrect' | null;
|
|
3
|
+
export declare function useReplayMissBoard({ feedback, expectedUci, positionFen, answerArrowColor, autoShowWrongMoves, engineOptions, }: {
|
|
4
|
+
feedback: ReplayMissFeedback;
|
|
5
|
+
expectedUci: string | null;
|
|
6
|
+
positionFen: string;
|
|
7
|
+
answerArrowColor: string;
|
|
8
|
+
autoShowWrongMoves?: boolean;
|
|
9
|
+
engineOptions?: AnalysisEngineOptions;
|
|
10
|
+
}): {
|
|
11
|
+
missSequence: {
|
|
12
|
+
sequence: import("./replayMissDisplay").MissSequenceState | null;
|
|
13
|
+
refutation: import("..").ReplayRefutationResult;
|
|
14
|
+
display: import("./replayMissDisplay").ReplayMissDisplay;
|
|
15
|
+
startSequence: (setupFen: string, attemptedUci: string) => void;
|
|
16
|
+
clearSequence: () => void;
|
|
17
|
+
};
|
|
18
|
+
refutation: import("..").ReplayRefutationResult;
|
|
19
|
+
customArrows: [string, string, string][];
|
|
20
|
+
boardPosition: string;
|
|
21
|
+
boardAnimating: boolean;
|
|
22
|
+
wrapDropHandler: (onDrop: (source: string, target: string, piece: string) => boolean, { enabled, dropFen, expectedMoveUci, }: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
dropFen?: string;
|
|
25
|
+
expectedMoveUci?: string | null;
|
|
26
|
+
}) => (source: string, target: string, piece: string) => boolean;
|
|
27
|
+
};
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AnalysisEngineOptions } from 'react-chess-core';
|
|
2
|
+
import { type MissSequencePhase, type MissSequenceState, type ReplayMissDisplay } from './replayMissDisplay';
|
|
3
|
+
export type { MissSequencePhase, ReplayMissDisplay };
|
|
4
|
+
export declare function useReplayMissSequence(feedback: 'correct' | 'incorrect' | null, expectedUci: string | null, engineOptions: AnalysisEngineOptions, answerArrowColor: string, autoShowWrongMoves: boolean): {
|
|
5
|
+
sequence: MissSequenceState | null;
|
|
6
|
+
refutation: import("..").ReplayRefutationResult;
|
|
7
|
+
display: ReplayMissDisplay;
|
|
8
|
+
startSequence: (setupFen: string, attemptedUci: string) => void;
|
|
9
|
+
clearSequence: () => void;
|
|
10
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type AnalysisEngineOptions } from 'react-chess-core';
|
|
2
|
+
import { type ReplayRefutationResult } from '../refutation/replayRefutation';
|
|
3
|
+
export declare function useReplayRefutation(setupFen: string | null, attemptedUci: string | null, expectedUci: string | null, enabled: boolean, engineOptions: AnalysisEngineOptions): ReplayRefutationResult;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AnalysisEngineOptions, type EngineEvaluation, type EngineLine } from 'react-chess-core';
|
|
2
|
+
/** Minimum eval loss (pawns) from the wrong move before showing a refutation. */
|
|
3
|
+
export declare const REPLAY_REFUTATION_EVAL_GAP_PAWNS = 0.5;
|
|
4
|
+
export declare const REPLAY_REFUTATION_EVAL_GAP_CP: number;
|
|
5
|
+
export type ReplayRefutationResult = {
|
|
6
|
+
fenAfterWrong: string | null;
|
|
7
|
+
refutationUci: string | null;
|
|
8
|
+
refutationSan: string | null;
|
|
9
|
+
refutationLine: string | null;
|
|
10
|
+
loading: boolean;
|
|
11
|
+
error: string | null;
|
|
12
|
+
};
|
|
13
|
+
export declare const replayRefutationEngineOptions: AnalysisEngineOptions;
|
|
14
|
+
export declare function fenAfterUci(fen: string, uci: string): string | null;
|
|
15
|
+
/** Centipawn score from side to move, comparable across sibling positions. */
|
|
16
|
+
export declare function lineEvalCpForGap(line: EngineLine | undefined): number | null;
|
|
17
|
+
/** How much better the opponent's eval is after the wrong move vs the correct one. */
|
|
18
|
+
export declare function refutationEvalGapCp(evalAfterWrong: EngineEvaluation, evalAfterCorrect: EngineEvaluation): number | null;
|
|
19
|
+
export declare function refutationFromEvaluation(fenAfterWrong: string, evaluation: EngineEvaluation, evalGapCp: number | null, evalGapApplies: boolean, evalGapLoading: boolean): Omit<ReplayRefutationResult, 'fenAfterWrong'>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Chess } from 'chess.js';
|
|
2
|
-
import type { ReplaySide } from './types';
|
|
2
|
+
import type { ReplaySide, TrainColor } from './types';
|
|
3
3
|
/** Compare positions ignoring move clocks (first four FEN fields). */
|
|
4
4
|
export declare function normalizeFen(fen: string): string;
|
|
5
5
|
export declare function applyUci(chess: Chess, uci: string): void;
|
|
@@ -8,5 +8,4 @@ export declare function fenAtPly(movesUci: string[], ply: number): string;
|
|
|
8
8
|
/** Index of the next move to play to reach `targetFen`, or 0 if not found. */
|
|
9
9
|
export declare function findPlyIndexForFen(movesUci: string[], targetFen: string): number;
|
|
10
10
|
export declare function sideToMove(fen: string): ReplaySide;
|
|
11
|
-
|
|
12
|
-
export declare function uciFromDrop(fen: string, sourceSquare: string, targetSquare: string, piece: string): string | null;
|
|
11
|
+
export declare function isTrainSideToMove(trainColor: TrainColor, side: ReplaySide): boolean;
|
package/dist/index.esm.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
|
3
|
-
import { ChessboardDnDProvider } from 'react-
|
|
4
|
-
|
|
5
|
-
export { AnalysisBoard, AnalysisBoardCore, AnalysisErrorBoundary, DEFAULT_ANALYSIS_LAYOUT, DefaultPlyNavigation, PlyNavigation, defaultRenderPlyNavigation } from 'react-chess-core';
|
|
3
|
+
import { createExpectedMoveDropHandler, ThemeProvider, ChessboardDnDProvider, HighlightChessboard, PlyNavigation, AnalysisErrorBoundary, AnalysisBoard } from 'react-chess-core';
|
|
4
|
+
export { AnalysisBoard, AnalysisBoardCore, AnalysisErrorBoundary, DEFAULT_ANALYSIS_LAYOUT, DefaultPlyNavigation, PlyNavigation, MISS_MOVE_ANIMATION_MS as REPLAY_MISS_MOVE_ANIMATION_MS, MISS_REFUTATION_MAX_WAIT_MS as REPLAY_MISS_REFUTATION_MAX_WAIT_MS, MISS_REFUTATION_PAUSE_MS as REPLAY_MISS_REFUTATION_PAUSE_MS, MISS_WRONG_PAUSE_MS as REPLAY_MISS_WRONG_PAUSE_MS, REFUTATION_EVAL_GAP_CP as REPLAY_REFUTATION_EVAL_GAP_CP, REFUTATION_EVAL_GAP_PAWNS as REPLAY_REFUTATION_EVAL_GAP_PAWNS, defaultRenderPlyNavigation, fenAfterUci, getMissDisplay as getReplayMissDisplay, refutationEvalGapCp, refutationFromEvaluation, refutationEngineOptions as replayRefutationEngineOptions, uciFromDrop, useMissBoard as useReplayMissBoard, useMissSequence as useReplayMissSequence, useMissRefutation as useReplayRefutation } from 'react-chess-core';
|
|
6
5
|
import { Chess } from 'chess.js';
|
|
7
6
|
|
|
8
7
|
/** Standard chess starting position; game move lists replay from here. */
|
|
9
8
|
const REPLAY_START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
|
10
9
|
const DEFAULT_BOARD_WIDTH = 480;
|
|
10
|
+
/** Delay between half-moves during browse autoplay (ms). */
|
|
11
|
+
const REPLAY_AUTOPLAY_STEP_MS = 500;
|
|
11
12
|
|
|
12
13
|
/** Build a core {@link AnalysisContext} for the current replay browse position. */
|
|
13
14
|
function buildReplayAnalysisContext(game, plyIndex, boardOrientation) {
|
|
@@ -58,18 +59,10 @@ function findPlyIndexForFen(movesUci, targetFen) {
|
|
|
58
59
|
function sideToMove(fen) {
|
|
59
60
|
return fen.trim().split(/\s+/)[1] === 'b' ? 'b' : 'w';
|
|
60
61
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const pieceType = (_a = piece[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
66
|
-
const legal = chess
|
|
67
|
-
.moves({ square: sourceSquare, verbose: true })
|
|
68
|
-
.find((move) => move.to === targetSquare &&
|
|
69
|
-
(!move.promotion || move.promotion === pieceType));
|
|
70
|
-
if (!legal)
|
|
71
|
-
return null;
|
|
72
|
-
return `${legal.from}${legal.to}${(_b = legal.promotion) !== null && _b !== void 0 ? _b : ''}`;
|
|
62
|
+
function isTrainSideToMove(trainColor, side) {
|
|
63
|
+
return (trainColor === 'both' ||
|
|
64
|
+
(trainColor === 'white' && side === 'w') ||
|
|
65
|
+
(trainColor === 'black' && side === 'b'));
|
|
73
66
|
}
|
|
74
67
|
|
|
75
68
|
/** Pause (ms) before the opponent's reply is auto-played in single-color drills. */
|
|
@@ -83,6 +76,7 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
83
76
|
const [plyIndex, setPlyIndex] = useState(0);
|
|
84
77
|
const [feedback, setFeedback] = useState(null);
|
|
85
78
|
const [expectedSan, setExpectedSan] = useState(null);
|
|
79
|
+
const [autoplayActive, setAutoplayActive] = useState(false);
|
|
86
80
|
const [expectedUci, setExpectedUci] = useState(null);
|
|
87
81
|
const fetchGameRef = useRef(fetchGame);
|
|
88
82
|
fetchGameRef.current = fetchGame;
|
|
@@ -92,6 +86,8 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
92
86
|
onCompleteRef.current = onComplete;
|
|
93
87
|
const recordedRef = useRef(new Set());
|
|
94
88
|
const completedFiredRef = useRef(false);
|
|
89
|
+
const modeRef = useRef('browse');
|
|
90
|
+
const trainColorRef = useRef('both');
|
|
95
91
|
useEffect(() => {
|
|
96
92
|
let cancelled = false;
|
|
97
93
|
setLoading(true);
|
|
@@ -100,10 +96,13 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
100
96
|
recordedRef.current = new Set();
|
|
101
97
|
completedFiredRef.current = false;
|
|
102
98
|
setMode('browse');
|
|
99
|
+
modeRef.current = 'browse';
|
|
103
100
|
setTrainColor('both');
|
|
101
|
+
trainColorRef.current = 'both';
|
|
104
102
|
setFeedback(null);
|
|
105
103
|
setExpectedSan(null);
|
|
106
104
|
setExpectedUci(null);
|
|
105
|
+
setAutoplayActive(false);
|
|
107
106
|
fetchGameRef
|
|
108
107
|
.current(gameId)
|
|
109
108
|
.then((loaded) => {
|
|
@@ -134,15 +133,19 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
134
133
|
const fen = useMemo(() => fenAtPly(movesUci, plyIndex), [movesUci, plyIndex]);
|
|
135
134
|
const complete = plyIndex >= totalPly && totalPly > 0;
|
|
136
135
|
const sideToMove$1 = sideToMove(fen);
|
|
137
|
-
const isUserTurn = trainColor
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
const isUserTurn = isTrainSideToMove(trainColor, sideToMove$1);
|
|
137
|
+
modeRef.current = mode;
|
|
138
|
+
trainColorRef.current = trainColor;
|
|
140
139
|
const clearTransient = useCallback(() => {
|
|
141
140
|
setFeedback(null);
|
|
142
141
|
setExpectedSan(null);
|
|
143
142
|
setExpectedUci(null);
|
|
144
143
|
}, []);
|
|
144
|
+
const stopAutoplay = useCallback(() => {
|
|
145
|
+
setAutoplayActive(false);
|
|
146
|
+
}, []);
|
|
145
147
|
const goTo = useCallback((ply) => {
|
|
148
|
+
setAutoplayActive(false);
|
|
146
149
|
const clamped = Math.max(0, Math.min(ply, totalPly));
|
|
147
150
|
completedFiredRef.current = clamped >= totalPly ? completedFiredRef.current : false;
|
|
148
151
|
setPlyIndex(clamped);
|
|
@@ -152,12 +155,31 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
152
155
|
const goPrev = useCallback(() => goTo(plyIndex - 1), [goTo, plyIndex]);
|
|
153
156
|
const goNext = useCallback(() => goTo(plyIndex + 1), [goTo, plyIndex]);
|
|
154
157
|
const goLast = useCallback(() => goTo(totalPly), [goTo, totalPly]);
|
|
158
|
+
const startAutoplay = useCallback(() => {
|
|
159
|
+
if (plyIndex >= totalPly) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
setMode('browse');
|
|
163
|
+
clearTransient();
|
|
164
|
+
setAutoplayActive(true);
|
|
165
|
+
}, [plyIndex, totalPly, clearTransient]);
|
|
166
|
+
const toggleAutoplay = useCallback(() => {
|
|
167
|
+
if (autoplayActive) {
|
|
168
|
+
stopAutoplay();
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
startAutoplay();
|
|
172
|
+
}, [autoplayActive, startAutoplay, stopAutoplay]);
|
|
155
173
|
const startTraining = useCallback((color = 'both') => {
|
|
174
|
+
setAutoplayActive(false);
|
|
156
175
|
setTrainColor(color);
|
|
176
|
+
trainColorRef.current = color;
|
|
177
|
+
modeRef.current = 'train';
|
|
157
178
|
setMode('train');
|
|
158
179
|
clearTransient();
|
|
159
180
|
}, [clearTransient]);
|
|
160
181
|
const stopTraining = useCallback(() => {
|
|
182
|
+
modeRef.current = 'browse';
|
|
161
183
|
setMode('browse');
|
|
162
184
|
clearTransient();
|
|
163
185
|
}, [clearTransient]);
|
|
@@ -193,29 +215,30 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
193
215
|
setFeedback('incorrect');
|
|
194
216
|
recordMiss(plyIndex);
|
|
195
217
|
}, [complete, game, movesUci, plyIndex, recordMiss, isUserTurn]);
|
|
196
|
-
const handleDrop = useCallback((source, target, piece) => {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const uci = uciFromDrop(fen, source, target, piece);
|
|
204
|
-
if (!uci)
|
|
205
|
-
return false;
|
|
206
|
-
if (uci.toLowerCase() === expectedUci.toLowerCase()) {
|
|
218
|
+
const handleDrop = useCallback((source, target, piece) => createExpectedMoveDropHandler({
|
|
219
|
+
fen,
|
|
220
|
+
expectedUci: movesUci[plyIndex],
|
|
221
|
+
enabled: modeRef.current === 'train' &&
|
|
222
|
+
!complete &&
|
|
223
|
+
isTrainSideToMove(trainColorRef.current, sideToMove$1),
|
|
224
|
+
onCorrect: () => {
|
|
207
225
|
setFeedback('correct');
|
|
208
226
|
setExpectedSan(null);
|
|
209
227
|
setExpectedUci(null);
|
|
210
228
|
setPlyIndex((p) => p + 1);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
229
|
+
},
|
|
230
|
+
onIncorrect: () => {
|
|
231
|
+
var _a, _b;
|
|
232
|
+
const expectedUci = movesUci[plyIndex];
|
|
233
|
+
if (!expectedUci) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
setFeedback('incorrect');
|
|
237
|
+
setExpectedSan((_b = (_a = game === null || game === void 0 ? void 0 : game.movesSan) === null || _a === void 0 ? void 0 : _a[plyIndex]) !== null && _b !== void 0 ? _b : expectedUci);
|
|
238
|
+
setExpectedUci(expectedUci);
|
|
239
|
+
recordMiss(plyIndex);
|
|
240
|
+
},
|
|
241
|
+
})(source, target, piece), [complete, movesUci, plyIndex, fen, game, recordMiss, sideToMove$1]);
|
|
219
242
|
useEffect(() => {
|
|
220
243
|
var _a;
|
|
221
244
|
if (mode === 'train' && complete && !completedFiredRef.current) {
|
|
@@ -223,6 +246,21 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
223
246
|
(_a = onCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onCompleteRef);
|
|
224
247
|
}
|
|
225
248
|
}, [mode, complete]);
|
|
249
|
+
// Browse autoplay: advance one ply at a fixed interval until the game ends.
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (!autoplayActive || mode === 'train') {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (plyIndex >= totalPly) {
|
|
255
|
+
setAutoplayActive(false);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const id = setTimeout(() => {
|
|
259
|
+
setPlyIndex((p) => (p < totalPly ? p + 1 : p));
|
|
260
|
+
clearTransient();
|
|
261
|
+
}, REPLAY_AUTOPLAY_STEP_MS);
|
|
262
|
+
return () => clearTimeout(id);
|
|
263
|
+
}, [autoplayActive, mode, plyIndex, totalPly, clearTransient]);
|
|
226
264
|
// In single-color drills, auto-play the opponent's reply once it's their turn
|
|
227
265
|
// (e.g. after the user guesses correctly, or when training starts mid-game).
|
|
228
266
|
useEffect(() => {
|
|
@@ -261,6 +299,10 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
261
299
|
stopTraining,
|
|
262
300
|
revealMove,
|
|
263
301
|
handleDrop,
|
|
302
|
+
autoplayActive,
|
|
303
|
+
startAutoplay,
|
|
304
|
+
stopAutoplay,
|
|
305
|
+
toggleAutoplay,
|
|
264
306
|
};
|
|
265
307
|
}
|
|
266
308
|
|
|
@@ -355,7 +397,7 @@ function buttonStyle(colors, variant) {
|
|
|
355
397
|
* next / last / slider) never records anything; once the user hits "Train from
|
|
356
398
|
* here" each wrong move is reported through {@link ReplayTrainerProps.onMiss}.
|
|
357
399
|
*/
|
|
358
|
-
const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme = 'dark', boardWidth = DEFAULT_BOARD_WIDTH, orientation = 'white', engine, renderPlyNavigation, showPlyScrubber = true, }) => {
|
|
400
|
+
const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme = 'dark', boardTheme, boardWidth = DEFAULT_BOARD_WIDTH, orientation = 'white', engine, renderPlyNavigation, showPlyScrubber = true, }) => {
|
|
359
401
|
var _a, _b, _c;
|
|
360
402
|
const state = useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete });
|
|
361
403
|
const colors = palette(theme);
|
|
@@ -370,9 +412,10 @@ const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit
|
|
|
370
412
|
if (!state.game) {
|
|
371
413
|
return;
|
|
372
414
|
}
|
|
415
|
+
state.stopAutoplay();
|
|
373
416
|
setAnalysisSnapshot(buildReplayAnalysisContext(state.game, state.plyIndex, boardOrientation));
|
|
374
417
|
setAnalysisOpen(true);
|
|
375
|
-
}, [state.game, state.plyIndex, boardOrientation]);
|
|
418
|
+
}, [state.game, state.plyIndex, boardOrientation, state.stopAutoplay]);
|
|
376
419
|
const closeAnalysis = useCallback(() => {
|
|
377
420
|
setAnalysisOpen(false);
|
|
378
421
|
}, []);
|
|
@@ -394,17 +437,17 @@ const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit
|
|
|
394
437
|
]
|
|
395
438
|
: [];
|
|
396
439
|
const draggable = training && !state.complete;
|
|
397
|
-
return (jsxs(ThemeProvider, { theme: theme, children: [jsxs("div", { style: mainContainerStyle(boardWidth, colors), children: [jsxs("div", { style: headerStyle, children: [jsxs("span", { style: playerNameStyle, children: [((_b = game.white) !== null && _b !== void 0 ? _b : 'White'), " vs ", ((_c = game.black) !== null && _c !== void 0 ? _c : 'Black')] }), game.result && jsx("span", { style: subtleTextStyle(colors), children: game.result })] }), jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, position: state.fen, boardOrientation: boardOrientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => {
|
|
440
|
+
return (jsxs(ThemeProvider, { theme: theme, boardTheme: boardTheme, children: [jsxs("div", { style: mainContainerStyle(boardWidth, colors), children: [jsxs("div", { style: headerStyle, children: [jsxs("span", { style: playerNameStyle, children: [((_b = game.white) !== null && _b !== void 0 ? _b : 'White'), " vs ", ((_c = game.black) !== null && _c !== void 0 ? _c : 'Black')] }), game.result && jsx("span", { style: subtleTextStyle(colors), children: game.result })] }), jsx(ChessboardDnDProvider, { children: jsx(HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, position: state.fen, boardOrientation: boardOrientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => {
|
|
398
441
|
if (state.trainColor === 'white')
|
|
399
442
|
return piece[0] === 'w';
|
|
400
443
|
if (state.trainColor === 'black')
|
|
401
444
|
return piece[0] === 'b';
|
|
402
445
|
return piece[0] === state.sideToMove;
|
|
403
|
-
}, onPieceDrop: (source, target, piece) => state.handleDrop(source, target, piece), customArrows: customArrows,
|
|
446
|
+
}, onPieceDrop: (source, target, piece) => state.handleDrop(source, target, piece), customArrows: customArrows, promotionDialogVariant: "modal", areArrowsAllowed: false, customBoardStyle: customBoardStyle }) }), jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4 }, children: [jsx(PlyNavigation, { plyIndex: state.plyIndex, totalPly: state.totalPly, canPrev: state.canPrev, canNext: state.canNext, onGoFirst: state.goFirst, onGoPrev: state.goPrev, onGoNext: state.goNext, onGoLast: state.goLast, onGoTo: state.goTo, theme: theme, showScrubber: showPlyScrubber, renderPlyNavigation: renderPlyNavigation }), jsx("button", { type: "button", onClick: state.toggleAutoplay, disabled: !state.autoplayActive && !state.canNext, style: buttonStyle(colors, state.autoplayActive ? 'ghost' : 'primary'), "aria-label": state.autoplayActive ? 'Stop autoplay' : 'Autoplay game', children: state.autoplayActive ? 'Stop' : 'Play' })] }), jsxs("div", { style: statusLineStyle(colors), children: ["Half move ", Math.min(state.plyIndex + (state.complete ? 0 : 1), state.totalPly), " of", ' ', state.totalPly, training && !state.complete && (jsxs(Fragment, { children: [` · ${TRAIN_COLOR_LABEL[state.trainColor]}`, state.trainColor === 'both'
|
|
404
447
|
? ` · ${state.sideToMove === 'b' ? 'Black' : 'White'} to move`
|
|
405
448
|
: state.isUserTurn
|
|
406
449
|
? ' · Your move'
|
|
407
450
|
: ' · Opponent replying…'] }))] }), jsxs("div", { style: controlsRowStyle, children: [jsx("button", { type: "button", onClick: openAnalysis, style: buttonStyle(colors, 'primary'), children: "Analyze" }), !training && (jsxs(Fragment, { children: [jsx("button", { type: "button", onClick: () => state.startTraining('white'), disabled: state.complete, style: buttonStyle(colors, 'primary'), children: "Train White" }), jsx("button", { type: "button", onClick: () => state.startTraining('black'), disabled: state.complete, style: buttonStyle(colors, 'primary'), children: "Train Black" }), jsx("button", { type: "button", onClick: () => state.startTraining('both'), disabled: state.complete, style: buttonStyle(colors, 'primary'), children: "Train Both" })] })), training && (jsxs(Fragment, { children: [jsx("button", { type: "button", onClick: state.revealMove, disabled: state.complete || !state.isUserTurn, style: buttonStyle(colors, 'ghost'), children: "Show move" }), jsx("button", { type: "button", onClick: state.stopTraining, style: buttonStyle(colors, 'ghost'), children: "Stop drilling" })] })), onExit && (jsx("button", { type: "button", onClick: onExit, style: buttonStyle(colors, 'ghost'), children: "Exit" }))] }), jsxs("div", { style: feedbackContainerStyle, children: [state.complete && training && (jsx("span", { style: feedbackMessageStyle(colors, 'success'), children: "End of game \u2014 drill complete" })), !state.complete && state.feedback === 'correct' && (jsx("span", { style: feedbackMessageStyle(colors, 'success'), children: "Correct" })), !state.complete && state.feedback === 'incorrect' && (jsxs("span", { style: feedbackMessageStyle(colors, 'error'), children: ["Game move was ", state.expectedSan] }))] })] }), analysisOpen && analysisSnapshot && (jsx(AnalysisErrorBoundary, { onClose: closeAnalysis, children: jsx(AnalysisBoard, { analysisContext: analysisSnapshot, onClose: closeAnalysis, theme: theme, engine: engine }) }))] }));
|
|
408
451
|
};
|
|
409
452
|
|
|
410
|
-
export { DEFAULT_BOARD_WIDTH, REPLAY_START_FEN, ReplayTrainer, buildReplayAnalysisContext, fenAtPly, findPlyIndexForFen, normalizeFen, sideToMove,
|
|
453
|
+
export { DEFAULT_BOARD_WIDTH, REPLAY_AUTOPLAY_STEP_MS, REPLAY_START_FEN, ReplayTrainer, buildReplayAnalysisContext, fenAtPly, findPlyIndexForFen, normalizeFen, sideToMove, useReplayTrainer };
|
package/dist/index.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
var jsxRuntime = require('react/jsx-runtime');
|
|
4
4
|
var react = require('react');
|
|
5
|
-
var reactChessboard = require('react-chessboard');
|
|
6
5
|
var reactChessCore = require('react-chess-core');
|
|
7
6
|
var chess_js = require('chess.js');
|
|
8
7
|
|
|
9
8
|
/** Standard chess starting position; game move lists replay from here. */
|
|
10
9
|
const REPLAY_START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
|
11
10
|
const DEFAULT_BOARD_WIDTH = 480;
|
|
11
|
+
/** Delay between half-moves during browse autoplay (ms). */
|
|
12
|
+
const REPLAY_AUTOPLAY_STEP_MS = 500;
|
|
12
13
|
|
|
13
14
|
/** Build a core {@link AnalysisContext} for the current replay browse position. */
|
|
14
15
|
function buildReplayAnalysisContext(game, plyIndex, boardOrientation) {
|
|
@@ -59,18 +60,10 @@ function findPlyIndexForFen(movesUci, targetFen) {
|
|
|
59
60
|
function sideToMove(fen) {
|
|
60
61
|
return fen.trim().split(/\s+/)[1] === 'b' ? 'b' : 'w';
|
|
61
62
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const pieceType = (_a = piece[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
67
|
-
const legal = chess
|
|
68
|
-
.moves({ square: sourceSquare, verbose: true })
|
|
69
|
-
.find((move) => move.to === targetSquare &&
|
|
70
|
-
(!move.promotion || move.promotion === pieceType));
|
|
71
|
-
if (!legal)
|
|
72
|
-
return null;
|
|
73
|
-
return `${legal.from}${legal.to}${(_b = legal.promotion) !== null && _b !== void 0 ? _b : ''}`;
|
|
63
|
+
function isTrainSideToMove(trainColor, side) {
|
|
64
|
+
return (trainColor === 'both' ||
|
|
65
|
+
(trainColor === 'white' && side === 'w') ||
|
|
66
|
+
(trainColor === 'black' && side === 'b'));
|
|
74
67
|
}
|
|
75
68
|
|
|
76
69
|
/** Pause (ms) before the opponent's reply is auto-played in single-color drills. */
|
|
@@ -84,6 +77,7 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
84
77
|
const [plyIndex, setPlyIndex] = react.useState(0);
|
|
85
78
|
const [feedback, setFeedback] = react.useState(null);
|
|
86
79
|
const [expectedSan, setExpectedSan] = react.useState(null);
|
|
80
|
+
const [autoplayActive, setAutoplayActive] = react.useState(false);
|
|
87
81
|
const [expectedUci, setExpectedUci] = react.useState(null);
|
|
88
82
|
const fetchGameRef = react.useRef(fetchGame);
|
|
89
83
|
fetchGameRef.current = fetchGame;
|
|
@@ -93,6 +87,8 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
93
87
|
onCompleteRef.current = onComplete;
|
|
94
88
|
const recordedRef = react.useRef(new Set());
|
|
95
89
|
const completedFiredRef = react.useRef(false);
|
|
90
|
+
const modeRef = react.useRef('browse');
|
|
91
|
+
const trainColorRef = react.useRef('both');
|
|
96
92
|
react.useEffect(() => {
|
|
97
93
|
let cancelled = false;
|
|
98
94
|
setLoading(true);
|
|
@@ -101,10 +97,13 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
101
97
|
recordedRef.current = new Set();
|
|
102
98
|
completedFiredRef.current = false;
|
|
103
99
|
setMode('browse');
|
|
100
|
+
modeRef.current = 'browse';
|
|
104
101
|
setTrainColor('both');
|
|
102
|
+
trainColorRef.current = 'both';
|
|
105
103
|
setFeedback(null);
|
|
106
104
|
setExpectedSan(null);
|
|
107
105
|
setExpectedUci(null);
|
|
106
|
+
setAutoplayActive(false);
|
|
108
107
|
fetchGameRef
|
|
109
108
|
.current(gameId)
|
|
110
109
|
.then((loaded) => {
|
|
@@ -135,15 +134,19 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
135
134
|
const fen = react.useMemo(() => fenAtPly(movesUci, plyIndex), [movesUci, plyIndex]);
|
|
136
135
|
const complete = plyIndex >= totalPly && totalPly > 0;
|
|
137
136
|
const sideToMove$1 = sideToMove(fen);
|
|
138
|
-
const isUserTurn = trainColor
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
const isUserTurn = isTrainSideToMove(trainColor, sideToMove$1);
|
|
138
|
+
modeRef.current = mode;
|
|
139
|
+
trainColorRef.current = trainColor;
|
|
141
140
|
const clearTransient = react.useCallback(() => {
|
|
142
141
|
setFeedback(null);
|
|
143
142
|
setExpectedSan(null);
|
|
144
143
|
setExpectedUci(null);
|
|
145
144
|
}, []);
|
|
145
|
+
const stopAutoplay = react.useCallback(() => {
|
|
146
|
+
setAutoplayActive(false);
|
|
147
|
+
}, []);
|
|
146
148
|
const goTo = react.useCallback((ply) => {
|
|
149
|
+
setAutoplayActive(false);
|
|
147
150
|
const clamped = Math.max(0, Math.min(ply, totalPly));
|
|
148
151
|
completedFiredRef.current = clamped >= totalPly ? completedFiredRef.current : false;
|
|
149
152
|
setPlyIndex(clamped);
|
|
@@ -153,12 +156,31 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
153
156
|
const goPrev = react.useCallback(() => goTo(plyIndex - 1), [goTo, plyIndex]);
|
|
154
157
|
const goNext = react.useCallback(() => goTo(plyIndex + 1), [goTo, plyIndex]);
|
|
155
158
|
const goLast = react.useCallback(() => goTo(totalPly), [goTo, totalPly]);
|
|
159
|
+
const startAutoplay = react.useCallback(() => {
|
|
160
|
+
if (plyIndex >= totalPly) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
setMode('browse');
|
|
164
|
+
clearTransient();
|
|
165
|
+
setAutoplayActive(true);
|
|
166
|
+
}, [plyIndex, totalPly, clearTransient]);
|
|
167
|
+
const toggleAutoplay = react.useCallback(() => {
|
|
168
|
+
if (autoplayActive) {
|
|
169
|
+
stopAutoplay();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
startAutoplay();
|
|
173
|
+
}, [autoplayActive, startAutoplay, stopAutoplay]);
|
|
156
174
|
const startTraining = react.useCallback((color = 'both') => {
|
|
175
|
+
setAutoplayActive(false);
|
|
157
176
|
setTrainColor(color);
|
|
177
|
+
trainColorRef.current = color;
|
|
178
|
+
modeRef.current = 'train';
|
|
158
179
|
setMode('train');
|
|
159
180
|
clearTransient();
|
|
160
181
|
}, [clearTransient]);
|
|
161
182
|
const stopTraining = react.useCallback(() => {
|
|
183
|
+
modeRef.current = 'browse';
|
|
162
184
|
setMode('browse');
|
|
163
185
|
clearTransient();
|
|
164
186
|
}, [clearTransient]);
|
|
@@ -194,29 +216,30 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
194
216
|
setFeedback('incorrect');
|
|
195
217
|
recordMiss(plyIndex);
|
|
196
218
|
}, [complete, game, movesUci, plyIndex, recordMiss, isUserTurn]);
|
|
197
|
-
const handleDrop = react.useCallback((source, target, piece) => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const uci = uciFromDrop(fen, source, target, piece);
|
|
205
|
-
if (!uci)
|
|
206
|
-
return false;
|
|
207
|
-
if (uci.toLowerCase() === expectedUci.toLowerCase()) {
|
|
219
|
+
const handleDrop = react.useCallback((source, target, piece) => reactChessCore.createExpectedMoveDropHandler({
|
|
220
|
+
fen,
|
|
221
|
+
expectedUci: movesUci[plyIndex],
|
|
222
|
+
enabled: modeRef.current === 'train' &&
|
|
223
|
+
!complete &&
|
|
224
|
+
isTrainSideToMove(trainColorRef.current, sideToMove$1),
|
|
225
|
+
onCorrect: () => {
|
|
208
226
|
setFeedback('correct');
|
|
209
227
|
setExpectedSan(null);
|
|
210
228
|
setExpectedUci(null);
|
|
211
229
|
setPlyIndex((p) => p + 1);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
230
|
+
},
|
|
231
|
+
onIncorrect: () => {
|
|
232
|
+
var _a, _b;
|
|
233
|
+
const expectedUci = movesUci[plyIndex];
|
|
234
|
+
if (!expectedUci) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
setFeedback('incorrect');
|
|
238
|
+
setExpectedSan((_b = (_a = game === null || game === void 0 ? void 0 : game.movesSan) === null || _a === void 0 ? void 0 : _a[plyIndex]) !== null && _b !== void 0 ? _b : expectedUci);
|
|
239
|
+
setExpectedUci(expectedUci);
|
|
240
|
+
recordMiss(plyIndex);
|
|
241
|
+
},
|
|
242
|
+
})(source, target, piece), [complete, movesUci, plyIndex, fen, game, recordMiss, sideToMove$1]);
|
|
220
243
|
react.useEffect(() => {
|
|
221
244
|
var _a;
|
|
222
245
|
if (mode === 'train' && complete && !completedFiredRef.current) {
|
|
@@ -224,6 +247,21 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
224
247
|
(_a = onCompleteRef.current) === null || _a === void 0 ? void 0 : _a.call(onCompleteRef);
|
|
225
248
|
}
|
|
226
249
|
}, [mode, complete]);
|
|
250
|
+
// Browse autoplay: advance one ply at a fixed interval until the game ends.
|
|
251
|
+
react.useEffect(() => {
|
|
252
|
+
if (!autoplayActive || mode === 'train') {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (plyIndex >= totalPly) {
|
|
256
|
+
setAutoplayActive(false);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const id = setTimeout(() => {
|
|
260
|
+
setPlyIndex((p) => (p < totalPly ? p + 1 : p));
|
|
261
|
+
clearTransient();
|
|
262
|
+
}, REPLAY_AUTOPLAY_STEP_MS);
|
|
263
|
+
return () => clearTimeout(id);
|
|
264
|
+
}, [autoplayActive, mode, plyIndex, totalPly, clearTransient]);
|
|
227
265
|
// In single-color drills, auto-play the opponent's reply once it's their turn
|
|
228
266
|
// (e.g. after the user guesses correctly, or when training starts mid-game).
|
|
229
267
|
react.useEffect(() => {
|
|
@@ -262,6 +300,10 @@ function useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete, })
|
|
|
262
300
|
stopTraining,
|
|
263
301
|
revealMove,
|
|
264
302
|
handleDrop,
|
|
303
|
+
autoplayActive,
|
|
304
|
+
startAutoplay,
|
|
305
|
+
stopAutoplay,
|
|
306
|
+
toggleAutoplay,
|
|
265
307
|
};
|
|
266
308
|
}
|
|
267
309
|
|
|
@@ -356,7 +398,7 @@ function buttonStyle(colors, variant) {
|
|
|
356
398
|
* next / last / slider) never records anything; once the user hits "Train from
|
|
357
399
|
* here" each wrong move is reported through {@link ReplayTrainerProps.onMiss}.
|
|
358
400
|
*/
|
|
359
|
-
const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme = 'dark', boardWidth = DEFAULT_BOARD_WIDTH, orientation = 'white', engine, renderPlyNavigation, showPlyScrubber = true, }) => {
|
|
401
|
+
const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit, theme = 'dark', boardTheme, boardWidth = DEFAULT_BOARD_WIDTH, orientation = 'white', engine, renderPlyNavigation, showPlyScrubber = true, }) => {
|
|
360
402
|
var _a, _b, _c;
|
|
361
403
|
const state = useReplayTrainer({ gameId, startFen, fetchGame, onMiss, onComplete });
|
|
362
404
|
const colors = palette(theme);
|
|
@@ -371,9 +413,10 @@ const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit
|
|
|
371
413
|
if (!state.game) {
|
|
372
414
|
return;
|
|
373
415
|
}
|
|
416
|
+
state.stopAutoplay();
|
|
374
417
|
setAnalysisSnapshot(buildReplayAnalysisContext(state.game, state.plyIndex, boardOrientation));
|
|
375
418
|
setAnalysisOpen(true);
|
|
376
|
-
}, [state.game, state.plyIndex, boardOrientation]);
|
|
419
|
+
}, [state.game, state.plyIndex, boardOrientation, state.stopAutoplay]);
|
|
377
420
|
const closeAnalysis = react.useCallback(() => {
|
|
378
421
|
setAnalysisOpen(false);
|
|
379
422
|
}, []);
|
|
@@ -395,13 +438,13 @@ const ReplayTrainer = ({ gameId, fetchGame, startFen, onMiss, onComplete, onExit
|
|
|
395
438
|
]
|
|
396
439
|
: [];
|
|
397
440
|
const draggable = training && !state.complete;
|
|
398
|
-
return (jsxRuntime.jsxs(reactChessCore.ThemeProvider, { theme: theme, children: [jsxRuntime.jsxs("div", { style: mainContainerStyle(boardWidth, colors), children: [jsxRuntime.jsxs("div", { style: headerStyle, children: [jsxRuntime.jsxs("span", { style: playerNameStyle, children: [((_b = game.white) !== null && _b !== void 0 ? _b : 'White'), " vs ", ((_c = game.black) !== null && _c !== void 0 ? _c : 'Black')] }), game.result && jsxRuntime.jsx("span", { style: subtleTextStyle(colors), children: game.result })] }), jsxRuntime.jsx(
|
|
441
|
+
return (jsxRuntime.jsxs(reactChessCore.ThemeProvider, { theme: theme, boardTheme: boardTheme, children: [jsxRuntime.jsxs("div", { style: mainContainerStyle(boardWidth, colors), children: [jsxRuntime.jsxs("div", { style: headerStyle, children: [jsxRuntime.jsxs("span", { style: playerNameStyle, children: [((_b = game.white) !== null && _b !== void 0 ? _b : 'White'), " vs ", ((_c = game.black) !== null && _c !== void 0 ? _c : 'Black')] }), game.result && jsxRuntime.jsx("span", { style: subtleTextStyle(colors), children: game.result })] }), jsxRuntime.jsx(reactChessCore.ChessboardDnDProvider, { children: jsxRuntime.jsx(reactChessCore.HighlightChessboard, { boardWidth: boardWidth, checkSquare: "", hintSquare: null, incorrectMoveSquare: null, position: state.fen, boardOrientation: boardOrientation, arePiecesDraggable: draggable, isDraggablePiece: ({ piece }) => {
|
|
399
442
|
if (state.trainColor === 'white')
|
|
400
443
|
return piece[0] === 'w';
|
|
401
444
|
if (state.trainColor === 'black')
|
|
402
445
|
return piece[0] === 'b';
|
|
403
446
|
return piece[0] === state.sideToMove;
|
|
404
|
-
}, onPieceDrop: (source, target, piece) => state.handleDrop(source, target, piece), customArrows: customArrows,
|
|
447
|
+
}, onPieceDrop: (source, target, piece) => state.handleDrop(source, target, piece), customArrows: customArrows, promotionDialogVariant: "modal", areArrowsAllowed: false, customBoardStyle: customBoardStyle }) }), jsxRuntime.jsxs("div", { style: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 4 }, children: [jsxRuntime.jsx(reactChessCore.PlyNavigation, { plyIndex: state.plyIndex, totalPly: state.totalPly, canPrev: state.canPrev, canNext: state.canNext, onGoFirst: state.goFirst, onGoPrev: state.goPrev, onGoNext: state.goNext, onGoLast: state.goLast, onGoTo: state.goTo, theme: theme, showScrubber: showPlyScrubber, renderPlyNavigation: renderPlyNavigation }), jsxRuntime.jsx("button", { type: "button", onClick: state.toggleAutoplay, disabled: !state.autoplayActive && !state.canNext, style: buttonStyle(colors, state.autoplayActive ? 'ghost' : 'primary'), "aria-label": state.autoplayActive ? 'Stop autoplay' : 'Autoplay game', children: state.autoplayActive ? 'Stop' : 'Play' })] }), jsxRuntime.jsxs("div", { style: statusLineStyle(colors), children: ["Half move ", Math.min(state.plyIndex + (state.complete ? 0 : 1), state.totalPly), " of", ' ', state.totalPly, training && !state.complete && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [` · ${TRAIN_COLOR_LABEL[state.trainColor]}`, state.trainColor === 'both'
|
|
405
448
|
? ` · ${state.sideToMove === 'b' ? 'Black' : 'White'} to move`
|
|
406
449
|
: state.isUserTurn
|
|
407
450
|
? ' · Your move'
|
|
@@ -432,11 +475,72 @@ Object.defineProperty(exports, "PlyNavigation", {
|
|
|
432
475
|
enumerable: true,
|
|
433
476
|
get: function () { return reactChessCore.PlyNavigation; }
|
|
434
477
|
});
|
|
478
|
+
Object.defineProperty(exports, "REPLAY_MISS_MOVE_ANIMATION_MS", {
|
|
479
|
+
enumerable: true,
|
|
480
|
+
get: function () { return reactChessCore.MISS_MOVE_ANIMATION_MS; }
|
|
481
|
+
});
|
|
482
|
+
Object.defineProperty(exports, "REPLAY_MISS_REFUTATION_MAX_WAIT_MS", {
|
|
483
|
+
enumerable: true,
|
|
484
|
+
get: function () { return reactChessCore.MISS_REFUTATION_MAX_WAIT_MS; }
|
|
485
|
+
});
|
|
486
|
+
Object.defineProperty(exports, "REPLAY_MISS_REFUTATION_PAUSE_MS", {
|
|
487
|
+
enumerable: true,
|
|
488
|
+
get: function () { return reactChessCore.MISS_REFUTATION_PAUSE_MS; }
|
|
489
|
+
});
|
|
490
|
+
Object.defineProperty(exports, "REPLAY_MISS_WRONG_PAUSE_MS", {
|
|
491
|
+
enumerable: true,
|
|
492
|
+
get: function () { return reactChessCore.MISS_WRONG_PAUSE_MS; }
|
|
493
|
+
});
|
|
494
|
+
Object.defineProperty(exports, "REPLAY_REFUTATION_EVAL_GAP_CP", {
|
|
495
|
+
enumerable: true,
|
|
496
|
+
get: function () { return reactChessCore.REFUTATION_EVAL_GAP_CP; }
|
|
497
|
+
});
|
|
498
|
+
Object.defineProperty(exports, "REPLAY_REFUTATION_EVAL_GAP_PAWNS", {
|
|
499
|
+
enumerable: true,
|
|
500
|
+
get: function () { return reactChessCore.REFUTATION_EVAL_GAP_PAWNS; }
|
|
501
|
+
});
|
|
435
502
|
Object.defineProperty(exports, "defaultRenderPlyNavigation", {
|
|
436
503
|
enumerable: true,
|
|
437
504
|
get: function () { return reactChessCore.defaultRenderPlyNavigation; }
|
|
438
505
|
});
|
|
506
|
+
Object.defineProperty(exports, "fenAfterUci", {
|
|
507
|
+
enumerable: true,
|
|
508
|
+
get: function () { return reactChessCore.fenAfterUci; }
|
|
509
|
+
});
|
|
510
|
+
Object.defineProperty(exports, "getReplayMissDisplay", {
|
|
511
|
+
enumerable: true,
|
|
512
|
+
get: function () { return reactChessCore.getMissDisplay; }
|
|
513
|
+
});
|
|
514
|
+
Object.defineProperty(exports, "refutationEvalGapCp", {
|
|
515
|
+
enumerable: true,
|
|
516
|
+
get: function () { return reactChessCore.refutationEvalGapCp; }
|
|
517
|
+
});
|
|
518
|
+
Object.defineProperty(exports, "refutationFromEvaluation", {
|
|
519
|
+
enumerable: true,
|
|
520
|
+
get: function () { return reactChessCore.refutationFromEvaluation; }
|
|
521
|
+
});
|
|
522
|
+
Object.defineProperty(exports, "replayRefutationEngineOptions", {
|
|
523
|
+
enumerable: true,
|
|
524
|
+
get: function () { return reactChessCore.refutationEngineOptions; }
|
|
525
|
+
});
|
|
526
|
+
Object.defineProperty(exports, "uciFromDrop", {
|
|
527
|
+
enumerable: true,
|
|
528
|
+
get: function () { return reactChessCore.uciFromDrop; }
|
|
529
|
+
});
|
|
530
|
+
Object.defineProperty(exports, "useReplayMissBoard", {
|
|
531
|
+
enumerable: true,
|
|
532
|
+
get: function () { return reactChessCore.useMissBoard; }
|
|
533
|
+
});
|
|
534
|
+
Object.defineProperty(exports, "useReplayMissSequence", {
|
|
535
|
+
enumerable: true,
|
|
536
|
+
get: function () { return reactChessCore.useMissSequence; }
|
|
537
|
+
});
|
|
538
|
+
Object.defineProperty(exports, "useReplayRefutation", {
|
|
539
|
+
enumerable: true,
|
|
540
|
+
get: function () { return reactChessCore.useMissRefutation; }
|
|
541
|
+
});
|
|
439
542
|
exports.DEFAULT_BOARD_WIDTH = DEFAULT_BOARD_WIDTH;
|
|
543
|
+
exports.REPLAY_AUTOPLAY_STEP_MS = REPLAY_AUTOPLAY_STEP_MS;
|
|
440
544
|
exports.REPLAY_START_FEN = REPLAY_START_FEN;
|
|
441
545
|
exports.ReplayTrainer = ReplayTrainer;
|
|
442
546
|
exports.buildReplayAnalysisContext = buildReplayAnalysisContext;
|
|
@@ -444,5 +548,4 @@ exports.fenAtPly = fenAtPly;
|
|
|
444
548
|
exports.findPlyIndexForFen = findPlyIndexForFen;
|
|
445
549
|
exports.normalizeFen = normalizeFen;
|
|
446
550
|
exports.sideToMove = sideToMove;
|
|
447
|
-
exports.uciFromDrop = uciFromDrop;
|
|
448
551
|
exports.useReplayTrainer = useReplayTrainer;
|
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "react-chess-replay-trainer",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "React component for browsing a chess game and drilling its moves, reporting misses (uses react-chess-core for board and analysis)",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "Robert Blackwell",
|
|
7
|
-
"main": "dist/index.js",
|
|
8
|
-
"module": "dist/index.esm.js",
|
|
9
|
-
"types": "dist/index.d.ts",
|
|
10
|
-
"files": [
|
|
11
|
-
"dist"
|
|
12
|
-
],
|
|
13
|
-
"scripts": {
|
|
14
|
-
"build": "rollup -c",
|
|
15
|
-
"prepublishOnly": "npm run build",
|
|
16
|
-
"storybook": "storybook dev -p 6008",
|
|
17
|
-
"build-storybook": "storybook build"
|
|
18
|
-
},
|
|
19
|
-
"dependencies": {
|
|
20
|
-
"rollup": "^4.22.2",
|
|
21
|
-
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
22
|
-
"typescript": "^5.6.2"
|
|
23
|
-
},
|
|
24
|
-
"peerDependencies": {
|
|
25
|
-
"chess.js": "^1.0.0-beta.8",
|
|
26
|
-
"react": "^18.3.1",
|
|
27
|
-
"react-chess-core": "^0.1.
|
|
28
|
-
"react-chessboard": "^4.7.1"
|
|
29
|
-
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"@chromatic-com/storybook": "^1.9.0",
|
|
32
|
-
"@rollup/plugin-commonjs": "^26.0.1",
|
|
33
|
-
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
34
|
-
"@rollup/plugin-typescript": "^12.3.0",
|
|
35
|
-
"@storybook/addon-essentials": "^8.2.9",
|
|
36
|
-
"@storybook/addon-interactions": "^8.2.9",
|
|
37
|
-
"@storybook/addon-links": "^8.2.9",
|
|
38
|
-
"@storybook/addon-onboarding": "^8.2.9",
|
|
39
|
-
"@storybook/blocks": "^8.2.9",
|
|
40
|
-
"@storybook/preset-typescript": "^3.0.0",
|
|
41
|
-
"@storybook/react": "^8.2.9",
|
|
42
|
-
"@storybook/react-vite": "^8.2.9",
|
|
43
|
-
"@storybook/test": "^8.2.9",
|
|
44
|
-
"@types/react": "^18.3.12",
|
|
45
|
-
"@types/react-dom": "^18.3.1",
|
|
46
|
-
"@vitejs/plugin-react": "^4.3.1",
|
|
47
|
-
"chess.js": "^1.0.0-beta.8",
|
|
48
|
-
"react": "^18.3.1",
|
|
49
|
-
"react-chess-core": "^0.1.
|
|
50
|
-
"react-chessboard": "^4.7.1",
|
|
51
|
-
"react-dom": "^18.3.1",
|
|
52
|
-
"storybook": "^8.2.9",
|
|
53
|
-
"tslib": "^2.8.1",
|
|
54
|
-
"vite-tsconfig-paths": "^5.0.1"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "react-chess-replay-trainer",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "React component for browsing a chess game and drilling its moves, reporting misses (uses react-chess-core for board and analysis)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Robert Blackwell",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"module": "dist/index.esm.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "rollup -c",
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"storybook": "storybook dev -p 6008",
|
|
17
|
+
"build-storybook": "storybook build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"rollup": "^4.22.2",
|
|
21
|
+
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
22
|
+
"typescript": "^5.6.2"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"chess.js": "^1.0.0-beta.8",
|
|
26
|
+
"react": "^18.3.1",
|
|
27
|
+
"react-chess-core": "^0.1.1",
|
|
28
|
+
"react-chessboard": "^4.7.1"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@chromatic-com/storybook": "^1.9.0",
|
|
32
|
+
"@rollup/plugin-commonjs": "^26.0.1",
|
|
33
|
+
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
34
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
35
|
+
"@storybook/addon-essentials": "^8.2.9",
|
|
36
|
+
"@storybook/addon-interactions": "^8.2.9",
|
|
37
|
+
"@storybook/addon-links": "^8.2.9",
|
|
38
|
+
"@storybook/addon-onboarding": "^8.2.9",
|
|
39
|
+
"@storybook/blocks": "^8.2.9",
|
|
40
|
+
"@storybook/preset-typescript": "^3.0.0",
|
|
41
|
+
"@storybook/react": "^8.2.9",
|
|
42
|
+
"@storybook/react-vite": "^8.2.9",
|
|
43
|
+
"@storybook/test": "^8.2.9",
|
|
44
|
+
"@types/react": "^18.3.12",
|
|
45
|
+
"@types/react-dom": "^18.3.1",
|
|
46
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
47
|
+
"chess.js": "^1.0.0-beta.8",
|
|
48
|
+
"react": "^18.3.1",
|
|
49
|
+
"react-chess-core": "^0.1.1",
|
|
50
|
+
"react-chessboard": "^4.7.1",
|
|
51
|
+
"react-dom": "^18.3.1",
|
|
52
|
+
"storybook": "^8.2.9",
|
|
53
|
+
"tslib": "^2.8.1",
|
|
54
|
+
"vite-tsconfig-paths": "^5.0.1"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { AnalysisEngineOptions } from 'react-chess-core';
|
|
3
|
-
import { type UseReplayAnalysisBoardArgs } from './hooks/useReplayAnalysisBoard';
|
|
4
|
-
import type { ReplayAnalysisContext } from './types';
|
|
5
|
-
export declare const DEFAULT_ANALYSIS_BOARD_WIDTH = 400;
|
|
6
|
-
export declare const DEFAULT_ANALYSIS_SIDEBAR_WIDTH = 280;
|
|
7
|
-
export declare const DEFAULT_ANALYSIS_COLUMN_GAP = 16;
|
|
8
|
-
export interface ReplayAnalysisBoardProps extends Omit<UseReplayAnalysisBoardArgs, 'boardWidth'> {
|
|
9
|
-
boardWidth?: number;
|
|
10
|
-
sidebarWidth?: number;
|
|
11
|
-
columnGap?: number;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Full-screen modal analysis board: draggable exploration, move list,
|
|
15
|
-
* variations, and Stockfish evaluation via react-chess-core.
|
|
16
|
-
*/
|
|
17
|
-
export declare const ReplayAnalysisBoard: ({ boardWidth, sidebarWidth, columnGap, ...modelArgs }: ReplayAnalysisBoardProps) => React.JSX.Element;
|
|
18
|
-
export type { ReplayAnalysisContext, AnalysisEngineOptions };
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { ReplayAnalysisContext, ReplayAnalysisHistoryRow, ReplaySolutionMoveDisplay } from './types';
|
|
2
|
-
export declare class ReplayAnalysisPosition {
|
|
3
|
-
private chess;
|
|
4
|
-
private readonly initialFen;
|
|
5
|
-
private readonly solutionMoves;
|
|
6
|
-
private readonly solutionSans;
|
|
7
|
-
private mainPly;
|
|
8
|
-
private variation;
|
|
9
|
-
private variationCursor;
|
|
10
|
-
constructor(context: ReplayAnalysisContext);
|
|
11
|
-
private static buildSolutionSans;
|
|
12
|
-
private fenAtMainPly;
|
|
13
|
-
private rebuildChess;
|
|
14
|
-
private findLegalMove;
|
|
15
|
-
private uciFromVerboseMove;
|
|
16
|
-
private matchesMainMove;
|
|
17
|
-
getNavPly(): number;
|
|
18
|
-
getMaxNavPly(): number;
|
|
19
|
-
getSolutionSans(): ReplaySolutionMoveDisplay[];
|
|
20
|
-
getHistoryRows(): ReplayAnalysisHistoryRow[];
|
|
21
|
-
isHistoryRowSelected(row: ReplayAnalysisHistoryRow): boolean;
|
|
22
|
-
selectHistoryRow(row: ReplayAnalysisHistoryRow): void;
|
|
23
|
-
selectMainLine(ply: number): void;
|
|
24
|
-
goToNavPly(navPly: number): void;
|
|
25
|
-
tryPlayMove(sourceSquare: string, targetSquare: string, piece: string): boolean;
|
|
26
|
-
getLastMoveSquares(): {
|
|
27
|
-
from: string;
|
|
28
|
-
to: string;
|
|
29
|
-
} | null;
|
|
30
|
-
fen(): string;
|
|
31
|
-
getCheckSquare(): string;
|
|
32
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { type EngineEvaluation } from 'react-chess-core';
|
|
3
|
-
export interface ReplayEngineEvaluationPanelProps {
|
|
4
|
-
fen: string;
|
|
5
|
-
evaluation: EngineEvaluation;
|
|
6
|
-
theme: 'light' | 'dark';
|
|
7
|
-
}
|
|
8
|
-
export declare const ReplayEngineEvaluationPanel: ({ fen, evaluation, theme, }: ReplayEngineEvaluationPanelProps) => React.JSX.Element;
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
export declare const analysisBoardHighlightColors: {
|
|
2
|
-
readonly lastMove: {
|
|
3
|
-
readonly light: "rgba(253, 216, 53, 0.55)";
|
|
4
|
-
readonly dark: "rgba(144, 202, 249, 0.5)";
|
|
5
|
-
};
|
|
6
|
-
};
|
|
7
|
-
export declare const getLastMoveSquareStyles: (from: string, to: string, theme: "light" | "dark") => Record<string, {
|
|
8
|
-
backgroundColor: string;
|
|
9
|
-
}>;
|
|
10
|
-
export declare const analysisSidebarColors: {
|
|
11
|
-
readonly activeMove: {
|
|
12
|
-
readonly light: "rgba(58, 123, 213, 0.25)";
|
|
13
|
-
readonly dark: "rgba(58, 123, 213, 0.35)";
|
|
14
|
-
};
|
|
15
|
-
readonly mainBand: {
|
|
16
|
-
readonly light: readonly ["rgba(0,0,0,0.02)", "rgba(0,0,0,0.05)"];
|
|
17
|
-
readonly dark: readonly ["rgba(255,255,255,0.03)", "rgba(255,255,255,0.06)"];
|
|
18
|
-
};
|
|
19
|
-
readonly variationBand: {
|
|
20
|
-
readonly light: readonly ["rgba(0,0,0,0.04)", "rgba(0,0,0,0.07)"];
|
|
21
|
-
readonly dark: readonly ["rgba(255,255,255,0.05)", "rgba(255,255,255,0.08)"];
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
export declare function getSidebarRowBackground(theme: 'light' | 'dark', row: {
|
|
25
|
-
kind: 'start' | 'main' | 'variation';
|
|
26
|
-
}, bandCounters: {
|
|
27
|
-
main: number;
|
|
28
|
-
variation: number;
|
|
29
|
-
}): string;
|
|
30
|
-
export declare function createSidebarRowBandCounters(): {
|
|
31
|
-
main: number;
|
|
32
|
-
variation: number;
|
|
33
|
-
};
|
|
34
|
-
export declare const analysisModalColors: {
|
|
35
|
-
readonly light: {
|
|
36
|
-
readonly panel: {
|
|
37
|
-
readonly backgroundColor: "#fafafa";
|
|
38
|
-
readonly color: "#1a1a1a";
|
|
39
|
-
};
|
|
40
|
-
readonly title: {
|
|
41
|
-
readonly color: "#1a1a1a";
|
|
42
|
-
};
|
|
43
|
-
readonly closeButton: {
|
|
44
|
-
readonly backgroundColor: "#eee";
|
|
45
|
-
readonly border: "1px solid #ccc";
|
|
46
|
-
readonly color: "#333";
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
readonly dark: {
|
|
50
|
-
readonly panel: {
|
|
51
|
-
readonly backgroundColor: "#2a2a2a";
|
|
52
|
-
readonly color: "#e8e8e8";
|
|
53
|
-
};
|
|
54
|
-
readonly title: {
|
|
55
|
-
readonly color: "#e8e8e8";
|
|
56
|
-
};
|
|
57
|
-
readonly closeButton: {
|
|
58
|
-
readonly backgroundColor: "#3a3a3a";
|
|
59
|
-
readonly border: "1px solid #555";
|
|
60
|
-
readonly color: "#e8e8e8";
|
|
61
|
-
};
|
|
62
|
-
};
|
|
63
|
-
};
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { ReplayGame } from '../types';
|
|
2
|
-
import type { ReplayAnalysisContext } from './types';
|
|
3
|
-
/** Build analysis context from the current replay browse position. */
|
|
4
|
-
export declare function buildReplayAnalysisContext(game: ReplayGame, plyIndex: number, boardOrientation: 'white' | 'black'): ReplayAnalysisContext;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { AnalysisEngineOptions, EngineEvaluation } from 'react-chess-core';
|
|
2
|
-
import type { ReplayAnalysisContext, ReplayAnalysisHistoryRow, ReplaySolutionMoveDisplay } from '../types';
|
|
3
|
-
export type UseReplayAnalysisBoardArgs = {
|
|
4
|
-
analysisContext: ReplayAnalysisContext;
|
|
5
|
-
onClose: () => void;
|
|
6
|
-
theme: 'light' | 'dark';
|
|
7
|
-
boardWidth: number;
|
|
8
|
-
engine?: AnalysisEngineOptions;
|
|
9
|
-
};
|
|
10
|
-
export type ReplayAnalysisBoardModel = {
|
|
11
|
-
theme: 'light' | 'dark';
|
|
12
|
-
boardWidth: number;
|
|
13
|
-
analysisContext: ReplayAnalysisContext;
|
|
14
|
-
fen: string;
|
|
15
|
-
ply: number;
|
|
16
|
-
maxPly: number;
|
|
17
|
-
historyRows: ReplayAnalysisHistoryRow[];
|
|
18
|
-
solutionSans: ReplaySolutionMoveDisplay[];
|
|
19
|
-
boardOrientation: 'white' | 'black';
|
|
20
|
-
engineEvaluation: EngineEvaluation;
|
|
21
|
-
engineEnabled: boolean;
|
|
22
|
-
lastMove: {
|
|
23
|
-
from: string;
|
|
24
|
-
to: string;
|
|
25
|
-
} | null;
|
|
26
|
-
checkSquare: string | null;
|
|
27
|
-
onSelectPly: (ply: number) => void;
|
|
28
|
-
onSelectHistoryRow: (row: ReplayAnalysisHistoryRow) => void;
|
|
29
|
-
isHistoryRowSelected: (row: ReplayAnalysisHistoryRow) => boolean;
|
|
30
|
-
onPieceDrop: (sourceSquare: string, targetSquare: string, piece: string) => boolean;
|
|
31
|
-
onBackdropMouseDown: () => void;
|
|
32
|
-
onClose: () => void;
|
|
33
|
-
};
|
|
34
|
-
export declare function useReplayAnalysisBoard({ analysisContext, onClose, theme, boardWidth, engine, }: UseReplayAnalysisBoardArgs): ReplayAnalysisBoardModel;
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { ReplayAnalysisBoard, DEFAULT_ANALYSIS_BOARD_WIDTH, DEFAULT_ANALYSIS_SIDEBAR_WIDTH, DEFAULT_ANALYSIS_COLUMN_GAP, type ReplayAnalysisBoardProps, } from './ReplayAnalysisBoard';
|
|
2
|
-
export { ReplayEngineEvaluationPanel } from './ReplayEngineEvaluationPanel';
|
|
3
|
-
export { buildReplayAnalysisContext } from './buildReplayAnalysisContext';
|
|
4
|
-
export { ReplayAnalysisPosition } from './ReplayAnalysisPosition';
|
|
5
|
-
export { useReplayAnalysisBoard, type UseReplayAnalysisBoardArgs, type ReplayAnalysisBoardModel, } from './hooks/useReplayAnalysisBoard';
|
|
6
|
-
export type { ReplayAnalysisContext, ReplayAnalysisHistoryRow, ReplaySolutionMoveDisplay, } from './types';
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/** Context for opening the replay analysis board at a point in the game. */
|
|
2
|
-
export type ReplayAnalysisContext = {
|
|
3
|
-
initialFen: string;
|
|
4
|
-
/** Full game main line in UCI notation. */
|
|
5
|
-
solutionMoves: string[];
|
|
6
|
-
/** Ply index to open at (0 = start position). */
|
|
7
|
-
startPly: number;
|
|
8
|
-
boardOrientation: 'white' | 'black';
|
|
9
|
-
};
|
|
10
|
-
export type ReplayAnalysisHistoryRow = {
|
|
11
|
-
key: string;
|
|
12
|
-
label: string;
|
|
13
|
-
indent: number;
|
|
14
|
-
kind: 'start' | 'main' | 'variation';
|
|
15
|
-
mainPly: number;
|
|
16
|
-
variationIndex: number;
|
|
17
|
-
};
|
|
18
|
-
export type ReplaySolutionMoveDisplay = {
|
|
19
|
-
ply: number;
|
|
20
|
-
uci: string;
|
|
21
|
-
san: string;
|
|
22
|
-
};
|