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