@scriptonita/chess-football-ui 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,6 +67,45 @@ expect(REQUIRED_TOKENS.filter(t => !css.includes(`${t}:`))).toEqual([])
67
67
  `--field-frame` is new in 0.9.0: the board frame was hardcoded `#1a3317` in three places,
68
68
  hand-tuned for the webapp's grass and visibly wrong against the CrazyGames pitch.
69
69
 
70
+ `--ball-color` joins the contract in 0.10.0: the ball was a hardcoded sky blue, the same
71
+ hue as `--pass-highlight`, while both apps defined a `--ball-color` nobody read.
72
+
73
+ ## Bot turn
74
+
75
+ `useBotTurn` owns a bot's turn end to end — entry gates (turn, pending goal, match over),
76
+ timed replay, cancellation on unmount, error + retry, and skip. The app only says where the
77
+ turn comes from, so the same hook serves a local search and a server call:
78
+
79
+ ```tsx
80
+ import { useBotTurn } from '@scriptonita/chess-football-ui'
81
+ import { playBotTurn } from '@scriptonita/chess-football-engine'
82
+
83
+ const { botThinking, error, retry, skip } = useBotTurn({
84
+ boardState, botSide,
85
+ getTurn: (board, side) => playBotTurn(bot, board, side), // local
86
+ // getTurn: (board) => fetch('/api/turn', …).then(r => r.json()), // server
87
+ onApplyState: setBoardState,
88
+ matchOver: !!matchResult,
89
+ goalPending: !!goalScored,
90
+ })
91
+ ```
92
+
93
+ Pacing is fixed in one place: `BOT_TURN_START_DELAY_MS` (400) before the first action,
94
+ `BOT_TURN_STEP_MS` (900) between actions and `BOT_TURN_SETTLE_MS` (500) for the last one to
95
+ land. The closing end-of-turn state takes no step of its own, so a full 5-AP turn lasts 4.5 s.
96
+
97
+ ## First-match hints
98
+
99
+ `<GameHints userSide seen onSeen />` shows one dismissible sentence the first time each
100
+ situation happens (`GAME_HINT_IDS`: first turn, selecting a piece, holding the ball, last
101
+ action point). The texts are `hints.*` keys of `GAME_I18N_KEYS`. The app decides when to mount
102
+ it and where `seen` is stored.
103
+
104
+ ## Forgiving taps
105
+
106
+ `<GameBoard forgivingTaps />` resolves a touch on an empty square that borders exactly one
107
+ legal target to that target. Opt-in, touch only, never when the tap is ambiguous.
108
+
70
109
  ## Peer dependencies
71
110
 
72
111
  `react`, `react-dom`, `framer-motion`, `lucide-react`.
package/dist/index.cjs CHANGED
@@ -44,12 +44,17 @@ var EMBOSS_FILTER = {
44
44
  white: "drop-shadow(0 -0.75px 0 rgba(255,255,255,0.65)) drop-shadow(0 1.25px 1px rgba(0,0,0,0.45))",
45
45
  black: "drop-shadow(0 -0.5px 0 rgba(255,255,255,0.3)) drop-shadow(0 1.25px 1.5px rgba(0,0,0,0.8))"
46
46
  };
47
- var PieceIcon = ({ type, side }) => {
47
+ var MOVED_GLYPH_COLOR = {
48
+ white: "#8b8b94",
49
+ black: "#bdbdc3"
50
+ };
51
+ var PieceIcon = ({ type, side, moved = false }) => {
48
52
  const Icon = PIECE_ICON[type] ?? lucideReact.ShieldQuestion;
49
53
  const style = {
50
54
  width: "clamp(16px, 7.4cqw, 44px)",
51
55
  height: "clamp(16px, 7.4cqw, 44px)",
52
- filter: EMBOSS_FILTER[side]
56
+ filter: EMBOSS_FILTER[side],
57
+ ...moved ? { color: MOVED_GLYPH_COLOR[side] } : null
53
58
  };
54
59
  return /* @__PURE__ */ jsxRuntime.jsx(Icon, { style, strokeWidth: 2, "aria-hidden": "true" });
55
60
  };
@@ -93,24 +98,20 @@ var ON_BOARD_CHIP_STYLE = {
93
98
  ].join(", ")
94
99
  }
