doubletwelve 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jessica Mulein
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # doubletwelve
2
+
3
+ Double-12 Mexican Train dominoes for React — tile rendering, train layout (linear, offset, chicken foot), and a configurable rules engine.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install doubletwelve
9
+ # peer dependency
10
+ npm install react react-dom
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```tsx
16
+ import { DoubleTwelve, MexicanTrainGame, DEFAULT_PIP_COLORS } from 'doubletwelve';
17
+
18
+ // Single tile
19
+ <DoubleTwelve value1={6} value2={3} pipColors={DEFAULT_PIP_COLORS} />
20
+
21
+ // Full demo table (generates sample trains on mount)
22
+ <MexicanTrainGame width={1200} height={800} />
23
+ ```
24
+
25
+ ## What's included
26
+
27
+ | Layer | Exports |
28
+ |-------|---------|
29
+ | **Components** | `DoubleTwelve`, `DominoTrain`, `DominoHub`, `MexicanTrainGame` |
30
+ | **Layout** | `computeTrainLayout`, `computeTrainTree`, overlap detection, bounds |
31
+ | **Rules** | `playMove`, `getLegalMoves`, `resolveRules`, domino set helpers |
32
+ | **Theming** | `DEFAULT_PIP_COLORS`, `mergePipColors`, pip layout grids |
33
+ | **Validation** | `validateTrainLayout`, `validateTrainTree`, test fixtures |
34
+
35
+ ## Layout styles
36
+
37
+ - **`linear`** — tiles run straight along the train axis
38
+ - **`offset`** — brick-style zigzag in two fixed rows; doubles center inbound/outbound tiles
39
+ - **Chicken foot** — doubles fan three toes (±45° + center); recursive via `computeTrainTree`
40
+
41
+ ## Development (this repo)
42
+
43
+ ```bash
44
+ yarn install
45
+ yarn start # demo app + visual harnesses at localhost:4200
46
+ yarn test # unit tests (176)
47
+ yarn build:lib # npm package → dist/
48
+ yarn pack:dry-run # preview the published tarball
49
+ ```
50
+
51
+ Harness routes (local demo): `/harness`, `/harness/trains`, `/harness/chicken-foot?layout=offset`
52
+
53
+ ## Publish to npm
54
+
55
+ This package is configured to publish to the public registry at [npmjs.com](https://www.npmjs.com/package/doubletwelve).
56
+
57
+ ```bash
58
+ # one-time: log in (opens browser or prompts for OTP)
59
+ npm login --registry=https://registry.npmjs.org/
60
+
61
+ # dry run
62
+ yarn pack:dry-run
63
+
64
+ # publish (runs tests + build:lib via prepublishOnly)
65
+ npm publish
66
+ ```
67
+
68
+ `publishConfig.registry` in `package.json` and `.npmrc` both point at `https://registry.npmjs.org/`, so publish won't accidentally go to GitHub Packages or another registry even if your global npm config differs.
69
+
70
+ **CI / automation:** set an npm automation token as `NPM_TOKEN` and run:
71
+
72
+ ```bash
73
+ npm publish --provenance --access public
74
+ ```
75
+
76
+ (Provenance is optional but recommended for supply-chain transparency on npm.)
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1,9 @@
1
+ import { FC } from 'react';
2
+ import { PipColorMap } from './pipColors';
3
+ interface DominoHalfProps {
4
+ value: number;
5
+ pipColor: string;
6
+ pipColors?: PipColorMap;
7
+ }
8
+ export declare const DominoHalf: FC<DominoHalfProps>;
9
+ export default DominoHalf;
@@ -0,0 +1,17 @@
1
+ import { FC } from 'react';
2
+ import { TrainData } from '../game/TrainData';
3
+ import { PipColorMap } from './pipColors';
4
+ interface DominoHubProps {
5
+ playerCount: number;
6
+ centerX: number;
7
+ centerY: number;
8
+ radius: number;
9
+ engineValue: number;
10
+ trains: TrainData[];
11
+ layoutStyle: 'offset' | 'linear';
12
+ tableWidth: number;
13
+ tableHeight: number;
14
+ pipColors?: PipColorMap;
15
+ }
16
+ export declare const DominoHub: FC<DominoHubProps>;
17
+ export default DominoHub;
@@ -0,0 +1,17 @@
1
+ import { FC } from 'react';
2
+ import { TrainData } from '../game/TrainData';
3
+ import { PipColorMap } from './pipColors';
4
+ interface DominoTrainProps {
5
+ startX: number;
6
+ startY: number;
7
+ angle: number;
8
+ trainData: TrainData;
9
+ layoutStyle: 'offset' | 'linear';
10
+ tableWidth: number;
11
+ tableHeight: number;
12
+ centerX: number;
13
+ centerY: number;
14
+ pipColors?: PipColorMap;
15
+ }
16
+ export declare const DominoTrain: FC<DominoTrainProps>;
17
+ export default DominoTrain;
@@ -0,0 +1,19 @@
1
+ import { FC } from 'react';
2
+ import { PipColorMap } from './pipColors';
3
+ export interface DoubleTwelveProps {
4
+ /** Pip count on the top half (0–12). Defaults to 0 (blank). */
5
+ value1?: number;
6
+ /** Pip count on the bottom half (0–12). Defaults to 0 (blank). */
7
+ value2?: number;
8
+ width?: number;
9
+ height?: number;
10
+ backgroundColor?: string;
11
+ /** Fallback pip color when pipColors is not set. */
12
+ pipColor?: string;
13
+ /** Per-value pip colors. Pass DEFAULT_PIP_COLORS or a custom/merged map. */
14
+ pipColors?: PipColorMap;
15
+ borderColor?: string;
16
+ rotation?: number;
17
+ }
18
+ export declare const DoubleTwelve: FC<DoubleTwelveProps>;
19
+ export default DoubleTwelve;
@@ -0,0 +1,12 @@
1
+ import { FC } from 'react';
2
+ import { GameState } from '../game/GameState';
3
+ import { PipColorMap } from './pipColors';
4
+ interface MexicanTrainGameProps {
5
+ initialState?: GameState;
6
+ width?: number;
7
+ height?: number;
8
+ pipColors?: PipColorMap;
9
+ onPipColorsChange?: (pipColors: PipColorMap | undefined) => void;
10
+ }
11
+ export declare const MexicanTrainGame: FC<MexicanTrainGameProps>;
12
+ export default MexicanTrainGame;
@@ -0,0 +1,9 @@
1
+ import { FC } from 'react';
2
+ interface MexicanTrainHubProps {
3
+ centerX: number;
4
+ centerY: number;
5
+ radius: number;
6
+ numberOfDominoes: number;
7
+ }
8
+ export declare const MexicanTrainHub: FC<MexicanTrainHubProps>;
9
+ export default MexicanTrainHub;
@@ -0,0 +1,13 @@
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>;
13
+ export default Pip;
@@ -0,0 +1,9 @@
1
+ import { FC } from 'react';
2
+ import { PipColorMap } from './pipColors';
3
+ interface PipPatternProps {
4
+ value: number;
5
+ pipColor: string;
6
+ pipColors?: PipColorMap;
7
+ }
8
+ export declare const PipPattern: FC<PipPatternProps>;
9
+ export default PipPattern;
@@ -0,0 +1,16 @@
1
+ export interface PipColorStyle {
2
+ color: string;
3
+ hollow?: boolean;
4
+ }
5
+ /** Partial map of domino values (0–12) to pip styles. */
6
+ export type PipColorMap = Partial<Record<number, PipColorStyle>>;
7
+ /** Standard double-12 domino pip colors by value. */
8
+ export declare const DEFAULT_PIP_COLORS: PipColorMap;
9
+ /** @deprecated Use DEFAULT_PIP_COLORS instead. */
10
+ export declare const PIP_COLORS: Partial<Record<number, PipColorStyle>>;
11
+ /** Merge custom overrides onto the default double-12 color set. */
12
+ export declare function mergePipColors(overrides?: PipColorMap): PipColorMap;
13
+ /** Resolve the pip style for a value when colored pips are enabled. */
14
+ export declare function resolvePipStyle(value: number, pipColors?: PipColorMap): PipColorStyle | undefined;
15
+ /** @deprecated Use resolvePipStyle instead. */
16
+ export declare function getPipStyle(value: number): PipColorStyle;
@@ -0,0 +1,14 @@
1
+ export type PipGridSize = '3x3' | '3x4' | '4x3';
2
+ export interface PipLayoutCell {
3
+ row: number;
4
+ col: number;
5
+ gridSize: PipGridSize;
6
+ top?: string;
7
+ left?: string;
8
+ }
9
+ export declare function resolvePipPosition(cell: PipLayoutCell): {
10
+ top: string;
11
+ left: string;
12
+ width: string;
13
+ height: string;
14
+ };
@@ -0,0 +1,4 @@
1
+ import { PipLayoutCell } from './pipGrid';
2
+ /** Canonical double-12 pip layouts (0–12). */
3
+ export declare const PIP_LAYOUTS: Record<number, readonly PipLayoutCell[]>;
4
+ export declare function getPipLayout(value: number): readonly PipLayoutCell[];
@@ -0,0 +1,144 @@
1
+ import { DominoValue } from '../game/DominoValue';
2
+ import { TrainBranch } from '../game/TrainData';
3
+ export declare const DOMINO_WIDTH = 60;
4
+ export declare const DOMINO_HEIGHT = 120;
5
+ /**
6
+ * Side-toe angles (degrees) relative to the branch direction for a chicken-foot
7
+ * double. The 0° center toe is the straight main-line continuation and is not
8
+ * listed here; these are the two angled toes that fan off the double's open end.
9
+ */
10
+ export declare const CHICKEN_FOOT_TOE_ANGLES: readonly [-45, 45];
11
+ export type TrainLayoutStyle = 'offset' | 'linear';
12
+ export interface TrainLayoutEntry {
13
+ x: number;
14
+ y: number;
15
+ rotation: number;
16
+ isDouble: boolean;
17
+ value1: number;
18
+ value2: number;
19
+ }
20
+ export interface ComputeTrainLayoutInput {
21
+ startX: number;
22
+ startY: number;
23
+ angle: number;
24
+ dominoes: readonly DominoValue[];
25
+ layoutStyle: TrainLayoutStyle;
26
+ dominoWidth?: number;
27
+ dominoHeight?: number;
28
+ /**
29
+ * Distance from (startX, startY) to the center of the first tile, along the
30
+ * train direction. Defaults to a small hub gap; chicken-foot toes pass half a
31
+ * domino-height so the first toe tile butts against the host double's far end.
32
+ */
33
+ leadGap?: number;
34
+ /**
35
+ * Which side the offset zigzag seeds on (+1 / -1). Defaults to the natural
36
+ * outward side for `angle`. Chicken-foot toes override this so each toe's
37
+ * zigzag starts toward the outside of the foot, clear of the center row.
38
+ */
39
+ outwardSign?: number;
40
+ /**
41
+ * Offset mode only: index of a chicken-foot double that should act as a
42
+ * centered hub. The double and the tile feeding into it are snapped onto the
43
+ * train axis (perp 0) so the inbound tile reads as centered on the double and
44
+ * the offset center toe fans out symmetrically — which lets the two angled
45
+ * toes sit at equal, close distances on either side.
46
+ */
47
+ hubIndex?: number;
48
+ }
49
+ export declare function halfExtentAlongTrain(isDouble: boolean, dominoWidth?: number, dominoHeight?: number): number;
50
+ export declare function stepAlongTrain(fromIsDouble: boolean, toIsDouble: boolean, dominoWidth?: number, dominoHeight?: number): number;
51
+ export declare function trainDirection(angle: number): {
52
+ dirX: number;
53
+ dirY: number;
54
+ };
55
+ export declare function trainPerpendicular(angle: number): {
56
+ perpX: number;
57
+ perpY: number;
58
+ };
59
+ export declare function orientDominoValues(dominoes: DominoValue[], layoutStyle: TrainLayoutStyle): DominoValue[];
60
+ export declare function outwardPerpSign(angle: number): number;
61
+ export declare function nextPerpOffset(current: number, outwardSign: number): number;
62
+ export declare function computeTrainLayout({ startX, startY, angle, dominoes, layoutStyle, dominoWidth, dominoHeight, leadGap, outwardSign: outwardSignInput, hubIndex, }: ComputeTrainLayoutInput): TrainLayoutEntry[];
63
+ /** The four world-space corners of a tile (its rotated rectangle). */
64
+ export declare function tileCorners(entry: TrainLayoutEntry, dominoWidth?: number, dominoHeight?: number): Array<{
65
+ x: number;
66
+ y: number;
67
+ }>;
68
+ /**
69
+ * True when two tiles physically overlap (separating-axis test on their rotated
70
+ * rectangles). Tiles that merely touch (within `epsilon`) are not overlapping,
71
+ * so legitimately adjacent dominoes — bricked, end-to-end, or butted against a
72
+ * double — pass cleanly while real collisions are caught.
73
+ */
74
+ export declare function tilesOverlap(a: TrainLayoutEntry, b: TrainLayoutEntry, epsilon?: number, dominoWidth?: number, dominoHeight?: number): boolean;
75
+ /**
76
+ * A single straight run of dominoes within a chicken-foot tree: the main line
77
+ * or one toe. `depth` is 0 for the main line, 1 for its toes, and so on.
78
+ */
79
+ export interface TrainSegment {
80
+ angle: number;
81
+ depth: number;
82
+ layoutStyle: TrainLayoutStyle;
83
+ /** Outward side this segment's zigzag seeds on (needed to re-derive layout). */
84
+ outwardSign: number;
85
+ dominoes: readonly DominoValue[];
86
+ layout: TrainLayoutEntry[];
87
+ /** Anchor point this segment hangs off (host double's open end), if any. */
88
+ anchor?: {
89
+ x: number;
90
+ y: number;
91
+ };
92
+ }
93
+ export interface ComputeTrainTreeInput {
94
+ startX: number;
95
+ startY: number;
96
+ angle: number;
97
+ branch: TrainBranch;
98
+ layoutStyle: TrainLayoutStyle;
99
+ dominoWidth?: number;
100
+ dominoHeight?: number;
101
+ leadGap?: number;
102
+ depth?: number;
103
+ anchor?: {
104
+ x: number;
105
+ y: number;
106
+ };
107
+ outwardSign?: number;
108
+ /**
109
+ * Accumulator of every tile already placed in the tree. Toes are nudged
110
+ * outward until they clear everything in here, so no two dominoes overlap.
111
+ * Callers normally omit this; the recursion threads it through.
112
+ */
113
+ placed?: TrainLayoutEntry[];
114
+ /**
115
+ * Unit direction a toe may be nudged along (outward, parallel to the host
116
+ * double's open edge) to resolve overlaps. The trunk passes none.
117
+ */
118
+ pushAxis?: {
119
+ x: number;
120
+ y: number;
121
+ };
122
+ /**
123
+ * Minimum number of nudge steps to apply before checking for clearance. Both
124
+ * toes of a foot share this so they stay symmetric about the double even when
125
+ * only one side is crowded by the offset center toe.
126
+ */
127
+ minPushSteps?: number;
128
+ }
129
+ /**
130
+ * Lays out a branch and, recursively, the chicken-foot side toes hanging off any
131
+ * of its doubles. Returns a flat list of segments (main line first, then toes in
132
+ * depth-first order) so callers can render every tile and validate each run.
133
+ */
134
+ export declare function computeTrainTree({ startX, startY, angle, branch, layoutStyle, dominoWidth, dominoHeight, leadGap, depth, anchor, outwardSign, placed, pushAxis, minPushSteps, }: ComputeTrainTreeInput): TrainSegment[];
135
+ /** Flattens a list of segments into a single list of tiles for rendering. */
136
+ export declare function flattenSegments(segments: readonly TrainSegment[]): TrainLayoutEntry[];
137
+ export interface TrainLayoutBounds {
138
+ width: number;
139
+ height: number;
140
+ offsetX: number;
141
+ offsetY: number;
142
+ }
143
+ /** Bounding box for rendering a train layout on a felt canvas. */
144
+ export declare function getTrainLayoutBounds(layout: readonly TrainLayoutEntry[], padding?: number, dominoWidth?: number, dominoHeight?: number): TrainLayoutBounds;
@@ -0,0 +1,4 @@
1
+ export interface DominoValue {
2
+ value1: number;
3
+ value2: number;
4
+ }
@@ -0,0 +1,6 @@
1
+ import { TrainData } from './TrainData';
2
+ export interface GameState {
3
+ playerCount: number;
4
+ trains: TrainData[];
5
+ engineValue: number;
6
+ }
@@ -0,0 +1,22 @@
1
+ import { DominoValue } from './DominoValue';
2
+ /**
3
+ * A run of dominoes that can sprout chicken-foot side toes at its doubles.
4
+ *
5
+ * The structure is recursive: each side toe is itself a TrainBranch, so a toe
6
+ * can contain its own doubles and feet (nested feet).
7
+ */
8
+ export interface TrainBranch {
9
+ dominoes: DominoValue[];
10
+ /**
11
+ * Chicken-foot side toes, keyed by the index (within `dominoes`) of the double
12
+ * they hang off. A double fans three toes from its open end (-45°, 0°, +45°);
13
+ * the center (0°) toe is the straight main-line continuation and stays inline
14
+ * in `dominoes`, so only the two angled side toes are stored here (at most two
15
+ * entries: index 0 → -45°, index 1 → +45°).
16
+ */
17
+ feet?: Record<number, TrainBranch[]>;
18
+ }
19
+ export interface TrainData extends TrainBranch {
20
+ playerId: number;
21
+ isPublic: boolean;
22
+ }
@@ -0,0 +1,9 @@
1
+ import { TrainData } from './TrainData';
2
+ import { tileKey } from '../rules/dominoSet';
3
+ export { tileKey };
4
+ export interface GenerateSampleTrainsOptions {
5
+ /** Attach chicken-foot side toes (±45°) to every double. */
6
+ chickenFeet?: boolean;
7
+ }
8
+ /** Demo train generator that respects double-12 tile uniqueness constraints. */
9
+ export declare function generateSampleTrains(playerCount: number, engineValue?: number, options?: GenerateSampleTrainsOptions): TrainData[];
@@ -0,0 +1,10 @@
1
+ export interface DominoFixture {
2
+ id: string;
3
+ label: string;
4
+ value1: number;
5
+ value2: number;
6
+ rotation?: number;
7
+ }
8
+ export declare const DOUBLE_FIXTURES: DominoFixture[];
9
+ export declare const MIXED_FIXTURES: DominoFixture[];
10
+ export declare const ROTATION_FIXTURES: DominoFixture[];
@@ -0,0 +1,49 @@
1
+ import { DominoValue } from '../game/DominoValue';
2
+ import { TrainBranch } from '../game/TrainData';
3
+ import { TrainLayoutEntry, TrainLayoutStyle, TrainSegment } from '../app/trainLayout';
4
+ export interface LayoutValidationIssue {
5
+ code: string;
6
+ message: string;
7
+ index?: number;
8
+ }
9
+ export interface LayoutValidationResult {
10
+ valid: boolean;
11
+ issues: LayoutValidationIssue[];
12
+ }
13
+ export declare function projectOnTrainAxis(dx: number, dy: number, angle: number): number;
14
+ export declare function projectOnPerpendicularAxis(dx: number, dy: number, angle: number): number;
15
+ export declare function validateDominoChain(dominoes: readonly DominoValue[]): LayoutValidationResult;
16
+ export interface AxisPosition {
17
+ along: number;
18
+ perp: number;
19
+ }
20
+ /**
21
+ * Reconstructs the expected position of every tile in train-axis space
22
+ * (along the train and perpendicular to it), mirroring computeTrainLayout.
23
+ * Positions are relative to the first tile, so only deltas are meaningful.
24
+ */
25
+ export declare function expectedAxisLayout(layout: readonly TrainLayoutEntry[], layoutStyle: TrainLayoutStyle, outwardSign: number, dominoWidth?: number, dominoHeight?: number): AxisPosition[];
26
+ export declare function validateConsecutiveSpacing(layout: readonly TrainLayoutEntry[], angle: number, layoutStyle: TrainLayoutStyle, tolerance?: number, outwardSignOverride?: number): LayoutValidationResult;
27
+ export declare function validateNoPairOverlap(layout: readonly TrainLayoutEntry[], angle: number, layoutStyle: TrainLayoutStyle, tolerance?: number, outwardSignOverride?: number): LayoutValidationResult;
28
+ export declare function validateTrainLayout(layout: readonly TrainLayoutEntry[], dominoes: readonly DominoValue[], angle: number, layoutStyle: TrainLayoutStyle, tolerance?: number, outwardSignOverride?: number): LayoutValidationResult;
29
+ /**
30
+ * Validates the chicken-foot branch tree as data: every chain links up, every
31
+ * foot hangs off a real double, and every toe's first tile matches the double's
32
+ * value. Recurses into nested feet.
33
+ */
34
+ export declare function validateChickenFootChain(branch: TrainBranch): LayoutValidationResult;
35
+ /**
36
+ * Validates a laid-out chicken-foot tree. Each run's tiles must link up by value
37
+ * and match its tile count; each toe must start the right distance out from its
38
+ * host double (measured along the toe's axis); and — the hard physical rule — no
39
+ * two tiles anywhere in the tree may overlap.
40
+ *
41
+ * The center toe is a centered linear run while the inbound spine is offset, so
42
+ * a single run can mix layout styles; the per-tile overlap test below checks the
43
+ * real constraint directly rather than reconstructing each style's spacing.
44
+ */
45
+ export declare function validateTrainTree(segments: readonly TrainSegment[], tolerance?: number): LayoutValidationResult;
46
+ export declare function dominoCorners(entry: TrainLayoutEntry, dominoWidth?: number, dominoHeight?: number): {
47
+ x: number;
48
+ y: number;
49
+ }[];
@@ -0,0 +1,23 @@
1
+ import { DominoValue } from '../game/DominoValue';
2
+ import { TrainBranch } from '../game/TrainData';
3
+ import { TrainLayoutStyle } from '../app/trainLayout';
4
+ export interface TrainFixture {
5
+ id: string;
6
+ name: string;
7
+ description: string;
8
+ angle: number;
9
+ dominoes: DominoValue[];
10
+ layoutStyles: TrainLayoutStyle[];
11
+ }
12
+ export interface ChickenFootFixture {
13
+ id: string;
14
+ name: string;
15
+ description: string;
16
+ angle: number;
17
+ branch: TrainBranch;
18
+ layoutStyles: TrainLayoutStyle[];
19
+ }
20
+ export declare const TRAIN_FIXTURES: TrainFixture[];
21
+ export declare function getTrainFixture(id: string): TrainFixture | undefined;
22
+ export declare const CHICKEN_FOOT_FIXTURES: ChickenFootFixture[];
23
+ export declare function getChickenFootFixture(id: string): ChickenFootFixture | undefined;
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const v=require("react/jsx-runtime"),j=require("react"),Ee={"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 de(e){const o=Ee[e.gridSize];return{top:e.top??`${o.rows[e.row]}%`,left:e.left??`${o.cols[e.col]}%`,width:o.size,height:o.size}}const je=({row:e,col:o,gridSize:t,color:n,hollow:r,top:i,left:l})=>{const s=de({row:e,col:o,gridSize:t,top:i,left:l});return v.jsx("div",{"data-testid":"pip","data-row":e,"data-col":o,"data-grid":t,style:{position:"absolute",backgroundColor:r?"transparent":n,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}})},fe={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 pe(e){return fe[e]??[]}const F={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"}},Fe=F;function Ae(e){return{...F,...e}}function K(e,o){if(o!==void 0)return o[e]??F[e]??{color:"#1a1a1a"}}function Le(e){return K(e,F)}const _e=(e,o,t)=>{const n=K(e,t);return n?{color:n.color,hollow:n.hollow}:{color:o}},Re=({value:e,pipColor:o,pipColors:t})=>{const{color:n,hollow:r}=_e(e,o,t),i=pe(e);return v.jsx(v.Fragment,{children:i.map((l,s)=>v.jsx(je,{row:l.row,col:l.col,gridSize:l.gridSize,color:n,hollow:r,top:l.top,left:l.left},s))})},ae=({value:e,pipColor:o,pipColors:t})=>v.jsx("div",{style:{width:"100%",height:"100%",position:"relative",padding:"0",overflow:"hidden"},children:v.jsx(Re,{value:e,pipColor:o,pipColors:t})}),J=({value1:e=0,value2:o=0,width:t=100,height:n=200,backgroundColor:r="white",pipColor:i="black",pipColors:l,borderColor:s="black",rotation:a=0})=>{const u=Math.min(Math.max(e,0),12),f=Math.min(Math.max(o,0),12);return v.jsxs("div",{style:{width:`${t}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"},children:[v.jsx("div",{style:{flex:1,position:"relative",borderBottomWidth:"1px",borderBottomStyle:"solid",borderBottomColor:s},children:v.jsx(ae,{value:u,pipColor:i,pipColors:l})}),v.jsx("div",{style:{flex:1,position:"relative"},children:v.jsx(ae,{value:f,pipColor:i,pipColors:l})})]})},D=60,O=120,ve=[-45,45];function ue(e,o=D,t=O){return e?o/2:t/2}function z(e,o,t=D,n=O){return ue(e,t,n)+ue(o,t,n)}function _(e){const o=e*Math.PI/180;return{dirX:Math.cos(o),dirY:Math.sin(o)}}function Y(e){const{dirX:o,dirY:t}=_(e);return{perpX:-t,perpY:o}}function Xe(e,o){const t=e.map(n=>({...n}));for(let n=1;n<t.length;n++){const r=t[n],l=t[n-1].value2,s=r.value1===r.value2;if(o==="linear"&&!s){t[n]={value1:r.value2,value2:r.value1};continue}o==="offset"&&!s&&r.value1!==l&&r.value2===l&&(t[n]={value1:r.value2,value2:r.value1})}return t}function R(e){const{dirX:o,dirY:t}=_(e);return Math.abs(o)>=Math.abs(t)?o>=0?1:-1:t>=0?1:-1}function xe(e,o){return e===0?o:e===o?-o:o}function ge({startX:e,startY:o,angle:t,dominoes:n,layoutStyle:r,dominoWidth:i=D,dominoHeight:l=O,leadGap:s=l*.3,outwardSign:a,hubIndex:u}){const f=[],{dirX:c,dirY:d}=_(t),{perpX:x,perpY:y}=Y(t),g=Xe([...n],r),S=a??R(t),T=r==="offset"&&u!=null,C=[];let m=e+c*s,p=o+d*s,b=0,M=0;const I=i/2,A=h=>{const w=(h-b)*I;m+=x*w,p+=y*w,b=h};for(let h=0;h<g.length;h++){const w=g[h],k=w.value1===w.value2,$=h>0&&g[h-1].value1===g[h-1].value2;r==="linear"?h>0&&(k?(m+=c*z($,!0,i,l),p+=d*z($,!0,i,l)):$?(m+=c*z(!0,!1,i,l),p+=d*z(!0,!1,i,l)):(m+=c*l,p+=d*l)):k?h>0&&(m+=c*z($,!0,i,l),p+=d*z($,!0,i,l)):(h===0?M=S:$?(m+=c*z(!0,!1,i,l),p+=d*z(!0,!1,i,l)):(m+=c*(l/2),p+=d*(l/2),M=xe(M,S)),A(M)),C.push(b),f.push({x:m,y:p,rotation:k?t+180:t-90,isDouble:k,value1:w.value1,value2:w.value2})}if(T&&u!=null){const h=-C[u]*I;if(h!==0)for(let w=0;w<f.length;w++)f[w]={...f[w],x:f[w].x+x*h,y:f[w].y+y*h}}return f}function G(e,o=D,t=O){const n=e.rotation*Math.PI/180,r=Math.cos(n),i=Math.sin(n),l=o/2,s=t/2;return[[-l,-s],[l,-s],[l,s],[-l,s]].map(([a,u])=>({x:e.x+a*r-u*i,y:e.y+a*i+u*r}))}function Ne(e,o,t){let n=1/0,r=-1/0,i=1/0,l=-1/0;for(const s of e){const a=s.x*t.x+s.y*t.y;n=Math.min(n,a),r=Math.max(r,a)}for(const s of o){const a=s.x*t.x+s.y*t.y;i=Math.min(i,a),l=Math.max(l,a)}return Math.min(r,l)-Math.max(n,i)}function Q(e,o,t=1,n=D,r=O){const i=G(e,n,r),l=G(o,n,r);for(const s of[i,l])for(let a=0;a<4;a++){const u=s[a],f=s[(a+1)%4],c=f.x-u.x,d=f.y-u.y,x=Math.hypot(c,d)||1,y={x:-d/x,y:c/x};if(Ne(i,l,y)<=t)return!1}return!0}function Ye(e,o,t,n){return o.some(r=>Q(e,r,1,t,n))}const P=D/4,ce=24;function Z({startX:e,startY:o,angle:t,branch:n,layoutStyle:r,dominoWidth:i=D,dominoHeight:l=O,leadGap:s,depth:a=0,anchor:u,outwardSign:f,placed:c=[],pushAxis:d,minPushSteps:x=0}){const y=f??R(t),g=n.feet?Object.keys(n.feet).map(Number).filter(p=>{const b=n.dominoes[p];return b&&b.value1===b.value2}).sort((p,b)=>p-b)[0]:void 0,S=(p,b)=>ge({startX:p,startY:b,angle:t,dominoes:n.dominoes,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:s,outwardSign:y,hubIndex:g});let T=S(e+(d?.x??0)*P*x,o+(d?.y??0)*P*x),C=u&&d?{x:u.x+d.x*P*x,y:u.y+d.y*P*x}:u;if(d&&c.length>0)for(let p=x;p<=ce;p++){const b=e+d.x*P*p,M=o+d.y*P*p,I=S(b,M);if(!I.some(h=>Ye(h,c,i,l))||p===ce){T=I,C=u&&{x:u.x+d.x*P*p,y:u.y+d.y*P*p};break}}c.push(...T);const m=[{angle:t,depth:a,layoutStyle:r,outwardSign:y,dominoes:n.dominoes,layout:T,anchor:C}];if(n.feet){const{dirX:p,dirY:b}=_(t),{perpX:M,perpY:I}=Y(t),A=i/2,h=l/2;for(const w of Object.keys(n.feet)){const k=Number(w),$=T[k],B=n.feet[k];if(!(!$||!$.isDouble||!B))for(let X=0;X<B.length;X++){const Ie=B[X],ne=ve[X]??0,L=Math.sign(ne),re=t+ne,ie=Y(re),V=-L,Pe=$.x+p*(i/2)+M*(l/2)*L,ke=$.y+b*(i/2)+I*(l/2)*L,le=Pe-ie.perpX*V*A,se=ke-ie.perpY*V*A;m.push(...Z({startX:le,startY:se,angle:re,branch:Ie,layoutStyle:r,dominoWidth:i,dominoHeight:l,leadGap:h,outwardSign:V,depth:a+1,anchor:{x:le,y:se},placed:c,pushAxis:{x:M*L,y:I*L}}))}}}return m}function he(e){return e.flatMap(o=>o.layout)}function Ue(e,o=24,t=D,n=O){const r=Math.hypot(t,n)/2;if(e.length===0)return{width:o*2+t,height:o*2+n,offsetX:o,offsetY:o};let i=1/0,l=1/0,s=-1/0,a=-1/0;for(const u of e)i=Math.min(i,u.x-r),l=Math.min(l,u.y-r),s=Math.max(s,u.x+r),a=Math.max(a,u.y+r);return{width:Math.ceil(s-i+o*2),height:Math.ceil(a-l+o*2),offsetX:o-i,offsetY:o-l}}const be=({startX:e,startY:o,angle:t,trainData:n,layoutStyle:r,tableWidth:i,tableHeight:l,centerX:s,centerY:a,pipColors:u})=>{const f=j.useMemo(()=>he(Z({startX:e,startY:o,angle:t,branch:{dominoes:n.dominoes,feet:n.feet},layoutStyle:r})),[e,o,t,n.dominoes,n.feet,r,i,l]);return v.jsx(v.Fragment,{children:f.map((c,d)=>{const x=n.isPublic;return v.jsx("div",{style:{position:"absolute",left:`${c.x-D/2}px`,top:`${c.y-O/2}px`,zIndex:5},children:v.jsx(J,{value1:c.value1,value2:c.value2,width:D,height:O,backgroundColor:"white",pipColor:"black",pipColors:u,borderColor:x?"red":"black",rotation:c.rotation})},`main-train-${n.playerId}-${d}`)})})},Se=({playerCount:e,centerX:o,centerY:t,radius:n,engineValue:r,trains:i,layoutStyle:l,tableWidth:s,tableHeight:a,pipColors:u})=>{const f=Math.max(8,e),c=120;return v.jsxs("div",{style:{position:"relative",width:"100%",height:"100%"},children:[v.jsx("div",{style:{position:"absolute",width:`${c}px`,height:`${c}px`,left:`${o-c/2}px`,top:`${t-c/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:v.jsx("div",{style:{transform:"rotate(0deg)"},children:v.jsx(J,{value1:r,value2:r,width:60,height:120,backgroundColor:"white",pipColor:"black",pipColors:u,borderColor:"#333"})})}),Array.from({length:f}).map((y,g)=>{const S=g*360/f,T=S*Math.PI/180,C=o+(n+20)*Math.cos(T),m=t+(n+20)*Math.sin(T),p=i.find(b=>b.playerId===g)||{dominoes:[],playerId:g,isPublic:!1};return v.jsx(be,{startX:C,startY:m,angle:S,trainData:p,layoutStyle:l,tableWidth:s,tableHeight:a,centerX:o,centerY:t,pipColors:u},g)})]})};function E(e,o){return e<=o?`${e}:${o}`:`${o}:${e}`}function H(e){return E(e.value1,e.value2)}function W(e){return e.value1===e.value2}function qe(e,o){return e.value1===o||e.value2===o}function Be(e,o){return e.value1===o?e.value2:e.value2===o?e.value1:null}function ee(e,o){return e.value1===o?{value1:e.value1,value2:e.value2}:e.value2===o?{value1:e.value2,value2:e.value1}:null}function Ve(e){const o=[];for(let t=0;t<=e;t++)for(let n=t;n<=e;n++)o.push({value1:t,value2:n});return o}function Ge(e){const o=e+1;return o*(o+1)/2}function Ke(e,o=12,t={}){const n=new Set([E(o,o)]),r=[];for(let i=0;i<e;i++){const l=4+Math.floor(Math.random()*7),s=[];let a=o,u=!1;for(let c=0;c<l;c++){const d=He(a,u,c===0,o,n);if(d===null)break;const x=d===a;n.add(E(a,d)),s.push({value1:a,value2:d}),u=x,a=d}const f=t.chickenFeet?Je(s,n):void 0;r.push({playerId:i,dominoes:s,isPublic:Math.random()>.7,...f?{feet:f}:{}})}return r}function Je(e,o){const t={};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=Qe(r,o);s&&i.push(s)}i.length&&(t[n]=i)}return Object.keys(t).length?t:void 0}function Qe(e,o){const t=1+Math.floor(Math.random()*2),n=[];let r=e;for(let i=0;i<t;i++){const l=Ze(r,o);if(l===null)break;o.add(E(r,l)),n.push({value1:r,value2:l}),r=l}return n.length?{dominoes:n}:null}function Ze(e,o){const t=[];for(let n=0;n<13;n++)n!==e&&(o.has(E(e,n))||t.push(n));return t.length===0?null:t[Math.floor(Math.random()*t.length)]}function He(e,o,t,n,r){const i=Array.from({length:13},(l,s)=>s).filter(l=>We(e,l,o,t,n,r));return i.length===0?null:i[Math.floor(Math.random()*i.length)]}function We(e,o,t,n,r,i){const l=o===e;return!(n&&l&&e===r||l&&t||i.has(E(e,o)))}const eo={playerCount:8,trains:[],engineValue:12},oo=({initialState:e=eo,width:o=1200,height:t=800,pipColors:n,onPipColorsChange:r})=>{const[i,l]=j.useState(e),[s,a]=j.useState("offset"),[u,f]=j.useState(!1),[c,d]=j.useState(void 0),x=n??c,y=r??d,g=x!==void 0,S=o/2,T=t/2,C=(p=u)=>{const b=Ke(i.playerCount,i.engineValue,{chickenFeet:p});l(M=>({...M,trains:b}))};j.useEffect(()=>{C()},[]);const m=()=>{const p=!u;f(p),C(p)};return v.jsxs("div",{style:{width:`${o}px`,height:`${t}px`,position:"relative",backgroundColor:"#1f8a55",borderRadius:"8px",boxShadow:"0 4px 12px rgba(0,0,0,0.2)",overflow:"hidden"},children:[v.jsxs("div",{style:{position:"absolute",top:"10px",left:"10px",zIndex:100},children:[v.jsx("button",{onClick:()=>C(),style:{padding:"8px 12px",backgroundColor:"#fff",border:"1px solid #ccc",borderRadius:"4px",marginRight:"10px",cursor:"pointer"},children:"New trains"}),v.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"]}),v.jsxs("button",{onClick:m,style:{padding:"8px 12px",backgroundColor:u?"#fef3c7":"#fff",border:`1px solid ${u?"#f59e0b":"#ccc"}`,borderRadius:"4px",marginRight:"10px",cursor:"pointer"},children:["Chicken Feet: ",u?"On":"Off"]}),v.jsxs("button",{onClick:()=>y(g?void 0:F),style:{padding:"8px 12px",backgroundColor:g?"#fef3c7":"#fff",border:`1px solid ${g?"#f59e0b":"#ccc"}`,borderRadius:"4px",cursor:"pointer"},children:["Pip Colors: ",g?"On":"Off"]})]}),v.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:[v.jsxs("div",{children:["Engine: Double-",i.engineValue]}),v.jsxs("div",{children:["Players: ",i.playerCount]})]}),v.jsx(Se,{playerCount:i.playerCount,centerX:S,centerY:T,radius:80,engineValue:i.engineValue,trains:i.trains,layoutStyle:s,tableWidth:o,tableHeight:t,pipColors:x})]})},U=1;function we(e,o,t){const{dirX:n,dirY:r}=_(t);return e*n+o*r}function to(e,o,t){const{perpX:n,perpY:r}=Y(t);return e*n+o*r}function oe(e){const o=[];for(let t=1;t<e.length;t++)e[t].value1!==e[t-1].value2&&o.push({code:"chain-break",message:`Domino ${t} does not connect to domino ${t-1}`,index:t});for(let t=1;t<e.length;t++){const n=e[t-1].value1===e[t-1].value2,r=e[t].value1===e[t].value2;n&&r&&o.push({code:"consecutive-doubles",message:`Consecutive doubles at index ${t-1} and ${t}`,index:t})}return{valid:o.length===0,issues:o}}function ye(e,o,t,n=D,r=O){const i=n/2,l=e.map(c=>c.isDouble),s=[];let a=0,u=0,f=0;for(let c=0;c<e.length;c++){const d=l[c],x=c>0&&l[c-1];o==="linear"?(c>0&&(a+=z(x,d,n,r)),u=0):d?c>0&&(a+=z(x,!0,n,r)):c===0?(f=t,u=f*i):x?a+=z(!0,!1,n,r):(a+=r/2,f=xe(f,t),u=f*i),s.push({along:a,perp:u})}return s}function no(e,o,t,n=U,r){const i=[],l=r??R(o),s=ye(e,t,l);for(let a=1;a<e.length;a++){const u=e[a-1],f=e[a],c=f.x-u.x,d=f.y-u.y,x=we(c,d,o),y=to(c,d,o),g=s[a].along-s[a-1].along,S=s[a].perp-s[a-1].perp;Math.abs(x-g)>n&&i.push({code:"spacing-along-train",message:`Along-train spacing between domino ${a-1} and ${a} is ${x.toFixed(2)}px (expected ${g}px)`,index:a}),Math.abs(y-S)>n&&i.push({code:"spacing-perpendicular",message:`Perpendicular spacing between domino ${a-1} and ${a} is ${y.toFixed(2)}px (expected ${S}px)`,index:a})}return{valid:i.length===0,issues:i}}function ro(e,o,t,n=U,r){const i=[],l=r??R(o),s=ye(e,t,l);for(let a=1;a<e.length;a++){const u=e[a-1],f=e[a],c=f.x-u.x,d=f.y-u.y,x=Math.hypot(c,d),y=s[a].along-s[a-1].along,g=s[a].perp-s[a-1].perp,S=Math.hypot(y,g)*.9;x+n<S&&i.push({code:"overlap",message:`Domino ${a-1} and ${a} centers are ${x.toFixed(2)}px apart (minimum ${S.toFixed(2)}px)`,index:a})}return{valid:i.length===0,issues:i}}function io(e,o,t,n,r=U,i){const l=[...oe(o).issues,...no(e,t,n,r,i).issues,...ro(e,t,n,r,i).issues];return e.length!==o.length&&l.push({code:"layout-length",message:`Layout length ${e.length} does not match domino count ${o.length}`}),{valid:l.length===0,issues:l}}function lo(e){const o=[],t=(n,r)=>{if(o.push(...oe(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){o.push({code:"foot-host-missing",message:`[${r}] Foot references missing tile ${l}`});continue}s.value1!==s.value2&&o.push({code:"foot-host-not-double",message:`[${r}] Foot host tile ${l} is not a double`}),a.length>2&&o.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((u,f)=>{const c=u.dominoes[0];c&&c.value1!==s.value1&&o.push({code:"foot-connection",message:`[${r}] Toe ${f} on double ${l} starts with ${c.value1} but the double is ${s.value1}`}),t(u,`${r}.${l}.${f}`)})}};return t(e,"main"),{valid:o.length===0,issues:o}}function so(e,o=U){const t=[];e.forEach((r,i)=>{if(t.push(...oe(r.dominoes).issues.map(l=>({...l,message:`[segment ${i} @${r.angle}°] ${l.message}`}))),r.layout.length!==r.dominoes.length&&t.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=we(l.x-r.anchor.x,l.y-r.anchor.y,r.angle),a=O/2;Math.abs(s-a)>o&&t.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++)Q(n[r],n[i])&&t.push({code:"tile-overlap",message:`Tiles ${r} and ${i} overlap`,index:i});return{valid:t.length===0,issues:t}}const ao=[{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"]}],uo=[{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"]}],N={maxPips:12,engineValue:12,allowConsecutiveDoubles:!1,requireUniqueTiles:!0,requireSequential:!0,doubleObligation:"cover",chickenFoot:{toeCount:3,sideToeAngles:[-45,45]}};function me(e){switch(e.doubleObligation){case"chicken-foot":return Math.max(1,e.chickenFoot.toeCount);case"cover":return 1;default:return 0}}function ze(e){return e.doubleObligation==="chicken-foot"?Math.max(0,e.chickenFoot.toeCount-1):0}function co(e={}){const o=e.maxPips??N.maxPips;return{...N,...e,maxPips:o,engineValue:e.engineValue??o,chickenFoot:{...N.chickenFoot,...e.chickenFoot??{}}}}function Te(e,o){let t=e;for(const n of o)if(t=t?.feet?.[n.doubleIndex]?.[n.toeIndex],!t)return;return t}function q(e,o,t){if(t(e,o),!!e.feet)for(const n of Object.keys(e.feet)){const r=Number(n);e.feet[r].forEach((i,l)=>{q(i,[...o,{doubleIndex:r,toeIndex:l}],t)})}}function fo(e){const o=[];return q(e,[],(t,n)=>{t.dominoes.forEach((r,i)=>{if(r.value1!==r.value2)return;const l=i<t.dominoes.length-1,s=t.feet?.[i]?.length??0;o.push({path:n,doubleIndex:i,value:r.value1,hasCenter:l,sideToes:s,answers:(l?1:0)+s})})}),o}function Ce(e,o){const t=me(o);return t<=0?[]:fo(e).filter(n=>n.answers<t)}function Me(e){const o=new Set;return q(e,[],t=>{for(const n of t.dominoes)o.add(H(n))}),o}function $e(e,o,t){if(e.dominoes.length===0)return[{path:[],attach:"run-tail",value:o,attachToDouble:!0,obligation:!1}];const n=Ce(e,t);if(t.doubleObligation!=="none"&&n.length>0){const i=ze(t),l=[];for(const s of n){const a=Te(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 q(e,[],(i,l)=>{const s=i.dominoes[i.dominoes.length-1];s&&r.push({path:l,attach:"run-tail",value:s.value2,attachToDouble:W(s),obligation:!1})}),r}function te(e,o,t,n){const r=[],i=ee(e,o.value);return n.requireSequential&&!i&&r.push("value-mismatch"),n.requireUniqueTiles&&t.has(H(e))&&r.push("duplicate-tile"),!n.allowConsecutiveDoubles&&o.attachToDouble&&W(e)&&r.push("consecutive-doubles"),{legal:r.length===0,violations:r}}function po(e,o,t,n,r){const i=$e(e,o,r),l=[];for(const s of i)for(const a of t)te(a,s,n,r).legal&&l.push({end:s,tile:a});return l}function De(e,o,t){if(o.length===0)return t(e);const[n,...r]=o,l=(e.feet?.[n.doubleIndex]??[]).map((s,a)=>a===n.toeIndex?De(s,r,t):s);return{...e,feet:{...e.feet,[n.doubleIndex]:l}}}function Oe(e,o,t){const n=ee(o.tile,o.end.value)??{...o.tile};return De(e,o.end.path,r=>{if(o.end.attach==="run-tail")return{...r,dominoes:[...r.dominoes,n]};const i=o.end.doubleIndex??0,l=o.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 vo(e,o,t){const n=te(o.tile,o.end,Me(e),t);return n.legal?{ok:!0,board:Oe(e,o),violations:[]}:{ok:!1,board:e,violations:n.violations}}exports.CHICKEN_FOOT_FIXTURES=uo;exports.CHICKEN_FOOT_TOE_ANGLES=ve;exports.DEFAULT_PIP_COLORS=F;exports.DEFAULT_RULES=N;exports.DominoHub=Se;exports.DominoTrain=be;exports.DoubleTwelve=J;exports.MexicanTrainGame=oo;exports.PIP_COLORS=Fe;exports.PIP_LAYOUTS=fe;exports.TRAIN_FIXTURES=ao;exports.applyMove=Oe;exports.collectPlayedKeys=Me;exports.computeTrainLayout=ge;exports.computeTrainTree=Z;exports.dominoKey=H;exports.dominoSetSize=Ge;exports.evaluatePlacement=te;exports.flattenSegments=he;exports.generateDominoSet=Ve;exports.getBranchAt=Te;exports.getLegalMoves=po;exports.getOpenEnds=$e;exports.getPipLayout=pe;exports.getPipStyle=Le;exports.getTrainLayoutBounds=Ue;exports.getUnsatisfiedDoubles=Ce;exports.isDouble=W;exports.mergePipColors=Ae;exports.orientForConnection=ee;exports.otherEnd=Be;exports.outwardPerpSign=R;exports.playMove=vo;exports.requiredDoubleAnswers=me;exports.resolvePipPosition=de;exports.resolvePipStyle=K;exports.resolveRules=co;exports.sideToeSlots=ze;exports.stepAlongTrain=z;exports.tileCorners=G;exports.tileHasValue=qe;exports.tileKey=E;exports.tilesOverlap=Q;exports.validateChickenFootChain=lo;exports.validateTrainLayout=io;exports.validateTrainTree=so;
2
+ //# sourceMappingURL=index.cjs.map