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 +21 -0
- package/README.md +80 -0
- package/dist/app/DominoHalf.d.ts +9 -0
- package/dist/app/DominoHub.d.ts +17 -0
- package/dist/app/DominoTrain.d.ts +17 -0
- package/dist/app/DoubleTwelve.d.ts +19 -0
- package/dist/app/MexicanTrainGame.d.ts +12 -0
- package/dist/app/MexicanTrainHub.d.ts +9 -0
- package/dist/app/Pip.d.ts +13 -0
- package/dist/app/PipPattern.d.ts +9 -0
- package/dist/app/pipColors.d.ts +16 -0
- package/dist/app/pipGrid.d.ts +14 -0
- package/dist/app/pipLayouts.d.ts +4 -0
- package/dist/app/trainLayout.d.ts +144 -0
- package/dist/game/DominoValue.d.ts +4 -0
- package/dist/game/GameState.d.ts +6 -0
- package/dist/game/TrainData.d.ts +22 -0
- package/dist/game/generateSampleTrains.d.ts +9 -0
- package/dist/harness/dominoFixtures.d.ts +10 -0
- package/dist/harness/layoutValidation.d.ts +49 -0
- package/dist/harness/trainFixtures.d.ts +23 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +1481 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/dominoSet.d.ts +20 -0
- package/dist/rules/placement.d.ts +70 -0
- package/dist/rules/rulesConfig.d.ts +43 -0
- package/package.json +115 -0
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,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,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,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,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,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
|