95
100
  };
96
- var MOVED_STYLE = {
97
- filter: "saturate(0.15) brightness(0.75)"
98
- };
99
101
  function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
100
102
  const chipStyle = ON_BOARD_CHIP_STYLE[piece.side];
101
103
  return /* @__PURE__ */ jsxRuntime.jsxs(
102
104
  "div",
103
105
  {
104
106
  onClick,
105
- style: piece.hasMovedThisTurn ? MOVED_STYLE : void 0,
107
+ "data-moved": piece.hasMovedThisTurn ? "true" : "false",
106
108
  className: cn(
107
109
  "relative w-[88%] h-[88%] rounded-full cursor-pointer z-10 transition-transform duration-200 ease-out motion-reduce:transition-none",
108
110
  // `--primary` was the package's one remaining shadcn token, and
109
111
  // CrazyGames had to define it by hand just to make the selection
110
112
  // ring visible. It is part of the design system now.
111
113
  isSelected && "ring-4 ring-accent-green scale-110 z-30",
112
- isMoving && "scale-[1.08] z-40",
113
- piece.hasMovedThisTurn && "opacity-70"
114
+ isMoving && "scale-[1.08] z-40"
114
115
  ),
115
116
  children: [
116
117
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -132,7 +133,7 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
132
133
  style: chipStyle,
133
134
  className: "absolute inset-0 rounded-full flex items-center justify-center",
134
135
  children: [
135
- /* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side }),
136
+ /* @__PURE__ */ jsxRuntime.jsx(PieceIcon, { type: piece.type, side: piece.side, moved: piece.hasMovedThisTurn }),
136
137
  hasBall && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full ring-2 ring-orange-400 pointer-events-none" }),
137
138
  isSelected && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 rounded-full animate-pulse motion-reduce:animate-none ring-2 ring-yellow-400 ring-offset-2 ring-offset-transparent" })
138
139
  ]
@@ -142,6 +143,8 @@ function GamePiece({ piece, isSelected, hasBall, onClick, isMoving = false }) {
142
143
  }
143
144
  );
144
145
  }
146
+ var BALL_COLOR = "var(--ball-color, #ffffff)";
147
+ var BALL_SHADE = "color-mix(in srgb, var(--ball-color, #ffffff) 74%, #000000)";
145
148
  var Football = ({ className }) => {
146
149
  const bodyId = React.useId();
147
150
  const hiId = `${bodyId}-h`;
@@ -155,9 +158,9 @@ var Football = ({ className }) => {
155
158
  children: [
156
159
  /* @__PURE__ */ jsxRuntime.jsxs("defs", { children: [
157
160
  /* @__PURE__ */ jsxRuntime.jsxs("radialGradient", { id: bodyId, cx: "36%", cy: "32%", r: "68%", children: [
158
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: "#f0f9ff" }),
159
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "55%", stopColor: "#bae6fd" }),
160
- /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", stopColor: "#38bdf8" })
161
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", style: { stopColor: "#ffffff" } }),
162
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "55%", style: { stopColor: BALL_COLOR } }),
163
+ /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "100%", style: { stopColor: BALL_SHADE } })
161
164
  ] }),
