doubletwelve 0.2.0 → 0.3.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.
@@ -0,0 +1,29 @@
1
+ import { TrainData } from '../game/TrainData';
2
+ import { AiAction, AiObservation, AiPlayAction, CandidateGenerator } from './types';
3
+ /**
4
+ * Indices (into `obs.trains`) the player may legally build on under standard
5
+ * Mexican Train access: your own train, plus any train flagged public. Games
6
+ * with richer access rules (Warp12's Distress Beacon, locked fractures, …)
7
+ * supply their own {@link CandidateGenerator}.
8
+ */
9
+ export declare function getAccessibleTrainIndices(obs: AiObservation): number[];
10
+ /** Union of every played tile's key across all trains (uniqueness is global). */
11
+ export declare function collectAllPlayedKeys(trains: readonly TrainData[]): Set<string>;
12
+ /** Every legal placement of a hand tile onto an accessible train. */
13
+ export declare function generatePlayActions(obs: AiObservation): AiPlayAction[];
14
+ export interface CandidateGeneratorOptions {
15
+ /**
16
+ * Offer `draw` even when legal plays exist. Off by default (canonical "must
17
+ * play if you can"). Turn on for variants where drawing is always optional —
18
+ * combined with a high blunder rate this is what makes a beginner draw when
19
+ * they didn't have to.
20
+ */
21
+ allowOptionalDraw?: boolean;
22
+ }
23
+ /**
24
+ * Builds the standard candidate set: all legal plays, plus `draw` when the pile
25
+ * isn't empty (and either there are no plays, or optional drawing is enabled),
26
+ * falling back to `pass` only when nothing else is possible.
27
+ */
28
+ export declare function createCandidateGenerator(options?: CandidateGeneratorOptions): CandidateGenerator<AiAction>;
29
+ export declare const defaultCandidateGenerator: CandidateGenerator<AiAction>;
@@ -0,0 +1,14 @@
1
+ import { AiAction, AiActionBase, AiPlayer, CreateAiPlayerOptions } from './types';
2
+ /**
3
+ * Builds an offline, heuristic-driven domino player over the standard double-N
4
+ * model. The decision flow per turn:
5
+ *
6
+ * observation → candidate generator → weighted heuristics → policy → action
7
+ *
8
+ * Every stage is injectable: swap the generator to change rules access, append
9
+ * heuristics (including ones that read custom `kind`s or `obs.meta`) to teach it
10
+ * variant-specific tactics, and pick/clone a {@link SkillProfile} to set strength.
11
+ * Pass a seeded {@link Rng} for fully reproducible games. Under the hood this is
12
+ * a thin adapter over the model-agnostic {@link createPolicyPlayer}.
13
+ */
14
+ export declare function createAiPlayer<TAction extends AiActionBase = AiAction>(options: CreateAiPlayerOptions<TAction>): AiPlayer<TAction>;
@@ -0,0 +1,13 @@
1
+ import { Heuristic } from './types';
2
+ /** Stable ids so skill profiles and overrides can reference heuristics by name. */
3
+ export declare const HEURISTIC_IDS: {
4
+ readonly preferPlay: "prefer-play";
5
+ readonly dumpPips: "dump-pips";
6
+ readonly doublesEarly: "play-doubles-early";
7
+ readonly ownTrain: "own-train";
8
+ readonly obligationRelief: "obligation-relief";
9
+ readonly handFlexibility: "hand-flexibility";
10
+ readonly defensivePublic: "defensive-public";
11
+ };
12
+ /** The stock, game-agnostic heuristic set. Append/replace by `id` to customize. */
13
+ export declare const DEFAULT_HEURISTICS: Heuristic[];
@@ -0,0 +1,12 @@
1
+ export type { Rng, AiActionBase, AiPlayAction, AiDrawAction, AiPassAction, AiAction, AiObservation, EvalContext, Heuristic, SkillProfile, CandidateGenerator, AiPlayer, CreateAiPlayerOptions, } from './types';
2
+ export { isPlayAction } from './types';
3
+ export type { CandidateGeneratorOptions } from './candidate-generator';
4
+ export { getAccessibleTrainIndices, collectAllPlayedKeys, generatePlayActions, createCandidateGenerator, defaultCandidateGenerator, } from './candidate-generator';
5
+ export { HEURISTIC_IDS, DEFAULT_HEURISTICS } from './heuristics';
6
+ export type { SkillLevel } from './skill-profiles';
7
+ export { SKILL_PRESETS, getSkillProfile } from './skill-profiles';
8
+ export { createAiPlayer } from './create-ai-player';
9
+ export type { GenericHeuristic, PolicyPlayer, PolicyPlayerConfig, } from './policy';
10
+ export { scoreWithHeuristics, argmaxIndex, softmaxIndex, chooseActionIndex, createPolicyPlayer, } from './policy';
11
+ export type { PlayerRef, SearchModel, SearchOptions, ScoredAction, } from './search';
12
+ export { searchActionValues } from './search';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Model-agnostic decision core shared by every game built on this library.
3
+ *
4
+ * Nothing here knows about dominoes, trains, or any specific rule set: it only
5
+ * knows how to turn a set of candidate actions into one chosen action, given a
6
+ * skill profile and a way to score actions. The domino-specific player
7
+ * (`createAiPlayer`) and downstream variants (e.g. Warp12) are thin adapters
8
+ * over {@link createPolicyPlayer}.
9
+ */
10
+ /** Pseudo-random source in [0, 1). Inject a seeded one for deterministic play. */
11
+ export type Rng = () => number;
12
+ /**
13
+ * The dials that define "skill". The same engine spans beginner→advanced purely
14
+ * by changing which heuristics are active, their weights, and how sharply (or
15
+ * randomly) the policy commits to the highest-scoring action.
16
+ */
17
+ export interface SkillProfile {
18
+ readonly id: string;
19
+ /** Softmax temperature over candidate scores. 0 = argmax; higher = noisier. */
20
+ readonly temperature: number;
21
+ /** Probability of ignoring the policy and picking a uniformly random action. */
22
+ readonly blunderRate: number;
23
+ /** Plies of simulation (0 = greedy). Reserved; greedy-only in this release. */
24
+ readonly lookaheadDepth: number;
25
+ readonly weights: Readonly<Record<string, number>>;
26
+ readonly enabled: ReadonlySet<string>;
27
+ }
28
+ /**
29
+ * A single, pure rule-of-thumb over actions of type `TAction`, given a turn
30
+ * context of type `TCtx`. Higher score = more attractive; return 0 when the
31
+ * heuristic doesn't apply so it stays weight-neutral.
32
+ */
33
+ export interface GenericHeuristic<TAction, TCtx> {
34
+ readonly id: string;
35
+ score(action: TAction, ctx: TCtx): number;
36
+ }
37
+ /** Weighted sum of the enabled heuristics for one action. */
38
+ export declare function scoreWithHeuristics<TAction, TCtx>(action: TAction, ctx: TCtx, byId: ReadonlyMap<string, GenericHeuristic<TAction, TCtx>>, skill: SkillProfile): number;
39
+ /** Index of the max score, breaking ties uniformly at random. */
40
+ export declare function argmaxIndex(scores: readonly number[], rng: Rng): number;
41
+ /** Sample an index proportional to exp(score / temperature). */
42
+ export declare function softmaxIndex(scores: readonly number[], temperature: number, rng: Rng): number;
43
+ /** Temperature-controlled choice: argmax at 0, softmax sampling above it. */
44
+ export declare function chooseActionIndex(scores: readonly number[], skill: SkillProfile, rng: Rng): number;
45
+ export interface PolicyPlayerConfig<TObs, TAction, TCtx> {
46
+ skill: SkillProfile;
47
+ heuristics: ReadonlyArray<GenericHeuristic<TAction, TCtx>>;
48
+ generateCandidates: (obs: TObs) => TAction[];
49
+ buildContext: (obs: TObs, candidates: readonly TAction[]) => TCtx;
50
+ /** Returned when the generator yields no candidates at all. */
51
+ fallback: (obs: TObs) => TAction;
52
+ rng?: Rng;
53
+ }
54
+ export interface PolicyPlayer<TObs, TAction> {
55
+ decide(obs: TObs): TAction;
56
+ }
57
+ /**
58
+ * The reusable decision engine. Per turn:
59
+ *
60
+ * observation → candidates → (blunder?) → weighted heuristics → policy → action
61
+ *
62
+ * Context is built once per decision and shared across heuristics. A single
63
+ * candidate short-circuits scoring; an empty set returns `fallback`.
64
+ */
65
+ export declare function createPolicyPlayer<TObs, TAction, TCtx>(config: PolicyPlayerConfig<TObs, TAction, TCtx>): PolicyPlayer<TObs, TAction>;
@@ -0,0 +1,43 @@
1
+ import { Rng } from './policy';
2
+ export type PlayerRef = number | string;
3
+ /**
4
+ * The forward model the search drives. Implement these over your engine:
5
+ * `applyAction` is the transition function, `evaluate` is the leaf heuristic
6
+ * (higher = better for `perspective`), and `determinize` samples the hidden
7
+ * state so the search isn't allowed to peek at information a player shouldn't
8
+ * have. `orderActions` is an optional breadth control (good move ordering lets
9
+ * `maxBranch` prune to the promising moves).
10
+ */
11
+ export interface SearchModel<TState, TAction> {
12
+ legalActions(state: TState): TAction[];
13
+ applyAction(state: TState, action: TAction): TState;
14
+ isTerminal(state: TState): boolean;
15
+ currentPlayer(state: TState): PlayerRef;
16
+ /** Position value from `perspective`'s point of view (higher is better). */
17
+ evaluate(state: TState, perspective: PlayerRef): number;
18
+ /** Sample a concrete world consistent with `perspective`'s knowledge. */
19
+ determinize?(state: TState, perspective: PlayerRef, rng: Rng): TState;
20
+ /** Reorder actions best-first; the search expands only the first `maxBranch`. */
21
+ orderActions?(state: TState, actions: TAction[]): TAction[];
22
+ }
23
+ export interface SearchOptions {
24
+ /** Plies to look ahead, including the root action itself (>= 1). */
25
+ depth: number;
26
+ perspective: PlayerRef;
27
+ rng?: Rng;
28
+ /** Worlds to sample for imperfect-information averaging (default 1). */
29
+ determinizations?: number;
30
+ /** Cap candidates expanded per node (default unlimited). */
31
+ maxBranch?: number;
32
+ }
33
+ export interface ScoredAction<TAction> {
34
+ readonly action: TAction;
35
+ readonly value: number;
36
+ }
37
+ /**
38
+ * Value every root action by simulating it forward. For each action we average
39
+ * its minimax value across `determinizations` sampled worlds. Returns one entry
40
+ * per (ordered, breadth-capped) root action; the caller turns these values into
41
+ * a choice (e.g. skill-scaled softmax via {@link chooseActionIndex}).
42
+ */
43
+ export declare function searchActionValues<TState, TAction>(rootState: TState, model: SearchModel<TState, TAction>, options: SearchOptions): ScoredAction<TAction>[];
@@ -0,0 +1,16 @@
1
+ import { SkillProfile } from './types';
2
+ /**
3
+ * Stock skill tiers. Each is just a configuration of the same engine:
4
+ *
5
+ * - **beginner** — only cares about playing and lightly about dumping pips, with
6
+ * high temperature and a real blunder rate: erratic, often suboptimal plays.
7
+ * - **intermediate** — adds doubles-early and own-train sense, low noise.
8
+ * - **advanced** — full heuristic suite (obligations, flexibility, defense),
9
+ * near-deterministic, no blunders.
10
+ *
11
+ * Clone and tweak (`{ ...SKILL_PRESETS.advanced, temperature: 0.3 }`) for any
12
+ * point on the spectrum.
13
+ */
14
+ export declare const SKILL_PRESETS: Record<'beginner' | 'intermediate' | 'advanced', SkillProfile>;
15
+ export type SkillLevel = keyof typeof SKILL_PRESETS;
16
+ export declare function getSkillProfile(level: SkillLevel): SkillProfile;
@@ -0,0 +1,9 @@
1
+ import { DominoValue } from '../game/DominoValue';
2
+ import { TrainData } from '../game/TrainData';
3
+ import { RulesConfig } from '../rules/rulesConfig';
4
+ import { AiObservation } from './types';
5
+ /** Deterministic mulberry32 RNG for reproducible AI tests. */
6
+ export declare function mulberry32(seed: number): () => number;
7
+ export declare const DEFAULT_TEST_RULES: RulesConfig;
8
+ export declare function train(playerId: number, dominoes: DominoValue[], isPublic?: boolean): TrainData;
9
+ export declare function makeObs(overrides?: Partial<AiObservation>): AiObservation;
@@ -0,0 +1,84 @@
1
+ import { DominoValue } from '../game/DominoValue';
2
+ import { TrainData } from '../game/TrainData';
3
+ import { RulesConfig } from '../rules/rulesConfig';
4
+ import { Move } from '../rules/placement';
5
+ import { GenericHeuristic, Rng, SkillProfile } from './policy';
6
+ export type { Rng, SkillProfile } from './policy';
7
+ /**
8
+ * Base shape every action shares. Games extend the action space by declaring
9
+ * their own `kind` literals (e.g. Warp12's `'deploy-beacon'`) and unioning them
10
+ * with {@link AiAction}; the scoring pipeline treats unknown kinds opaquely.
11
+ */
12
+ export interface AiActionBase {
13
+ readonly kind: string;
14
+ }
15
+ /** Attach `tile` at `move.end` of the train at `trainIndex` in the observation. */
16
+ export interface AiPlayAction extends AiActionBase {
17
+ readonly kind: 'play';
18
+ readonly trainIndex: number;
19
+ readonly move: Move;
20
+ }
21
+ export interface AiDrawAction extends AiActionBase {
22
+ readonly kind: 'draw';
23
+ }
24
+ export interface AiPassAction extends AiActionBase {
25
+ readonly kind: 'pass';
26
+ }
27
+ /** The base action space shared by all double-N variants. */
28
+ export type AiAction = AiPlayAction | AiDrawAction | AiPassAction;
29
+ /** Narrows any action to a play action (kind discriminant on the base is widened). */
30
+ export declare function isPlayAction(action: AiActionBase): action is AiPlayAction;
31
+ /**
32
+ * Everything the bot is allowed to see this turn. Game-specific extras (beacon
33
+ * flags, fracture state, scores, turn order…) ride along in {@link meta} so
34
+ * custom heuristics can read them without changing this interface.
35
+ */
36
+ export interface AiObservation {
37
+ readonly selfPlayerId: number;
38
+ readonly hand: readonly DominoValue[];
39
+ readonly rules: RulesConfig;
40
+ readonly trains: readonly TrainData[];
41
+ readonly engineValue: number;
42
+ /** Tiles left to draw; omit for "unlimited/unknown". 0 forbids drawing. */
43
+ readonly drawPileSize?: number;
44
+ /**
45
+ * Set by the host once this player has already taken their single draw this
46
+ * turn. Standard Mexican Train allows exactly one draw when you can't play;
47
+ * if the drawn tile still can't be played you must pass (which marks your
48
+ * train public). When true the generator stops offering `draw`, so the bot
49
+ * falls through to `pass` instead of draining the pile.
50
+ */
51
+ readonly turnDrawUsed?: boolean;
52
+ readonly meta?: Readonly<Record<string, unknown>>;
53
+ }
54
+ /** Shared, pre-computed turn data handed to every heuristic (built once per decision). */
55
+ export interface EvalContext {
56
+ readonly obs: AiObservation;
57
+ /** Canonical keys of every tile already on the table (global uniqueness). */
58
+ readonly playedKeys: ReadonlySet<string>;
59
+ readonly candidates: readonly AiActionBase[];
60
+ readonly playCandidates: readonly AiPlayAction[];
61
+ /** Tiles neither played nor in hand — the basis for tile-counting heuristics. */
62
+ readonly unseen: readonly DominoValue[];
63
+ readonly rng: Rng;
64
+ }
65
+ /**
66
+ * A single, pure rule-of-thumb over the domino action space. Higher score =
67
+ * more attractive; return 0 when it doesn't apply so it stays weight-neutral.
68
+ * This is the domino specialization of the generic {@link GenericHeuristic}.
69
+ */
70
+ export type Heuristic = GenericHeuristic<AiActionBase, EvalContext>;
71
+ /** Produces the legal/considered actions for a turn. Override to change rules access. */
72
+ export type CandidateGenerator<TAction extends AiActionBase = AiAction> = (obs: AiObservation) => TAction[];
73
+ export interface AiPlayer<TAction extends AiActionBase = AiAction> {
74
+ decide(obs: AiObservation): TAction;
75
+ }
76
+ export interface CreateAiPlayerOptions<TAction extends AiActionBase = AiAction> {
77
+ skill: SkillProfile;
78
+ /** Defaults to {@link DEFAULT_HEURISTICS}. Append your own to extend behavior. */
79
+ heuristics?: Heuristic[];
80
+ /** Defaults to {@link defaultCandidateGenerator}. */
81
+ generateCandidates?: CandidateGenerator<TAction>;
82
+ /** Defaults to `Math.random`. Inject a seeded RNG for reproducible games/tests. */
83
+ rng?: Rng;
84
+ }
@@ -0,0 +1,9 @@
1
+ import { FC } from 'react';
2
+ import { PipRenderContext, DominoTheme } from './dominoTheme';
3
+ export interface DefaultPipProps {
4
+ ctx: PipRenderContext;
5
+ theme?: DominoTheme;
6
+ }
7
+ /** Stock domino pip — solid or hollow circle using the resolved pip color. */
8
+ export declare const DefaultPip: FC<DefaultPipProps>;
9
+ export default DefaultPip;
@@ -0,0 +1,8 @@
1
+ import { FC, ReactNode } from 'react';
2
+ import { DominoTheme } from './dominoTheme';
3
+ export interface DominoThemeProviderProps {
4
+ theme?: DominoTheme;
5
+ children: ReactNode;
6
+ }
7
+ export declare const DominoThemeProvider: FC<DominoThemeProviderProps>;
8
+ export declare function useDominoTheme(override?: DominoTheme): DominoTheme;
@@ -1,4 +1,5 @@
1
1
  import { FC } from 'react';
