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