162
165
  /* @__PURE__ */ jsxRuntime.jsxs("radialGradient", { id: hiId, cx: "32%", cy: "28%", r: "22%", children: [
163
166
  /* @__PURE__ */ jsxRuntime.jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: "0.85" }),
@@ -200,6 +203,11 @@ var STATIC_I18N_KEYS = [
200
203
  "endTurnConfirmDescription",
201
204
  "endTurnConfirmYes",
202
205
  "endTurnKeepPlaying",
206
+ "hints.dismiss",
207
+ "hints.hasBall",
208
+ "hints.lastActionPoint",
209
+ "hints.selectPiece",
210
+ "hints.start",
203
211
  "history.empty",
204
212
  "history.goal",
205
213
  "history.interception",
@@ -462,9 +470,10 @@ var ROWS = 12;
462
470
  var clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
463
471
  var cellX = (x) => `${x * 100}%`;
464
472
  var cellY = (y) => `${(ROWS - 1 - y) * 100}%`;
473
+ var TOUCH_CLICK_WINDOW_MS = 800;
465
474
  var PIECE_SLIDE = { type: "spring", stiffness: 190, damping: 28, mass: 1 };
466
475
  var BALL_SLIDE = { type: "spring", stiffness: 300, damping: 26, mass: 0.6 };
467
- function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true, className }) {
476
+ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, toolbar = true, forgivingTaps = false, className }) {
468
477
  const t = useGameT();
469
478
  const boardId = React.useId();
470
479
  const prefersReducedMotion = framerMotion.useReducedMotion();
@@ -508,6 +517,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
508
517
  setInvalidClickAt({ x, y, nonce: invalidClickNonceRef.current });
509
518
  };
510
519
  const [hoveredPassAt, setHoveredPassAt] = React.useState(null);
520
+ const lastTouchAtRef = React.useRef(0);
511
521
  const [movingIds, setMovingIds] = React.useState(() => /* @__PURE__ */ new Set());
