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.
Files changed (36) hide show
  1. package/README.md +46 -46
  2. package/dist/features/analysis/core/AnalysisBoardCore.d.ts +8 -2
  3. package/dist/features/chessboard/ChessboardDnDProvider.d.ts +4 -0
  4. package/dist/features/chessboard/boardThemes.d.ts +19 -0
  5. package/dist/features/chessboard/chessboardTheme.d.ts +6 -2
  6. package/dist/features/chessboard/index.d.ts +2 -0
  7. package/dist/features/engine/AnalysisEngineContext.d.ts +22 -0
  8. package/dist/features/engine/index.d.ts +1 -0
  9. package/dist/features/engine/types.d.ts +10 -0
  10. package/dist/features/engine/useAnalysisEngine.d.ts +2 -0
  11. package/dist/features/navigation/PlyNavigation.d.ts +1 -1
  12. package/dist/features/navigation/index.d.ts +2 -0
  13. package/dist/features/navigation/positionKeyboardNav.d.ts +12 -0
  14. package/dist/features/navigation/types.d.ts +5 -0
  15. package/dist/features/navigation/usePositionKeyboardNav.d.ts +12 -0
  16. package/dist/features/training/expectedMoveDrop.d.ts +24 -0
  17. package/dist/features/training/expectedMoveDrop.test.d.ts +1 -0
  18. package/dist/features/training/index.d.ts +4 -0
  19. package/dist/features/training/miss/index.d.ts +5 -0
  20. package/dist/features/training/miss/missDisplay.d.ts +16 -0
  21. package/dist/features/training/miss/refutation.d.ts +19 -0
  22. package/dist/features/training/miss/useMissBoard.d.ts +28 -0
  23. package/dist/features/training/miss/useMissRefutation.d.ts +3 -0
  24. package/dist/features/training/miss/useMissSequence.d.ts +10 -0
  25. package/dist/features/training/uciFromDrop.d.ts +3 -0
  26. package/dist/features/training/useBoardRevision.d.ts +9 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.esm.js +816 -88
  29. package/dist/index.js +846 -86
  30. package/dist/stories/AnalysisBoard.stories.d.ts +9 -0
  31. package/dist/stories/analysisFixtures.d.ts +5 -0
  32. package/dist/stories/regressions/StockfishAnalysisRegressions.stories.d.ts +7 -0
  33. package/dist/stories/regressions/fixtures.d.ts +17 -0
  34. package/dist/stories/regressions/regressionAnalysisContext.d.ts +6 -0
  35. package/dist/stories/storybookLayout.d.ts +4 -0
  36. 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, useState, useRef, useEffect, useLayoutEffect, useMemo, Component } from 'react';
3
- import { Chessboard, ChessboardDnDProvider } from 'react-chessboard';
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
- const getStylesForTheme = (theme) => {
18
- if (theme === 'dark') {
19
- return {
20
- customDarkSquareStyle: { backgroundColor: '#838387' },
21
- customLightSquareStyle: { backgroundColor: '#e1e1e3' },
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
- const { enabled = true, depth = 16, multiPv = 2, scriptUrl = DEFAULT_STOCKFISH_SCRIPT_URL, } = options;
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 (!enabled || typeof Worker === 'undefined') {
569
- setEvaluation(emptyEngineEvaluation());
570
- setEngineReady(false);
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
- export { AnalysisBoard, AnalysisBoardCore, AnalysisBoardCoreView, AnalysisBoardLayout, AnalysisChessboardView, AnalysisErrorBoundary, AnalysisPosition, ChessboardThemeContext, DEFAULT_ANALYSIS_LAYOUT, DEFAULT_STOCKFISH_SCRIPT_URL, DefaultAnalysisContainer, DefaultAnalysisSidebar, DefaultPlyNavigation, EngineEvaluationPanel, HighlightChessboard, PlyNavigation, StockfishBrowserEngine, ThemeProvider, analysisBoardHighlightColors, analysisSidebarColors, applyUciMove, boardSquareHighlightColors, createSidebarRowBandCounters, defaultRenderPlyNavigation, emptyEngineEvaluation, formatEvaluation, formatPvPreview, getAnalysisModalStyles, getCheckSquareFromChess, getLastMoveSquareStyles, getSidebarRowBackground, getStylesForTheme, isAnalyzableFen, navButtonStyle, navRowStyle, normalizeEvalForWhite, normalizePvMoves, parseUciInfoLine, parseUciMove, plyLabelStyle, palette as plyNavigationPalette, resolveStockfishScriptUrl, resolveStockfishWasmUrl, resolveStockfishWorkerUrl, scrubberInputStyle, splitWorkerLines, uciPvToSan, useAnalysisBoardModel, useAnalysisEngine, useChessboardTheme, useTheme };
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 };