react-chess-core 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -46
- package/dist/features/analysis/core/AnalysisBoardCore.d.ts +8 -2
- package/dist/features/chessboard/ChessboardDnDProvider.d.ts +4 -0
- package/dist/features/chessboard/boardThemes.d.ts +19 -0
- package/dist/features/chessboard/chessboardTheme.d.ts +6 -2
- package/dist/features/chessboard/index.d.ts +2 -0
- package/dist/features/engine/AnalysisEngineContext.d.ts +22 -0
- package/dist/features/engine/index.d.ts +1 -0
- package/dist/features/engine/types.d.ts +10 -0
- package/dist/features/engine/useAnalysisEngine.d.ts +2 -0
- package/dist/features/navigation/PlyNavigation.d.ts +1 -1
- package/dist/features/navigation/index.d.ts +2 -0
- package/dist/features/navigation/positionKeyboardNav.d.ts +12 -0
- package/dist/features/navigation/types.d.ts +5 -0
- package/dist/features/navigation/usePositionKeyboardNav.d.ts +12 -0
- package/dist/features/training/expectedMoveDrop.d.ts +24 -0
- package/dist/features/training/expectedMoveDrop.test.d.ts +1 -0
- package/dist/features/training/index.d.ts +4 -0
- package/dist/features/training/miss/index.d.ts +5 -0
- package/dist/features/training/miss/missDisplay.d.ts +16 -0
- package/dist/features/training/miss/refutation.d.ts +19 -0
- package/dist/features/training/miss/useMissBoard.d.ts +28 -0
- package/dist/features/training/miss/useMissRefutation.d.ts +3 -0
- package/dist/features/training/miss/useMissSequence.d.ts +10 -0
- package/dist/features/training/uciFromDrop.d.ts +3 -0
- package/dist/features/training/useBoardRevision.d.ts +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +816 -88
- package/dist/index.js +846 -86
- package/dist/stories/AnalysisBoard.stories.d.ts +9 -0
- package/dist/stories/analysisFixtures.d.ts +5 -0
- package/dist/stories/regressions/StockfishAnalysisRegressions.stories.d.ts +7 -0
- package/dist/stories/regressions/fixtures.d.ts +17 -0
- package/dist/stories/regressions/regressionAnalysisContext.d.ts +6 -0
- package/dist/stories/storybookLayout.d.ts +4 -0
- package/package.json +65 -65
package/dist/index.js
CHANGED
|
@@ -6,6 +6,80 @@ var reactChessboard = require('react-chessboard');
|
|
|
6
6
|
var chess_js = require('chess.js');
|
|
7
7
|
var reactDom = require('react-dom');
|
|
8
8
|
|
|
9
|
+
const BOARD_THEME_IDS = [
|
|
10
|
+
'classic',
|
|
11
|
+
'forest',
|
|
12
|
+
'marine',
|
|
13
|
+
'marble',
|
|
14
|
+
'ivory',
|
|
15
|
+
'ocean',
|
|
16
|
+
'copper',
|
|
17
|
+
'bubblegum',
|
|
18
|
+
'sunset',
|
|
19
|
+
];
|
|
20
|
+
const DEFAULT_BOARD_THEME = 'classic';
|
|
21
|
+
const BOARD_THEMES = {
|
|
22
|
+
classic: {
|
|
23
|
+
label: 'Classic',
|
|
24
|
+
darkSquare: '#b58863',
|
|
25
|
+
lightSquare: '#f0d9b5',
|
|
26
|
+
},
|
|
27
|
+
forest: {
|
|
28
|
+
label: 'Forest',
|
|
29
|
+
darkSquare: '#769656',
|
|
30
|
+
lightSquare: '#eeeed2',
|
|
31
|
+
},
|
|
32
|
+
marine: {
|
|
33
|
+
label: 'Marine',
|
|
34
|
+
darkSquare: '#5b7c99',
|
|
35
|
+
lightSquare: '#d6e4f0',
|
|
36
|
+
},
|
|
37
|
+
marble: {
|
|
38
|
+
label: 'Marble',
|
|
39
|
+
darkSquare: '#a8a8a8',
|
|
40
|
+
lightSquare: '#ffffff',
|
|
41
|
+
},
|
|
42
|
+
ivory: {
|
|
43
|
+
label: 'Ivory',
|
|
44
|
+
darkSquare: '#c9a66b',
|
|
45
|
+
lightSquare: '#fff8e7',
|
|
46
|
+
},
|
|
47
|
+
ocean: {
|
|
48
|
+
label: 'Ocean',
|
|
49
|
+
darkSquare: '#2d6a7e',
|
|
50
|
+
lightSquare: '#a8dadc',
|
|
51
|
+
},
|
|
52
|
+
copper: {
|
|
53
|
+
label: 'Copper',
|
|
54
|
+
darkSquare: '#9c6644',
|
|
55
|
+
lightSquare: '#f4e4d4',
|
|
56
|
+
},
|
|
57
|
+
bubblegum: {
|
|
58
|
+
label: 'Bubblegum',
|
|
59
|
+
darkSquare: '#c77dff',
|
|
60
|
+
lightSquare: '#ffd6ff',
|
|
61
|
+
},
|
|
62
|
+
sunset: {
|
|
63
|
+
label: 'Sunset',
|
|
64
|
+
darkSquare: '#e76f51',
|
|
65
|
+
lightSquare: '#ffe8d6',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
function isBoardThemeId(value) {
|
|
69
|
+
return (typeof value === 'string' &&
|
|
70
|
+
BOARD_THEME_IDS.includes(value));
|
|
71
|
+
}
|
|
72
|
+
function boardThemeFromLegacyUiTheme(_theme) {
|
|
73
|
+
return DEFAULT_BOARD_THEME;
|
|
74
|
+
}
|
|
75
|
+
function getBoardThemeStyles(boardTheme) {
|
|
76
|
+
const { darkSquare, lightSquare } = BOARD_THEMES[boardTheme];
|
|
77
|
+
return {
|
|
78
|
+
customDarkSquareStyle: { backgroundColor: darkSquare },
|
|
79
|
+
customLightSquareStyle: { backgroundColor: lightSquare },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
9
83
|
const ChessboardThemeContext = react.createContext(undefined);
|
|
10
84
|
const useChessboardTheme = () => {
|
|
11
85
|
const context = react.useContext(ChessboardThemeContext);
|
|
@@ -16,23 +90,18 @@ const useChessboardTheme = () => {
|
|
|
16
90
|
};
|
|
17
91
|
/** @deprecated Use {@link useChessboardTheme}. */
|
|
18
92
|
const useTheme = useChessboardTheme;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
return {
|
|
27
|
-
customDarkSquareStyle: { backgroundColor: '#b58863' },
|
|
28
|
-
customLightSquareStyle: { backgroundColor: '#f0d9b5' },
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
const ThemeProvider = ({ children, theme }) => {
|
|
32
|
-
const { customDarkSquareStyle, customLightSquareStyle } = getStylesForTheme(theme);
|
|
93
|
+
/** @deprecated Use {@link getBoardThemeStyles}. */
|
|
94
|
+
const getStylesForTheme = (theme) => getBoardThemeStyles(boardThemeFromLegacyUiTheme());
|
|
95
|
+
const ThemeProvider = ({ children, theme = 'dark', boardTheme, }) => {
|
|
96
|
+
const resolvedBoardTheme = boardTheme !== null && boardTheme !== void 0 ? boardTheme : boardThemeFromLegacyUiTheme();
|
|
97
|
+
const { customDarkSquareStyle, customLightSquareStyle } = getBoardThemeStyles(resolvedBoardTheme);
|
|
33
98
|
return (jsxRuntime.jsx(ChessboardThemeContext.Provider, { value: { customDarkSquareStyle, customLightSquareStyle }, children: children }));
|
|
34
99
|
};
|
|
35
100
|
|
|
101
|
+
/** Touch DnD also accepts mouse events (Chrome device emulation, touch laptops). */
|
|
102
|
+
const TOUCH_DND_OPTIONS = { enableMouseEvents: true };
|
|
103
|
+
const ChessboardDnDProvider = ({ children, }) => (jsxRuntime.jsx(reactChessboard.ChessboardDnDProvider, { options: TOUCH_DND_OPTIONS, children: children }));
|
|
104
|
+
|
|
36
105
|
/******************************************************************************
|
|
37
106
|
Copyright (c) Microsoft Corporation.
|
|
38
107
|
|
|
@@ -560,16 +629,208 @@ class StockfishBrowserEngine {
|
|
|
560
629
|
}
|
|
561
630
|
}
|
|
562
631
|
|
|
632
|
+
const AnalysisEngineContext = react.createContext(null);
|
|
633
|
+
const useAnalysisEngineContext = () => react.useContext(AnalysisEngineContext);
|
|
634
|
+
const normalizeSubscriberOptions = (fen, options = {}) => {
|
|
635
|
+
var _a, _b, _c, _d;
|
|
636
|
+
return ({
|
|
637
|
+
fen,
|
|
638
|
+
enabled: (_a = options.enabled) !== null && _a !== void 0 ? _a : true,
|
|
639
|
+
depth: (_b = options.depth) !== null && _b !== void 0 ? _b : 16,
|
|
640
|
+
multiPv: (_c = options.multiPv) !== null && _c !== void 0 ? _c : 2,
|
|
641
|
+
priority: (_d = options.priority) !== null && _d !== void 0 ? _d : 0,
|
|
642
|
+
});
|
|
643
|
+
};
|
|
644
|
+
const analyzingForFen = (fen, evaluation) => (Object.assign(Object.assign({}, emptyEngineEvaluation()), { status: evaluation.status === 'error'
|
|
645
|
+
? 'error'
|
|
646
|
+
: evaluation.status === 'loading'
|
|
647
|
+
? 'loading'
|
|
648
|
+
: 'analyzing', error: evaluation.error, fen }));
|
|
649
|
+
const AnalysisEngineProvider = ({ scriptUrl = DEFAULT_STOCKFISH_SCRIPT_URL, children, }) => {
|
|
650
|
+
const engineRef = react.useRef(null);
|
|
651
|
+
const subscribersRef = react.useRef(new Map());
|
|
652
|
+
const nextIdRef = react.useRef(0);
|
|
653
|
+
const activeKeyRef = react.useRef('');
|
|
654
|
+
const readyRef = react.useRef(false);
|
|
655
|
+
const scheduleRef = react.useRef(null);
|
|
656
|
+
const lastEvaluationRef = react.useRef(emptyEngineEvaluation());
|
|
657
|
+
const pickActive = react.useCallback(() => {
|
|
658
|
+
const enabled = [...subscribersRef.current.values()].filter((subscriber) => subscriber.options.enabled);
|
|
659
|
+
if (enabled.length === 0) {
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
return enabled.sort((left, right) => right.options.priority - left.options.priority)[0];
|
|
663
|
+
}, []);
|
|
664
|
+
const notifyAll = react.useCallback((evaluation) => {
|
|
665
|
+
lastEvaluationRef.current = evaluation;
|
|
666
|
+
const active = pickActive();
|
|
667
|
+
for (const subscriber of subscribersRef.current.values()) {
|
|
668
|
+
const { fen, enabled } = subscriber.options;
|
|
669
|
+
if (!enabled) {
|
|
670
|
+
subscriber.listener(emptyEngineEvaluation());
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (evaluation.fen && evaluation.fen === fen) {
|
|
674
|
+
subscriber.listener(evaluation);
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
if ((active === null || active === void 0 ? void 0 : active.id) === subscriber.id) {
|
|
678
|
+
subscriber.listener(analyzingForFen(fen, evaluation));
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}, [pickActive]);
|
|
682
|
+
const syncAnalysis = react.useCallback(() => {
|
|
683
|
+
const engine = engineRef.current;
|
|
684
|
+
if (!engine || !readyRef.current) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const active = pickActive();
|
|
688
|
+
if (!active) {
|
|
689
|
+
activeKeyRef.current = '';
|
|
690
|
+
engine.stop();
|
|
691
|
+
notifyAll(emptyEngineEvaluation());
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const { fen, depth, multiPv } = active.options;
|
|
695
|
+
const key = `${active.id}|${fen}|${depth}|${multiPv}`;
|
|
696
|
+
if (key === activeKeyRef.current) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
activeKeyRef.current = key;
|
|
700
|
+
if (scheduleRef.current !== null) {
|
|
701
|
+
window.clearTimeout(scheduleRef.current);
|
|
702
|
+
}
|
|
703
|
+
scheduleRef.current = window.setTimeout(() => {
|
|
704
|
+
scheduleRef.current = null;
|
|
705
|
+
engine.analyze(fen, depth, multiPv);
|
|
706
|
+
}, 75);
|
|
707
|
+
}, [notifyAll, pickActive]);
|
|
708
|
+
react.useEffect(() => {
|
|
709
|
+
if (typeof Worker === 'undefined') {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const engine = new StockfishBrowserEngine(scriptUrl);
|
|
713
|
+
engineRef.current = engine;
|
|
714
|
+
let cancelled = false;
|
|
715
|
+
const unsubscribe = engine.subscribe((evaluation) => {
|
|
716
|
+
if (!cancelled) {
|
|
717
|
+
notifyAll(evaluation);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
engine
|
|
721
|
+
.init()
|
|
722
|
+
.then(() => {
|
|
723
|
+
if (cancelled) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
readyRef.current = true;
|
|
727
|
+
syncAnalysis();
|
|
728
|
+
})
|
|
729
|
+
.catch((error) => {
|
|
730
|
+
if (cancelled) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const message = error instanceof Error ? error.message : 'Failed to start engine';
|
|
734
|
+
notifyAll(Object.assign(Object.assign({}, emptyEngineEvaluation()), { status: 'error', error: message }));
|
|
735
|
+
});
|
|
736
|
+
return () => {
|
|
737
|
+
cancelled = true;
|
|
738
|
+
readyRef.current = false;
|
|
739
|
+
activeKeyRef.current = '';
|
|
740
|
+
if (scheduleRef.current !== null) {
|
|
741
|
+
window.clearTimeout(scheduleRef.current);
|
|
742
|
+
scheduleRef.current = null;
|
|
743
|
+
}
|
|
744
|
+
unsubscribe();
|
|
745
|
+
engine.dispose();
|
|
746
|
+
engineRef.current = null;
|
|
747
|
+
};
|
|
748
|
+
}, [notifyAll, scriptUrl, syncAnalysis]);
|
|
749
|
+
const register = react.useCallback((options, listener) => {
|
|
750
|
+
const id = ++nextIdRef.current;
|
|
751
|
+
subscribersRef.current.set(id, { id, options, listener });
|
|
752
|
+
if (options.enabled) {
|
|
753
|
+
const active = pickActive();
|
|
754
|
+
if ((active === null || active === void 0 ? void 0 : active.id) === id) {
|
|
755
|
+
listener(analyzingForFen(options.fen, lastEvaluationRef.current));
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
else {
|
|
759
|
+
listener(emptyEngineEvaluation());
|
|
760
|
+
}
|
|
761
|
+
syncAnalysis();
|
|
762
|
+
return id;
|
|
763
|
+
}, [pickActive, syncAnalysis]);
|
|
764
|
+
const update = react.useCallback((id, options) => {
|
|
765
|
+
const subscriber = subscribersRef.current.get(id);
|
|
766
|
+
if (!subscriber) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
subscriber.options = options;
|
|
770
|
+
if (!options.enabled) {
|
|
771
|
+
subscriber.listener(emptyEngineEvaluation());
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
const active = pickActive();
|
|
775
|
+
if ((active === null || active === void 0 ? void 0 : active.id) === id) {
|
|
776
|
+
subscriber.listener(analyzingForFen(options.fen, lastEvaluationRef.current));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
activeKeyRef.current = '';
|
|
780
|
+
syncAnalysis();
|
|
781
|
+
}, [pickActive, syncAnalysis]);
|
|
782
|
+
const unregister = react.useCallback((id) => {
|
|
783
|
+
subscribersRef.current.delete(id);
|
|
784
|
+
activeKeyRef.current = '';
|
|
785
|
+
syncAnalysis();
|
|
786
|
+
}, [syncAnalysis]);
|
|
787
|
+
const value = react.useMemo(() => ({
|
|
788
|
+
register,
|
|
789
|
+
update,
|
|
790
|
+
unregister,
|
|
791
|
+
}), [register, unregister, update]);
|
|
792
|
+
return (jsxRuntime.jsx(AnalysisEngineContext.Provider, { value: value, children: children }));
|
|
793
|
+
};
|
|
794
|
+
|
|
563
795
|
const useAnalysisEngine = (fen, options = {}) => {
|
|
564
|
-
|
|
796
|
+
var _a;
|
|
797
|
+
const context = useAnalysisEngineContext();
|
|
798
|
+
const useShared = ((_a = options.shared) !== null && _a !== void 0 ? _a : true) && context !== null;
|
|
799
|
+
const { enabled = true, depth = 16, multiPv = 2, priority = 0, scriptUrl = DEFAULT_STOCKFISH_SCRIPT_URL, } = options;
|
|
565
800
|
const [evaluation, setEvaluation] = react.useState(emptyEngineEvaluation());
|
|
566
801
|
const [engineReady, setEngineReady] = react.useState(false);
|
|
567
802
|
const engineRef = react.useRef(null);
|
|
568
803
|
const mountGenerationRef = react.useRef(0);
|
|
804
|
+
const subscriberIdRef = react.useRef(null);
|
|
805
|
+
const subscriberOptions = react.useMemo(() => normalizeSubscriberOptions(fen, options), [fen, enabled, depth, multiPv, priority]);
|
|
806
|
+
react.useLayoutEffect(() => {
|
|
807
|
+
if (!useShared || !context) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (subscriberIdRef.current === null) {
|
|
811
|
+
subscriberIdRef.current = context.register(subscriberOptions, setEvaluation);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
context.update(subscriberIdRef.current, subscriberOptions);
|
|
815
|
+
}, [context, subscriberOptions, useShared]);
|
|
569
816
|
react.useEffect(() => {
|
|
570
|
-
if (!
|
|
571
|
-
|
|
572
|
-
|
|
817
|
+
if (!useShared || !context) {
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const contextValue = context;
|
|
821
|
+
return () => {
|
|
822
|
+
if (subscriberIdRef.current !== null) {
|
|
823
|
+
contextValue.unregister(subscriberIdRef.current);
|
|
824
|
+
subscriberIdRef.current = null;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
}, [context, useShared]);
|
|
828
|
+
react.useEffect(() => {
|
|
829
|
+
if (useShared || !enabled || typeof Worker === 'undefined') {
|
|
830
|
+
if (!useShared && !enabled) {
|
|
831
|
+
setEvaluation(emptyEngineEvaluation());
|
|
832
|
+
setEngineReady(false);
|
|
833
|
+
}
|
|
573
834
|
return;
|
|
574
835
|
}
|
|
575
836
|
const mountGeneration = ++mountGenerationRef.current;
|
|
@@ -605,9 +866,9 @@ const useAnalysisEngine = (fen, options = {}) => {
|
|
|
605
866
|
engineRef.current = null;
|
|
606
867
|
}
|
|
607
868
|
};
|
|
608
|
-
}, [enabled, scriptUrl]);
|
|
869
|
+
}, [enabled, scriptUrl, useShared]);
|
|
609
870
|
react.useLayoutEffect(() => {
|
|
610
|
-
if (!enabled || !engineReady || !engineRef.current) {
|
|
871
|
+
if (useShared || !enabled || !engineReady || !engineRef.current) {
|
|
611
872
|
return;
|
|
612
873
|
}
|
|
613
874
|
const engine = engineRef.current;
|
|
@@ -617,7 +878,7 @@ const useAnalysisEngine = (fen, options = {}) => {
|
|
|
617
878
|
return () => {
|
|
618
879
|
window.clearTimeout(timer);
|
|
619
880
|
};
|
|
620
|
-
}, [enabled, engineReady, fen, depth, multiPv]);
|
|
881
|
+
}, [useShared, enabled, engineReady, fen, depth, multiPv]);
|
|
621
882
|
return react.useMemo(() => {
|
|
622
883
|
if (evaluation.fen !== fen) {
|
|
623
884
|
return Object.assign(Object.assign({}, emptyEngineEvaluation()), { status: evaluation.status === 'error'
|
|
@@ -702,10 +963,151 @@ class AnalysisErrorBoundary extends react.Component {
|
|
|
702
963
|
}
|
|
703
964
|
}
|
|
704
965
|
|
|
966
|
+
const navRowStyle = {
|
|
967
|
+
display: 'flex',
|
|
968
|
+
alignItems: 'center',
|
|
969
|
+
gap: 6,
|
|
970
|
+
};
|
|
971
|
+
const scrubberInputStyle = {
|
|
972
|
+
flex: 1,
|
|
973
|
+
};
|
|
974
|
+
const plyLabelStyle = {
|
|
975
|
+
minWidth: 56,
|
|
976
|
+
textAlign: 'center',
|
|
977
|
+
fontSize: 14,
|
|
978
|
+
};
|
|
979
|
+
function palette(theme) {
|
|
980
|
+
return {
|
|
981
|
+
text: theme === 'dark' ? '#e8e8e8' : '#1a1a1a',
|
|
982
|
+
border: theme === 'dark' ? '#3a3a3a' : '#d0d0d0',
|
|
983
|
+
surface: theme === 'dark' ? '#262626' : '#f5f5f5',
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
function navButtonStyle(colors) {
|
|
987
|
+
return {
|
|
988
|
+
padding: '4px 10px',
|
|
989
|
+
borderRadius: 6,
|
|
990
|
+
cursor: 'pointer',
|
|
991
|
+
fontSize: 14,
|
|
992
|
+
fontWeight: 600,
|
|
993
|
+
border: `1px solid ${colors.border}`,
|
|
994
|
+
background: colors.surface,
|
|
995
|
+
color: colors.text,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/** Library-default ply navigation (inline styles). */
|
|
1000
|
+
const DefaultPlyNavigation = ({ plyIndex, totalPly, canPrev, canNext, onGoFirst, onGoPrev, onGoNext, onGoLast, onGoTo, theme, showScrubber, showPlyLabel, }) => {
|
|
1001
|
+
const colors = palette(theme);
|
|
1002
|
+
const buttonStyle = navButtonStyle(colors);
|
|
1003
|
+
return (jsxRuntime.jsxs("div", { style: navRowStyle, children: [jsxRuntime.jsx("button", { type: "button", onClick: onGoFirst, disabled: !canPrev, style: buttonStyle, "aria-label": "First move", children: "\u23EE" }), jsxRuntime.jsx("button", { type: "button", onClick: onGoPrev, disabled: !canPrev, style: buttonStyle, "aria-label": "Previous move", children: "\u25C0" }), showScrubber ? (jsxRuntime.jsx("input", { type: "range", min: 0, max: totalPly, value: plyIndex, onChange: (e) => onGoTo(Number(e.target.value)), style: scrubberInputStyle, "aria-label": "Scrub through game" })) : showPlyLabel ? (jsxRuntime.jsxs("span", { style: Object.assign(Object.assign({}, plyLabelStyle), { color: colors.text }), children: [plyIndex, " / ", totalPly] })) : null, jsxRuntime.jsx("button", { type: "button", onClick: onGoNext, disabled: !canNext, style: buttonStyle, "aria-label": "Next move", children: "\u25B6" }), jsxRuntime.jsx("button", { type: "button", onClick: onGoLast, disabled: !canNext, style: buttonStyle, "aria-label": "Last move", children: "\u23ED" })] }));
|
|
1004
|
+
};
|
|
1005
|
+
const defaultRenderPlyNavigation = (props) => (jsxRuntime.jsx(DefaultPlyNavigation, Object.assign({}, props)));
|
|
1006
|
+
|
|
1007
|
+
/** True when the event target is a field where arrow keys should type, not navigate. */
|
|
1008
|
+
function isEditableKeyboardTarget(target) {
|
|
1009
|
+
if (!(target instanceof HTMLElement)) {
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
if (target.isContentEditable) {
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
const tag = target.tagName;
|
|
1016
|
+
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* Global keyboard shortcuts for browsing positions:
|
|
1021
|
+
* - ArrowLeft: previous
|
|
1022
|
+
* - ArrowRight: next
|
|
1023
|
+
* - Home: first (when {@link PositionKeyboardNavOptions.onFirst} is provided)
|
|
1024
|
+
* - End: last (when {@link PositionKeyboardNavOptions.onLast} is provided)
|
|
1025
|
+
*
|
|
1026
|
+
* Ignores keypresses while focus is in an input, textarea, select, or
|
|
1027
|
+
* contenteditable element, and ignores modified keys (Alt/Ctrl/Meta).
|
|
1028
|
+
*/
|
|
1029
|
+
function usePositionKeyboardNav({ enabled = true, canPrev, canNext, onPrev, onNext, onFirst, onLast, }) {
|
|
1030
|
+
react.useEffect(() => {
|
|
1031
|
+
if (!enabled) {
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const handleKeyDown = (event) => {
|
|
1035
|
+
if (isEditableKeyboardTarget(event.target)) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
if (event.altKey || event.ctrlKey || event.metaKey) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
switch (event.key) {
|
|
1042
|
+
case 'ArrowLeft':
|
|
1043
|
+
if (!canPrev) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
event.preventDefault();
|
|
1047
|
+
onPrev();
|
|
1048
|
+
break;
|
|
1049
|
+
case 'ArrowRight':
|
|
1050
|
+
if (!canNext) {
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
event.preventDefault();
|
|
1054
|
+
onNext();
|
|
1055
|
+
break;
|
|
1056
|
+
case 'Home':
|
|
1057
|
+
if (!onFirst || !canPrev) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
event.preventDefault();
|
|
1061
|
+
onFirst();
|
|
1062
|
+
break;
|
|
1063
|
+
case 'End':
|
|
1064
|
+
if (!onLast || !canNext) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
event.preventDefault();
|
|
1068
|
+
onLast();
|
|
1069
|
+
break;
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
1073
|
+
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
1074
|
+
}, [enabled, canPrev, canNext, onPrev, onNext, onFirst, onLast]);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Step through a fixed move list. Omit {@link PlyNavigationProps.renderPlyNavigation}
|
|
1079
|
+
* for the default inline-styled UI, or pass a custom renderer (e.g. MUI controls).
|
|
1080
|
+
*/
|
|
1081
|
+
const PlyNavigation = ({ plyIndex, totalPly, canPrev, canNext, onGoFirst, onGoPrev, onGoNext, onGoLast, onGoTo, theme = 'dark', keyboardNav = true, showScrubber = true, showPlyLabel, renderPlyNavigation = defaultRenderPlyNavigation, }) => {
|
|
1082
|
+
usePositionKeyboardNav({
|
|
1083
|
+
enabled: keyboardNav,
|
|
1084
|
+
canPrev,
|
|
1085
|
+
canNext,
|
|
1086
|
+
onPrev: onGoPrev,
|
|
1087
|
+
onNext: onGoNext,
|
|
1088
|
+
onFirst: onGoFirst,
|
|
1089
|
+
onLast: onGoLast,
|
|
1090
|
+
});
|
|
1091
|
+
return renderPlyNavigation({
|
|
1092
|
+
plyIndex,
|
|
1093
|
+
totalPly,
|
|
1094
|
+
canPrev,
|
|
1095
|
+
canNext,
|
|
1096
|
+
onGoFirst,
|
|
1097
|
+
onGoPrev,
|
|
1098
|
+
onGoNext,
|
|
1099
|
+
onGoLast,
|
|
1100
|
+
onGoTo,
|
|
1101
|
+
theme,
|
|
1102
|
+
showScrubber,
|
|
1103
|
+
showPlyLabel: showPlyLabel !== null && showPlyLabel !== void 0 ? showPlyLabel : !showScrubber,
|
|
1104
|
+
});
|
|
1105
|
+
};
|
|
1106
|
+
|
|
705
1107
|
/** Draggable analysis board (no surrounding layout chrome). */
|
|
706
1108
|
const AnalysisChessboardView = ({ model }) => {
|
|
707
1109
|
var _a;
|
|
708
|
-
return (jsxRuntime.jsx(
|
|
1110
|
+
return (jsxRuntime.jsx(ChessboardDnDProvider, { children: jsxRuntime.jsx(HighlightChessboard, { checkSquare: (_a = model.checkSquare) !== null && _a !== void 0 ? _a : '', hintSquare: null, incorrectMoveSquare: null, position: model.fen, boardOrientation: model.boardOrientation, boardWidth: model.boardWidth, arePiecesDraggable: true, onPieceDrop: model.onPieceDrop, promotionDialogVariant: "modal", customSquareStyles: model.lastMove
|
|
709
1111
|
? getLastMoveSquareStyles(model.lastMove.from, model.lastMove.to, model.theme)
|
|
710
1112
|
: {} }) }));
|
|
711
1113
|
};
|
|
@@ -1055,12 +1457,28 @@ const useAnalysisBoardModel = ({ analysisContext, onClose, theme, boardWidth, en
|
|
|
1055
1457
|
* No layout divs — use {@link renderMain} (e.g. `AnalysisBoardLayout` from `analysis/defaults` or a host layout).
|
|
1056
1458
|
*/
|
|
1057
1459
|
const AnalysisBoardCore = (_a) => {
|
|
1058
|
-
var { renderContainer, renderMain, renderSidebar, renderEngineEvaluation } = _a, modelArgs = __rest(_a, ["renderContainer", "renderMain", "renderSidebar", "renderEngineEvaluation"]);
|
|
1460
|
+
var { renderContainer, renderMain, renderSidebar, renderEngineEvaluation, keyboardNav = true } = _a, modelArgs = __rest(_a, ["renderContainer", "renderMain", "renderSidebar", "renderEngineEvaluation", "keyboardNav"]);
|
|
1059
1461
|
const model = useAnalysisBoardModel(modelArgs);
|
|
1060
|
-
return (jsxRuntime.jsx(AnalysisBoardCoreView, { model: model, renderContainer: renderContainer, renderMain: renderMain, renderSidebar: renderSidebar, renderEngineEvaluation: renderEngineEvaluation }));
|
|
1462
|
+
return (jsxRuntime.jsx(AnalysisBoardCoreView, { model: model, keyboardNav: keyboardNav, renderContainer: renderContainer, renderMain: renderMain, renderSidebar: renderSidebar, renderEngineEvaluation: renderEngineEvaluation }));
|
|
1061
1463
|
};
|
|
1062
1464
|
/** Pure composition (no layout styles) for testing and reuse. */
|
|
1063
|
-
const AnalysisBoardCoreView = ({ model, renderContainer, renderMain, renderSidebar, renderEngineEvaluation, }) => {
|
|
1465
|
+
const AnalysisBoardCoreView = ({ model, keyboardNav, renderContainer, renderMain, renderSidebar, renderEngineEvaluation, }) => {
|
|
1466
|
+
const { ply, maxPly, onSelectPly } = model;
|
|
1467
|
+
const canPrev = ply > 0;
|
|
1468
|
+
const canNext = ply < maxPly;
|
|
1469
|
+
const goFirst = react.useCallback(() => onSelectPly(0), [onSelectPly]);
|
|
1470
|
+
const goPrev = react.useCallback(() => onSelectPly(ply - 1), [onSelectPly, ply]);
|
|
1471
|
+
const goNext = react.useCallback(() => onSelectPly(ply + 1), [onSelectPly, ply]);
|
|
1472
|
+
const goLast = react.useCallback(() => onSelectPly(maxPly), [maxPly, onSelectPly]);
|
|
1473
|
+
usePositionKeyboardNav({
|
|
1474
|
+
enabled: keyboardNav,
|
|
1475
|
+
canPrev,
|
|
1476
|
+
canNext,
|
|
1477
|
+
onPrev: goPrev,
|
|
1478
|
+
onNext: goNext,
|
|
1479
|
+
onFirst: goFirst,
|
|
1480
|
+
onLast: goLast,
|
|
1481
|
+
});
|
|
1064
1482
|
const board = jsxRuntime.jsx(AnalysisChessboardView, { model: model });
|
|
1065
1483
|
const engineEvaluationPanel = model.engineEnabled
|
|
1066
1484
|
? renderEngineEvaluation({
|
|
@@ -1328,66 +1746,6 @@ const closeButtonStyle = {
|
|
|
1328
1746
|
fontSize: 14,
|
|
1329
1747
|
};
|
|
1330
1748
|
|
|
1331
|
-
const navRowStyle = {
|
|
1332
|
-
display: 'flex',
|
|
1333
|
-
alignItems: 'center',
|
|
1334
|
-
gap: 6,
|
|
1335
|
-
};
|
|
1336
|
-
const scrubberInputStyle = {
|
|
1337
|
-
flex: 1,
|
|
1338
|
-
};
|
|
1339
|
-
const plyLabelStyle = {
|
|
1340
|
-
minWidth: 56,
|
|
1341
|
-
textAlign: 'center',
|
|
1342
|
-
fontSize: 14,
|
|
1343
|
-
};
|
|
1344
|
-
function palette(theme) {
|
|
1345
|
-
return {
|
|
1346
|
-
text: theme === 'dark' ? '#e8e8e8' : '#1a1a1a',
|
|
1347
|
-
border: theme === 'dark' ? '#3a3a3a' : '#d0d0d0',
|
|
1348
|
-
surface: theme === 'dark' ? '#262626' : '#f5f5f5',
|
|
1349
|
-
};
|
|
1350
|
-
}
|
|
1351
|
-
function navButtonStyle(colors) {
|
|
1352
|
-
return {
|
|
1353
|
-
padding: '4px 10px',
|
|
1354
|
-
borderRadius: 6,
|
|
1355
|
-
cursor: 'pointer',
|
|
1356
|
-
fontSize: 14,
|
|
1357
|
-
fontWeight: 600,
|
|
1358
|
-
border: `1px solid ${colors.border}`,
|
|
1359
|
-
background: colors.surface,
|
|
1360
|
-
color: colors.text,
|
|
1361
|
-
};
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
/** Library-default ply navigation (inline styles). */
|
|
1365
|
-
const DefaultPlyNavigation = ({ plyIndex, totalPly, canPrev, canNext, onGoFirst, onGoPrev, onGoNext, onGoLast, onGoTo, theme, showScrubber, showPlyLabel, }) => {
|
|
1366
|
-
const colors = palette(theme);
|
|
1367
|
-
const buttonStyle = navButtonStyle(colors);
|
|
1368
|
-
return (jsxRuntime.jsxs("div", { style: navRowStyle, children: [jsxRuntime.jsx("button", { type: "button", onClick: onGoFirst, disabled: !canPrev, style: buttonStyle, "aria-label": "First move", children: "\u23EE" }), jsxRuntime.jsx("button", { type: "button", onClick: onGoPrev, disabled: !canPrev, style: buttonStyle, "aria-label": "Previous move", children: "\u25C0" }), showScrubber ? (jsxRuntime.jsx("input", { type: "range", min: 0, max: totalPly, value: plyIndex, onChange: (e) => onGoTo(Number(e.target.value)), style: scrubberInputStyle, "aria-label": "Scrub through game" })) : showPlyLabel ? (jsxRuntime.jsxs("span", { style: Object.assign(Object.assign({}, plyLabelStyle), { color: colors.text }), children: [plyIndex, " / ", totalPly] })) : null, jsxRuntime.jsx("button", { type: "button", onClick: onGoNext, disabled: !canNext, style: buttonStyle, "aria-label": "Next move", children: "\u25B6" }), jsxRuntime.jsx("button", { type: "button", onClick: onGoLast, disabled: !canNext, style: buttonStyle, "aria-label": "Last move", children: "\u23ED" })] }));
|
|
1369
|
-
};
|
|
1370
|
-
const defaultRenderPlyNavigation = (props) => (jsxRuntime.jsx(DefaultPlyNavigation, Object.assign({}, props)));
|
|
1371
|
-
|
|
1372
|
-
/**
|
|
1373
|
-
* Step through a fixed move list. Omit {@link PlyNavigationProps.renderPlyNavigation}
|
|
1374
|
-
* for the default inline-styled UI, or pass a custom renderer (e.g. MUI controls).
|
|
1375
|
-
*/
|
|
1376
|
-
const PlyNavigation = ({ plyIndex, totalPly, canPrev, canNext, onGoFirst, onGoPrev, onGoNext, onGoLast, onGoTo, theme = 'dark', showScrubber = true, showPlyLabel, renderPlyNavigation = defaultRenderPlyNavigation, }) => renderPlyNavigation({
|
|
1377
|
-
plyIndex,
|
|
1378
|
-
totalPly,
|
|
1379
|
-
canPrev,
|
|
1380
|
-
canNext,
|
|
1381
|
-
onGoFirst,
|
|
1382
|
-
onGoPrev,
|
|
1383
|
-
onGoNext,
|
|
1384
|
-
onGoLast,
|
|
1385
|
-
onGoTo,
|
|
1386
|
-
theme,
|
|
1387
|
-
showScrubber,
|
|
1388
|
-
showPlyLabel: showPlyLabel !== null && showPlyLabel !== void 0 ? showPlyLabel : !showScrubber,
|
|
1389
|
-
});
|
|
1390
|
-
|
|
1391
1749
|
const DefaultAnalysisSidebar = ({ historyRows, isHistoryRowSelected, onSelectHistoryRow, ply, maxPly, onSelectPly, theme, engineEvaluationPanel, }) => {
|
|
1392
1750
|
const rowBands = createSidebarRowBandCounters();
|
|
1393
1751
|
const baseChipStyle = {
|
|
@@ -1395,7 +1753,7 @@ const DefaultAnalysisSidebar = ({ historyRows, isHistoryRowSelected, onSelectHis
|
|
|
1395
1753
|
padding: '4px 8px',
|
|
1396
1754
|
borderRadius: 4,
|
|
1397
1755
|
};
|
|
1398
|
-
return (jsxRuntime.jsxs("div", { style: sidebarStyle, children: [jsxRuntime.jsxs("div", { style: navBlockStyle, children: [jsxRuntime.jsx(PlyNavigation, { plyIndex: ply, totalPly: maxPly, canPrev: ply > 0, canNext: ply < maxPly, onGoFirst: () => onSelectPly(0), onGoPrev: () => onSelectPly(ply - 1), onGoNext: () => onSelectPly(ply + 1), onGoLast: () => onSelectPly(maxPly), onGoTo: onSelectPly, theme: theme, showScrubber: false }), jsxRuntime.jsx("p", { style: sectionTitleStyle, children: "Move history" })] }), jsxRuntime.jsxs("div", { style: contentRowStyle, children: [jsxRuntime.jsx("ol", { style: moveListStyle, children: historyRows.length === 0 ? (jsxRuntime.jsx("li", { style: emptyRowStyle, children: "No moves played yet." })) : (historyRows.map((row) => {
|
|
1756
|
+
return (jsxRuntime.jsxs("div", { style: sidebarStyle, children: [jsxRuntime.jsxs("div", { style: navBlockStyle, children: [jsxRuntime.jsx(PlyNavigation, { plyIndex: ply, totalPly: maxPly, canPrev: ply > 0, canNext: ply < maxPly, onGoFirst: () => onSelectPly(0), onGoPrev: () => onSelectPly(ply - 1), onGoNext: () => onSelectPly(ply + 1), onGoLast: () => onSelectPly(maxPly), onGoTo: onSelectPly, theme: theme, keyboardNav: false, showScrubber: false }), jsxRuntime.jsx("p", { style: sectionTitleStyle, children: "Move history" })] }), jsxRuntime.jsxs("div", { style: contentRowStyle, children: [jsxRuntime.jsx("ol", { style: moveListStyle, "aria-label": "Move history", children: historyRows.length === 0 ? (jsxRuntime.jsx("li", { style: emptyRowStyle, children: "No moves played yet." })) : (historyRows.map((row) => {
|
|
1399
1757
|
const isSelected = isHistoryRowSelected(row);
|
|
1400
1758
|
const isVariation = row.kind === 'variation';
|
|
1401
1759
|
const backgroundColor = isSelected
|
|
@@ -1494,54 +1852,456 @@ const AnalysisBoard = (_a) => {
|
|
|
1494
1852
|
: () => null) })));
|
|
1495
1853
|
};
|
|
1496
1854
|
|
|
1855
|
+
/** Resolve a board drag into a legal UCI string, or null when illegal. */
|
|
1856
|
+
function uciFromDrop(fen, sourceSquare, targetSquare, piece) {
|
|
1857
|
+
var _a, _b;
|
|
1858
|
+
const chess = new chess_js.Chess(fen);
|
|
1859
|
+
const pieceType = (_a = piece[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
1860
|
+
const legal = chess
|
|
1861
|
+
.moves({ square: sourceSquare, verbose: true })
|
|
1862
|
+
.find((move) => move.to === targetSquare &&
|
|
1863
|
+
(!move.promotion || move.promotion === pieceType));
|
|
1864
|
+
if (!legal)
|
|
1865
|
+
return null;
|
|
1866
|
+
return `${legal.from}${legal.to}${(_b = legal.promotion) !== null && _b !== void 0 ? _b : ''}`;
|
|
1867
|
+
}
|
|
1868
|
+
function matchesExpectedUci(uci, expectedUci) {
|
|
1869
|
+
return uci.toLowerCase() === expectedUci.toLowerCase();
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/**
|
|
1873
|
+
* Evaluate a training drop without mutating board position.
|
|
1874
|
+
* Returns `false` for incorrect attempts so react-chessboard snaps the piece back.
|
|
1875
|
+
*/
|
|
1876
|
+
function evaluateExpectedMoveDrop(fen, sourceSquare, targetSquare, piece, expectedUci, enabled) {
|
|
1877
|
+
if (!enabled || !expectedUci) {
|
|
1878
|
+
return { kind: 'ignored' };
|
|
1879
|
+
}
|
|
1880
|
+
const uci = uciFromDrop(fen, sourceSquare, targetSquare, piece);
|
|
1881
|
+
if (!uci) {
|
|
1882
|
+
return { kind: 'illegal' };
|
|
1883
|
+
}
|
|
1884
|
+
if (matchesExpectedUci(uci, expectedUci)) {
|
|
1885
|
+
return { kind: 'correct', uci };
|
|
1886
|
+
}
|
|
1887
|
+
return { kind: 'incorrect', uci };
|
|
1888
|
+
}
|
|
1889
|
+
function createExpectedMoveDropHandler({ fen, expectedUci, enabled, onCorrect, onIncorrect, }) {
|
|
1890
|
+
return (sourceSquare, targetSquare, piece) => {
|
|
1891
|
+
const result = evaluateExpectedMoveDrop(fen, sourceSquare, targetSquare, piece, expectedUci, enabled);
|
|
1892
|
+
switch (result.kind) {
|
|
1893
|
+
case 'correct':
|
|
1894
|
+
onCorrect(result.uci);
|
|
1895
|
+
return true;
|
|
1896
|
+
case 'incorrect':
|
|
1897
|
+
onIncorrect(result.uci);
|
|
1898
|
+
return false;
|
|
1899
|
+
default:
|
|
1900
|
+
return false;
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
/**
|
|
1906
|
+
* Bump the revision counter to force a controlled chessboard re-render after a
|
|
1907
|
+
* rejected drop. Pair with returning `false` from `onPieceDrop` so the board
|
|
1908
|
+
* snaps back without changing the controlled `position` FEN.
|
|
1909
|
+
*/
|
|
1910
|
+
function useBoardRevision() {
|
|
1911
|
+
const [revision, setRevision] = react.useState(0);
|
|
1912
|
+
const bumpRevision = react.useCallback(() => {
|
|
1913
|
+
setRevision((current) => current + 1);
|
|
1914
|
+
}, []);
|
|
1915
|
+
return { revision, bumpRevision };
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/** Minimum eval loss (pawns) from the wrong move before showing a refutation. */
|
|
1919
|
+
const REFUTATION_EVAL_GAP_PAWNS = 0.5;
|
|
1920
|
+
const REFUTATION_EVAL_GAP_CP = REFUTATION_EVAL_GAP_PAWNS * 100;
|
|
1921
|
+
const refutationEngineOptions = {
|
|
1922
|
+
depth: 14,
|
|
1923
|
+
multiPv: 1,
|
|
1924
|
+
};
|
|
1925
|
+
function fenAfterUci(fen, uci) {
|
|
1926
|
+
const chess = new chess_js.Chess(fen);
|
|
1927
|
+
if (!applyUciMove(chess, uci)) {
|
|
1928
|
+
return null;
|
|
1929
|
+
}
|
|
1930
|
+
return chess.fen();
|
|
1931
|
+
}
|
|
1932
|
+
/** Centipawn score from side to move, comparable across sibling positions. */
|
|
1933
|
+
function lineEvalCpForGap(line) {
|
|
1934
|
+
if (!line) {
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
if (line.mate !== null) {
|
|
1938
|
+
return line.mate > 0 ? 10000 - line.mate : -1e4 + line.mate;
|
|
1939
|
+
}
|
|
1940
|
+
return line.centipawns;
|
|
1941
|
+
}
|
|
1942
|
+
/** How much better the opponent's eval is after the wrong move vs the correct one. */
|
|
1943
|
+
function refutationEvalGapCp(evalAfterWrong, evalAfterCorrect) {
|
|
1944
|
+
const wrongCp = lineEvalCpForGap(evalAfterWrong.lines[0]);
|
|
1945
|
+
const correctCp = lineEvalCpForGap(evalAfterCorrect.lines[0]);
|
|
1946
|
+
if (wrongCp === null || correctCp === null) {
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
1949
|
+
return wrongCp - correctCp;
|
|
1950
|
+
}
|
|
1951
|
+
function refutationFromEvaluation(fenAfterWrong, evaluation, evalGapCp, evalGapApplies, evalGapLoading) {
|
|
1952
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
1953
|
+
const loading = evaluation.status === 'loading' ||
|
|
1954
|
+
evaluation.status === 'analyzing' ||
|
|
1955
|
+
evalGapLoading;
|
|
1956
|
+
if (evaluation.status === 'error') {
|
|
1957
|
+
return {
|
|
1958
|
+
refutationUci: null,
|
|
1959
|
+
refutationSan: null,
|
|
1960
|
+
refutationLine: null,
|
|
1961
|
+
loading: false,
|
|
1962
|
+
error: (_a = evaluation.error) !== null && _a !== void 0 ? _a : 'Engine unavailable',
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
const meetsThreshold = !evalGapApplies ||
|
|
1966
|
+
(evalGapCp !== null && evalGapCp >= REFUTATION_EVAL_GAP_CP);
|
|
1967
|
+
if (!meetsThreshold) {
|
|
1968
|
+
return {
|
|
1969
|
+
refutationUci: null,
|
|
1970
|
+
refutationSan: null,
|
|
1971
|
+
refutationLine: null,
|
|
1972
|
+
loading,
|
|
1973
|
+
error: null,
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
const refutationUci = (_d = (_c = (_b = evaluation.lines[0]) === null || _b === void 0 ? void 0 : _b.pv) === null || _c === void 0 ? void 0 : _c[0]) !== null && _d !== void 0 ? _d : null;
|
|
1977
|
+
const refutationSan = refutationUci
|
|
1978
|
+
? ((_e = uciPvToSan(fenAfterWrong, [refutationUci])[0]) !== null && _e !== void 0 ? _e : refutationUci)
|
|
1979
|
+
: null;
|
|
1980
|
+
const refutationLine = ((_g = (_f = evaluation.lines[0]) === null || _f === void 0 ? void 0 : _f.pv) === null || _g === void 0 ? void 0 : _g.length)
|
|
1981
|
+
? formatPvPreview(fenAfterWrong, evaluation.lines[0].pv, 4)
|
|
1982
|
+
: null;
|
|
1983
|
+
return {
|
|
1984
|
+
refutationUci,
|
|
1985
|
+
refutationSan,
|
|
1986
|
+
refutationLine,
|
|
1987
|
+
loading,
|
|
1988
|
+
error: null,
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
const MISS_WRONG_PAUSE_MS = 450;
|
|
1993
|
+
const MISS_REFUTATION_PAUSE_MS = 900;
|
|
1994
|
+
const MISS_REFUTATION_MAX_WAIT_MS = 4000;
|
|
1995
|
+
const MISS_MOVE_ANIMATION_MS = 220;
|
|
1996
|
+
function moveArrow(uci, color) {
|
|
1997
|
+
if (!uci || uci.length < 4) {
|
|
1998
|
+
return [];
|
|
1999
|
+
}
|
|
2000
|
+
return [[uci.slice(0, 2), uci.slice(2, 4), color]];
|
|
2001
|
+
}
|
|
2002
|
+
function expectedMoveArrow(expectedUci, color) {
|
|
2003
|
+
return moveArrow(expectedUci, color);
|
|
2004
|
+
}
|
|
2005
|
+
function getMissDisplay(sequence, expectedUci, refutationUci, answerArrowColor) {
|
|
2006
|
+
var _a;
|
|
2007
|
+
if (!sequence) {
|
|
2008
|
+
return {
|
|
2009
|
+
fen: null,
|
|
2010
|
+
arrows: [],
|
|
2011
|
+
animating: false,
|
|
2012
|
+
};
|
|
2013
|
+
}
|
|
2014
|
+
const { setupFen, attemptedUci, phase } = sequence;
|
|
2015
|
+
const fenAfterWrong = fenAfterUci(setupFen, attemptedUci);
|
|
2016
|
+
switch (phase) {
|
|
2017
|
+
case 'wrong':
|
|
2018
|
+
return {
|
|
2019
|
+
fen: fenAfterWrong !== null && fenAfterWrong !== void 0 ? fenAfterWrong : setupFen,
|
|
2020
|
+
arrows: [],
|
|
2021
|
+
animating: false,
|
|
2022
|
+
};
|
|
2023
|
+
case 'refutation': {
|
|
2024
|
+
const fenAfterRefutation = fenAfterWrong && refutationUci
|
|
2025
|
+
? fenAfterUci(fenAfterWrong, refutationUci)
|
|
2026
|
+
: null;
|
|
2027
|
+
return {
|
|
2028
|
+
fen: (_a = fenAfterRefutation !== null && fenAfterRefutation !== void 0 ? fenAfterRefutation : fenAfterWrong) !== null && _a !== void 0 ? _a : setupFen,
|
|
2029
|
+
arrows: [],
|
|
2030
|
+
animating: Boolean(fenAfterRefutation),
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
case 'retry':
|
|
2034
|
+
return {
|
|
2035
|
+
fen: setupFen,
|
|
2036
|
+
arrows: [],
|
|
2037
|
+
animating: false,
|
|
2038
|
+
};
|
|
2039
|
+
case 'answer':
|
|
2040
|
+
return {
|
|
2041
|
+
fen: setupFen,
|
|
2042
|
+
arrows: expectedMoveArrow(expectedUci, answerArrowColor),
|
|
2043
|
+
animating: false,
|
|
2044
|
+
};
|
|
2045
|
+
default:
|
|
2046
|
+
return {
|
|
2047
|
+
fen: setupFen,
|
|
2048
|
+
arrows: [],
|
|
2049
|
+
animating: false,
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
function useMissRefutation(setupFen, attemptedUci, expectedUci, enabled, engineOptions) {
|
|
2055
|
+
const fenAfterWrong = react.useMemo(() => {
|
|
2056
|
+
if (!setupFen || !attemptedUci) {
|
|
2057
|
+
return null;
|
|
2058
|
+
}
|
|
2059
|
+
return fenAfterUci(setupFen, attemptedUci);
|
|
2060
|
+
}, [setupFen, attemptedUci]);
|
|
2061
|
+
const fenAfterCorrect = react.useMemo(() => {
|
|
2062
|
+
if (!setupFen || !expectedUci) {
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
return fenAfterUci(setupFen, expectedUci);
|
|
2066
|
+
}, [setupFen, expectedUci]);
|
|
2067
|
+
const wrongEvaluation = useAnalysisEngine(fenAfterWrong !== null && fenAfterWrong !== void 0 ? fenAfterWrong : '', Object.assign(Object.assign({}, engineOptions), { enabled: enabled && Boolean(fenAfterWrong), shared: false }));
|
|
2068
|
+
const correctEvaluation = useAnalysisEngine(fenAfterCorrect !== null && fenAfterCorrect !== void 0 ? fenAfterCorrect : '', Object.assign(Object.assign({}, engineOptions), { enabled: enabled && Boolean(fenAfterCorrect), shared: false }));
|
|
2069
|
+
return react.useMemo(() => {
|
|
2070
|
+
if (!fenAfterWrong) {
|
|
2071
|
+
return {
|
|
2072
|
+
fenAfterWrong: null,
|
|
2073
|
+
refutationUci: null,
|
|
2074
|
+
refutationSan: null,
|
|
2075
|
+
refutationLine: null,
|
|
2076
|
+
loading: false,
|
|
2077
|
+
error: null,
|
|
2078
|
+
};
|
|
2079
|
+
}
|
|
2080
|
+
const evalGapApplies = Boolean(fenAfterCorrect);
|
|
2081
|
+
const evalGapCp = evalGapApplies
|
|
2082
|
+
? refutationEvalGapCp(wrongEvaluation, correctEvaluation)
|
|
2083
|
+
: null;
|
|
2084
|
+
const evalGapLoading = evalGapApplies &&
|
|
2085
|
+
evalGapCp === null &&
|
|
2086
|
+
wrongEvaluation.status !== 'error' &&
|
|
2087
|
+
correctEvaluation.status !== 'error' &&
|
|
2088
|
+
(correctEvaluation.status === 'loading' ||
|
|
2089
|
+
correctEvaluation.status === 'analyzing' ||
|
|
2090
|
+
wrongEvaluation.status === 'loading' ||
|
|
2091
|
+
wrongEvaluation.status === 'analyzing');
|
|
2092
|
+
return Object.assign({ fenAfterWrong }, refutationFromEvaluation(fenAfterWrong, wrongEvaluation, evalGapCp, evalGapApplies, evalGapLoading));
|
|
2093
|
+
}, [fenAfterCorrect, fenAfterWrong, correctEvaluation, wrongEvaluation]);
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
function useMissSequence(feedback, expectedUci, engineOptions, answerArrowColor, autoShowWrongMoves) {
|
|
2097
|
+
var _a, _b;
|
|
2098
|
+
const [sequence, setSequence] = react.useState(null);
|
|
2099
|
+
const refutation = useMissRefutation((_a = sequence === null || sequence === void 0 ? void 0 : sequence.setupFen) !== null && _a !== void 0 ? _a : null, (_b = sequence === null || sequence === void 0 ? void 0 : sequence.attemptedUci) !== null && _b !== void 0 ? _b : null, expectedUci, sequence != null, engineOptions);
|
|
2100
|
+
const startSequence = react.useCallback((setupFen, attemptedUci) => {
|
|
2101
|
+
setSequence({
|
|
2102
|
+
setupFen,
|
|
2103
|
+
attemptedUci,
|
|
2104
|
+
phase: autoShowWrongMoves ? 'wrong' : 'retry',
|
|
2105
|
+
});
|
|
2106
|
+
}, [autoShowWrongMoves]);
|
|
2107
|
+
const clearSequence = react.useCallback(() => {
|
|
2108
|
+
setSequence(null);
|
|
2109
|
+
}, []);
|
|
2110
|
+
const prevFeedbackRef = react.useRef(feedback);
|
|
2111
|
+
react.useEffect(() => {
|
|
2112
|
+
const prevFeedback = prevFeedbackRef.current;
|
|
2113
|
+
prevFeedbackRef.current = feedback;
|
|
2114
|
+
if (prevFeedback === 'incorrect' && feedback !== 'incorrect') {
|
|
2115
|
+
setSequence(null);
|
|
2116
|
+
}
|
|
2117
|
+
}, [feedback]);
|
|
2118
|
+
react.useEffect(() => {
|
|
2119
|
+
if (!sequence || sequence.phase !== 'wrong' || !autoShowWrongMoves) {
|
|
2120
|
+
return undefined;
|
|
2121
|
+
}
|
|
2122
|
+
if (refutation.loading) {
|
|
2123
|
+
const maxWait = window.setTimeout(() => {
|
|
2124
|
+
setSequence((current) => (current === null || current === void 0 ? void 0 : current.phase) === 'wrong' ? Object.assign(Object.assign({}, current), { phase: 'answer' }) : current);
|
|
2125
|
+
}, MISS_REFUTATION_MAX_WAIT_MS);
|
|
2126
|
+
return () => window.clearTimeout(maxWait);
|
|
2127
|
+
}
|
|
2128
|
+
const delay = window.setTimeout(() => {
|
|
2129
|
+
setSequence((current) => {
|
|
2130
|
+
if (!current || current.phase !== 'wrong') {
|
|
2131
|
+
return current;
|
|
2132
|
+
}
|
|
2133
|
+
return Object.assign(Object.assign({}, current), { phase: refutation.refutationUci ? 'refutation' : 'answer' });
|
|
2134
|
+
});
|
|
2135
|
+
}, MISS_WRONG_PAUSE_MS);
|
|
2136
|
+
return () => window.clearTimeout(delay);
|
|
2137
|
+
}, [
|
|
2138
|
+
autoShowWrongMoves,
|
|
2139
|
+
refutation.loading,
|
|
2140
|
+
refutation.refutationUci,
|
|
2141
|
+
sequence,
|
|
2142
|
+
]);
|
|
2143
|
+
react.useEffect(() => {
|
|
2144
|
+
if (!sequence || sequence.phase !== 'refutation') {
|
|
2145
|
+
return undefined;
|
|
2146
|
+
}
|
|
2147
|
+
const delay = window.setTimeout(() => {
|
|
2148
|
+
setSequence((current) => (current === null || current === void 0 ? void 0 : current.phase) === 'refutation'
|
|
2149
|
+
? Object.assign(Object.assign({}, current), { phase: 'answer' }) : current);
|
|
2150
|
+
}, MISS_REFUTATION_PAUSE_MS);
|
|
2151
|
+
return () => window.clearTimeout(delay);
|
|
2152
|
+
}, [sequence]);
|
|
2153
|
+
const display = react.useMemo(() => getMissDisplay(sequence, expectedUci, refutation.refutationUci, answerArrowColor), [
|
|
2154
|
+
answerArrowColor,
|
|
2155
|
+
expectedUci,
|
|
2156
|
+
refutation.refutationUci,
|
|
2157
|
+
sequence,
|
|
2158
|
+
]);
|
|
2159
|
+
return {
|
|
2160
|
+
sequence,
|
|
2161
|
+
refutation,
|
|
2162
|
+
display,
|
|
2163
|
+
startSequence,
|
|
2164
|
+
clearSequence,
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function useMissBoard({ feedback, expectedUci, positionFen, answerArrowColor, autoShowWrongMoves = true, engineOptions, }) {
|
|
2169
|
+
var _a;
|
|
2170
|
+
const refutationEngine = react.useMemo(() => (Object.assign(Object.assign({}, refutationEngineOptions), engineOptions)), [engineOptions]);
|
|
2171
|
+
const missSequence = useMissSequence(feedback, expectedUci, refutationEngine, answerArrowColor, autoShowWrongMoves);
|
|
2172
|
+
const customArrows = react.useMemo(() => {
|
|
2173
|
+
if (feedback !== 'incorrect') {
|
|
2174
|
+
return [];
|
|
2175
|
+
}
|
|
2176
|
+
if (missSequence.sequence) {
|
|
2177
|
+
return missSequence.display.arrows;
|
|
2178
|
+
}
|
|
2179
|
+
if (expectedUci) {
|
|
2180
|
+
return [
|
|
2181
|
+
[
|
|
2182
|
+
expectedUci.slice(0, 2),
|
|
2183
|
+
expectedUci.slice(2, 4),
|
|
2184
|
+
answerArrowColor,
|
|
2185
|
+
],
|
|
2186
|
+
];
|
|
2187
|
+
}
|
|
2188
|
+
return [];
|
|
2189
|
+
}, [
|
|
2190
|
+
answerArrowColor,
|
|
2191
|
+
expectedUci,
|
|
2192
|
+
feedback,
|
|
2193
|
+
missSequence.display.arrows,
|
|
2194
|
+
missSequence.sequence,
|
|
2195
|
+
]);
|
|
2196
|
+
const boardPosition = (_a = missSequence.display.fen) !== null && _a !== void 0 ? _a : positionFen;
|
|
2197
|
+
const wrapDropHandler = react.useCallback((onDrop, { enabled, dropFen = boardPosition, expectedMoveUci = expectedUci, }) => (source, target, piece) => {
|
|
2198
|
+
if (enabled && expectedMoveUci) {
|
|
2199
|
+
const uci = uciFromDrop(dropFen, source, target, piece);
|
|
2200
|
+
if (uci && uci.toLowerCase() !== expectedMoveUci.toLowerCase()) {
|
|
2201
|
+
missSequence.startSequence(dropFen, uci);
|
|
2202
|
+
}
|
|
2203
|
+
else if (uci &&
|
|
2204
|
+
uci.toLowerCase() === expectedMoveUci.toLowerCase()) {
|
|
2205
|
+
missSequence.clearSequence();
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return onDrop(source, target, piece);
|
|
2209
|
+
}, [
|
|
2210
|
+
boardPosition,
|
|
2211
|
+
expectedUci,
|
|
2212
|
+
missSequence.clearSequence,
|
|
2213
|
+
missSequence.startSequence,
|
|
2214
|
+
]);
|
|
2215
|
+
return {
|
|
2216
|
+
missSequence,
|
|
2217
|
+
refutation: missSequence.refutation,
|
|
2218
|
+
customArrows,
|
|
2219
|
+
boardPosition,
|
|
2220
|
+
boardAnimating: missSequence.display.animating,
|
|
2221
|
+
wrapDropHandler,
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
|
|
1497
2225
|
exports.AnalysisBoard = AnalysisBoard;
|
|
1498
2226
|
exports.AnalysisBoardCore = AnalysisBoardCore;
|
|
1499
2227
|
exports.AnalysisBoardCoreView = AnalysisBoardCoreView;
|
|
1500
2228
|
exports.AnalysisBoardLayout = AnalysisBoardLayout;
|
|
1501
2229
|
exports.AnalysisChessboardView = AnalysisChessboardView;
|
|
2230
|
+
exports.AnalysisEngineProvider = AnalysisEngineProvider;
|
|
1502
2231
|
exports.AnalysisErrorBoundary = AnalysisErrorBoundary;
|
|
1503
2232
|
exports.AnalysisPosition = AnalysisPosition;
|
|
2233
|
+
exports.BOARD_THEMES = BOARD_THEMES;
|
|
2234
|
+
exports.BOARD_THEME_IDS = BOARD_THEME_IDS;
|
|
2235
|
+
exports.ChessboardDnDProvider = ChessboardDnDProvider;
|
|
1504
2236
|
exports.ChessboardThemeContext = ChessboardThemeContext;
|
|
1505
2237
|
exports.DEFAULT_ANALYSIS_LAYOUT = DEFAULT_ANALYSIS_LAYOUT;
|
|
2238
|
+
exports.DEFAULT_BOARD_THEME = DEFAULT_BOARD_THEME;
|
|
1506
2239
|
exports.DEFAULT_STOCKFISH_SCRIPT_URL = DEFAULT_STOCKFISH_SCRIPT_URL;
|
|
1507
2240
|
exports.DefaultAnalysisContainer = DefaultAnalysisContainer;
|
|
1508
2241
|
exports.DefaultAnalysisSidebar = DefaultAnalysisSidebar;
|
|
1509
2242
|
exports.DefaultPlyNavigation = DefaultPlyNavigation;
|
|
1510
2243
|
exports.EngineEvaluationPanel = EngineEvaluationPanel;
|
|
1511
2244
|
exports.HighlightChessboard = HighlightChessboard;
|
|
2245
|
+
exports.MISS_MOVE_ANIMATION_MS = MISS_MOVE_ANIMATION_MS;
|
|
2246
|
+
exports.MISS_REFUTATION_MAX_WAIT_MS = MISS_REFUTATION_MAX_WAIT_MS;
|
|
2247
|
+
exports.MISS_REFUTATION_PAUSE_MS = MISS_REFUTATION_PAUSE_MS;
|
|
2248
|
+
exports.MISS_WRONG_PAUSE_MS = MISS_WRONG_PAUSE_MS;
|
|
1512
2249
|
exports.PlyNavigation = PlyNavigation;
|
|
2250
|
+
exports.REFUTATION_EVAL_GAP_CP = REFUTATION_EVAL_GAP_CP;
|
|
2251
|
+
exports.REFUTATION_EVAL_GAP_PAWNS = REFUTATION_EVAL_GAP_PAWNS;
|
|
1513
2252
|
exports.StockfishBrowserEngine = StockfishBrowserEngine;
|
|
1514
2253
|
exports.ThemeProvider = ThemeProvider;
|
|
1515
2254
|
exports.analysisBoardHighlightColors = analysisBoardHighlightColors;
|
|
1516
2255
|
exports.analysisSidebarColors = analysisSidebarColors;
|
|
1517
2256
|
exports.applyUciMove = applyUciMove;
|
|
1518
2257
|
exports.boardSquareHighlightColors = boardSquareHighlightColors;
|
|
2258
|
+
exports.boardThemeFromLegacyUiTheme = boardThemeFromLegacyUiTheme;
|
|
2259
|
+
exports.createExpectedMoveDropHandler = createExpectedMoveDropHandler;
|
|
1519
2260
|
exports.createSidebarRowBandCounters = createSidebarRowBandCounters;
|
|
1520
2261
|
exports.defaultRenderPlyNavigation = defaultRenderPlyNavigation;
|
|
1521
2262
|
exports.emptyEngineEvaluation = emptyEngineEvaluation;
|
|
2263
|
+
exports.evaluateExpectedMoveDrop = evaluateExpectedMoveDrop;
|
|
2264
|
+
exports.fenAfterUci = fenAfterUci;
|
|
1522
2265
|
exports.formatEvaluation = formatEvaluation;
|
|
1523
2266
|
exports.formatPvPreview = formatPvPreview;
|
|
1524
2267
|
exports.getAnalysisModalStyles = getAnalysisModalStyles;
|
|
2268
|
+
exports.getBoardThemeStyles = getBoardThemeStyles;
|
|
1525
2269
|
exports.getCheckSquareFromChess = getCheckSquareFromChess;
|
|
1526
2270
|
exports.getLastMoveSquareStyles = getLastMoveSquareStyles;
|
|
2271
|
+
exports.getMissDisplay = getMissDisplay;
|
|
1527
2272
|
exports.getSidebarRowBackground = getSidebarRowBackground;
|
|
1528
2273
|
exports.getStylesForTheme = getStylesForTheme;
|
|
1529
2274
|
exports.isAnalyzableFen = isAnalyzableFen;
|
|
2275
|
+
exports.isBoardThemeId = isBoardThemeId;
|
|
2276
|
+
exports.isEditableKeyboardTarget = isEditableKeyboardTarget;
|
|
2277
|
+
exports.lineEvalCpForGap = lineEvalCpForGap;
|
|
2278
|
+
exports.matchesExpectedUci = matchesExpectedUci;
|
|
1530
2279
|
exports.navButtonStyle = navButtonStyle;
|
|
1531
2280
|
exports.navRowStyle = navRowStyle;
|
|
1532
2281
|
exports.normalizeEvalForWhite = normalizeEvalForWhite;
|
|
1533
2282
|
exports.normalizePvMoves = normalizePvMoves;
|
|
2283
|
+
exports.normalizeSubscriberOptions = normalizeSubscriberOptions;
|
|
1534
2284
|
exports.parseUciInfoLine = parseUciInfoLine;
|
|
1535
2285
|
exports.parseUciMove = parseUciMove;
|
|
1536
2286
|
exports.plyLabelStyle = plyLabelStyle;
|
|
1537
2287
|
exports.plyNavigationPalette = palette;
|
|
2288
|
+
exports.refutationEngineOptions = refutationEngineOptions;
|
|
2289
|
+
exports.refutationEvalGapCp = refutationEvalGapCp;
|
|
2290
|
+
exports.refutationFromEvaluation = refutationFromEvaluation;
|
|
1538
2291
|
exports.resolveStockfishScriptUrl = resolveStockfishScriptUrl;
|
|
1539
2292
|
exports.resolveStockfishWasmUrl = resolveStockfishWasmUrl;
|
|
1540
2293
|
exports.resolveStockfishWorkerUrl = resolveStockfishWorkerUrl;
|
|
1541
2294
|
exports.scrubberInputStyle = scrubberInputStyle;
|
|
1542
2295
|
exports.splitWorkerLines = splitWorkerLines;
|
|
2296
|
+
exports.uciFromDrop = uciFromDrop;
|
|
1543
2297
|
exports.uciPvToSan = uciPvToSan;
|
|
1544
2298
|
exports.useAnalysisBoardModel = useAnalysisBoardModel;
|
|
1545
2299
|
exports.useAnalysisEngine = useAnalysisEngine;
|
|
2300
|
+
exports.useAnalysisEngineContext = useAnalysisEngineContext;
|
|
2301
|
+
exports.useBoardRevision = useBoardRevision;
|
|
1546
2302
|
exports.useChessboardTheme = useChessboardTheme;
|
|
2303
|
+
exports.useMissBoard = useMissBoard;
|
|
2304
|
+
exports.useMissRefutation = useMissRefutation;
|
|
2305
|
+
exports.useMissSequence = useMissSequence;
|
|
2306
|
+
exports.usePositionKeyboardNav = usePositionKeyboardNav;
|
|
1547
2307
|
exports.useTheme = useTheme;
|