512
522
  const setMoving = (id, moving) => setMovingIds((prev) => {
513
523
  if (prev.has(id) === moving) return prev;
@@ -593,11 +603,24 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
593
603
  const passCarrier = boardState.pieces.find((p) => p.id === selectedPieceId && boardState.ball.holderId === p.id) ?? null;
594
604
  const trajectoryPath = hoveredPassAt && passCarrier ? passCarrier.type === "knight" ? [hoveredPassAt] : chessFootballEngine.getPath(passCarrier.pos, hoveredPassAt) : null;
595
605
  const trajectoryIntercepted = !!(trajectoryPath && passCarrier && trajectoryPath.some((sq) => boardState.pieces.some((p) => p.pos.x === sq.x && p.pos.y === sq.y && p.side !== passCarrier.side)));
596
- const handleSquareClick = (x, y) => {
606
+ const soleAdjacentTarget = (x, y) => {
607
+ const near = [...validMoves, ...validPasses].filter(
608
+ (t2, i, all) => Math.max(Math.abs(t2.x - x), Math.abs(t2.y - y)) === 1 && all.findIndex((o) => o.x === t2.x && o.y === t2.y) === i
609
+ );
610
+ return near.length === 1 ? near[0] : null;
611
+ };
612
+ const handleSquareClick = (x, y, fromTouch = false) => {
597
613
  if (replay) return;
598
614
  setCursor(null);
599
615
  const isValidMove = validMoves.some((m) => m.x === x && m.y === y);
600
616
  const isValidPass = validPasses.some((p) => p.x === x && p.y === y);
617
+ if (!isValidMove && !isValidPass && fromTouch && forgivingTaps && selectedPieceId) {
618
+ const target = soleAdjacentTarget(x, y);
619
+ if (target) {
620
+ handleSquareClick(target.x, target.y);
621
+ return;
622
+ }
623
+ }
601
624
  if (isValidMove && isValidPass) {
602
625
  setDisambiguateAt({ x, y });
603
626
  return;
@@ -754,12 +777,13 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
754
777
  role: "gridcell",
755
778
  "aria-label": squareLabel(x, y, pieceAt, isValidMove, isValidPass),
756
779
  "aria-selected": !!pieceAt && pieceAt.id === selectedPieceId,
757
- onClick: () => handleSquareClick(x, y),
780
+ onClick: () => handleSquareClick(x, y, Date.now() - lastTouchAtRef.current < TOUCH_CLICK_WINDOW_MS),
758
781
  onMouseEnter: () => {
759
782
  if (isValidPass) setHoveredPassAt({ x, y });
760
783
  },
761
784
  onMouseLeave: () => setHoveredPassAt(null),
762
785
  onTouchStart: () => {
786
+ lastTouchAtRef.current = Date.now();
763
787
  if (isValidPass) setHoveredPassAt({ x, y });
764
788
  },
765
789
  className: cn(
@@ -880,7 +904,7 @@ function GameBoard({ userSide, showCoordinates = false, keyboardNav = true, tool
880
904
  },
881
905
  "aria-label": t("shortcuts.buttonLabel"),
882
906
  "aria-expanded": shortcutsOpen,
883
- className: "w-8 h-8 flex items-center justify-center rounded-full bg-bg-secondary/80 border border-border-subtle text-fg-muted hover:text-fg-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
907
+ className: "w-11 h-11 flex items-center justify-center rounded-full bg-bg-secondary/80 border border-border-subtle text-fg-muted hover:text-fg-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
884
908
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.HelpCircle, { size: 16, strokeWidth: 2, "aria-hidden": "true" })
885
909
  }
886
910
  ),
@@ -1902,7 +1926,7 @@ function SheetHeader({ titleId, onClose }) {
1902
1926
  "button",
1903
1927
  {
1904
1928
  onClick: onClose,
1905
- className: "w-8 h-8 flex items-center justify-center rounded-full bg-bg-surface hover:bg-bg-surface-elevated text-fg-muted hover:text-fg-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
1929
+ className: "w-11 h-11 -mr-2 flex items-center justify-center rounded-full bg-bg-surface hover:bg-bg-surface-elevated text-fg-muted hover:text-fg-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
1906
1930
  "aria-label": t("history.close"),
1907
1931
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14, "aria-hidden": "true" })
1908
1932
  }
@@ -1956,6 +1980,160 @@ function HistoryChip({ lastMove, onClick, className }) {
1956
1980
  }
1957
1981
  );
1958
1982
  }
1983
+ var GAME_HINT_IDS = ["lastActionPoint", "hasBall", "selectPiece", "start"];
1984
+ function GameHints({ userSide, seen, onSeen, className }) {
1985
+ const t = useGameT();
1986
+ const boardState = chunkBSE4DUXM_cjs.useGameStore((s) => s.boardState);
1987
+ const selectedPieceId = chunkBSE4DUXM_cjs.useGameStore((s) => s.selectedPieceId);
1988
+ const [done, setDone] = React.useState([]);
1989
+ const myTurn = !!boardState && userSide !== null && boardState.turn === userSide;
1990
+ const selected = myTurn ? boardState.pieces.find((p) => p.id === selectedPieceId) : void 0;
1991
+ const applies = {
1992
+ lastActionPoint: myTurn && boardState.actionPoints === 1,
1993
+ hasBall: !!selected && boardState?.ball.holderId === selected.id,
1994
+ selectPiece: !!selected,
1995
+ start: myTurn && !selected
1996
+ };
1997
+ const current = GAME_HINT_IDS.find((id) => applies[id] && !seen.includes(id) && !done.includes(id)) ?? null;
1998
+ const onSeenRef = React.useRef(onSeen);
1999
+ onSeenRef.current = onSeen;
2000
+ const markSeen = (id) => {
2001
+ setDone((d) => d.includes(id) ? d : [...d, id]);
2002
+ onSeenRef.current(id);
2003
+ };
2004
+ const shownRef = React.useRef(null);
2005
+ React.useEffect(() => {
2006
+ const prev = shownRef.current;
2007
+ shownRef.current = current;
2008
+ if (prev && prev !== current && !done.includes(prev)) markSeen(prev);
2009
+ }, [current]);
2010
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: cn("pointer-events-none flex justify-center", className), children: current && /* @__PURE__ */ jsxRuntime.jsxs(
2011
+ "div",
2012
+ {
2013
+ "data-testid": "game-hint",
2014
+ "data-hint": current,
2015
+ className: "pointer-events-auto flex items-center gap-1 max-w-sm pl-4 pr-0.5 rounded-xl bg-bg-secondary/95 border border-border-subtle shadow-lg backdrop-blur-sm",
2016
+ children: [
2017
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-2 font-inter text-sm leading-snug text-fg-primary", children: t(`hints.${current}`) }),
2018
+ /* @__PURE__ */ jsxRuntime.jsx(
2019
+ "button",
2020
+ {
2021
+ type: "button",
2022
+ onClick: () => markSeen(current),
2023
+ "aria-label": t("hints.dismiss"),
2024
+ className: "shrink-0 w-11 h-11 flex items-center justify-center rounded-full text-fg-muted hover:text-fg-primary transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-green",
2025
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, "aria-hidden": "true" })
2026
+ }
2027
+ )
2028
+ ]
2029
+ }
2030
+ ) });
2031
+ }
2032
+ var BOT_TURN_START_DELAY_MS = 400;
2033
+ var BOT_TURN_STEP_MS = 900;
2034
+ var BOT_TURN_SETTLE_MS = 500;
2035
+ var PROVIDER_DEFER_MS = 30;
2036
+ function useBotTurn(opts) {
2037
+ const [botThinking, setBotThinking] = React.useState(false);
2038
+ const [error, setError] = React.useState(null);
2039
+ const [retryNonce, setRetryNonce] = React.useState(0);
2040
+ const optsRef = React.useRef(opts);
2041
+ optsRef.current = opts;
2042
+ const pending = React.useRef(false);
2043
+ const runId = React.useRef(0);
2044
+ const timers = React.useRef(/* @__PURE__ */ new Set());
2045
+ const skipRef = React.useRef(null);
2046
+ const clearTimers = () => {
2047
+ timers.current.forEach(clearTimeout);
2048
+ timers.current.clear();
2049
+ };
2050
+ React.useEffect(() => () => {
2051
+ runId.current++;
2052
+ clearTimers();
2053
+ pending.current = false;
2054
+ skipRef.current = null;
2055
+ }, []);
2056
+ const { boardState, enabled = true, matchOver = false, goalPending = false } = opts;
2057
+ React.useEffect(() => {
2058
+ const o = optsRef.current;
2059
+ const board = o.boardState;
2060
+ if (!board || !enabled || board.turn !== o.botSide) return;
2061
+ if (pending.current || matchOver || goalPending) return;
2062
+ if (chessFootballEngine.checkGoal(board)) return;
2063
+ const run = ++runId.current;
2064
+ const alive = () => runId.current === run;
2065
+ const wait = (ms) => new Promise((resolve) => {
2066
+ const id = setTimeout(() => {
2067
+ timers.current.delete(id);
2068
+ resolve();
2069
+ }, ms);
2070
+ timers.current.add(id);
2071
+ });
2072
+ const settle = () => {
2073
+ pending.current = false;
2074
+ skipRef.current = null;
2075
+ setBotThinking(false);
2076
+ optsRef.current.onProcessingChange?.(false);
2077
+ };
2078
+ pending.current = true;
2079
+ setBotThinking(true);
2080
+ setError(null);
2081
+ o.onProcessingChange?.(true);
2082
+ let skipRequested = false;
2083
+ skipRef.current = () => {
2084
+ skipRequested = true;
2085
+ };
2086
+ const startDelay = o.startDelayMs ?? BOT_TURN_START_DELAY_MS;
2087
+ const stepMs = o.stepMs ?? BOT_TURN_STEP_MS;
2088
+ const settleMs = o.settleMs ?? BOT_TURN_SETTLE_MS;
2089
+ (async () => {
2090
+ try {
2091
+ const [turn] = await Promise.all([
2092
+ wait(Math.min(PROVIDER_DEFER_MS, startDelay)).then(() => o.getTurn(board, o.botSide)),
2093
+ wait(startDelay)
2094
+ ]);
2095
+ if (!alive()) return;
2096
+ if (!turn?.states?.length) throw new Error("Bot turn returned no states");
2097
+ const { states } = turn;
2098
+ const last = states[states.length - 1];
2099
+ const shown = turn.closedByEndTurn ? states.length - 1 : states.length;
2100
+ const finish = () => {
2101
+ clearTimers();
2102
+ optsRef.current.onApplyState(last);
2103
+ settle();
2104
+ const scorer = turn.goalScored ?? null;
2105
+ if (scorer) optsRef.current.onGoal?.(scorer, last);
2106
+ };
2107
+ skipRef.current = () => {
2108
+ if (alive()) finish();
2109
+ };
2110
+ if (skipRequested) {
2111
+ finish();
2112
+ return;
2113
+ }
2114
+ for (let i = 0; i < shown; i++) {
2115
+ optsRef.current.onApplyState(states[i]);
2116
+ await wait(i === shown - 1 ? settleMs : stepMs);
2117
+ if (!alive()) return;
2118
+ }
2119
+ finish();
2120
+ } catch (err) {
2121
+ if (!alive()) return;
2122
+ clearTimers();
2123
+ settle();
2124
+ setError(err instanceof Error ? err : new Error(String(err)));
2125
+ }
2126
+ })();
2127
+ }, [boardState?.turn, enabled, matchOver, goalPending, retryNonce]);
2128
+ const retry = React.useCallback(() => {
2129
+ setError(null);
2130
+ setRetryNonce((n) => n + 1);
2131
+ }, []);
2132
+ const skip = React.useCallback(() => {
2133
+ skipRef.current?.();
2134
+ }, []);
2135
+ return { botThinking, error, retry, skip };
2136
+ }
1959
2137
 