2
+ import { DominoTheme } from './dominoTheme';
2
3
  import { PipColorMap } from './pipColors';
3
4
  export interface DoubleTwelveProps {
4
5
  /** Pip count on the top half (0–12). Defaults to 0 (blank). */
@@ -14,6 +15,8 @@ export interface DoubleTwelveProps {
14
15
  pipColors?: PipColorMap;
15
16
  borderColor?: string;
16
17
  rotation?: number;
18
+ /** Presentation overrides — also available via DominoThemeProvider. */
19
+ theme?: DominoTheme;
17
20
  }
18
21
  export declare const DoubleTwelve: FC<DoubleTwelveProps>;
19
22
  export default DoubleTwelve;
package/dist/app/Pip.d.ts CHANGED
@@ -1,13 +1,6 @@
1
1
  import { FC } from 'react';
2
- import { PipGridSize } from './pipGrid';
3
- export interface PipProps {
4
- row: number;
5
- col: number;
6
- gridSize: PipGridSize;
7
- color: string;
8
- hollow?: boolean;
9
- top?: string;
10
- left?: string;
11
- }
12
- export declare const Pip: FC<PipProps>;
2
+ import { PipRenderContext } from './dominoTheme';
3
+ export type PipProps = PipRenderContext;
4
+ /** @deprecated Prefer theme.renderPip or DefaultPip via DominoTheme. */
5
+ export declare const Pip: FC<PipRenderContext>;
13
6
  export default Pip;
@@ -0,0 +1,39 @@
1
+ import { CSSProperties, ReactNode } from 'react';
2
+ import { PipGridSize } from './pipGrid';
3
+ export interface PipRenderContext {
4
+ value: number;
5
+ row: number;
6
+ col: number;
7
+ gridSize: PipGridSize;
8
+ color: string;
9
+ hollow?: boolean;
10
+ top?: string;
11
+ left?: string;
12
+ positionStyle: CSSProperties;
13
+ }
14
+ export interface TileRenderContext {
15
+ value1: number;
16
+ value2: number;
17
+ width: number;
18
+ height: number;
19
+ backgroundColor: string;
20
+ borderColor: string;
21
+ rotation: number;
22
+ }
23
+ /** Optional presentation hooks for domino tiles and pips. */
24
+ export interface DominoTheme {
25
+ /** Root class on each tile — use for app-specific CSS modules. */
26
+ tileClassName?: string;
27
+ /** Extra data attributes for CSS selectors, e.g. holographic toggles. */
28
+ tileDataAttributes?: Record<string, string | boolean | number | undefined>;
29
+ tileStyle?: (ctx: TileRenderContext) => CSSProperties;
30
+ halfDividerStyle?: (ctx: TileRenderContext) => CSSProperties;
31
+ /** Merged onto each pip after layout positioning. */
32
+ pipStyle?: (ctx: PipRenderContext) => CSSProperties;
33
+ /** Replace the default pip element entirely. */
34
+ renderPip?: (ctx: PipRenderContext) => ReactNode;
35
+ }
36
+ export declare const DEFAULT_DOMINO_THEME: DominoTheme;
37
+ export declare function mergeDominoTheme(base: DominoTheme, patch?: DominoTheme): DominoTheme;
38
+ /** Convert theme tileDataAttributes to React data-* props. */
39
+ export declare function themeDataAttributes(attrs?: DominoTheme['tileDataAttributes']): Record<string, string>;
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const g=require("react/jsx-runtime"),k=require("react"),He={"3x3":{rows:[20,50,80],cols:[20,50,80],size:"18%"},"3x4":{rows:[24,50,76],cols:[22,40,60,78],size:"12%"},"4x3":{rows:[15,38,62,85],cols:[20,50,80],size:"14%"}};function Te(e){const t=He[e.gridSize];return{top:e.top??`${t.rows[e.row]}%`,left:e.left??`${t.cols[e.col]}%`,width:t.size,height:t.size}}const Je=({row:e,col:t,gridSize:n,color:o,hollow:r,top:i,left:l})=>{const s=Te({row:e,col:t,gridSize:n,top:i,left:l});return g.jsx("div",{"data-testid":"pip","data-row":e,"data-col":t,"data-grid":n,style:{position:"absolute",backgroundColor:r?"transparent":o,border:r?"2px solid #888":void 0,borderRadius:"50%",transform:"translate(-50%, -50%)",boxShadow:r?void 0:"1px 2px 3px rgba(0,0,0,0.3)",...s}})},Me={0:[],1:[{row:1,col:1,gridSize:"3x3"}],2:[{row:0,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"}],3:[{row:0,col:2,gridSize:"3x3"},{row:1,col:1,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"}],4:[{row:0,col:0,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],5:[{row:0,col:0,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:1,col:1,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],6:[{row:0,col:0,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:1,col:0,gridSize:"3x3"},{row:1,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],7:[{row:0,col:0,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:1,col:0,gridSize:"3x3"},{row:1,col:1,gridSize:"3x3"},{row:1,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],8:[{row:0,col:0,gridSize:"3x3"},{row:0,col:1,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:1,col:0,gridSize:"3x3"},{row:1,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:1,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],9:[{row:0,col:0,gridSize:"3x3"},{row:0,col:1,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:1,col:0,gridSize:"3x3"},{row:1,col:1,gridSize:"3x3"},{row:1,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:1,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],10:[{row:0,col:0,gridSize:"3x4"},{row:0,col:1,gridSize:"3x4"},{row:0,col:2,gridSize:"3x4"},{row:0,col:3,gridSize:"3x4"},{row:1,col:0,gridSize:"3x4"},{row:1,col:3,gridSize:"3x4"},{row:2,col:0,gridSize:"3x4"},{row:2,col:1,gridSize:"3x4"},{row:2,col:2,gridSize:"3x4"},{row:2,col:3,gridSize:"3x4"}],11:[{row:0,col:0,gridSize:"4x3"},{row:1,col:0,gridSize:"4x3"},{row:2,col:0,gridSize:"4x3"},{row:3,col:0,gridSize:"4x3"},{row:0,col:1,gridSize:"4x3"},{row:2,col:1,gridSize:"4x3",top:"50%"},{row:3,col:1,gridSize:"4x3"},{row:0,col:2,gridSize:"4x3"},{row:1,col:2,gridSize:"4x3"},{row:2,col:2,gridSize:"4x3"},{row:3,col:2,gridSize:"4x3"}],12:[{row:0,col:0,gridSize:"4x3"},{row:1,col:0,gridSize:"4x3"},{row:2,col:0,gridSize:"4x3"},{row:3,col:0,gridSize:"4x3"},{row:0,col:1,gridSize:"4x3"},{row:1,col:1,gridSize:"4x3"},{row:2,col:1,gridSize:"4x3"},{row:3,col:1,gridSize:"4x3"},{row:0,col:2,gridSize:"4x3"},{row:1,col:2,gridSize:"4x3"},{row:2,col:2,gridSize:"4x3"},{row:3,col:2,gridSize:"4x3"}]};function Ce(e){return Me[e]??[]}const Y={0:{color:"transparent"},1:{color:"#1a1a1a"},2:{color:"#8B1A1A"},3:{color:"#E6B800"},4:{color:"#e8e8e8",hollow:!0},5:{color:"#2E8B57"},6:{color:"#2563EB"},7:{color:"#E8A87C"},8:{color:"#DC2626"},9:{color:"#1E3A8A"},10:{color:"#EA580C"},11:{color:"#166534"},12:{color:"#DC2626"}},Qe=Y;function We(e){return{...Y,...e}}function se(e,t){if(t!==void 0)return t[e]??Y[e]??{color:"#1a1a1a"}}function et(e){return se(e,Y)}const tt=(e,t,n)=>{const o=se(e,n);return o?{color:o.color,hollow:o.hollow}:{color:t}},ot=({value:e,pipColor:t,pipColors:n})=>{const{color:o,hollow:r}=tt(e,t,n),i=Ce(e);return g.jsx(g.Fragment,{children:i.map((l,s)=>g.jsx(Je,{row:l.row,col:l.col,gridSize:l.gridSize,color:o,hollow:r,top:l.top,left:l.left},s))})},we=({value:e,pipColor:t,pipColors:n})=>g.jsx("div",{style:{width:"100%",height:"100%",position:"relative",padding:"0",overflow:"hidden"},children:g.jsx(ot,{value:e,pipColor:t,pipColors:n})}),ae=({value1:e=0,value2:t=0,width:n=100,height:o=200,backgroundColor:r="white",pipColor:i="black",pipColors:l,borderColor:s="black",rotation:a=0})=>{const c=Math.min(Math.max(e,0),12),d=Math.min(Math.max(t,0),12);return g.jsxs("div",{style:{width:`${n}px`,height:`${o}px`,backgroundColor:r,borderColor:s,borderWidth:"1px",borderStyle:"solid",borderRadius:"10px",transform:`rotate(${a}deg)`,transformOrigin:"center center",boxShadow:"0 1px 2px rgba(0,0,0,0.2)",display:"flex",flexDirection:"column",overflow:"hidden"},children:[g.jsx("div",{style:{flex:1,position:"relative",borderBottomWidth:"1px",borderBottomStyle:"solid",borderBottomColor:s},children:g.jsx(we,{value:c,pipColor:i,pipColors:l})}),g.jsx("div",{style:{flex:1,position:"relative"},children:g.jsx(we,{value:d,pipColor:i,pipColors:l})})]})},D=60,I=120,$e=[-45,45];function H(e,t=D,n=I){return e?t/2:n/2}function P(e,t,n=D,o=I){return H(e,n,o)+H(t,n,o)}function A(e){const t=e*Math.PI/180;return{dirX:Math.cos(t),dirY:Math.sin(t)}}function L(e){const{dirX:t,dirY:n}=A(e);return{perpX:-n,perpY:t}}function nt(e){const t=e.map(n=>({...n}));for(let n=1;n<t.length;n++){const o=t[n],r=t[n-1].value2;!(o.value1===o.value2)&&o.value1!==r&&o.value2===r&&(t[n]={value1:o.value2,value2:o.value1})}return t}function _(e){const{dirX:t,dirY:n}=A(e);return Math.abs(t)>=Math.abs(n)?t>=0?1:-1:n>=0?1:-1}function De(e,t){return e===0?t:e===t?-t:t}function Ie({orientedDominoes:e,startX:t,startY:n,angle:o,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:s,outwardSign:a,hubIndex:c}){const d=[],{dirX:u,dirY:f}=A(o),{perpX:p,perpY:y}=L(o),z=r==="offset"&&c!=null,m=[];let T=t+u*s,b=n+f*s,C=0,h=0;const w=i/2,$=v=>{const x=(v-C)*w;T+=p*x,b+=y*x,C=v};for(let v=0;v<e.length;v++){const x=e[v],S=x.value1===x.value2,M=v>0&&e[v-1].value1===e[v-1].value2;r==="linear"?v>0&&(S?(T+=u*P(M,!0,i,l),b+=f*P(M,!0,i,l)):M?(T+=u*P(!0,!1,i,l),b+=f*P(!0,!1,i,l)):(T+=u*l,b+=f*l)):S?v>0&&(T+=u*P(M,!0,i,l),b+=f*P(M,!0,i,l)):(v===0?h=a:M?(T+=u*P(!0,!1,i,l),b+=f*P(!0,!1,i,l)):(T+=u*(l/2),b+=f*(l/2),h=De(h,a)),$(h)),m.push(C),d.push({x:T,y:b,rotation:S?o+180:o-90,isDouble:S,value1:x.value1,value2:x.value2})}if(z&&c!=null){const v=-m[c]*w;if(v!==0)for(let x=0;x<d.length;x++)d[x]={...d[x],x:d[x].x+p*v,y:d[x].y+y*v}}return d}function ue(e,t){if(!e||e.length===0)return[];const n=new Map;for(const o of e)Number.isInteger(o.index)&&(o.index<=0||o.index>=t||n.set(o.index,o.turn));return[...n.entries()].map(([o,r])=>({index:o,turn:r})).sort((o,r)=>o.index-r.index)}function Pe(e,t,n,o=1/0){const r=ue(t,Number.isFinite(o)?o:n+1);let i=e;for(const l of r)if(l.index<=n)i+=l.turn;else break;return i}function rt(e,t,n,o){const{startX:r,startY:i,angle:l,layoutStyle:s,dominoWidth:a,dominoHeight:c,leadGap:d,outwardSign:u}=t,f=[0,...n.map(b=>b.index),e.length],p=[];let y=l,z=r,m=i,T=d;for(let b=0;b<f.length-1;b++){const C=e.slice(f[b],f[b+1]);if(C.length===0)continue;const h=b===0&&o!=null&&o<f[1]?o:void 0,w=Ie({orientedDominoes:C,startX:z,startY:m,angle:y,layoutStyle:s,dominoWidth:a,dominoHeight:c,leadGap:T,outwardSign:u,hubIndex:h});if(p.push(...w),b>=f.length-2)break;const v=w[w.length-1],x=A(y),S=H(v.isDouble,a,c);y+=n[b].turn;const M=e[f[b+1]],E=M.value1===M.value2,X=H(E,a,c),B=A(y),N=L(y),O=a/2,ne=v.x+x.dirX*(S-O)+B.dirX*(X+O),V=v.y+x.dirY*(S-O)+B.dirY*(X+O),j=s==="offset"&&!E,q=j?N.perpX*O*u:0,K=j?N.perpY*O*u:0;z=ne-q,m=V-K,T=0}return p}function ke({startX:e,startY:t,angle:n,dominoes:o,layoutStyle:r,dominoWidth:i=D,dominoHeight:l=I,leadGap:s=l*.3,outwardSign:a,hubIndex:c,bends:d}){const u=nt([...o]),f=a??_(n),p=ue(d,u.length);return p.length>0?rt(u,{startX:e,startY:t,angle:n,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:s,outwardSign:f},p,c):Ie({orientedDominoes:u,startX:e,startY:t,angle:n,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:s,outwardSign:f,hubIndex:c})}function ie(e,t=D,n=I){const o=e.rotation*Math.PI/180,r=Math.cos(o),i=Math.sin(o),l=t/2,s=n/2;return[[-l,-s],[l,-s],[l,s],[-l,s]].map(([a,c])=>({x:e.x+a*r-c*i,y:e.y+a*i+c*r}))}function it(e,t,n){let o=1/0,r=-1/0,i=1/0,l=-1/0;for(const s of e){const a=s.x*n.x+s.y*n.y;o=Math.min(o,a),r=Math.max(r,a)}for(const s of t){const a=s.x*n.x+s.y*n.y;i=Math.min(i,a),l=Math.max(l,a)}return Math.min(r,l)-Math.max(o,i)}function G(e,t,n=1,o=D,r=I){const i=ie(e,o,r),l=ie(t,o,r);for(const s of[i,l])for(let a=0;a<4;a++){const c=s[a],d=s[(a+1)%4],u=d.x-c.x,f=d.y-c.y,p=Math.hypot(u,f)||1,y={x:-f/p,y:u/p};if(it(i,l,y)<=n)return!1}return!0}function lt(e,t,n,o){return t.some(r=>G(e,r,1,n,o))}function ce(e,t,n=1,o=D,r=I){return e.some(i=>t.some(l=>G(i,l,n,o,r)))}function de(e,t=1,n=D,o=I){for(let r=0;r<e.length;r++)for(let i=r+1;i<e.length;i++)if(G(e[r],e[i],t,n,o))return!0;return!1}const R=D/4,ze=24;function Q({startX:e,startY:t,angle:n,branch:o,layoutStyle:r,dominoWidth:i=D,dominoHeight:l=I,leadGap:s,depth:a=0,anchor:c,outwardSign:d,placed:u=[],pushAxis:f,minPushSteps:p=0}){const y=d??_(n),z=o.feet?Object.keys(o.feet).map(Number).filter(h=>{const w=o.dominoes[h];return w&&w.value1===w.value2}).sort((h,w)=>h-w)[0]:void 0,m=(h,w)=>ke({startX:h,startY:w,angle:n,dominoes:o.dominoes,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:s,outwardSign:y,hubIndex:z,bends:o.bends});let T=m(e+(f?.x??0)*R*p,t+(f?.y??0)*R*p),b=c&&f?{x:c.x+f.x*R*p,y:c.y+f.y*R*p}:c;if(f&&u.length>0)for(let h=p;h<=ze;h++){const w=e+f.x*R*h,$=t+f.y*R*h,v=m(w,$);if(!v.some(S=>lt(S,u,i,l))||h===ze){T=v,b=c&&{x:c.x+f.x*R*h,y:c.y+f.y*R*h};break}}u.push(...T);const C=[{angle:n,depth:a,layoutStyle:r,outwardSign:y,dominoes:o.dominoes,layout:T,anchor:b}];if(o.feet){const h=i/2,w=l/2;for(const $ of Object.keys(o.feet)){const v=Number($),x=T[v],S=o.feet[v];if(!x||!x.isDouble||!S)continue;const M=Pe(n,o.bends,v,o.dominoes.length),{dirX:E,dirY:X}=A(M),{perpX:B,perpY:N}=L(M);for(let O=0;O<S.length;O++){const ne=S[O],V=$e[O]??0,j=Math.sign(V),q=M+V,K=L(q),re=-j,Ke=x.x+E*(i/2)+B*(l/2)*j,Ze=x.y+X*(i/2)+N*(l/2)*j,Se=Ke-K.perpX*re*h,me=Ze-K.perpY*re*h;C.push(...Q({startX:Se,startY:me,angle:q,branch:ne,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:w,outwardSign:re,depth:a+1,anchor:{x:Se,y:me},placed:u,pushAxis:{x:B*j,y:N*j}}))}}}return C}function fe(e){return e.flatMap(t=>t.layout)}function st(e,t=24,n=D,o=I){const r=Math.hypot(n,o)/2;if(e.length===0)return{width:t*2+n,height:t*2+o,offsetX:t,offsetY:t};let i=1/0,l=1/0,s=-1/0,a=-1/0;for(const c of e)i=Math.min(i,c.x-r),l=Math.min(l,c.y-r),s=Math.max(s,c.x+r),a=Math.max(a,c.y+r);return{width:Math.ceil(s-i+t*2),height:Math.ceil(a-l+t*2),offsetX:t-i,offsetY:t-l}}const W=1;function Oe(e,t,n){const{dirX:o,dirY:r}=A(n);return e*o+t*r}function at(e,t,n){const{perpX:o,perpY:r}=L(n);return e*o+t*r}function pe(e){const t=[];for(let n=1;n<e.length;n++)e[n].value1!==e[n-1].value2&&t.push({code:"chain-break",message:`Domino ${n} does not connect to domino ${n-1}`,index:n});for(let n=1;n<e.length;n++){const o=e[n-1].value1===e[n-1].value2,r=e[n].value1===e[n].value2;o&&r&&t.push({code:"consecutive-doubles",message:`Consecutive doubles at index ${n-1} and ${n}`,index:n})}return{valid:t.length===0,issues:t}}function Ee(e,t,n,o=D,r=I){const i=o/2,l=e.map(u=>u.isDouble),s=[];let a=0,c=0,d=0;for(let u=0;u<e.length;u++){const f=l[u],p=u>0&&l[u-1];t==="linear"?(u>0&&(a+=P(p,f,o,r)),c=0):f?u>0&&(a+=P(p,!0,o,r)):u===0?(d=n,c=d*i):p?a+=P(!0,!1,o,r):(a+=r/2,d=De(d,n),c=d*i),s.push({along:a,perp:c})}return s}function ut(e,t,n,o=W,r){const i=[],l=r??_(t),s=Ee(e,n,l);for(let a=1;a<e.length;a++){const c=e[a-1],d=e[a],u=d.x-c.x,f=d.y-c.y,p=Oe(u,f,t),y=at(u,f,t),z=s[a].along-s[a-1].along,m=s[a].perp-s[a-1].perp;Math.abs(p-z)>o&&i.push({code:"spacing-along-train",message:`Along-train spacing between domino ${a-1} and ${a} is ${p.toFixed(2)}px (expected ${z}px)`,index:a}),Math.abs(y-m)>o&&i.push({code:"spacing-perpendicular",message:`Perpendicular spacing between domino ${a-1} and ${a} is ${y.toFixed(2)}px (expected ${m}px)`,index:a})}return{valid:i.length===0,issues:i}}function ct(e,t,n,o=W,r){const i=[],l=r??_(t),s=Ee(e,n,l);for(let a=1;a<e.length;a++){const c=e[a-1],d=e[a],u=d.x-c.x,f=d.y-c.y,p=Math.hypot(u,f),y=s[a].along-s[a-1].along,z=s[a].perp-s[a-1].perp,m=Math.hypot(y,z)*.9;p+o<m&&i.push({code:"overlap",message:`Domino ${a-1} and ${a} centers are ${p.toFixed(2)}px apart (minimum ${m.toFixed(2)}px)`,index:a})}return{valid:i.length===0,issues:i}}function dt(e,t,n,o,r=W,i){const l=[...pe(t).issues,...ut(e,n,o,r,i).issues,...ct(e,n,o,r,i).issues];return e.length!==t.length&&l.push({code:"layout-length",message:`Layout length ${e.length} does not match domino count ${t.length}`}),{valid:l.length===0,issues:l}}function je(e){const t=[],n=(o,r)=>{if(t.push(...pe(o.dominoes).issues.map(i=>({...i,message:`[${r}] ${i.message}`}))),!!o.feet)for(const i of Object.keys(o.feet)){const l=Number(i),s=o.dominoes[l],a=o.feet[l]??[];if(!s){t.push({code:"foot-host-missing",message:`[${r}] Foot references missing tile ${l}`});continue}s.value1!==s.value2&&t.push({code:"foot-host-not-double",message:`[${r}] Foot host tile ${l} is not a double`}),a.length>2&&t.push({code:"foot-too-many-toes",message:`[${r}] Double ${l} has ${a.length} side toes (max 2; the center toe is the main line)`}),a.forEach((c,d)=>{const u=c.dominoes[0];u&&u.value1!==s.value1&&t.push({code:"foot-connection",message:`[${r}] Toe ${d} on double ${l} starts with ${u.value1} but the double is ${s.value1}`}),n(c,`${r}.${l}.${d}`)})}};return n(e,"main"),{valid:t.length===0,issues:t}}function ft(e,t=W){const n=[];e.forEach((r,i)=>{if(n.push(...pe(r.dominoes).issues.map(l=>({...l,message:`[segment ${i} @${r.angle}°] ${l.message}`}))),r.layout.length!==r.dominoes.length&&n.push({code:"layout-length",message:`[segment ${i}] Layout length ${r.layout.length} does not match domino count ${r.dominoes.length}`}),r.anchor&&r.layout.length>0){const l=r.layout[0],s=Oe(l.x-r.anchor.x,l.y-r.anchor.y,r.angle),a=I/2;Math.abs(s-a)>t&&n.push({code:"foot-anchor",message:`[segment ${i}] First toe tile sits ${s.toFixed(2)}px from the double along the toe (expected ${a}px)`,index:0})}});const o=e.flatMap(r=>r.layout);for(let r=0;r<o.length;r++)for(let i=r+1;i<o.length;i++)G(o[r],o[i])&&n.push({code:"tile-overlap",message:`Tiles ${r} and ${i} overlap`,index:i});return{valid:n.length===0,issues:n}}const pt=(()=>{try{return!1}catch{return!1}})(),Re=({startX:e,startY:t,angle:n,trainData:o,layoutStyle:r,tableWidth:i,tableHeight:l,centerX:s,centerY:a,pipColors:c})=>{k.useEffect(()=>{if(!pt)return;const u=je({dominoes:o.dominoes,feet:o.feet});u.valid||console.warn(`DominoTrain: player ${o.playerId} train does not follow the rules:`,u.issues.map(f=>f.message))},[o.dominoes,o.feet,o.playerId]);const d=k.useMemo(()=>fe(Q({startX:e,startY:t,angle:n,branch:{dominoes:o.dominoes,feet:o.feet},layoutStyle:r})),[e,t,n,o.dominoes,o.feet,r,i,l]);return g.jsx(g.Fragment,{children:d.map((u,f)=>{const p=o.isPublic;return g.jsx("div",{style:{position:"absolute",left:`${u.x-D/2}px`,top:`${u.y-I/2}px`,zIndex:5},children:g.jsx(ae,{value1:u.value1,value2:u.value2,width:D,height:I,backgroundColor:"white",pipColor:"black",pipColors:c,borderColor:p?"red":"black",rotation:u.rotation})},`main-train-${o.playerId}-${f}`)})})};function Ae(e,t,n,o){const r=n*(o==="offset"?2.5:1.3);return Math.max(t+20,Math.ceil(r*e/(2*Math.PI)))}const Fe=({playerCount:e,centerX:t,centerY:n,radius:o,engineValue:r,trains:i,layoutStyle:l,tableWidth:s,tableHeight:a,pipColors:c})=>{const d=Math.max(8,e),u=120,f=60,p=120,y=Ae(d,o,f,l);return g.jsxs("div",{style:{position:"relative",width:"100%",height:"100%"},children:[g.jsx("div",{style:{position:"absolute",width:`${u}px`,height:`${u}px`,left:`${t-u/2}px`,top:`${n-u/2}px`,backgroundColor:"#d1d5db",borderWidth:"3px",borderStyle:"solid",borderColor:"#6b7280",borderRadius:"50%",boxShadow:"0 4px 6px rgba(0,0,0,0.1)",zIndex:10,display:"flex",justifyContent:"center",alignItems:"center"},children:g.jsx("div",{style:{transform:"rotate(0deg)"},children:g.jsx(ae,{value1:r,value2:r,width:f,height:p,backgroundColor:"white",pipColor:"black",pipColors:c,borderColor:"#333"})})}),Array.from({length:d}).map((z,m)=>{const T=m*360/d,b=T*Math.PI/180,C=t+y*Math.cos(b),h=n+y*Math.sin(b),w=i.find($=>$.playerId===m)||{dominoes:[],playerId:m,isPublic:!1};return g.jsx(Re,{startX:C,startY:h,angle:T,trainData:w,layoutStyle:l,tableWidth:s,tableHeight:a,centerX:t,centerY:n,pipColors:c},m)})]})};function F(e,t){return e<=t?`${e}:${t}`:`${t}:${e}`}function xe(e){return F(e.value1,e.value2)}function ge(e){return e.value1===e.value2}function xt(e,t){return e.value1===t||e.value2===t}function gt(e,t){return e.value1===t?e.value2:e.value2===t?e.value1:null}function he(e,t){return e.value1===t?{value1:e.value1,value2:e.value2}:e.value2===t?{value1:e.value2,value2:e.value1}:null}function ht(e){const t=[];for(let n=0;n<=e;n++)for(let o=n;o<=e;o++)t.push({value1:n,value2:o});return t}function vt(e){const t=e+1;return t*(t+1)/2}function Xe(e,t=12,n={}){const o=new Set([F(t,t)]),r=[];for(let i=0;i<e;i++){const l=4+Math.floor(Math.random()*7),s=[];let a=t,c=!1;for(let u=0;u<l;u++){const f=mt(a,c,u===0,t,o);if(f===null)break;const p=f===a;o.add(F(a,f)),s.push({value1:a,value2:f}),c=p,a=f}const d=n.chickenFeet?bt(s,o):void 0;r.push({playerId:i,dominoes:s,isPublic:Math.random()>.7,...d?{feet:d}:{}})}return r}function bt(e,t){const n={};for(let o=0;o<e.length;o++){if(e[o].value1!==e[o].value2)continue;const r=e[o].value1,i=[];for(let l=0;l<2;l++){const s=yt(r,t);s&&i.push(s)}i.length&&(n[o]=i)}return Object.keys(n).length?n:void 0}function yt(e,t){const n=1+Math.floor(Math.random()*2),o=[];let r=e;for(let i=0;i<n;i++){const l=St(r,t);if(l===null)break;t.add(F(r,l)),o.push({value1:r,value2:l}),r=l}return o.length?{dominoes:o}:null}function St(e,t){const n=[];for(let o=0;o<13;o++)o!==e&&(t.has(F(e,o))||n.push(o));return n.length===0?null:n[Math.floor(Math.random()*n.length)]}function mt(e,t,n,o,r){const i=Array.from({length:13},(l,s)=>s).filter(l=>wt(e,l,t,n,o,r));return i.length===0?null:i[Math.floor(Math.random()*i.length)]}function wt(e,t,n,o,r,i){const l=t===e;return!(o&&l&&e===r||l&&n||i.has(F(e,t)))}const zt={playerCount:8,trains:[],engineValue:12},Tt=({initialState:e=zt,width:t=1200,height:n=800,pipColors:o,onPipColorsChange:r})=>{const[i,l]=k.useState(e),[s,a]=k.useState("offset"),[c,d]=k.useState(!1),[u,f]=k.useState(void 0),p=o??u,y=r??f,z=p!==void 0,m=t/2,T=n/2,b=(h=c)=>{const w=Xe(i.playerCount,i.engineValue,{chickenFeet:h});l($=>({...$,trains:w}))};k.useEffect(()=>{b()},[]);const C=()=>{const h=!c;d(h),b(h)};return g.jsxs("div",{style:{width:`${t}px`,height:`${n}px`,position:"relative",backgroundColor:"#1f8a55",borderRadius:"8px",boxShadow:"0 4px 12px rgba(0,0,0,0.2)",overflow:"hidden"},children:[g.jsxs("div",{style:{position:"absolute",top:"10px",left:"10px",zIndex:100},children:[g.jsx("button",{onClick:()=>b(),style:{padding:"8px 12px",backgroundColor:"#fff",border:"1px solid #ccc",borderRadius:"4px",marginRight:"10px",cursor:"pointer"},children:"New trains"}),g.jsxs("button",{onClick:()=>a(s==="offset"?"linear":"offset"),style:{padding:"8px 12px",backgroundColor:"#fff",border:"1px solid #ccc",borderRadius:"4px",marginRight:"10px",cursor:"pointer"},children:["Layout: ",s==="offset"?"Offset":"Linear"]}),g.jsxs("button",{onClick:C,style:{padding:"8px 12px",backgroundColor:c?"#fef3c7":"#fff",border:`1px solid ${c?"#f59e0b":"#ccc"}`,borderRadius:"4px",marginRight:"10px",cursor:"pointer"},children:["Chicken Feet: ",c?"On":"Off"]}),g.jsxs("button",{onClick:()=>y(z?void 0:Y),style:{padding:"8px 12px",backgroundColor:z?"#fef3c7":"#fff",border:`1px solid ${z?"#f59e0b":"#ccc"}`,borderRadius:"4px",cursor:"pointer"},children:["Pip Colors: ",z?"On":"Off"]})]}),g.jsxs("div",{style:{position:"absolute",top:"10px",right:"10px",zIndex:100,backgroundColor:"rgba(255,255,255,0.8)",padding:"8px",borderRadius:"4px",fontSize:"14px"},children:[g.jsxs("div",{children:["Engine: Double-",i.engineValue]}),g.jsxs("div",{children:["Players: ",i.playerCount]})]}),g.jsx(Fe,{playerCount:i.playerCount,centerX:m,centerY:T,radius:80,engineValue:i.engineValue,trains:i.trains,layoutStyle:s,tableWidth:t,tableHeight:n,pipColors:p})]})},ee=90;function J(e,t=ee){return e==="right"?t:-t}function ve(e){return e==="right"?"left":"right"}function Mt(e,t){return(t??_(e))>=0?"left":"right"}function Ct(e,t,n){const{perpX:o,perpY:r}=L(t),i=(a,c)=>{let d=1/0;return a>0?d=Math.min(d,(n.width-e.x)/a):a<0&&(d=Math.min(d,(0-e.x)/a)),c>0?d=Math.min(d,(n.height-e.y)/c):c<0&&(d=Math.min(d,(0-e.y)/c)),Number.isFinite(d)?Math.max(0,d):1/0},l=i(o,r),s=i(-o,-r);return l>=s?"right":"left"}function be(e,t){return fe(Q({startX:t.startX,startY:t.startY,angle:t.angle,branch:e,layoutStyle:t.layoutStyle}))}function U(e,t,n){const o=(e??[]).filter(r=>r.index!==t);return n===null?o:[...o,{index:t,turn:n}].sort((r,i)=>r.index-i.index)}function $t({branch:e,index:t,build:n,obstacles:o,preferredSide:r,degrees:i=ee}){const l=[r,ve(r)];for(const s of l){const a=J(s,i),c={...e,bends:U(e.bends,t,a)},d=be(c,n);if(!de(d)&&!ce(d,o))return{turn:a}}return{turn:null,reason:"blocked"}}function Dt(e,t,n,o,r,i=ee){const l=(e.bends??[]).find(u=>u.index===t),s=J(r,i),a=J(ve(r),i),c=u=>{const f={...e,bends:U(e.bends,t,u)},p=be(f,n);return!de(p)&&!ce(p,o)};let d;l?l.turn===s?d=[a,null]:l.turn===a?d=[null]:d=[s,a,null]:d=[s,a];for(const u of d){if(u===null)return{bends:U(e.bends,t,null),changed:!0,blocked:!1};if(c(u))return{bends:U(e.bends,t,u),changed:!0,blocked:!1}}return{bends:e.bends??[],changed:!1,blocked:!0}}function te(e,t,n){return Math.min(n,Math.max(t,e))}function le(e,t,n,o,r){const i=te(e.scale*t,o,r),l=i/e.scale;return{scale:i,x:n.x-(n.x-e.x)*l,y:n.y-(n.y-e.y)*l}}function Le(e,t,n,o,r){const i=Math.max(1,e.width),l=Math.max(1,e.height),s=Math.min((t.width-n*2)/i,(t.height-n*2)/l),a=te(s,o,r);return{scale:a,x:(t.width-i*a)/2,y:(t.height-l*a)/2}}function It(e,t){return{x:(t.x-e.x)/e.scale,y:(t.y-e.y)/e.scale}}const Pt=3,kt=({width:e,height:t,contentWidth:n,contentHeight:o,children:r,minScale:i=.2,maxScale:l=4,zoomStep:s=1.15,padding:a=40,background:c="#1f8a55",showControls:d=!0,testId:u="viewport"})=>{const f=k.useRef(null),p=k.useCallback(()=>Le({width:n,height:o},{width:e,height:t},a,i,l),[n,o,e,t,a,i,l]),[y,z]=k.useState(p);k.useEffect(()=>{z(p())},[p]);const m=k.useRef(null),T=(x,S)=>{const M=f.current?.getBoundingClientRect();return{x:x-(M?.left??0),y:S-(M?.top??0)}},b=x=>{m.current={pointerX:x.clientX,pointerY:x.clientY,startX:y.x,startY:y.y,moved:!1}},C=x=>{const S=m.current;if(!S)return;const M=x.clientX-S.pointerX,E=x.clientY-S.pointerY;!S.moved&&Math.hypot(M,E)<Pt||(S.moved=!0,f.current?.setPointerCapture?.(x.pointerId),z(X=>({...X,x:S.startX+M,y:S.startY+E})))},h=x=>{m.current?.moved&&x.preventDefault(),m.current=null},w=x=>{x.preventDefault();const S=T(x.clientX,x.clientY),M=x.deltaY<0?s:1/s;z(E=>le(E,M,S,i,l))},$=x=>z(S=>le(S,x,{x:e/2,y:t/2},i,l)),v={width:32,height:32,fontSize:18,lineHeight:"30px",textAlign:"center",cursor:"pointer",background:"#fff",border:"1px solid #d1d5db",borderRadius:6,userSelect:"none"};return g.jsxs("div",{ref:f,"data-testid":u,onPointerDown:b,onPointerMove:C,onPointerUp:h,onPointerLeave:h,onWheel:w,style:{position:"relative",width:e,height:t,overflow:"hidden",background:c,borderRadius:8,cursor:"grab",touchAction:"none"},children:[g.jsx("div",{"data-testid":`${u}-content`,style:{position:"absolute",left:0,top:0,transformOrigin:"0 0",transform:`translate(${y.x}px, ${y.y}px) scale(${y.scale})`},children:r}),d&&g.jsxs("div",{"data-testid":`${u}-controls`,style:{position:"absolute",right:12,bottom:12,display:"flex",flexDirection:"column",gap:6},children:[g.jsx("div",{style:v,role:"button","aria-label":"Zoom in",onClick:()=>$(s),children:"+"}),g.jsx("div",{style:v,role:"button","aria-label":"Zoom out",onClick:()=>$(1/s),children:"−"}),g.jsx("div",{style:{...v,fontSize:12,lineHeight:"30px"},role:"button","aria-label":"Reset view",onClick:()=>z(p()),children:"⤢"}),g.jsx("div",{"data-testid":`${u}-zoom-readout`,style:{...v,fontSize:11,cursor:"default"},children:Math.round(te(y.scale,i,l)*100)})]})]})},Ot=[{id:"regular-after-double",name:"Regular after double",description:"Double followed by a two-tile offset run",angle:0,dominoes:[{value1:12,value2:6},{value1:6,value2:6},{value1:6,value2:3},{value1:3,value2:1}],layoutStyles:["linear","offset"]},{id:"double-after-regular",name:"Double after regular",description:"Offset run, a double, then another offset run",angle:0,dominoes:[{value1:12,value2:9},{value1:9,value2:4},{value1:4,value2:4},{value1:4,value2:2},{value1:2,value2:7}],layoutStyles:["linear","offset"]},{id:"double-after-double",name:"Double after double",description:"Offset runs at the head, middle, and tail around two doubles",angle:90,dominoes:[{value1:12,value2:7},{value1:7,value2:8},{value1:8,value2:8},{value1:8,value2:3},{value1:3,value2:5},{value1:5,value2:5},{value1:5,value2:2},{value1:2,value2:1}],layoutStyles:["linear","offset"]},{id:"offset-zigzag",name:"Offset zigzag",description:"Alternating perpendicular tiles without doubles",angle:0,dominoes:[{value1:12,value2:5},{value1:5,value2:9},{value1:9,value2:2},{value1:2,value2:7},{value1:7,value2:1}],layoutStyles:["offset"]},{id:"horizontal-open",name:"Horizontal train",description:"Rightward train: offset head, double, offset tail",angle:0,dominoes:[{value1:5,value2:12},{value1:12,value2:11},{value1:11,value2:11},{value1:11,value2:6},{value1:6,value2:2}],layoutStyles:["linear","offset"]},{id:"vertical-open",name:"Vertical train",description:"Downward train: offset head, double, offset tail",angle:90,dominoes:[{value1:3,value2:12},{value1:12,value2:10},{value1:10,value2:10},{value1:10,value2:4},{value1:4,value2:1}],layoutStyles:["linear","offset"]}],Et=[{id:"single-foot",name:"Single foot",description:"A double fans two angled toes (±45°) while the main line continues straight as the center toe",angle:0,branch:{dominoes:[{value1:12,value2:6},{value1:6,value2:6},{value1:6,value2:3},{value1:3,value2:1}],feet:{1:[{dominoes:[{value1:6,value2:2},{value1:2,value2:5}]},{dominoes:[{value1:6,value2:4},{value1:4,value2:0}]}]}},layoutStyles:["linear","offset"]},{id:"foot-no-center",name:"Foot at the tail",description:"Double ends the main line, so both side toes are present with no straight continuation",angle:0,branch:{dominoes:[{value1:9,value2:7},{value1:7,value2:7}],feet:{1:[{dominoes:[{value1:7,value2:3},{value1:3,value2:8}]},{dominoes:[{value1:7,value2:5},{value1:5,value2:0}]}]}},layoutStyles:["linear","offset"]},{id:"nested-foot",name:"Nested foot",description:"A side toe contains its own double, which sprouts a second-level foot",angle:90,branch:{dominoes:[{value1:12,value2:8},{value1:8,value2:8},{value1:8,value2:3}],feet:{1:[{dominoes:[{value1:8,value2:5},{value1:5,value2:5},{value1:5,value2:2}],feet:{1:[{dominoes:[{value1:5,value2:9},{value1:9,value2:1}]},{dominoes:[{value1:5,value2:4},{value1:4,value2:6}]}]}},{dominoes:[{value1:8,value2:1},{value1:1,value2:7}]}]}},layoutStyles:["linear","offset"]}],Z={maxPips:12,engineValue:12,allowConsecutiveDoubles:!1,requireUniqueTiles:!0,requireSequential:!0,doubleObligation:"cover",chickenFoot:{toeCount:3,sideToeAngles:[-45,45]}};function Ye(e){switch(e.doubleObligation){case"chicken-foot":return Math.max(1,e.chickenFoot.toeCount);case"cover":return 1;default:return 0}}function _e(e){return e.doubleObligation==="chicken-foot"?Math.max(0,e.chickenFoot.toeCount-1):0}function jt(e={}){const t=e.maxPips??Z.maxPips;return{...Z,...e,maxPips:t,engineValue:e.engineValue??t,chickenFoot:{...Z.chickenFoot,...e.chickenFoot??{}}}}function Be(e,t){let n=e;for(const o of t)if(n=n?.feet?.[o.doubleIndex]?.[o.toeIndex],!n)return;return n}function oe(e,t,n){if(n(e,t),!!e.feet)for(const o of Object.keys(e.feet)){const r=Number(o);e.feet[r].forEach((i,l)=>{oe(i,[...t,{doubleIndex:r,toeIndex:l}],n)})}}function Rt(e){const t=[];return oe(e,[],(n,o)=>{n.dominoes.forEach((r,i)=>{if(r.value1!==r.value2)return;const l=i<n.dominoes.length-1,s=n.feet?.[i]?.length??0;t.push({path:o,doubleIndex:i,value:r.value1,hasCenter:l,sideToes:s,answers:(l?1:0)+s})})}),t}function Ne(e,t){const n=Ye(t);return n<=0?[]:Rt(e).filter(o=>o.answers<n)}function Ue(e){const t=new Set;return oe(e,[],n=>{for(const o of n.dominoes)t.add(xe(o))}),t}function Ge(e,t,n){if(e.dominoes.length===0)return[{path:[],attach:"run-tail",value:t,attachToDouble:!0,obligation:!1}];const o=Ne(e,n);if(n.doubleObligation!=="none"&&o.length>0){const i=_e(n),l=[];for(const s of o){const a=Be(e,s.path);a&&(!s.hasCenter&&s.doubleIndex===a.dominoes.length-1&&l.push({path:s.path,attach:"run-tail",value:s.value,attachToDouble:!0,obligation:!0}),s.sideToes<i&&l.push({path:s.path,attach:"side-toe",value:s.value,doubleIndex:s.doubleIndex,toeSlot:s.sideToes,attachToDouble:!0,obligation:!0}))}return l}const r=[];return oe(e,[],(i,l)=>{const s=i.dominoes[i.dominoes.length-1];s&&r.push({path:l,attach:"run-tail",value:s.value2,attachToDouble:ge(s),obligation:!1})}),r}function ye(e,t,n,o){const r=[],i=he(e,t.value);return o.requireSequential&&!i&&r.push("value-mismatch"),o.requireUniqueTiles&&n.has(xe(e))&&r.push("duplicate-tile"),!o.allowConsecutiveDoubles&&t.attachToDouble&&ge(e)&&r.push("consecutive-doubles"),{legal:r.length===0,violations:r}}function At(e,t,n,o,r){const i=Ge(e,t,r),l=[];for(const s of i)for(const a of n)ye(a,s,o,r).legal&&l.push({end:s,tile:a});return l}function Ve(e,t,n){if(t.length===0)return n(e);const[o,...r]=t,l=(e.feet?.[o.doubleIndex]??[]).map((s,a)=>a===o.toeIndex?Ve(s,r,n):s);return{...e,feet:{...e.feet,[o.doubleIndex]:l}}}function qe(e,t,n){const o=he(t.tile,t.end.value)??{...t.tile};return Ve(e,t.end.path,r=>{if(t.end.attach==="run-tail")return{...r,dominoes:[...r.dominoes,o]};const i=t.end.doubleIndex??0,l=t.end.toeSlot??r.feet?.[i]?.length??0,s=r.feet?.[i]?[...r.feet[i]]:[];return s[l]={dominoes:[o]},{...r,feet:{...r.feet,[i]:s}}})}function Ft(e,t,n){const o=ye(t.tile,t.end,Ue(e),n);return o.legal?{ok:!0,board:qe(e,t),violations:[]}:{ok:!1,board:e,violations:o.violations}}exports.CHICKEN_FOOT_FIXTURES=Et;exports.CHICKEN_FOOT_TOE_ANGLES=$e;exports.DEFAULT_PIP_COLORS=Y;exports.DEFAULT_RULES=Z;exports.DominoHub=Fe;exports.DominoTrain=Re;exports.DoubleTwelve=ae;exports.MexicanTrainGame=Tt;exports.PIP_COLORS=Qe;exports.PIP_LAYOUTS=Me;exports.TRAIN_FIXTURES=Ot;exports.TURN_DEGREES=ee;exports.Viewport=kt;exports.applyMove=qe;exports.buildBranchTiles=be;exports.clampScale=te;exports.collectPlayedKeys=Ue;exports.computeTrainLayout=ke;exports.computeTrainTree=Q;exports.cycleBendAt=Dt;exports.dominoKey=xe;exports.dominoSetSize=vt;exports.evaluatePlacement=ye;exports.fitToBounds=Le;exports.flattenSegments=fe;exports.generateDominoSet=ht;exports.generateSampleTrains=Xe;exports.getBranchAt=Be;exports.getLegalMoves=At;exports.getOpenEnds=Ge;exports.getPipLayout=Ce;exports.getPipStyle=et;exports.getTrainLayoutBounds=st;exports.getUnsatisfiedDoubles=Ne;exports.headingAtIndex=Pe;exports.hubTrainStartDistance=Ae;exports.isDouble=ge;exports.layoutSelfIntersects=de;exports.layoutsCollide=ce;exports.linearDefaultSide=Ct;exports.mergePipColors=We;exports.normalizeBends=ue;exports.offsetDefaultSide=Mt;exports.oppositeSide=ve;exports.orientForConnection=he;exports.otherEnd=gt;exports.outwardPerpSign=_;exports.playMove=Ft;exports.requiredDoubleAnswers=Ye;exports.resolveBend=$t;exports.resolvePipPosition=Te;exports.resolvePipStyle=se;exports.resolveRules=jt;exports.screenToContent=It;exports.sideToTurn=J;exports.sideToeSlots=_e;exports.stepAlongTrain=P;exports.tileCorners=ie;exports.tileHasValue=xt;exports.tileKey=F;exports.tilesOverlap=G;exports.validateChickenFootChain=je;exports.validateTrainLayout=dt;exports.validateTrainTree=ft;exports.withBendAt=U;exports.zoomAt=le;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const x=require("react/jsx-runtime"),M=require("react"),$e={};function pe(e,t){return t?{...e,...t,tileDataAttributes:{...e.tileDataAttributes,...t.tileDataAttributes}}:e}function Oe(e){if(!e)return{};const t={};for(const[o,n]of Object.entries(e))n===void 0||n===!1||(t[`data-${o}`]=n===!0?"true":String(n));return t}const ce=M.createContext($e),Tt=({theme:e,children:t})=>{const o=M.useContext(ce),n=pe(o,e);return x.jsx(ce.Provider,{value:n,children:t})};function he(e){const t=M.useContext(ce);return pe(t,e)}const Re=({ctx:e,theme:t})=>{const{row:o,col:n,gridSize:r,color:i,hollow:l,positionStyle:s}=e,a=t?.pipStyle?.(e)??{};return x.jsx("div",{"data-testid":"pip","data-row":o,"data-col":n,"data-grid":r,"data-pip-value":e.value,"data-hollow":l?"true":void 0,style:{position:"absolute",borderRadius:"50%",transform:"translate(-50%, -50%)",backgroundColor:l?"transparent":i,border:l?`2px solid ${i}`:void 0,boxShadow:l?void 0:"1px 2px 3px rgba(0,0,0,0.3)",...s,...a}})},It={"3x3":{rows:[20,50,80],cols:[20,50,80],size:"18%"},"3x4":{rows:[24,50,76],cols:[22,40,60,78],size:"12%"},"4x3":{rows:[15,38,62,85],cols:[20,50,80],size:"14%"}};function Fe(e){const t=It[e.gridSize];return{top:e.top??`${t.rows[e.row]}%`,left:e.left??`${t.cols[e.col]}%`,width:t.size,height:t.size}}const je={0:[],1:[{row:1,col:1,gridSize:"3x3"}],2:[{row:0,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"}],3:[{row:0,col:2,gridSize:"3x3"},{row:1,col:1,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"}],4:[{row:0,col:0,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],5:[{row:0,col:0,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:1,col:1,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],6:[{row:0,col:0,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:1,col:0,gridSize:"3x3"},{row:1,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],7:[{row:0,col:0,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:1,col:0,gridSize:"3x3"},{row:1,col:1,gridSize:"3x3"},{row:1,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],8:[{row:0,col:0,gridSize:"3x3"},{row:0,col:1,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:1,col:0,gridSize:"3x3"},{row:1,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:1,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],9:[{row:0,col:0,gridSize:"3x3"},{row:0,col:1,gridSize:"3x3"},{row:0,col:2,gridSize:"3x3"},{row:1,col:0,gridSize:"3x3"},{row:1,col:1,gridSize:"3x3"},{row:1,col:2,gridSize:"3x3"},{row:2,col:0,gridSize:"3x3"},{row:2,col:1,gridSize:"3x3"},{row:2,col:2,gridSize:"3x3"}],10:[{row:0,col:0,gridSize:"3x4"},{row:0,col:1,gridSize:"3x4"},{row:0,col:2,gridSize:"3x4"},{row:0,col:3,gridSize:"3x4"},{row:1,col:0,gridSize:"3x4"},{row:1,col:3,gridSize:"3x4"},{row:2,col:0,gridSize:"3x4"},{row:2,col:1,gridSize:"3x4"},{row:2,col:2,gridSize:"3x4"},{row:2,col:3,gridSize:"3x4"}],11:[{row:0,col:0,gridSize:"4x3"},{row:1,col:0,gridSize:"4x3"},{row:2,col:0,gridSize:"4x3"},{row:3,col:0,gridSize:"4x3"},{row:0,col:1,gridSize:"4x3"},{row:2,col:1,gridSize:"4x3",top:"50%"},{row:3,col:1,gridSize:"4x3"},{row:0,col:2,gridSize:"4x3"},{row:1,col:2,gridSize:"4x3"},{row:2,col:2,gridSize:"4x3"},{row:3,col:2,gridSize:"4x3"}],12:[{row:0,col:0,gridSize:"4x3"},{row:1,col:0,gridSize:"4x3"},{row:2,col:0,gridSize:"4x3"},{row:3,col:0,gridSize:"4x3"},{row:0,col:1,gridSize:"4x3"},{row:1,col:1,gridSize:"4x3"},{row:2,col:1,gridSize:"4x3"},{row:3,col:1,gridSize:"4x3"},{row:0,col:2,gridSize:"4x3"},{row:1,col:2,gridSize:"4x3"},{row:2,col:2,gridSize:"4x3"},{row:3,col:2,gridSize:"4x3"}]};function _e(e){return je[e]??[]}const U={0:{color:"transparent"},1:{color:"#1a1a1a"},2:{color:"#8B1A1A"},3:{color:"#E6B800"},4:{color:"#e8e8e8",hollow:!0},5:{color:"#2E8B57"},6:{color:"#2563EB"},7:{color:"#E8A87C"},8:{color:"#DC2626"},9:{color:"#1E3A8A"},10:{color:"#EA580C"},11:{color:"#166534"},12:{color:"#DC2626"}},Pt=U;function zt(e){return{...U,...e}}function xe(e,t){if(t!==void 0)return t[e]??U[e]??{color:"#1a1a1a"}}function Dt(e){return xe(e,U)}const Mt=(e,t,o)=>{const n=xe(e,o);return n?{color:n.color,hollow:n.hollow}:{color:t}},Ct=({value:e,pipColor:t,pipColors:o})=>{const n=he(),{color:r,hollow:i}=Mt(e,t,o),l=_e(e);return x.jsx(x.Fragment,{children:l.map((s,a)=>{const c={value:e,row:s.row,col:s.col,gridSize:s.gridSize,color:r,hollow:i,top:s.top,left:s.left,positionStyle:Fe(s)};return n.renderPip?x.jsx("span",{children:n.renderPip(c)},a):x.jsx(Re,{ctx:c,theme:n},a)})})},ke=({value:e,pipColor:t,pipColors:o})=>x.jsx("div",{style:{width:"100%",height:"100%",position:"relative",padding:"0",overflow:"hidden"},children:x.jsx(Ct,{value:e,pipColor:t,pipColors:o})}),ge=({value1:e=0,value2:t=0,width:o=100,height:n=200,backgroundColor:r="white",pipColor:i="black",pipColors:l,borderColor:s="black",rotation:a=0,theme:c})=>{const d=he(c),u=Math.min(Math.max(e,0),12),f=Math.min(Math.max(t,0),12),p={value1:u,value2:f,width:o,height:n,backgroundColor:r,borderColor:s,rotation:a},v=d.tileStyle?.(p)??{},T=d.halfDividerStyle?.(p)??{};return x.jsxs("div",{className:d.tileClassName,...Oe(d.tileDataAttributes),style:{width:`${o}px`,height:`${n}px`,backgroundColor:r,borderColor:s,borderWidth:"1px",borderStyle:"solid",borderRadius:"10px",transform:`rotate(${a}deg)`,transformOrigin:"center center",boxShadow:"0 1px 2px rgba(0,0,0,0.2)",display:"flex",flexDirection:"column",overflow:"hidden",...v},children:[x.jsx("div",{style:{flex:1,position:"relative",borderBottomWidth:"1px",borderBottomStyle:"solid",borderBottomColor:s,...T},children:x.jsx(ke,{value:u,pipColor:i,pipColors:l})}),x.jsx("div",{style:{flex:1,position:"relative"},children:x.jsx(ke,{value:f,pipColor:i,pipColors:l})})]})},E=60,k=120,Ne=[-45,45];function ee(e,t=E,o=k){return e?t/2:o/2}function A(e,t,o=E,n=k){return ee(e,o,n)+ee(t,o,n)}function N(e){const t=e*Math.PI/180;return{dirX:Math.cos(t),dirY:Math.sin(t)}}function B(e){const{dirX:t,dirY:o}=N(e);return{perpX:-o,perpY:t}}function Et(e){const t=e.map(o=>({...o}));for(let o=1;o<t.length;o++){const n=t[o],r=t[o-1].value2;!(n.value1===n.value2)&&n.value1!==r&&n.value2===r&&(t[o]={value1:n.value2,value2:n.value1})}return t}function V(e){const{dirX:t,dirY:o}=N(e);return Math.abs(t)>=Math.abs(o)?t>=0?1:-1:o>=0?1:-1}function Le(e,t){return e===0?t:e===t?-t:t}function Ye({orientedDominoes:e,startX:t,startY:o,angle:n,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:s,outwardSign:a,hubIndex:c}){const d=[],{dirX:u,dirY:f}=N(n),{perpX:p,perpY:v}=B(n),T=r==="offset"&&c!=null,S=[];let P=t+u*s,y=o+f*s,D=0,g=0;const I=i/2,C=b=>{const h=(b-D)*I;P+=p*h,y+=v*h,D=b};for(let b=0;b<e.length;b++){const h=e[b],m=h.value1===h.value2,z=b>0&&e[b-1].value1===e[b-1].value2;r==="linear"?b>0&&(m?(P+=u*A(z,!0,i,l),y+=f*A(z,!0,i,l)):z?(P+=u*A(!0,!1,i,l),y+=f*A(!0,!1,i,l)):(P+=u*l,y+=f*l)):m?b>0&&(P+=u*A(z,!0,i,l),y+=f*A(z,!0,i,l)):(b===0?g=a:z?(P+=u*A(!0,!1,i,l),y+=f*A(!0,!1,i,l)):(P+=u*(l/2),y+=f*(l/2),g=Le(g,a)),C(g)),S.push(D),d.push({x:P,y,rotation:m?n+180:n-90,isDouble:m,value1:h.value1,value2:h.value2})}if(T&&c!=null){const b=-S[c]*I;if(b!==0)for(let h=0;h<d.length;h++)d[h]={...d[h],x:d[h].x+p*b,y:d[h].y+v*b}}return d}function ve(e,t){if(!e||e.length===0)return[];const o=new Map;for(const n of e)Number.isInteger(n.index)&&(n.index<=0||n.index>=t||o.set(n.index,n.turn));return[...o.entries()].map(([n,r])=>({index:n,turn:r})).sort((n,r)=>n.index-r.index)}function Xe(e,t,o,n=1/0){const r=ve(t,Number.isFinite(n)?n:o+1);let i=e;for(const l of r)if(l.index<=o)i+=l.turn;else break;return i}function kt(e,t,o,n){const{startX:r,startY:i,angle:l,layoutStyle:s,dominoWidth:a,dominoHeight:c,leadGap:d,outwardSign:u}=t,f=[0,...o.map(y=>y.index),e.length],p=[];let v=l,T=r,S=i,P=d;for(let y=0;y<f.length-1;y++){const D=e.slice(f[y],f[y+1]);if(D.length===0)continue;const g=y===0&&n!=null&&n<f[1]?n:void 0,I=Ye({orientedDominoes:D,startX:T,startY:S,angle:v,layoutStyle:s,dominoWidth:a,dominoHeight:c,leadGap:P,outwardSign:u,hubIndex:g});if(p.push(...I),y>=f.length-2)break;const b=I[I.length-1],h=N(v),m=ee(b.isDouble,a,c);v+=o[y].turn;const z=e[f[y+1]],O=z.value1===z.value2,X=ee(O,a,c),G=N(v),K=B(v),$=a/2,ae=b.x+h.dirX*(m-$)+G.dirX*(X+$),Z=b.y+h.dirY*(m-$)+G.dirY*(X+$),F=s==="offset"&&!O,J=F?K.perpX*$*u:0,Q=F?K.perpY*$*u:0;T=ae-J,S=Z-Q,P=0}return p}function Be({startX:e,startY:t,angle:o,dominoes:n,layoutStyle:r,dominoWidth:i=E,dominoHeight:l=k,leadGap:s=l*.3,outwardSign:a,hubIndex:c,bends:d}){const u=Et([...n]),f=a??V(o),p=ve(d,u.length);return p.length>0?kt(u,{startX:e,startY:t,angle:o,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:s,outwardSign:f},p,c):Ye({orientedDominoes:u,startX:e,startY:t,angle:o,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:s,outwardSign:f,hubIndex:c})}function de(e,t=E,o=k){const n=e.rotation*Math.PI/180,r=Math.cos(n),i=Math.sin(n),l=t/2,s=o/2;return[[-l,-s],[l,-s],[l,s],[-l,s]].map(([a,c])=>({x:e.x+a*r-c*i,y:e.y+a*i+c*r}))}function At(e,t,o){let n=1/0,r=-1/0,i=1/0,l=-1/0;for(const s of e){const a=s.x*o.x+s.y*o.y;n=Math.min(n,a),r=Math.max(r,a)}for(const s of t){const a=s.x*o.x+s.y*o.y;i=Math.min(i,a),l=Math.max(l,a)}return Math.min(r,l)-Math.max(n,i)}function H(e,t,o=1,n=E,r=k){const i=de(e,n,r),l=de(t,n,r);for(const s of[i,l])for(let a=0;a<4;a++){const c=s[a],d=s[(a+1)%4],u=d.x-c.x,f=d.y-c.y,p=Math.hypot(u,f)||1,v={x:-f/p,y:u/p};if(At(i,l,v)<=o)return!1}return!0}function $t(e,t,o,n){return t.some(r=>H(e,r,1,o,n))}function be(e,t,o=1,n=E,r=k){return e.some(i=>t.some(l=>H(i,l,o,n,r)))}function ye(e,t=1,o=E,n=k){for(let r=0;r<e.length;r++)for(let i=r+1;i<e.length;i++)if(H(e[r],e[i],t,o,n))return!0;return!1}const j=E/4,Ae=24;function ne({startX:e,startY:t,angle:o,branch:n,layoutStyle:r,dominoWidth:i=E,dominoHeight:l=k,leadGap:s,depth:a=0,anchor:c,outwardSign:d,placed:u=[],pushAxis:f,minPushSteps:p=0}){const v=d??V(o),T=n.feet?Object.keys(n.feet).map(Number).filter(g=>{const I=n.dominoes[g];return I&&I.value1===I.value2}).sort((g,I)=>g-I)[0]:void 0,S=(g,I)=>Be({startX:g,startY:I,angle:o,dominoes:n.dominoes,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:s,outwardSign:v,hubIndex:T,bends:n.bends});let P=S(e+(f?.x??0)*j*p,t+(f?.y??0)*j*p),y=c&&f?{x:c.x+f.x*j*p,y:c.y+f.y*j*p}:c;if(f&&u.length>0)for(let g=p;g<=Ae;g++){const I=e+f.x*j*g,C=t+f.y*j*g,b=S(I,C);if(!b.some(m=>$t(m,u,i,l))||g===Ae){P=b,y=c&&{x:c.x+f.x*j*g,y:c.y+f.y*j*g};break}}u.push(...P);const D=[{angle:o,depth:a,layoutStyle:r,outwardSign:v,dominoes:n.dominoes,layout:P,anchor:y}];if(n.feet){const g=i/2,I=l/2;for(const C of Object.keys(n.feet)){const b=Number(C),h=P[b],m=n.feet[b];if(!h||!h.isDouble||!m)continue;const z=Xe(o,n.bends,b,n.dominoes.length),{dirX:O,dirY:X}=N(z),{perpX:G,perpY:K}=B(z);for(let $=0;$<m.length;$++){const ae=m[$],Z=Ne[$]??0,F=Math.sign(Z),J=z+Z,Q=B(J),ue=-F,St=h.x+O*(i/2)+G*(l/2)*F,wt=h.y+X*(i/2)+K*(l/2)*F,Ce=St-Q.perpX*ue*g,Ee=wt-Q.perpY*ue*g;D.push(...ne({startX:Ce,startY:Ee,angle:J,branch:ae,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:I,outwardSign:ue,depth:a+1,anchor:{x:Ce,y:Ee},placed:u,pushAxis:{x:G*F,y:K*F}}))}}}return D}function me(e){return e.flatMap(t=>t.layout)}function Ot(e,t=24,o=E,n=k){const r=Math.hypot(o,n)/2;if(e.length===0)return{width:t*2+o,height:t*2+n,offsetX:t,offsetY:t};let i=1/0,l=1/0,s=-1/0,a=-1/0;for(const c of e)i=Math.min(i,c.x-r),l=Math.min(l,c.y-r),s=Math.max(s,c.x+r),a=Math.max(a,c.y+r);return{width:Math.ceil(s-i+t*2),height:Math.ceil(a-l+t*2),offsetX:t-i,offsetY:t-l}}const oe=1;function Ue(e,t,o){const{dirX:n,dirY:r}=N(o);return e*n+t*r}function Rt(e,t,o){const{perpX:n,perpY:r}=B(o);return e*n+t*r}function Se(e){const t=[];for(let o=1;o<e.length;o++)e[o].value1!==e[o-1].value2&&t.push({code:"chain-break",message:`Domino ${o} does not connect to domino ${o-1}`,index:o});for(let o=1;o<e.length;o++){const n=e[o-1].value1===e[o-1].value2,r=e[o].value1===e[o].value2;n&&r&&t.push({code:"consecutive-doubles",message:`Consecutive doubles at index ${o-1} and ${o}`,index:o})}return{valid:t.length===0,issues:t}}function Ve(e,t,o,n=E,r=k){const i=n/2,l=e.map(u=>u.isDouble),s=[];let a=0,c=0,d=0;for(let u=0;u<e.length;u++){const f=l[u],p=u>0&&l[u-1];t==="linear"?(u>0&&(a+=A(p,f,n,r)),c=0):f?u>0&&(a+=A(p,!0,n,r)):u===0?(d=o,c=d*i):p?a+=A(!0,!1,n,r):(a+=r/2,d=Le(d,o),c=d*i),s.push({along:a,perp:c})}return s}function Ft(e,t,o,n=oe,r){const i=[],l=r??V(t),s=Ve(e,o,l);for(let a=1;a<e.length;a++){const c=e[a-1],d=e[a],u=d.x-c.x,f=d.y-c.y,p=Ue(u,f,t),v=Rt(u,f,t),T=s[a].along-s[a-1].along,S=s[a].perp-s[a-1].perp;Math.abs(p-T)>n&&i.push({code:"spacing-along-train",message:`Along-train spacing between domino ${a-1} and ${a} is ${p.toFixed(2)}px (expected ${T}px)`,index:a}),Math.abs(v-S)>n&&i.push({code:"spacing-perpendicular",message:`Perpendicular spacing between domino ${a-1} and ${a} is ${v.toFixed(2)}px (expected ${S}px)`,index:a})}return{valid:i.length===0,issues:i}}function jt(e,t,o,n=oe,r){const i=[],l=r??V(t),s=Ve(e,o,l);for(let a=1;a<e.length;a++){const c=e[a-1],d=e[a],u=d.x-c.x,f=d.y-c.y,p=Math.hypot(u,f),v=s[a].along-s[a-1].along,T=s[a].perp-s[a-1].perp,S=Math.hypot(v,T)*.9;p+n<S&&i.push({code:"overlap",message:`Domino ${a-1} and ${a} centers are ${p.toFixed(2)}px apart (minimum ${S.toFixed(2)}px)`,index:a})}return{valid:i.length===0,issues:i}}function _t(e,t,o,n,r=oe,i){const l=[...Se(t).issues,...Ft(e,o,n,r,i).issues,...jt(e,o,n,r,i).issues];return e.length!==t.length&&l.push({code:"layout-length",message:`Layout length ${e.length} does not match domino count ${t.length}`}),{valid:l.length===0,issues:l}}function Ge(e){const t=[],o=(n,r)=>{if(t.push(...Se(n.dominoes).issues.map(i=>({...i,message:`[${r}] ${i.message}`}))),!!n.feet)for(const i of Object.keys(n.feet)){const l=Number(i),s=n.dominoes[l],a=n.feet[l]??[];if(!s){t.push({code:"foot-host-missing",message:`[${r}] Foot references missing tile ${l}`});continue}s.value1!==s.value2&&t.push({code:"foot-host-not-double",message:`[${r}] Foot host tile ${l} is not a double`}),a.length>2&&t.push({code:"foot-too-many-toes",message:`[${r}] Double ${l} has ${a.length} side toes (max 2; the center toe is the main line)`}),a.forEach((c,d)=>{const u=c.dominoes[0];u&&u.value1!==s.value1&&t.push({code:"foot-connection",message:`[${r}] Toe ${d} on double ${l} starts with ${u.value1} but the double is ${s.value1}`}),o(c,`${r}.${l}.${d}`)})}};return o(e,"main"),{valid:t.length===0,issues:t}}function Nt(e,t=oe){const o=[];e.forEach((r,i)=>{if(o.push(...Se(r.dominoes).issues.map(l=>({...l,message:`[segment ${i} @${r.angle}°] ${l.message}`}))),r.layout.length!==r.dominoes.length&&o.push({code:"layout-length",message:`[segment ${i}] Layout length ${r.layout.length} does not match domino count ${r.dominoes.length}`}),r.anchor&&r.layout.length>0){const l=r.layout[0],s=Ue(l.x-r.anchor.x,l.y-r.anchor.y,r.angle),a=k/2;Math.abs(s-a)>t&&o.push({code:"foot-anchor",message:`[segment ${i}] First toe tile sits ${s.toFixed(2)}px from the double along the toe (expected ${a}px)`,index:0})}});const n=e.flatMap(r=>r.layout);for(let r=0;r<n.length;r++)for(let i=r+1;i<n.length;i++)H(n[r],n[i])&&o.push({code:"tile-overlap",message:`Tiles ${r} and ${i} overlap`,index:i});return{valid:o.length===0,issues:o}}const Lt=(()=>{try{return!1}catch{return!1}})(),Ke=({startX:e,startY:t,angle:o,trainData:n,layoutStyle:r,tableWidth:i,tableHeight:l,centerX:s,centerY:a,pipColors:c})=>{M.useEffect(()=>{if(!Lt)return;const u=Ge({dominoes:n.dominoes,feet:n.feet});u.valid||console.warn(`DominoTrain: player ${n.playerId} train does not follow the rules:`,u.issues.map(f=>f.message))},[n.dominoes,n.feet,n.playerId]);const d=M.useMemo(()=>me(ne({startX:e,startY:t,angle:o,branch:{dominoes:n.dominoes,feet:n.feet},layoutStyle:r})),[e,t,o,n.dominoes,n.feet,r,i,l]);return x.jsx(x.Fragment,{children:d.map((u,f)=>{const p=n.isPublic;return x.jsx("div",{style:{position:"absolute",left:`${u.x-E/2}px`,top:`${u.y-k/2}px`,zIndex:5},children:x.jsx(ge,{value1:u.value1,value2:u.value2,width:E,height:k,backgroundColor:"white",pipColor:"black",pipColors:c,borderColor:p?"red":"black",rotation:u.rotation})},`main-train-${n.playerId}-${f}`)})})};function qe(e,t,o,n){const r=o*(n==="offset"?2.5:1.3);return Math.max(t+20,Math.ceil(r*e/(2*Math.PI)))}const He=({playerCount:e,centerX:t,centerY:o,radius:n,engineValue:r,trains:i,layoutStyle:l,tableWidth:s,tableHeight:a,pipColors:c})=>{const d=Math.max(8,e),u=120,f=60,p=120,v=qe(d,n,f,l);return x.jsxs("div",{style:{position:"relative",width:"100%",height:"100%"},children:[x.jsx("div",{style:{position:"absolute",width:`${u}px`,height:`${u}px`,left:`${t-u/2}px`,top:`${o-u/2}px`,backgroundColor:"#d1d5db",borderWidth:"3px",borderStyle:"solid",borderColor:"#6b7280",borderRadius:"50%",boxShadow:"0 4px 6px rgba(0,0,0,0.1)",zIndex:10,display:"flex",justifyContent:"center",alignItems:"center"},children:x.jsx("div",{style:{transform:"rotate(0deg)"},children:x.jsx(ge,{value1:r,value2:r,width:f,height:p,backgroundColor:"white",pipColor:"black",pipColors:c,borderColor:"#333"})})}),Array.from({length:d}).map((T,S)=>{const P=S*360/d,y=P*Math.PI/180,D=t+v*Math.cos(y),g=o+v*Math.sin(y),I=i.find(C=>C.playerId===S)||{dominoes:[],playerId:S,isPublic:!1};return x.jsx(Ke,{startX:D,startY:g,angle:P,trainData:I,layoutStyle:l,tableWidth:s,tableHeight:a,centerX:t,centerY:o,pipColors:c},S)})]})};function L(e,t){return e<=t?`${e}:${t}`:`${t}:${e}`}function Y(e){return L(e.value1,e.value2)}function we(e){return e.value1===e.value2}function Te(e,t){return e.value1===t||e.value2===t}function Yt(e,t){return e.value1===t?e.value2:e.value2===t?e.value1:null}function re(e,t){return e.value1===t?{value1:e.value1,value2:e.value2}:e.value2===t?{value1:e.value2,value2:e.value1}:null}function Ze(e){const t=[];for(let o=0;o<=e;o++)for(let n=o;n<=e;n++)t.push({value1:o,value2:n});return t}function Xt(e){const t=e+1;return t*(t+1)/2}function Je(e,t=12,o={}){const n=new Set([L(t,t)]),r=[];for(let i=0;i<e;i++){const l=4+Math.floor(Math.random()*7),s=[];let a=t,c=!1;for(let u=0;u<l;u++){const f=Gt(a,c,u===0,t,n);if(f===null)break;const p=f===a;n.add(L(a,f)),s.push({value1:a,value2:f}),c=p,a=f}const d=o.chickenFeet?Bt(s,n):void 0;r.push({playerId:i,dominoes:s,isPublic:Math.random()>.7,...d?{feet:d}:{}})}return r}function Bt(e,t){const o={};for(let n=0;n<e.length;n++){if(e[n].value1!==e[n].value2)continue;const r=e[n].value1,i=[];for(let l=0;l<2;l++){const s=Ut(r,t);s&&i.push(s)}i.length&&(o[n]=i)}return Object.keys(o).length?o:void 0}function Ut(e,t){const o=1+Math.floor(Math.random()*2),n=[];let r=e;for(let i=0;i<o;i++){const l=Vt(r,t);if(l===null)break;t.add(L(r,l)),n.push({value1:r,value2:l}),r=l}return n.length?{dominoes:n}:null}function Vt(e,t){const o=[];for(let n=0;n<13;n++)n!==e&&(t.has(L(e,n))||o.push(n));return o.length===0?null:o[Math.floor(Math.random()*o.length)]}function Gt(e,t,o,n,r){const i=Array.from({length:13},(l,s)=>s).filter(l=>Kt(e,l,t,o,n,r));return i.length===0?null:i[Math.floor(Math.random()*i.length)]}function Kt(e,t,o,n,r,i){const l=t===e;return!(n&&l&&e===r||l&&o||i.has(L(e,t)))}const qt={playerCount:8,trains:[],engineValue:12},Ht=({initialState:e=qt,width:t=1200,height:o=800,pipColors:n,onPipColorsChange:r})=>{const[i,l]=M.useState(e),[s,a]=M.useState("offset"),[c,d]=M.useState(!1),[u,f]=M.useState(void 0),p=n??u,v=r??f,T=p!==void 0,S=t/2,P=o/2,y=(g=c)=>{const I=Je(i.playerCount,i.engineValue,{chickenFeet:g});l(C=>({...C,trains:I}))};M.useEffect(()=>{y()},[]);const D=()=>{const g=!c;d(g),y(g)};return x.jsxs("div",{style:{width:`${t}px`,height:`${o}px`,position:"relative",backgroundColor:"#1f8a55",borderRadius:"8px",boxShadow:"0 4px 12px rgba(0,0,0,0.2)",overflow:"hidden"},children:[x.jsxs("div",{style:{position:"absolute",top:"10px",left:"10px",zIndex:100},children:[x.jsx("button",{onClick:()=>y(),style:{padding:"8px 12px",backgroundColor:"#fff",border:"1px solid #ccc",borderRadius:"4px",marginRight:"10px",cursor:"pointer"},children:"New trains"}),x.jsxs("button",{onClick:()=>a(s==="offset"?"linear":"offset"),style:{padding:"8px 12px",backgroundColor:"#fff",border:"1px solid #ccc",borderRadius:"4px",marginRight:"10px",cursor:"pointer"},children:["Layout: ",s==="offset"?"Offset":"Linear"]}),x.jsxs("button",{onClick:D,style:{padding:"8px 12px",backgroundColor:c?"#fef3c7":"#fff",border:`1px solid ${c?"#f59e0b":"#ccc"}`,borderRadius:"4px",marginRight:"10px",cursor:"pointer"},children:["Chicken Feet: ",c?"On":"Off"]}),x.jsxs("button",{onClick:()=>v(T?void 0:U),style:{padding:"8px 12px",backgroundColor:T?"#fef3c7":"#fff",border:`1px solid ${T?"#f59e0b":"#ccc"}`,borderRadius:"4px",cursor:"pointer"},children:["Pip Colors: ",T?"On":"Off"]})]}),x.jsxs("div",{style:{position:"absolute",top:"10px",right:"10px",zIndex:100,backgroundColor:"rgba(255,255,255,0.8)",padding:"8px",borderRadius:"4px",fontSize:"14px"},children:[x.jsxs("div",{children:["Engine: Double-",i.engineValue]}),x.jsxs("div",{children:["Players: ",i.playerCount]})]}),x.jsx(He,{playerCount:i.playerCount,centerX:S,centerY:P,radius:80,engineValue:i.engineValue,trains:i.trains,layoutStyle:s,tableWidth:t,tableHeight:o,pipColors:p})]})},ie=90;function te(e,t=ie){return e==="right"?t:-t}function Ie(e){return e==="right"?"left":"right"}function Zt(e,t){return(t??V(e))>=0?"left":"right"}function Jt(e,t,o){const{perpX:n,perpY:r}=B(t),i=(a,c)=>{let d=1/0;return a>0?d=Math.min(d,(o.width-e.x)/a):a<0&&(d=Math.min(d,(0-e.x)/a)),c>0?d=Math.min(d,(o.height-e.y)/c):c<0&&(d=Math.min(d,(0-e.y)/c)),Number.isFinite(d)?Math.max(0,d):1/0},l=i(n,r),s=i(-n,-r);return l>=s?"right":"left"}function Pe(e,t){return me(ne({startX:t.startX,startY:t.startY,angle:t.angle,branch:e,layoutStyle:t.layoutStyle}))}function q(e,t,o){const n=(e??[]).filter(r=>r.index!==t);return o===null?n:[...n,{index:t,turn:o}].sort((r,i)=>r.index-i.index)}function Qt({branch:e,index:t,build:o,obstacles:n,preferredSide:r,degrees:i=ie}){const l=[r,Ie(r)];for(const s of l){const a=te(s,i),c={...e,bends:q(e.bends,t,a)},d=Pe(c,o);if(!ye(d)&&!be(d,n))return{turn:a}}return{turn:null,reason:"blocked"}}function Wt(e,t,o,n,r,i=ie){const l=(e.bends??[]).find(u=>u.index===t),s=te(r,i),a=te(Ie(r),i),c=u=>{const f={...e,bends:q(e.bends,t,u)},p=Pe(f,o);return!ye(p)&&!be(p,n)};let d;l?l.turn===s?d=[a,null]:l.turn===a?d=[null]:d=[s,a,null]:d=[s,a];for(const u of d){if(u===null)return{bends:q(e.bends,t,null),changed:!0,blocked:!1};if(c(u))return{bends:q(e.bends,t,u),changed:!0,blocked:!1}}return{bends:e.bends??[],changed:!1,blocked:!0}}function le(e,t,o){return Math.min(o,Math.max(t,e))}function fe(e,t,o,n,r){const i=le(e.scale*t,n,r),l=i/e.scale;return{scale:i,x:o.x-(o.x-e.x)*l,y:o.y-(o.y-e.y)*l}}function Qe(e,t,o,n,r){const i=Math.max(1,e.width),l=Math.max(1,e.height),s=Math.min((t.width-o*2)/i,(t.height-o*2)/l),a=le(s,n,r);return{scale:a,x:(t.width-i*a)/2,y:(t.height-l*a)/2}}function en(e,t){return{x:(t.x-e.x)/e.scale,y:(t.y-e.y)/e.scale}}const tn=3,nn=({width:e,height:t,contentWidth:o,contentHeight:n,children:r,minScale:i=.2,maxScale:l=4,zoomStep:s=1.15,padding:a=40,background:c="#1f8a55",showControls:d=!0,testId:u="viewport"})=>{const f=M.useRef(null),p=M.useCallback(()=>Qe({width:o,height:n},{width:e,height:t},a,i,l),[o,n,e,t,a,i,l]),[v,T]=M.useState(p);M.useEffect(()=>{T(p())},[p]);const S=M.useRef(null),P=(h,m)=>{const z=f.current?.getBoundingClientRect();return{x:h-(z?.left??0),y:m-(z?.top??0)}},y=h=>{S.current={pointerX:h.clientX,pointerY:h.clientY,startX:v.x,startY:v.y,moved:!1}},D=h=>{const m=S.current;if(!m)return;const z=h.clientX-m.pointerX,O=h.clientY-m.pointerY;!m.moved&&Math.hypot(z,O)<tn||(m.moved=!0,f.current?.setPointerCapture?.(h.pointerId),T(X=>({...X,x:m.startX+z,y:m.startY+O})))},g=h=>{S.current?.moved&&h.preventDefault(),S.current=null},I=h=>{h.preventDefault();const m=P(h.clientX,h.clientY),z=h.deltaY<0?s:1/s;T(O=>fe(O,z,m,i,l))},C=h=>T(m=>fe(m,h,{x:e/2,y:t/2},i,l)),b={width:32,height:32,fontSize:18,lineHeight:"30px",textAlign:"center",cursor:"pointer",background:"#fff",border:"1px solid #d1d5db",borderRadius:6,userSelect:"none"};return x.jsxs("div",{ref:f,"data-testid":u,onPointerDown:y,onPointerMove:D,onPointerUp:g,onPointerLeave:g,onWheel:I,style:{position:"relative",width:e,height:t,overflow:"hidden",background:c,borderRadius:8,cursor:"grab",touchAction:"none"},children:[x.jsx("div",{"data-testid":`${u}-content`,style:{position:"absolute",left:0,top:0,transformOrigin:"0 0",transform:`translate(${v.x}px, ${v.y}px) scale(${v.scale})`},children:r}),d&&x.jsxs("div",{"data-testid":`${u}-controls`,style:{position:"absolute",right:12,bottom:12,display:"flex",flexDirection:"column",gap:6},children:[x.jsx("div",{style:b,role:"button","aria-label":"Zoom in",onClick:()=>C(s),children:"+"}),x.jsx("div",{style:b,role:"button","aria-label":"Zoom out",onClick:()=>C(1/s),children:"−"}),x.jsx("div",{style:{...b,fontSize:12,lineHeight:"30px"},role:"button","aria-label":"Reset view",onClick:()=>T(p()),children:"⤢"}),x.jsx("div",{"data-testid":`${u}-zoom-readout`,style:{...b,fontSize:11,cursor:"default"},children:Math.round(le(v.scale,i,l)*100)})]})]})},on=[{id:"regular-after-double",name:"Regular after double",description:"Double followed by a two-tile offset run",angle:0,dominoes:[{value1:12,value2:6},{value1:6,value2:6},{value1:6,value2:3},{value1:3,value2:1}],layoutStyles:["linear","offset"]},{id:"double-after-regular",name:"Double after regular",description:"Offset run, a double, then another offset run",angle:0,dominoes:[{value1:12,value2:9},{value1:9,value2:4},{value1:4,value2:4},{value1:4,value2:2},{value1:2,value2:7}],layoutStyles:["linear","offset"]},{id:"double-after-double",name:"Double after double",description:"Offset runs at the head, middle, and tail around two doubles",angle:90,dominoes:[{value1:12,value2:7},{value1:7,value2:8},{value1:8,value2:8},{value1:8,value2:3},{value1:3,value2:5},{value1:5,value2:5},{value1:5,value2:2},{value1:2,value2:1}],layoutStyles:["linear","offset"]},{id:"offset-zigzag",name:"Offset zigzag",description:"Alternating perpendicular tiles without doubles",angle:0,dominoes:[{value1:12,value2:5},{value1:5,value2:9},{value1:9,value2:2},{value1:2,value2:7},{value1:7,value2:1}],layoutStyles:["offset"]},{id:"horizontal-open",name:"Horizontal train",description:"Rightward train: offset head, double, offset tail",angle:0,dominoes:[{value1:5,value2:12},{value1:12,value2:11},{value1:11,value2:11},{value1:11,value2:6},{value1:6,value2:2}],layoutStyles:["linear","offset"]},{id:"vertical-open",name:"Vertical train",description:"Downward train: offset head, double, offset tail",angle:90,dominoes:[{value1:3,value2:12},{value1:12,value2:10},{value1:10,value2:10},{value1:10,value2:4},{value1:4,value2:1}],layoutStyles:["linear","offset"]}],rn=[{id:"single-foot",name:"Single foot",description:"A double fans two angled toes (±45°) while the main line continues straight as the center toe",angle:0,branch:{dominoes:[{value1:12,value2:6},{value1:6,value2:6},{value1:6,value2:3},{value1:3,value2:1}],feet:{1:[{dominoes:[{value1:6,value2:2},{value1:2,value2:5}]},{dominoes:[{value1:6,value2:4},{value1:4,value2:0}]}]}},layoutStyles:["linear","offset"]},{id:"foot-no-center",name:"Foot at the tail",description:"Double ends the main line, so both side toes are present with no straight continuation",angle:0,branch:{dominoes:[{value1:9,value2:7},{value1:7,value2:7}],feet:{1:[{dominoes:[{value1:7,value2:3},{value1:3,value2:8}]},{dominoes:[{value1:7,value2:5},{value1:5,value2:0}]}]}},layoutStyles:["linear","offset"]},{id:"nested-foot",name:"Nested foot",description:"A side toe contains its own double, which sprouts a second-level foot",angle:90,branch:{dominoes:[{value1:12,value2:8},{value1:8,value2:8},{value1:8,value2:3}],feet:{1:[{dominoes:[{value1:8,value2:5},{value1:5,value2:5},{value1:5,value2:2}],feet:{1:[{dominoes:[{value1:5,value2:9},{value1:9,value2:1}]},{dominoes:[{value1:5,value2:4},{value1:4,value2:6}]}]}},{dominoes:[{value1:8,value2:1},{value1:1,value2:7}]}]}},layoutStyles:["linear","offset"]}],W={maxPips:12,engineValue:12,allowConsecutiveDoubles:!1,requireUniqueTiles:!0,requireSequential:!0,doubleObligation:"cover",chickenFoot:{toeCount:3,sideToeAngles:[-45,45]}};function We(e){switch(e.doubleObligation){case"chicken-foot":return Math.max(1,e.chickenFoot.toeCount);case"cover":return 1;default:return 0}}function et(e){return e.doubleObligation==="chicken-foot"?Math.max(0,e.chickenFoot.toeCount-1):0}function ln(e={}){const t=e.maxPips??W.maxPips;return{...W,...e,maxPips:t,engineValue:e.engineValue??t,chickenFoot:{...W.chickenFoot,...e.chickenFoot??{}}}}function tt(e,t){let o=e;for(const n of t)if(o=o?.feet?.[n.doubleIndex]?.[n.toeIndex],!o)return;return o}function se(e,t,o){if(o(e,t),!!e.feet)for(const n of Object.keys(e.feet)){const r=Number(n);e.feet[r].forEach((i,l)=>{se(i,[...t,{doubleIndex:r,toeIndex:l}],o)})}}function sn(e){const t=[];return se(e,[],(o,n)=>{o.dominoes.forEach((r,i)=>{if(r.value1!==r.value2)return;const l=i<o.dominoes.length-1,s=o.feet?.[i]?.length??0;t.push({path:n,doubleIndex:i,value:r.value1,hasCenter:l,sideToes:s,answers:(l?1:0)+s})})}),t}function nt(e,t){const o=We(t);return o<=0?[]:sn(e).filter(n=>n.answers<o)}function ze(e){const t=new Set;return se(e,[],o=>{for(const n of o.dominoes)t.add(Y(n))}),t}function ot(e,t,o){if(e.dominoes.length===0)return[{path:[],attach:"run-tail",value:t,attachToDouble:!0,obligation:!1}];const n=nt(e,o);if(o.doubleObligation!=="none"&&n.length>0){const i=et(o),l=[];for(const s of n){const a=tt(e,s.path);a&&(!s.hasCenter&&s.doubleIndex===a.dominoes.length-1&&l.push({path:s.path,attach:"run-tail",value:s.value,attachToDouble:!0,obligation:!0}),s.sideToes<i&&l.push({path:s.path,attach:"side-toe",value:s.value,doubleIndex:s.doubleIndex,toeSlot:s.sideToes,attachToDouble:!0,obligation:!0}))}return l}const r=[];return se(e,[],(i,l)=>{const s=i.dominoes[i.dominoes.length-1];s&&r.push({path:l,attach:"run-tail",value:s.value2,attachToDouble:we(s),obligation:!1})}),r}function De(e,t,o,n){const r=[],i=re(e,t.value);return n.requireSequential&&!i&&r.push("value-mismatch"),n.requireUniqueTiles&&o.has(Y(e))&&r.push("duplicate-tile"),!n.allowConsecutiveDoubles&&t.attachToDouble&&we(e)&&r.push("consecutive-doubles"),{legal:r.length===0,violations:r}}function rt(e,t,o,n,r){const i=ot(e,t,r),l=[];for(const s of i)for(const a of o)De(a,s,n,r).legal&&l.push({end:s,tile:a});return l}function it(e,t,o){if(t.length===0)return o(e);const[n,...r]=t,l=(e.feet?.[n.doubleIndex]??[]).map((s,a)=>a===n.toeIndex?it(s,r,o):s);return{...e,feet:{...e.feet,[n.doubleIndex]:l}}}function lt(e,t,o){const n=re(t.tile,t.end.value)??{...t.tile};return it(e,t.end.path,r=>{if(t.end.attach==="run-tail")return{...r,dominoes:[...r.dominoes,n]};const i=t.end.doubleIndex??0,l=t.end.toeSlot??r.feet?.[i]?.length??0,s=r.feet?.[i]?[...r.feet[i]]:[];return s[l]={dominoes:[n]},{...r,feet:{...r.feet,[i]:s}}})}function an(e,t,o){const n=De(t.tile,t.end,ze(e),o);return n.legal?{ok:!0,board:lt(e,t),violations:[]}:{ok:!1,board:e,violations:n.violations}}function _(e){return e.kind==="play"}function st(e){const t=[];return e.trains.forEach((o,n)=>{(o.playerId===e.selfPlayerId||o.isPublic)&&t.push(n)}),t}function Me(e){const t=new Set;for(const o of e)for(const n of ze(o))t.add(n);return t}function at(e){const t=Me(e.trains),o=[];for(const n of st(e)){const r=e.trains[n],i=rt(r,e.engineValue,e.hand,t,e.rules);for(const l of i)o.push({kind:"play",trainIndex:n,move:l})}return o}function ut(e={}){return t=>{const o=at(t),n=[...o];return(t.drawPileSize??Number.POSITIVE_INFINITY)>0&&!t.turnDrawUsed&&(o.length===0||e.allowOptionalDraw)&&n.push({kind:"draw"}),n.length===0&&n.push({kind:"pass"}),n}}const ct=ut(),R={preferPlay:"prefer-play",dumpPips:"dump-pips",doublesEarly:"play-doubles-early",ownTrain:"own-train",obligationRelief:"obligation-relief",handFlexibility:"hand-flexibility",defensivePublic:"defensive-public"};function dt(e){const t=re(e.move.tile,e.move.end.value);return t?t.value2:e.move.tile.value2}const un={id:R.preferPlay,score(e){return e.kind==="play"?100:e.kind==="draw"?0:-50}},cn={id:R.dumpPips,score(e){if(!_(e))return 0;const t=e.move.tile;return t.value1+t.value2}},dn={id:R.doublesEarly,score(e,t){if(!_(e))return 0;const o=e.move.tile;return o.value1!==o.value2?0:Math.min(t.obs.hand.length,12)}},fn={id:R.ownTrain,score(e,t){if(!_(e))return 0;const o=t.obs.trains[e.trainIndex];return o&&o.playerId===t.obs.selfPlayerId?8:0}},pn={id:R.obligationRelief,score(e){return _(e)&&e.move.end.obligation?10:0}},hn={id:R.handFlexibility,score(e,t){if(!_(e))return 0;const o=dt(e),n=Y(e.move.tile);let r=!1,i=0;for(const l of t.obs.hand){if(!r&&Y(l)===n){r=!0;continue}Te(l,o)&&i++}return i*3}},xn={id:R.defensivePublic,score(e,t){if(!_(e))return 0;const o=t.obs.trains[e.trainIndex];if(!o||o.playerId===t.obs.selfPlayerId)return 0;const n=dt(e);let r=0;for(const i of t.unseen)Te(i,n)&&r++;return-r}},ft=[un,cn,dn,fn,pn,hn,xn],w=R,pt={beginner:{id:"beginner",temperature:2.5,blunderRate:.25,lookaheadDepth:0,enabled:new Set([w.preferPlay,w.dumpPips]),weights:{[w.preferPlay]:1,[w.dumpPips]:.2}},intermediate:{id:"intermediate",temperature:.6,blunderRate:.05,lookaheadDepth:0,enabled:new Set([w.preferPlay,w.dumpPips,w.doublesEarly,w.ownTrain]),weights:{[w.preferPlay]:1,[w.dumpPips]:1,[w.doublesEarly]:1,[w.ownTrain]:1}},advanced:{id:"advanced",temperature:.15,blunderRate:0,lookaheadDepth:0,enabled:new Set([w.preferPlay,w.dumpPips,w.doublesEarly,w.ownTrain,w.obligationRelief,w.handFlexibility,w.defensivePublic]),weights:{[w.preferPlay]:1,[w.dumpPips]:1.2,[w.doublesEarly]:1.5,[w.ownTrain]:1,[w.obligationRelief]:1.5,[w.handFlexibility]:1,[w.defensivePublic]:1.5}}};function gn(e){return pt[e]}function ht(e,t,o,n){let r=0;for(const i of n.enabled){const l=o.get(i);if(!l)continue;const s=n.weights[i]??1;r+=s*l.score(e,t)}return r}function xt(e,t){let o=Number.NEGATIVE_INFINITY,n=[];return e.forEach((r,i)=>{r>o?(o=r,n=[i]):r===o&&n.push(i)}),n[Math.floor(t()*n.length)]}function gt(e,t,o){const n=Math.max(...e),r=e.map(s=>Math.exp((s-n)/t)),i=r.reduce((s,a)=>s+a,0);let l=o()*i;for(let s=0;s<r.length;s++)if(l-=r[s],l<=0)return s;return r.length-1}function vt(e,t,o){return e.length<=1?0:t.temperature<=0?xt(e,o):gt(e,t.temperature,o)}function bt(e){const{skill:t,generateCandidates:o,buildContext:n,fallback:r}=e,i=e.rng??Math.random,l=new Map(e.heuristics.map(s=>[s.id,s]));return{decide(s){const a=o(s);if(a.length===0)return r(s);if(a.length===1)return a[0];if(t.blunderRate>0&&i()<t.blunderRate)return a[Math.floor(i()*a.length)];const c=n(s,a),d=a.map(u=>ht(u,c,l,t));return a[vt(d,t,i)]}}}function vn(e,t,o){const n=Me(e.trains),r=new Set(n);for(const l of e.hand)r.add(Y(l));const i=Ze(e.rules.maxPips).filter(l=>!r.has(Y(l)));return{obs:e,playedKeys:n,candidates:t,playCandidates:t.filter(_),unseen:i,rng:o}}function bn(e){const t=e.heuristics??ft,o=e.generateCandidates??ct,n=e.rng??Math.random;return bt({skill:e.skill,heuristics:t,generateCandidates:o,buildContext:(r,i)=>vn(r,i,n),fallback:()=>({kind:"pass"}),rng:n})}function yt(e,t,o){let n=t.legalActions(e);return t.orderActions&&(n=t.orderActions(e,n)),Number.isFinite(o)&&n.length>o&&(n=n.slice(0,o)),n}function mt(e,t,o,n,r){if(o<=0||t.isTerminal(e))return t.evaluate(e,n);const i=yt(e,t,r);if(i.length===0)return t.evaluate(e,n);const l=t.currentPlayer(e)===n;let s=l?Number.NEGATIVE_INFINITY:Number.POSITIVE_INFINITY;for(const a of i){const c=mt(t.applyAction(e,a),t,o-1,n,r);s=l?Math.max(s,c):Math.min(s,c)}return s}function yn(e,t,o){const n=o.rng??Math.random,r=Math.max(1,o.determinizations??1),i=o.maxBranch??Number.POSITIVE_INFINITY,l=Math.max(1,o.depth);return yt(e,t,i).map(a=>{let c=0;for(let d=0;d<r;d++){const u=t.determinize?t.determinize(e,o.perspective,n):e;c+=mt(t.applyAction(u,a),t,l-1,o.perspective,i)}return{action:a,value:c/r}})}exports.CHICKEN_FOOT_FIXTURES=rn;exports.CHICKEN_FOOT_TOE_ANGLES=Ne;exports.DEFAULT_DOMINO_THEME=$e;exports.DEFAULT_HEURISTICS=ft;exports.DEFAULT_PIP_COLORS=U;exports.DEFAULT_RULES=W;exports.DefaultPip=Re;exports.DominoHub=He;exports.DominoThemeProvider=Tt;exports.DominoTrain=Ke;exports.DoubleTwelve=ge;exports.HEURISTIC_IDS=R;exports.MexicanTrainGame=Ht;exports.PIP_COLORS=Pt;exports.PIP_LAYOUTS=je;exports.SKILL_PRESETS=pt;exports.TRAIN_FIXTURES=on;exports.TURN_DEGREES=ie;exports.Viewport=nn;exports.applyMove=lt;exports.argmaxIndex=xt;exports.buildBranchTiles=Pe;exports.chooseActionIndex=vt;exports.clampScale=le;exports.collectAllPlayedKeys=Me;exports.collectPlayedKeys=ze;exports.computeTrainLayout=Be;exports.computeTrainTree=ne;exports.createAiPlayer=bn;exports.createCandidateGenerator=ut;exports.createPolicyPlayer=bt;exports.cycleBendAt=Wt;exports.defaultCandidateGenerator=ct;exports.dominoKey=Y;exports.dominoSetSize=Xt;exports.evaluatePlacement=De;exports.fitToBounds=Qe;exports.flattenSegments=me;exports.generateDominoSet=Ze;exports.generatePlayActions=at;exports.generateSampleTrains=Je;exports.getAccessibleTrainIndices=st;exports.getBranchAt=tt;exports.getLegalMoves=rt;exports.getOpenEnds=ot;exports.getPipLayout=_e;exports.getPipStyle=Dt;exports.getSkillProfile=gn;exports.getTrainLayoutBounds=Ot;exports.getUnsatisfiedDoubles=nt;exports.headingAtIndex=Xe;exports.hubTrainStartDistance=qe;exports.isDouble=we;exports.isPlayAction=_;exports.layoutSelfIntersects=ye;exports.layoutsCollide=be;exports.linearDefaultSide=Jt;exports.mergeDominoTheme=pe;exports.mergePipColors=zt;exports.normalizeBends=ve;exports.offsetDefaultSide=Zt;exports.oppositeSide=Ie;exports.orientForConnection=re;exports.otherEnd=Yt;exports.outwardPerpSign=V;exports.playMove=an;exports.requiredDoubleAnswers=We;exports.resolveBend=Qt;exports.resolvePipPosition=Fe;exports.resolvePipStyle=xe;exports.resolveRules=ln;exports.scoreWithHeuristics=ht;exports.screenToContent=en;exports.searchActionValues=yn;exports.sideToTurn=te;exports.sideToeSlots=et;exports.softmaxIndex=gt;exports.stepAlongTrain=A;exports.themeDataAttributes=Oe;exports.tileCorners=de;exports.tileHasValue=Te;exports.tileKey=L;exports.tilesOverlap=H;exports.useDominoTheme=he;exports.validateChickenFootChain=Ge;exports.validateTrainLayout=_t;exports.validateTrainTree=Nt;exports.withBendAt=q;exports.zoomAt=fe;
2
2
  //# sourceMappingURL=index.cjs.map