1960
2138
  // src/tokens.ts
1961
2139
  var REQUIRED_TOKENS = [
@@ -1983,22 +2161,28 @@ var REQUIRED_TOKENS = [
1983
2161
  "--field-frame",
1984
2162
  "--move-highlight",
1985
2163
  "--pass-highlight",
1986
- "--last-move-highlight"
2164
+ "--last-move-highlight",
2165
+ "--ball-color"
1987
2166
  ];
1988
2167
 
1989
2168
  exports.ActionPoints = ActionPoints;
1990
2169
  exports.Avatar = Avatar;
1991
2170
  exports.AvatarFallback = AvatarFallback;
1992
2171
  exports.AvatarImage = AvatarImage;
2172
+ exports.BOT_TURN_SETTLE_MS = BOT_TURN_SETTLE_MS;
2173
+ exports.BOT_TURN_START_DELAY_MS = BOT_TURN_START_DELAY_MS;
2174
+ exports.BOT_TURN_STEP_MS = BOT_TURN_STEP_MS;
1993
2175
  exports.BallHolderChip = BallHolderChip;
1994
2176
  exports.Button = Button;
1995
2177
  exports.ConfirmDialog = ConfirmDialog;
1996
2178
  exports.EventToast = EventToast;
1997
2179
  exports.Football = Football;
2180
+ exports.GAME_HINT_IDS = GAME_HINT_IDS;
1998
2181
  exports.GAME_I18N_KEYS = GAME_I18N_KEYS;
1999
2182
  exports.GOAL_AREA_SHADE_CLASS = GOAL_AREA_SHADE_CLASS;
2000
2183
  exports.GameBoard = GameBoard;
2001
2184
  exports.GameControls = GameControls;
2185
+ exports.GameHints = GameHints;
2002
2186
  exports.GameI18nProvider = GameI18nProvider;
2003
2187
  exports.GamePiece = GamePiece;
2004
2188
  exports.GrassOverlay = GrassOverlay;
@@ -2019,6 +2203,7 @@ exports.TurnBanner = TurnBanner;
2019
2203
  exports.cn = cn;
2020
2204
  exports.isGoalAreaSquare = isGoalAreaSquare;
2021
2205
  exports.pitchSquareClass = pitchSquareClass;
2206
+ exports.useBotTurn = useBotTurn;
2022
2207
  exports.useDialogA11y = useDialogA11y;
2023
2208
  exports.useGameT = useGameT;
2024
2209
  //# sourceMappingURL=index.cjs.map