@dreamboard-games/sdk 0.5.0-alpha.2 → 0.5.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +462 -53
- package/dist/board-element-CqtxU5VZ.d.ts +13 -0
- package/dist/chunk-CZRPUWCT.js +1602 -0
- package/dist/chunk-CZRPUWCT.js.map +1 -0
- package/dist/chunk-DMSXVMYY.js +7199 -0
- package/dist/chunk-DMSXVMYY.js.map +1 -0
- package/dist/chunk-HVNA4RAM.js +1792 -0
- package/dist/chunk-HVNA4RAM.js.map +1 -0
- package/dist/chunk-U6R7FAH4.js +630 -0
- package/dist/chunk-U6R7FAH4.js.map +1 -0
- package/dist/chunk-YT3RPO5J.js +635 -0
- package/dist/chunk-YT3RPO5J.js.map +1 -0
- package/dist/diagnostics-ZAPEBlJP.d.ts +64 -0
- package/dist/extract-CwJWDrSD.d.ts +6850 -0
- package/dist/index.d.ts +5177 -1
- package/dist/index.js +1990 -6
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +158 -0
- package/dist/react.js +1119 -0
- package/dist/react.js.map +1 -0
- package/dist/reducer.d.ts +1028 -97
- package/dist/reducer.js +4892 -125
- package/dist/reducer.js.map +1 -1
- package/dist/shortcuts-B_RrufOM.d.ts +822 -0
- package/dist/testing.d.ts +529 -569
- package/dist/testing.js +2588 -934
- package/dist/testing.js.map +1 -1
- package/dist/types-BxXRINRF.d.ts +118 -0
- package/package.json +39 -144
- package/dist/HandView-Bi2lxzQP.d.ts +0 -79
- package/dist/ResourceCounter-BFxJknqp.d.ts +0 -98
- package/dist/ThemeProvider-CElaL2ik.d.ts +0 -99
- package/dist/attributes-DbvyMbXw.d.ts +0 -68
- package/dist/browser-interaction.d.ts +0 -910
- package/dist/browser-interaction.js +0 -119
- package/dist/browser-interaction.js.map +0 -1
- package/dist/chunk-32PFKDV7.js +0 -11705
- package/dist/chunk-32PFKDV7.js.map +0 -1
- package/dist/chunk-3BAD4VC2.js +0 -263
- package/dist/chunk-3BAD4VC2.js.map +0 -1
- package/dist/chunk-3SKDNDPG.js +0 -104
- package/dist/chunk-3SKDNDPG.js.map +0 -1
- package/dist/chunk-5YV2WHJ4.js +0 -25
- package/dist/chunk-5YV2WHJ4.js.map +0 -1
- package/dist/chunk-ANRB4XKA.js +0 -90
- package/dist/chunk-ANRB4XKA.js.map +0 -1
- package/dist/chunk-BI4G3PM2.js +0 -1864
- package/dist/chunk-BI4G3PM2.js.map +0 -1
- package/dist/chunk-EVCLTY4Q.js +0 -935
- package/dist/chunk-EVCLTY4Q.js.map +0 -1
- package/dist/chunk-GDPKTBUR.js +0 -564
- package/dist/chunk-GDPKTBUR.js.map +0 -1
- package/dist/chunk-H6VDGFL5.js +0 -2903
- package/dist/chunk-H6VDGFL5.js.map +0 -1
- package/dist/chunk-LR3ZTWQF.js +0 -5960
- package/dist/chunk-LR3ZTWQF.js.map +0 -1
- package/dist/chunk-PF7L4BMG.js +0 -117
- package/dist/chunk-PF7L4BMG.js.map +0 -1
- package/dist/chunk-PN5O6GG2.js +0 -32
- package/dist/chunk-PN5O6GG2.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js +0 -10
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-S2JEDVWH.js +0 -7165
- package/dist/chunk-S2JEDVWH.js.map +0 -1
- package/dist/chunk-T3ZKNUZ7.js +0 -1
- package/dist/chunk-T3ZKNUZ7.js.map +0 -1
- package/dist/chunk-UGH54WZ2.js +0 -223
- package/dist/chunk-UGH54WZ2.js.map +0 -1
- package/dist/chunk-VCDWP5VL.js +0 -746
- package/dist/chunk-VCDWP5VL.js.map +0 -1
- package/dist/chunk-VDXOF4FW.js +0 -69
- package/dist/chunk-VDXOF4FW.js.map +0 -1
- package/dist/chunk-WHR5UW3F.js +0 -1988
- package/dist/chunk-WHR5UW3F.js.map +0 -1
- package/dist/chunk-Y75CFE77.js +0 -17
- package/dist/chunk-Y75CFE77.js.map +0 -1
- package/dist/chunk-Z7QCREWI.js +0 -3435
- package/dist/chunk-Z7QCREWI.js.map +0 -1
- package/dist/chunk-ZZGN2ATS.js +0 -1941
- package/dist/chunk-ZZGN2ATS.js.map +0 -1
- package/dist/components-gAbIz7hK.d.ts +0 -1284
- package/dist/definitions-BgmV_GhC.d.ts +0 -300
- package/dist/diagnostics-DIi3ina7.d.ts +0 -50
- package/dist/digest.d.ts +0 -8
- package/dist/game-CsZScpcn.d.ts +0 -763
- package/dist/hex-board-view-aKkblDp6.d.ts +0 -1228
- package/dist/index-CcT9Q7N7.d.ts +0 -193
- package/dist/index.d-DjzoK7zn.d.ts +0 -2101
- package/dist/package-set.d.ts +0 -13
- package/dist/package-set.js +0 -12
- package/dist/package-set.js.map +0 -1
- package/dist/player-state-Cqpyeql0.d.ts +0 -371
- package/dist/plugin-runtime-contract.d.ts +0 -17
- package/dist/plugin-runtime-contract.js +0 -92
- package/dist/plugin-runtime-contract.js.map +0 -1
- package/dist/primitive-props-BNHDkgd7.d.ts +0 -16
- package/dist/protocol-dYgafTYY.d.ts +0 -314
- package/dist/reducer/advanced.d.ts +0 -82
- package/dist/reducer/advanced.js +0 -51
- package/dist/reducer/advanced.js.map +0 -1
- package/dist/reducer-contract.d.ts +0 -11
- package/dist/reducer-contract.js +0 -16
- package/dist/reducer-contract.js.map +0 -1
- package/dist/reference-games/index.d.ts +0 -31
- package/dist/reference-games/index.js +0 -48
- package/dist/reference-games/index.js.map +0 -1
- package/dist/runtime/primitives.d.ts +0 -250
- package/dist/runtime/primitives.js +0 -189
- package/dist/runtime/primitives.js.map +0 -1
- package/dist/runtime/runtime-api.d.ts +0 -2
- package/dist/runtime/runtime-api.js +0 -1
- package/dist/runtime/runtime-api.js.map +0 -1
- package/dist/runtime/workspace-contract.d.ts +0 -392
- package/dist/runtime/workspace-contract.js +0 -28
- package/dist/runtime/workspace-contract.js.map +0 -1
- package/dist/runtime-RJ5orDJU.d.ts +0 -1699
- package/dist/runtime-api-Bz5pwNU_.d.ts +0 -296
- package/dist/runtime-json-CQ9QbLZ5.d.ts +0 -5
- package/dist/runtime.d.ts +0 -83
- package/dist/runtime.js +0 -242
- package/dist/runtime.js.map +0 -1
- package/dist/schema.d.ts +0 -379
- package/dist/stale-contract-artifact-error-XLaweZtF.d.ts +0 -18
- package/dist/testing-compiler.d.ts +0 -37
- package/dist/testing-compiler.js +0 -272
- package/dist/testing-compiler.js.map +0 -1
- package/dist/testing-runtime.d.ts +0 -119
- package/dist/testing-runtime.js +0 -212
- package/dist/testing-runtime.js.map +0 -1
- package/dist/types-DJj5MJkl.d.ts +0 -256
- package/dist/types-DR7DoB1x.d.ts +0 -28
- package/dist/types-JWCYHmu7.d.ts +0 -122
- package/dist/types.d.ts +0 -1958
- package/dist/types.js +0 -14
- package/dist/types.js.map +0 -1
- package/dist/ui/components.d.ts +0 -17
- package/dist/ui/components.js +0 -216
- package/dist/ui/components.js.map +0 -1
- package/dist/ui/defaults.d.ts +0 -19
- package/dist/ui/defaults.js +0 -104
- package/dist/ui/defaults.js.map +0 -1
- package/dist/ui/player-state.d.ts +0 -2
- package/dist/ui/player-state.js +0 -1
- package/dist/ui/player-state.js.map +0 -1
- package/dist/ui/plugin-styles.css +0 -2
- package/dist/ui-contract-GTUPkf8z.d.ts +0 -1168
- package/dist/ui.d.ts +0 -317
- package/dist/ui.js +0 -277
- package/dist/ui.js.map +0 -1
- package/dist/views-BpuWcOyN.d.ts +0 -1641
|
@@ -0,0 +1,1602 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/shared/domain/per-player-instance.ts
|
|
8
|
+
import * as z from "zod";
|
|
9
|
+
var GENERATED_ID_PREFIX = "@db/";
|
|
10
|
+
var instanceTuple = z.tuple([
|
|
11
|
+
z.enum(["board", "card", "piece", "die", "tile"]),
|
|
12
|
+
z.string().min(1),
|
|
13
|
+
z.string().min(1)
|
|
14
|
+
]);
|
|
15
|
+
function perPlayerInstanceId(family, baseId, playerId) {
|
|
16
|
+
instanceTuple.parse([family, baseId, playerId]);
|
|
17
|
+
return `${GENERATED_ID_PREFIX}${JSON.stringify([family, baseId, playerId])}`;
|
|
18
|
+
}
|
|
19
|
+
function parsePerPlayerInstanceId(value) {
|
|
20
|
+
if (typeof value !== "string" || !value.startsWith(GENERATED_ID_PREFIX))
|
|
21
|
+
return null;
|
|
22
|
+
let candidate;
|
|
23
|
+
try {
|
|
24
|
+
candidate = JSON.parse(value.slice(GENERATED_ID_PREFIX.length));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const parsed = instanceTuple.safeParse(candidate);
|
|
29
|
+
if (!parsed.success) return null;
|
|
30
|
+
const [family, baseId, playerId] = parsed.data;
|
|
31
|
+
const id = perPlayerInstanceId(family, baseId, playerId);
|
|
32
|
+
if (id !== value) return null;
|
|
33
|
+
return { id, family, baseId, playerId };
|
|
34
|
+
}
|
|
35
|
+
function perPlayerInstanceSchema(family, bases) {
|
|
36
|
+
const known = new Set(bases);
|
|
37
|
+
return z.custom(
|
|
38
|
+
(value) => {
|
|
39
|
+
const parsed = parsePerPlayerInstanceId(value);
|
|
40
|
+
return parsed?.family === family && known.has(parsed.baseId);
|
|
41
|
+
},
|
|
42
|
+
{ error: "Expected a canonical instance of a declared family and base." }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/shared/domain/board-element.ts
|
|
47
|
+
import * as z2 from "zod";
|
|
48
|
+
var elementTuple = z2.tuple([
|
|
49
|
+
z2.enum(["board-edge", "board-vertex"]),
|
|
50
|
+
z2.enum(["hex", "square"]),
|
|
51
|
+
z2.string().min(1),
|
|
52
|
+
z2.string().min(1)
|
|
53
|
+
]);
|
|
54
|
+
function validLattice(kind, layout, value) {
|
|
55
|
+
const pattern = layout === "hex" ? kind === "board-edge" ? /^(-?\d+),(-?\d+):e[012]$/ : /^(-?\d+),(-?\d+):v[01]$/ : kind === "board-edge" ? /^(-?\d+),(-?\d+):[hv]$/ : /^(-?\d+),(-?\d+)$/;
|
|
56
|
+
const match = pattern.exec(value);
|
|
57
|
+
return match !== null && [match[1], match[2]].every(
|
|
58
|
+
(part) => Number.isSafeInteger(Number(part)) && String(Number(part)) === part
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
function encode(kind, layout, boardId, latticeId) {
|
|
62
|
+
elementTuple.parse([kind, layout, boardId, latticeId]);
|
|
63
|
+
if (!validLattice(kind, layout, latticeId))
|
|
64
|
+
throw new Error("Invalid canonical board lattice identity.");
|
|
65
|
+
return `${GENERATED_ID_PREFIX}${JSON.stringify([kind, layout, boardId, latticeId])}`;
|
|
66
|
+
}
|
|
67
|
+
function boardEdgeId(layout, boardId, latticeId) {
|
|
68
|
+
return encode(
|
|
69
|
+
"board-edge",
|
|
70
|
+
layout,
|
|
71
|
+
boardId,
|
|
72
|
+
latticeId
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
function boardVertexId(layout, boardId, latticeId) {
|
|
76
|
+
return encode(
|
|
77
|
+
"board-vertex",
|
|
78
|
+
layout,
|
|
79
|
+
boardId,
|
|
80
|
+
latticeId
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
function parseBoardElementId(value) {
|
|
84
|
+
if (typeof value !== "string" || !value.startsWith(GENERATED_ID_PREFIX))
|
|
85
|
+
return null;
|
|
86
|
+
let candidate;
|
|
87
|
+
try {
|
|
88
|
+
candidate = JSON.parse(value.slice(GENERATED_ID_PREFIX.length));
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const parsed = elementTuple.safeParse(candidate);
|
|
93
|
+
if (!parsed.success) return null;
|
|
94
|
+
const [kind, layout, boardId, latticeId] = parsed.data;
|
|
95
|
+
if (!validLattice(kind, layout, latticeId) || encode(kind, layout, boardId, latticeId) !== value)
|
|
96
|
+
return null;
|
|
97
|
+
return {
|
|
98
|
+
kind: kind === "board-edge" ? "edge" : "vertex",
|
|
99
|
+
layout,
|
|
100
|
+
boardId,
|
|
101
|
+
latticeId
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/shared/domain/tile-space.ts
|
|
106
|
+
import * as z3 from "zod";
|
|
107
|
+
var tileSpaceTuple = z3.tuple([
|
|
108
|
+
z3.literal("tile-space"),
|
|
109
|
+
z3.string().min(1),
|
|
110
|
+
z3.string().min(1)
|
|
111
|
+
]);
|
|
112
|
+
function tileSpaceId(tileId, cellId) {
|
|
113
|
+
tileSpaceTuple.parse(["tile-space", tileId, cellId]);
|
|
114
|
+
return `${GENERATED_ID_PREFIX}${JSON.stringify(["tile-space", tileId, cellId])}`;
|
|
115
|
+
}
|
|
116
|
+
function parseTileSpaceId(value) {
|
|
117
|
+
if (typeof value !== "string" || !value.startsWith(GENERATED_ID_PREFIX))
|
|
118
|
+
return null;
|
|
119
|
+
let candidate;
|
|
120
|
+
try {
|
|
121
|
+
candidate = JSON.parse(value.slice(GENERATED_ID_PREFIX.length));
|
|
122
|
+
} catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
const parsed = tileSpaceTuple.safeParse(candidate);
|
|
126
|
+
if (!parsed.success) return null;
|
|
127
|
+
const [, tileId, cellId] = parsed.data;
|
|
128
|
+
if (tileSpaceId(tileId, cellId) !== value) return null;
|
|
129
|
+
return { tileId, cellId };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/shared/runtime-json.ts
|
|
133
|
+
import * as z4 from "zod";
|
|
134
|
+
import { en } from "zod/locales";
|
|
135
|
+
z4.config(en());
|
|
136
|
+
var RuntimeJsonSchema = z4.json();
|
|
137
|
+
|
|
138
|
+
// src/shared/protocol/json.ts
|
|
139
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
140
|
+
import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
|
|
141
|
+
function canonicalizePluginRuntimeJson(value) {
|
|
142
|
+
return canonicalizeJson(
|
|
143
|
+
RuntimeJsonSchema.parse(omitOptionalProperties(value))
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
function encodeCanonicalPluginRuntimeJson(value) {
|
|
147
|
+
return JSON.stringify(canonicalizePluginRuntimeJson(value));
|
|
148
|
+
}
|
|
149
|
+
function digestPluginRuntimeJson(value) {
|
|
150
|
+
return `sha256:${sha256Hex(encodeCanonicalPluginRuntimeJson(value))}`;
|
|
151
|
+
}
|
|
152
|
+
function canonicalizeJson(value) {
|
|
153
|
+
if (value === null || typeof value === "boolean" || typeof value === "string") {
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
if (typeof value === "number") return value;
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
return value.map((item) => canonicalizeJson(item));
|
|
159
|
+
}
|
|
160
|
+
return Object.fromEntries(
|
|
161
|
+
Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, item]) => [key, canonicalizeJson(item)])
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
function omitOptionalProperties(value) {
|
|
165
|
+
if (Array.isArray(value)) return value.map(omitOptionalProperties);
|
|
166
|
+
if (value !== null && typeof value === "object" && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) {
|
|
167
|
+
return Object.fromEntries(
|
|
168
|
+
Object.entries(value).filter(([, item]) => item !== void 0).map(([key, item]) => [key, omitOptionalProperties(item)])
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function sha256Hex(input) {
|
|
174
|
+
return bytesToHex(sha256(utf8ToBytes(input)));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/shared/position-target.ts
|
|
178
|
+
import * as z5 from "zod";
|
|
179
|
+
var PositionTargetSchema = z5.strictObject({
|
|
180
|
+
zoneId: z5.string(),
|
|
181
|
+
hostId: z5.string(),
|
|
182
|
+
index: z5.number().int().nonnegative()
|
|
183
|
+
});
|
|
184
|
+
function isPositionTarget(value) {
|
|
185
|
+
return typeof value === "object" && value !== null && Object.keys(value).length === 3 && "zoneId" in value && typeof value.zoneId === "string" && "hostId" in value && typeof value.hostId === "string" && "index" in value && Number.isSafeInteger(value.index) && value.index >= 0;
|
|
186
|
+
}
|
|
187
|
+
function zonePositions(zones) {
|
|
188
|
+
return zones.flatMap(
|
|
189
|
+
({ zoneId, hostId, size }) => Array.from({ length: size + 1 }, (_, index) => ({ zoneId, hostId, index }))
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/shared/immutable.ts
|
|
194
|
+
function immutableCopy(value) {
|
|
195
|
+
return freeze(structuredClone(value));
|
|
196
|
+
}
|
|
197
|
+
function freeze(value) {
|
|
198
|
+
if (value !== null && typeof value === "object") {
|
|
199
|
+
for (const child of Object.values(value)) freeze(child);
|
|
200
|
+
Object.freeze(value);
|
|
201
|
+
}
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/shared/domain/board-coordinates.ts
|
|
206
|
+
var MAXIMUM_BOARD_COORDINATE = Math.floor(Number.MAX_SAFE_INTEGER / 4);
|
|
207
|
+
|
|
208
|
+
// src/shared/hex-board.ts
|
|
209
|
+
import {
|
|
210
|
+
defineHex,
|
|
211
|
+
Grid,
|
|
212
|
+
Orientation,
|
|
213
|
+
ring as ringTraversal,
|
|
214
|
+
line as lineTraversal
|
|
215
|
+
} from "honeycomb-grid";
|
|
216
|
+
var maximumCoordinate = MAXIMUM_BOARD_COORDINATE;
|
|
217
|
+
var coordinateKey = ({ q, r }) => `${q},${r}`;
|
|
218
|
+
var directions = [
|
|
219
|
+
{ q: 1, r: 0 },
|
|
220
|
+
{ q: 0, r: 1 },
|
|
221
|
+
{ q: -1, r: 1 },
|
|
222
|
+
{ q: -1, r: 0 },
|
|
223
|
+
{ q: 0, r: -1 },
|
|
224
|
+
{ q: 1, r: -1 }
|
|
225
|
+
];
|
|
226
|
+
var add = (a, b) => ({
|
|
227
|
+
q: a.q + b.q,
|
|
228
|
+
r: a.r + b.r
|
|
229
|
+
});
|
|
230
|
+
function edgeKey(hex, side) {
|
|
231
|
+
return side < 3 ? `${coordinateKey(hex)}:e${side}` : edgeKey(add(hex, directions[side]), side - 3);
|
|
232
|
+
}
|
|
233
|
+
function vertexCoordinates(hex, corner) {
|
|
234
|
+
return [
|
|
235
|
+
hex,
|
|
236
|
+
add(hex, directions[corner]),
|
|
237
|
+
add(hex, directions[(corner + 1) % 6])
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
function vertexKey(hex, corner) {
|
|
241
|
+
const hexes = vertexCoordinates(hex, corner);
|
|
242
|
+
for (const base of hexes) {
|
|
243
|
+
for (let shape = 0; shape < 2; shape++) {
|
|
244
|
+
if ([directions[shape], directions[shape + 1]].every(
|
|
245
|
+
(offset) => hexes.some(
|
|
246
|
+
(hex2) => coordinateKey(hex2) === coordinateKey(add(base, offset))
|
|
247
|
+
)
|
|
248
|
+
))
|
|
249
|
+
return `${coordinateKey(base)}:v${shape}`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
throw new Error("Invalid hex vertex.");
|
|
253
|
+
}
|
|
254
|
+
var compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
255
|
+
function hexClass(orientation, dimensions = 1) {
|
|
256
|
+
return defineHex({
|
|
257
|
+
dimensions,
|
|
258
|
+
orientation: orientation === "flat" ? Orientation.FLAT : Orientation.POINTY,
|
|
259
|
+
origin: { x: 0, y: 0 }
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
function createHexTopology(board2) {
|
|
263
|
+
const boardId = board2.id;
|
|
264
|
+
const spaces = board2.spaces.map(({ id, q, r }) => ({ id, q, r })).sort((a, b) => compare(a.id, b.id));
|
|
265
|
+
for (const space of spaces) {
|
|
266
|
+
if (!Number.isSafeInteger(space.q) || !Number.isSafeInteger(space.r))
|
|
267
|
+
throw new Error("Hex coordinates must be safe integers.");
|
|
268
|
+
if (Math.abs(space.q) > maximumCoordinate || Math.abs(space.r) > maximumCoordinate)
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Hex coordinates must be within +/-${maximumCoordinate} for exact cube arithmetic and safe one-step neighbours.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const orientation = board2.orientation ?? "pointy";
|
|
274
|
+
const grid = new Grid(hexClass(orientation), spaces);
|
|
275
|
+
const spacesById = new Map(spaces.map((space) => [space.id, space]));
|
|
276
|
+
if (spacesById.size !== spaces.length || grid.size !== spaces.length)
|
|
277
|
+
throw new Error(
|
|
278
|
+
`Hex board '${boardId}' contains duplicate space IDs or coordinates.`
|
|
279
|
+
);
|
|
280
|
+
const spacesByCoordinate = new Map(
|
|
281
|
+
spaces.map((space) => [coordinateKey(space), space])
|
|
282
|
+
);
|
|
283
|
+
const cornersBySpace = /* @__PURE__ */ new Map();
|
|
284
|
+
const edgesBySpace = /* @__PURE__ */ new Map();
|
|
285
|
+
const edgesById = /* @__PURE__ */ new Map();
|
|
286
|
+
const verticesById = /* @__PURE__ */ new Map();
|
|
287
|
+
const vertexPoints = /* @__PURE__ */ new Map();
|
|
288
|
+
const Tile = hexClass(orientation);
|
|
289
|
+
const cornerId = (space, corner) => boardVertexId("hex", boardId, vertexKey(space, corner));
|
|
290
|
+
for (const space of spaces) {
|
|
291
|
+
const cornerIds = Array.from(
|
|
292
|
+
{ length: 6 },
|
|
293
|
+
(_, corner) => cornerId(space, corner)
|
|
294
|
+
);
|
|
295
|
+
cornersBySpace.set(space.id, cornerIds);
|
|
296
|
+
const edgeIds = Array.from({ length: 6 }, (_, side) => {
|
|
297
|
+
const id = boardEdgeId("hex", boardId, edgeKey(space, side));
|
|
298
|
+
let edge = edgesById.get(id);
|
|
299
|
+
if (!edge) {
|
|
300
|
+
edge = {
|
|
301
|
+
id,
|
|
302
|
+
spaceIds: [],
|
|
303
|
+
vertexIds: [cornerIds[(side + 5) % 6], cornerIds[side]].sort(
|
|
304
|
+
compare
|
|
305
|
+
)
|
|
306
|
+
};
|
|
307
|
+
edgesById.set(id, edge);
|
|
308
|
+
}
|
|
309
|
+
edge.spaceIds.push(space.id);
|
|
310
|
+
return id;
|
|
311
|
+
});
|
|
312
|
+
edgesBySpace.set(space.id, edgeIds);
|
|
313
|
+
cornerIds.forEach((id, corner) => {
|
|
314
|
+
let vertex = verticesById.get(id);
|
|
315
|
+
if (!vertex) {
|
|
316
|
+
vertex = { id, spaceIds: [], edgeIds: [] };
|
|
317
|
+
verticesById.set(id, vertex);
|
|
318
|
+
const centers = vertexCoordinates(space, corner).map(
|
|
319
|
+
(coordinate7) => new Tile(coordinate7)
|
|
320
|
+
);
|
|
321
|
+
vertexPoints.set(id, {
|
|
322
|
+
x: centers.reduce((sum, hex) => sum + hex.x, 0) / 3,
|
|
323
|
+
y: centers.reduce((sum, hex) => sum + hex.y, 0) / 3
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
vertex.spaceIds.push(space.id);
|
|
327
|
+
for (const edgeId2 of [edgeIds[corner], edgeIds[(corner + 1) % 6]])
|
|
328
|
+
if (!vertex.edgeIds.includes(edgeId2)) vertex.edgeIds.push(edgeId2);
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
const edges = Object.freeze(
|
|
332
|
+
[...edgesById.values()].sort((a, b) => compare(a.id, b.id)).map(
|
|
333
|
+
(edge) => Object.freeze({
|
|
334
|
+
...edge,
|
|
335
|
+
spaceIds: Object.freeze(edge.spaceIds),
|
|
336
|
+
vertexIds: Object.freeze(edge.vertexIds)
|
|
337
|
+
})
|
|
338
|
+
)
|
|
339
|
+
);
|
|
340
|
+
const vertices = Object.freeze(
|
|
341
|
+
[...verticesById.values()].sort((a, b) => compare(a.id, b.id)).map(
|
|
342
|
+
(vertex) => Object.freeze({
|
|
343
|
+
...vertex,
|
|
344
|
+
spaceIds: Object.freeze(vertex.spaceIds),
|
|
345
|
+
edgeIds: Object.freeze(vertex.edgeIds.sort(compare))
|
|
346
|
+
})
|
|
347
|
+
)
|
|
348
|
+
);
|
|
349
|
+
const requireSpace = (id) => {
|
|
350
|
+
const space = spacesById.get(id);
|
|
351
|
+
if (!space) throw new Error(`Unknown space '${id}' on board '${boardId}'.`);
|
|
352
|
+
return space;
|
|
353
|
+
};
|
|
354
|
+
const selectedIds = (coordinates) => Array.from(coordinates).flatMap((coordinate7) => {
|
|
355
|
+
const space = spacesByCoordinate.get(coordinateKey(coordinate7));
|
|
356
|
+
return space ? [space.id] : [];
|
|
357
|
+
});
|
|
358
|
+
const topology = {
|
|
359
|
+
edges,
|
|
360
|
+
vertices,
|
|
361
|
+
neighbors(id) {
|
|
362
|
+
const space = requireSpace(id);
|
|
363
|
+
return directions.flatMap((direction) => {
|
|
364
|
+
const neighbor = spacesByCoordinate.get(
|
|
365
|
+
coordinateKey(add(space, direction))
|
|
366
|
+
);
|
|
367
|
+
return neighbor ? [neighbor.id] : [];
|
|
368
|
+
});
|
|
369
|
+
},
|
|
370
|
+
distance(from, to) {
|
|
371
|
+
requireSpace(from);
|
|
372
|
+
requireSpace(to);
|
|
373
|
+
const queue = [from];
|
|
374
|
+
const distances = /* @__PURE__ */ new Map([[from, 0]]);
|
|
375
|
+
for (let index = 0; index < queue.length; index++) {
|
|
376
|
+
const id = queue[index];
|
|
377
|
+
const distance = distances.get(id);
|
|
378
|
+
if (id === to) return distance;
|
|
379
|
+
const space = requireSpace(id);
|
|
380
|
+
for (const direction of directions) {
|
|
381
|
+
const neighbor = spacesByCoordinate.get(
|
|
382
|
+
coordinateKey(add(space, direction))
|
|
383
|
+
);
|
|
384
|
+
if (neighbor && !distances.has(neighbor.id)) {
|
|
385
|
+
distances.set(neighbor.id, distance + 1);
|
|
386
|
+
queue.push(neighbor.id);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return Infinity;
|
|
391
|
+
},
|
|
392
|
+
gridDistance(from, to) {
|
|
393
|
+
return grid.distance(requireSpace(from), requireSpace(to));
|
|
394
|
+
},
|
|
395
|
+
ring(id, radius) {
|
|
396
|
+
return selectedIds(
|
|
397
|
+
new Grid(
|
|
398
|
+
hexClass(orientation),
|
|
399
|
+
ringTraversal({ center: requireSpace(id), radius })
|
|
400
|
+
)
|
|
401
|
+
);
|
|
402
|
+
},
|
|
403
|
+
line(from, to) {
|
|
404
|
+
return selectedIds(
|
|
405
|
+
new Grid(
|
|
406
|
+
hexClass(orientation),
|
|
407
|
+
lineTraversal({ start: requireSpace(from), stop: requireSpace(to) })
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
},
|
|
411
|
+
edgesOf(id) {
|
|
412
|
+
requireSpace(id);
|
|
413
|
+
return [...edgesBySpace.get(id)];
|
|
414
|
+
},
|
|
415
|
+
verticesOf(id) {
|
|
416
|
+
requireSpace(id);
|
|
417
|
+
return [...cornersBySpace.get(id)];
|
|
418
|
+
},
|
|
419
|
+
edgeAt(id, side) {
|
|
420
|
+
requireSpace(id);
|
|
421
|
+
const key = edgesBySpace.get(id)[side];
|
|
422
|
+
if (!key)
|
|
423
|
+
throw new Error("Hex side must be an integer from 0 through 5.");
|
|
424
|
+
return key;
|
|
425
|
+
},
|
|
426
|
+
vertexAt(id, corner) {
|
|
427
|
+
requireSpace(id);
|
|
428
|
+
const key = cornersBySpace.get(id)[corner];
|
|
429
|
+
if (!key)
|
|
430
|
+
throw new Error("Hex corner must be an integer from 0 through 5.");
|
|
431
|
+
return key;
|
|
432
|
+
},
|
|
433
|
+
edge(a, b) {
|
|
434
|
+
requireSpace(a);
|
|
435
|
+
requireSpace(b);
|
|
436
|
+
const edge = edgesBySpace.get(a).map((id) => edgesById.get(id)).find((edge2) => edge2.spaceIds.includes(b));
|
|
437
|
+
if (!edge || a === b)
|
|
438
|
+
throw new Error(`Spaces '${a}' and '${b}' do not share an edge.`);
|
|
439
|
+
return edge.id;
|
|
440
|
+
},
|
|
441
|
+
vertex(a, b, c) {
|
|
442
|
+
requireSpace(a);
|
|
443
|
+
requireSpace(b);
|
|
444
|
+
requireSpace(c);
|
|
445
|
+
const vertex = cornersBySpace.get(a).map((id) => verticesById.get(id)).find(
|
|
446
|
+
(vertex2) => vertex2.spaceIds.includes(b) && vertex2.spaceIds.includes(c)
|
|
447
|
+
);
|
|
448
|
+
if (!vertex || (/* @__PURE__ */ new Set([a, b, c])).size !== 3)
|
|
449
|
+
throw new Error("Spaces do not share exactly one vertex.");
|
|
450
|
+
return vertex.id;
|
|
451
|
+
},
|
|
452
|
+
incidentEdges(vertexId2) {
|
|
453
|
+
const vertex = verticesById.get(vertexId2);
|
|
454
|
+
if (!vertex) throw new Error(`Unknown vertex '${vertexId2}'.`);
|
|
455
|
+
return [...vertex.edgeIds];
|
|
456
|
+
},
|
|
457
|
+
incidentVertices(edgeId2) {
|
|
458
|
+
const edge = edgesById.get(edgeId2);
|
|
459
|
+
if (!edge) throw new Error(`Unknown edge '${edgeId2}'.`);
|
|
460
|
+
return [...edge.vertexIds];
|
|
461
|
+
},
|
|
462
|
+
spacesAt(vertexId2) {
|
|
463
|
+
const vertex = verticesById.get(vertexId2);
|
|
464
|
+
if (!vertex) throw new Error(`Unknown vertex '${vertexId2}'.`);
|
|
465
|
+
return [...vertex.spaceIds];
|
|
466
|
+
},
|
|
467
|
+
spacesAlong(edgeId2) {
|
|
468
|
+
const edge = edgesById.get(edgeId2);
|
|
469
|
+
if (!edge) throw new Error(`Unknown edge '${edgeId2}'.`);
|
|
470
|
+
return [...edge.spaceIds];
|
|
471
|
+
},
|
|
472
|
+
getLayout({
|
|
473
|
+
hexSize,
|
|
474
|
+
origin: suppliedOrigin = { x: 0, y: 0 }
|
|
475
|
+
}) {
|
|
476
|
+
const origin = { x: suppliedOrigin.x, y: suppliedOrigin.y };
|
|
477
|
+
if (!Number.isFinite(hexSize) || hexSize <= 0)
|
|
478
|
+
throw new Error("hexSize must be positive and finite.");
|
|
479
|
+
if (![origin.x, origin.y].every(Number.isFinite))
|
|
480
|
+
throw new Error("Layout origin must be finite.");
|
|
481
|
+
const scaled = new Grid(hexClass(orientation, hexSize), spaces);
|
|
482
|
+
const layoutSpaces = spaces.map((space) => {
|
|
483
|
+
const hex = scaled.getHex(space);
|
|
484
|
+
return {
|
|
485
|
+
id: space.id,
|
|
486
|
+
center: { x: hex.x + origin.x, y: hex.y + origin.y },
|
|
487
|
+
corners: hex.corners.map((point) => ({
|
|
488
|
+
x: point.x + origin.x,
|
|
489
|
+
y: point.y + origin.y
|
|
490
|
+
}))
|
|
491
|
+
};
|
|
492
|
+
});
|
|
493
|
+
const points = layoutSpaces.flatMap((space) => space.corners);
|
|
494
|
+
const minX = points.length ? Math.min(...points.map((point) => point.x)) : 0;
|
|
495
|
+
const minY = points.length ? Math.min(...points.map((point) => point.y)) : 0;
|
|
496
|
+
const maxX = points.length ? Math.max(...points.map((point) => point.x)) : 0;
|
|
497
|
+
const maxY = points.length ? Math.max(...points.map((point) => point.y)) : 0;
|
|
498
|
+
const scaledVertexPoints = new Map(
|
|
499
|
+
[...vertexPoints].map(([id, point]) => [
|
|
500
|
+
id,
|
|
501
|
+
{ x: point.x * hexSize + origin.x, y: point.y * hexSize + origin.y }
|
|
502
|
+
])
|
|
503
|
+
);
|
|
504
|
+
return {
|
|
505
|
+
viewBox: Object.freeze({
|
|
506
|
+
x: minX,
|
|
507
|
+
y: minY,
|
|
508
|
+
width: maxX - minX,
|
|
509
|
+
height: maxY - minY
|
|
510
|
+
}),
|
|
511
|
+
spaces: Object.freeze(
|
|
512
|
+
layoutSpaces.map(
|
|
513
|
+
(space) => Object.freeze({
|
|
514
|
+
...space,
|
|
515
|
+
center: Object.freeze(space.center),
|
|
516
|
+
corners: Object.freeze(
|
|
517
|
+
space.corners.map((point) => Object.freeze(point))
|
|
518
|
+
)
|
|
519
|
+
})
|
|
520
|
+
)
|
|
521
|
+
),
|
|
522
|
+
edges: Object.freeze(
|
|
523
|
+
edges.map(
|
|
524
|
+
(edge) => Object.freeze({
|
|
525
|
+
id: edge.id,
|
|
526
|
+
from: Object.freeze(scaledVertexPoints.get(edge.vertexIds[0])),
|
|
527
|
+
to: Object.freeze(scaledVertexPoints.get(edge.vertexIds[1]))
|
|
528
|
+
})
|
|
529
|
+
)
|
|
530
|
+
),
|
|
531
|
+
vertices: Object.freeze(
|
|
532
|
+
vertices.map(
|
|
533
|
+
(vertex) => Object.freeze({
|
|
534
|
+
id: vertex.id,
|
|
535
|
+
center: Object.freeze(scaledVertexPoints.get(vertex.id))
|
|
536
|
+
})
|
|
537
|
+
)
|
|
538
|
+
),
|
|
539
|
+
pointToSpace(point) {
|
|
540
|
+
const hex = scaled.pointToHex(
|
|
541
|
+
{ x: point.x - origin.x, y: point.y - origin.y },
|
|
542
|
+
{ allowOutside: false }
|
|
543
|
+
);
|
|
544
|
+
return hex ? spacesByCoordinate.get(coordinateKey(hex))?.id : void 0;
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
let cachedLayout;
|
|
550
|
+
const buildLayout = topology.getLayout;
|
|
551
|
+
topology.getLayout = (options) => {
|
|
552
|
+
const key = JSON.stringify([
|
|
553
|
+
options.hexSize,
|
|
554
|
+
options.origin?.x ?? 0,
|
|
555
|
+
options.origin?.y ?? 0
|
|
556
|
+
]);
|
|
557
|
+
if (cachedLayout?.key !== key) {
|
|
558
|
+
const value = buildLayout(options);
|
|
559
|
+
cachedLayout = { key, value: Object.freeze(value) };
|
|
560
|
+
}
|
|
561
|
+
return cachedLayout.value;
|
|
562
|
+
};
|
|
563
|
+
return Object.freeze(topology);
|
|
564
|
+
}
|
|
565
|
+
function createHexTopologyCache() {
|
|
566
|
+
const entries = /* @__PURE__ */ new Map();
|
|
567
|
+
return (board2) => {
|
|
568
|
+
const orientation = board2.orientation ?? "pointy";
|
|
569
|
+
const cached = entries.get(board2.id);
|
|
570
|
+
if (cached?.orientation === orientation && cached.spaces.size === board2.spaces.length) {
|
|
571
|
+
const seen = /* @__PURE__ */ new Set();
|
|
572
|
+
const equal = board2.spaces.every((space) => {
|
|
573
|
+
const previous = cached.spaces.get(space.id);
|
|
574
|
+
if (seen.has(space.id)) return false;
|
|
575
|
+
seen.add(space.id);
|
|
576
|
+
return previous?.q === space.q && previous?.r === space.r;
|
|
577
|
+
});
|
|
578
|
+
if (equal) return cached.value;
|
|
579
|
+
}
|
|
580
|
+
const value = createHexTopology(board2);
|
|
581
|
+
if (!entries.has(board2.id) && entries.size >= 64)
|
|
582
|
+
entries.delete(entries.keys().next().value);
|
|
583
|
+
entries.set(board2.id, {
|
|
584
|
+
orientation,
|
|
585
|
+
spaces: new Map(board2.spaces.map(({ id, q, r }) => [id, { q, r }])),
|
|
586
|
+
value
|
|
587
|
+
});
|
|
588
|
+
return value;
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/shared/board-topology.ts
|
|
593
|
+
var BoardTopologyError = class extends Error {
|
|
594
|
+
name = "BoardTopologyError";
|
|
595
|
+
};
|
|
596
|
+
function coordinate(value) {
|
|
597
|
+
if (!Number.isSafeInteger(value) || Math.abs(value) > MAXIMUM_BOARD_COORDINATE)
|
|
598
|
+
throw new BoardTopologyError(
|
|
599
|
+
`Board coordinate must be an integer within +/-${MAXIMUM_BOARD_COORDINATE}.`
|
|
600
|
+
);
|
|
601
|
+
return value === 0 ? 0 : value;
|
|
602
|
+
}
|
|
603
|
+
function rotateHex(q, r, rotation) {
|
|
604
|
+
coordinate(q);
|
|
605
|
+
coordinate(r);
|
|
606
|
+
if (!Number.isInteger(rotation) || rotation < 0 || rotation > 5)
|
|
607
|
+
throw new BoardTopologyError(
|
|
608
|
+
"Hex rotation must be an integer from 0 through 5."
|
|
609
|
+
);
|
|
610
|
+
for (let step = 0; step < rotation; step++) [q, r] = [-r, q + r];
|
|
611
|
+
return { q: coordinate(q), r: coordinate(r) };
|
|
612
|
+
}
|
|
613
|
+
function rotateSquare(col, row, rotation) {
|
|
614
|
+
coordinate(col);
|
|
615
|
+
coordinate(row);
|
|
616
|
+
if (!Number.isInteger(rotation) || rotation < 0 || rotation > 3)
|
|
617
|
+
throw new BoardTopologyError(
|
|
618
|
+
"Square rotation must be an integer from 0 through 3."
|
|
619
|
+
);
|
|
620
|
+
for (let step = 0; step < rotation; step++) [col, row] = [-row, col];
|
|
621
|
+
return { col: coordinate(col), row: coordinate(row) };
|
|
622
|
+
}
|
|
623
|
+
function annotate(element, annotation) {
|
|
624
|
+
const merge = (key) => {
|
|
625
|
+
const before = element[key];
|
|
626
|
+
const after = annotation[key];
|
|
627
|
+
if (before !== void 0 && after !== void 0 && before !== after)
|
|
628
|
+
throw new BoardTopologyError(
|
|
629
|
+
`Conflicting ${key} metadata on '${element.id}'.`
|
|
630
|
+
);
|
|
631
|
+
return after ?? before;
|
|
632
|
+
};
|
|
633
|
+
const fields3 = { ...element.fields };
|
|
634
|
+
for (const [key, value] of Object.entries(annotation.fields)) {
|
|
635
|
+
if (Object.hasOwn(fields3, key) && encodeCanonicalPluginRuntimeJson(fields3[key]) !== encodeCanonicalPluginRuntimeJson(value))
|
|
636
|
+
throw new BoardTopologyError(
|
|
637
|
+
`Conflicting field '${key}' metadata on '${element.id}'.`
|
|
638
|
+
);
|
|
639
|
+
Object.defineProperty(fields3, key, {
|
|
640
|
+
value,
|
|
641
|
+
enumerable: true,
|
|
642
|
+
writable: true,
|
|
643
|
+
configurable: true
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return { ...element, typeId: merge("typeId"), label: merge("label"), fields: fields3 };
|
|
647
|
+
}
|
|
648
|
+
function deriveBoardTopology(table, definitions, boardId) {
|
|
649
|
+
if (!Object.hasOwn(table.boards, boardId))
|
|
650
|
+
throw new BoardTopologyError(`Unknown board '${boardId}'.`);
|
|
651
|
+
const instance = table.boards[boardId];
|
|
652
|
+
if (!Object.hasOwn(definitions.boardDefinitions, instance.baseId))
|
|
653
|
+
throw new BoardTopologyError(
|
|
654
|
+
`Unknown board definition '${instance.baseId}'.`
|
|
655
|
+
);
|
|
656
|
+
const definition = definitions.boardDefinitions[instance.baseId];
|
|
657
|
+
const playerId = definition.scope === "perPlayer" ? parsePerPlayerInstanceId(boardId)?.playerId : void 0;
|
|
658
|
+
const base = {
|
|
659
|
+
id: boardId,
|
|
660
|
+
baseId: instance.baseId,
|
|
661
|
+
name: definition.name,
|
|
662
|
+
scope: definition.scope,
|
|
663
|
+
...playerId === void 0 ? {} : { playerId },
|
|
664
|
+
typeId: definition.typeId,
|
|
665
|
+
fields: definition.fields,
|
|
666
|
+
relations: instance.relations
|
|
667
|
+
};
|
|
668
|
+
if (definition.layout === "generic") {
|
|
669
|
+
for (const tileId of Object.keys(table.tiles)) {
|
|
670
|
+
const location = table.componentLocations[tileId];
|
|
671
|
+
if (location?.type === "OnBoard" && "boardId" in location && location.boardId === boardId)
|
|
672
|
+
throw new BoardTopologyError(
|
|
673
|
+
`Tile '${tileId}' cannot be placed on generic board '${boardId}'.`
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
const result2 = {
|
|
677
|
+
...base,
|
|
678
|
+
layout: "generic",
|
|
679
|
+
spaces: definition.spaces
|
|
680
|
+
};
|
|
681
|
+
validateRelations(result2);
|
|
682
|
+
return immutableCopy(result2);
|
|
683
|
+
}
|
|
684
|
+
const hexSpaces = {};
|
|
685
|
+
const squareSpaces = {};
|
|
686
|
+
const occupied = /* @__PURE__ */ new Map();
|
|
687
|
+
const placed = [];
|
|
688
|
+
for (const [tileId, tile] of Object.entries(table.tiles).sort(
|
|
689
|
+
([a], [b]) => a < b ? -1 : a > b ? 1 : 0
|
|
690
|
+
)) {
|
|
691
|
+
const location = table.componentLocations[tileId];
|
|
692
|
+
if (location?.type !== "OnBoard" || !("boardId" in location) || location.boardId !== boardId)
|
|
693
|
+
continue;
|
|
694
|
+
if (!Object.hasOwn(definitions.tileDefinitions, tile.tileTypeId))
|
|
695
|
+
throw new BoardTopologyError(`Unknown tile type '${tile.tileTypeId}'.`);
|
|
696
|
+
const tileDefinition = definitions.tileDefinitions[tile.tileTypeId];
|
|
697
|
+
if (!("layout" in location) || location.layout !== definition.layout || tileDefinition.layout !== definition.layout)
|
|
698
|
+
throw new BoardTopologyError(
|
|
699
|
+
`Tile '${tileId}' layout does not match board '${boardId}'.`
|
|
700
|
+
);
|
|
701
|
+
const placement2 = location;
|
|
702
|
+
placed.push({ tileId, definition: tileDefinition, placement: placement2 });
|
|
703
|
+
if (tileDefinition.layout === "hex" && placement2.layout === "hex") {
|
|
704
|
+
coordinate(placement2.q);
|
|
705
|
+
coordinate(placement2.r);
|
|
706
|
+
for (const cell2 of tileDefinition.cells) {
|
|
707
|
+
const rotated = rotateHex(cell2.at.q, cell2.at.r, placement2.rotation);
|
|
708
|
+
const q = coordinate(rotated.q + placement2.q), r = coordinate(rotated.r + placement2.r);
|
|
709
|
+
const key = `${q},${r}`;
|
|
710
|
+
if (occupied.has(key))
|
|
711
|
+
throw new BoardTopologyError(
|
|
712
|
+
`Overlapping tile cells from tiles '${occupied.get(key)}' and '${tileId}' on board '${boardId}' at ${key}.`
|
|
713
|
+
);
|
|
714
|
+
occupied.set(key, tileId);
|
|
715
|
+
const id = tileSpaceId(tileId, cell2.id);
|
|
716
|
+
hexSpaces[id] = {
|
|
717
|
+
id,
|
|
718
|
+
tileId,
|
|
719
|
+
localCellId: cell2.id,
|
|
720
|
+
q,
|
|
721
|
+
r,
|
|
722
|
+
name: cell2.name,
|
|
723
|
+
typeId: cell2.typeId,
|
|
724
|
+
fields: cell2.fields
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
} else if (tileDefinition.layout === "square" && placement2.layout === "square") {
|
|
728
|
+
coordinate(placement2.col);
|
|
729
|
+
coordinate(placement2.row);
|
|
730
|
+
for (const cell2 of tileDefinition.cells) {
|
|
731
|
+
const rotated = rotateSquare(
|
|
732
|
+
cell2.at.col,
|
|
733
|
+
cell2.at.row,
|
|
734
|
+
placement2.rotation
|
|
735
|
+
);
|
|
736
|
+
const col = coordinate(rotated.col + placement2.col), row = coordinate(rotated.row + placement2.row);
|
|
737
|
+
const key = `${col},${row}`;
|
|
738
|
+
if (occupied.has(key))
|
|
739
|
+
throw new BoardTopologyError(
|
|
740
|
+
`Overlapping tile cells from tiles '${occupied.get(key)}' and '${tileId}' on board '${boardId}' at ${key}.`
|
|
741
|
+
);
|
|
742
|
+
occupied.set(key, tileId);
|
|
743
|
+
const id = tileSpaceId(tileId, cell2.id);
|
|
744
|
+
squareSpaces[id] = {
|
|
745
|
+
id,
|
|
746
|
+
tileId,
|
|
747
|
+
localCellId: cell2.id,
|
|
748
|
+
col,
|
|
749
|
+
row,
|
|
750
|
+
name: cell2.name,
|
|
751
|
+
typeId: cell2.typeId,
|
|
752
|
+
fields: cell2.fields
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (definition.layout === "hex") {
|
|
758
|
+
const lattice2 = createHexTopology({
|
|
759
|
+
id: boardId,
|
|
760
|
+
orientation: definition.orientation,
|
|
761
|
+
spaces: Object.values(hexSpaces)
|
|
762
|
+
});
|
|
763
|
+
const edges = new Map(
|
|
764
|
+
lattice2.edges.map((edge) => [edge.id, { ...edge, fields: {} }])
|
|
765
|
+
);
|
|
766
|
+
const vertices = new Map(
|
|
767
|
+
lattice2.vertices.map((vertex) => [vertex.id, { ...vertex, fields: {} }])
|
|
768
|
+
);
|
|
769
|
+
for (const tile of placed) {
|
|
770
|
+
if (tile.definition.layout !== "hex" || tile.placement.layout !== "hex")
|
|
771
|
+
continue;
|
|
772
|
+
for (const annotation of tile.definition.edges) {
|
|
773
|
+
const id = lattice2.edgeAt(
|
|
774
|
+
tileSpaceId(tile.tileId, annotation.cellId),
|
|
775
|
+
hexSide(annotation.side + tile.placement.rotation)
|
|
776
|
+
);
|
|
777
|
+
edges.set(id, annotate(edges.get(id), annotation));
|
|
778
|
+
}
|
|
779
|
+
for (const annotation of tile.definition.vertices) {
|
|
780
|
+
const id = lattice2.vertexAt(
|
|
781
|
+
tileSpaceId(tile.tileId, annotation.cellId),
|
|
782
|
+
hexSide(annotation.corner + tile.placement.rotation)
|
|
783
|
+
);
|
|
784
|
+
vertices.set(id, annotate(vertices.get(id), annotation));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
const result2 = {
|
|
788
|
+
...base,
|
|
789
|
+
layout: "hex",
|
|
790
|
+
orientation: definition.orientation,
|
|
791
|
+
spaces: hexSpaces,
|
|
792
|
+
edges: [...edges.values()].sort(
|
|
793
|
+
(a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
794
|
+
),
|
|
795
|
+
vertices: [...vertices.values()].sort(
|
|
796
|
+
(a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
797
|
+
)
|
|
798
|
+
};
|
|
799
|
+
validateRelations(result2);
|
|
800
|
+
return immutableCopy(result2);
|
|
801
|
+
}
|
|
802
|
+
const lattice = squareLattice(boardId, Object.values(squareSpaces));
|
|
803
|
+
for (const tile of placed) {
|
|
804
|
+
if (tile.definition.layout !== "square" || tile.placement.layout !== "square")
|
|
805
|
+
continue;
|
|
806
|
+
for (const annotation of tile.definition.edges) {
|
|
807
|
+
const id = lattice.edgeAt(
|
|
808
|
+
squareSpaces[tileSpaceId(tile.tileId, annotation.cellId)],
|
|
809
|
+
(annotation.side + tile.placement.rotation) % 4
|
|
810
|
+
);
|
|
811
|
+
lattice.edges.set(id, annotate(lattice.edges.get(id), annotation));
|
|
812
|
+
}
|
|
813
|
+
for (const annotation of tile.definition.vertices) {
|
|
814
|
+
const id = lattice.vertexAt(
|
|
815
|
+
squareSpaces[tileSpaceId(tile.tileId, annotation.cellId)],
|
|
816
|
+
(annotation.corner + tile.placement.rotation) % 4
|
|
817
|
+
);
|
|
818
|
+
lattice.vertices.set(id, annotate(lattice.vertices.get(id), annotation));
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
const result = {
|
|
822
|
+
...base,
|
|
823
|
+
layout: "square",
|
|
824
|
+
spaces: squareSpaces,
|
|
825
|
+
edges: [...lattice.edges.values()].sort(
|
|
826
|
+
(a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
827
|
+
),
|
|
828
|
+
vertices: [...lattice.vertices.values()].sort(
|
|
829
|
+
(a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0
|
|
830
|
+
)
|
|
831
|
+
};
|
|
832
|
+
validateRelations(result);
|
|
833
|
+
return immutableCopy(result);
|
|
834
|
+
}
|
|
835
|
+
function hexSide(value) {
|
|
836
|
+
const side = value % 6;
|
|
837
|
+
switch (side) {
|
|
838
|
+
case 0:
|
|
839
|
+
case 1:
|
|
840
|
+
case 2:
|
|
841
|
+
case 3:
|
|
842
|
+
case 4:
|
|
843
|
+
case 5:
|
|
844
|
+
return side;
|
|
845
|
+
default:
|
|
846
|
+
throw new BoardTopologyError("Invalid hex side.");
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
function validateRelations(board2) {
|
|
850
|
+
const identities = /* @__PURE__ */ new Set();
|
|
851
|
+
for (const relation of board2.relations) {
|
|
852
|
+
if (typeof relation.id !== "string" || relation.id.length === 0)
|
|
853
|
+
throw new BoardTopologyError(
|
|
854
|
+
`Relation identity must be nonempty on board '${board2.id}'.`
|
|
855
|
+
);
|
|
856
|
+
if (identities.has(relation.id))
|
|
857
|
+
throw new BoardTopologyError(
|
|
858
|
+
`Duplicate relation identity '${relation.id}' on board '${board2.id}'.`
|
|
859
|
+
);
|
|
860
|
+
identities.add(relation.id);
|
|
861
|
+
if (!Object.hasOwn(board2.spaces, relation.fromSpaceId) || !Object.hasOwn(board2.spaces, relation.toSpaceId))
|
|
862
|
+
throw new BoardTopologyError(
|
|
863
|
+
`Relation on board '${board2.id}' references an absent space.`
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function squareLattice(boardId, spaces) {
|
|
868
|
+
const edges = /* @__PURE__ */ new Map();
|
|
869
|
+
const vertices = /* @__PURE__ */ new Map();
|
|
870
|
+
const corners = [
|
|
871
|
+
[1, 1],
|
|
872
|
+
[0, 1],
|
|
873
|
+
[0, 0],
|
|
874
|
+
[1, 0]
|
|
875
|
+
];
|
|
876
|
+
const vertexAt = (space, corner) => {
|
|
877
|
+
const offset = corners[corner];
|
|
878
|
+
if (!space || !offset)
|
|
879
|
+
throw new BoardTopologyError("Unknown square cell or corner.");
|
|
880
|
+
return boardVertexId(
|
|
881
|
+
"square",
|
|
882
|
+
boardId,
|
|
883
|
+
`${space.col + offset[0]},${space.row + offset[1]}`
|
|
884
|
+
);
|
|
885
|
+
};
|
|
886
|
+
const edgeAt = (space, side) => {
|
|
887
|
+
if (!space || !Number.isInteger(side) || side < 0 || side > 3)
|
|
888
|
+
throw new BoardTopologyError("Unknown square cell or side.");
|
|
889
|
+
switch (side) {
|
|
890
|
+
case 0:
|
|
891
|
+
return boardEdgeId(
|
|
892
|
+
"square",
|
|
893
|
+
boardId,
|
|
894
|
+
`${space.col + 1},${space.row}:v`
|
|
895
|
+
);
|
|
896
|
+
case 1:
|
|
897
|
+
return boardEdgeId(
|
|
898
|
+
"square",
|
|
899
|
+
boardId,
|
|
900
|
+
`${space.col},${space.row + 1}:h`
|
|
901
|
+
);
|
|
902
|
+
case 2:
|
|
903
|
+
return boardEdgeId("square", boardId, `${space.col},${space.row}:v`);
|
|
904
|
+
default:
|
|
905
|
+
return boardEdgeId("square", boardId, `${space.col},${space.row}:h`);
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
for (const space of spaces) {
|
|
909
|
+
for (let side = 0; side < 4; side++) {
|
|
910
|
+
const id = edgeAt(space, side);
|
|
911
|
+
const vertexIds = [vertexAt(space, (side + 3) % 4), vertexAt(space, side)];
|
|
912
|
+
const before = edges.get(id);
|
|
913
|
+
edges.set(id, {
|
|
914
|
+
id,
|
|
915
|
+
spaceIds: [...before?.spaceIds ?? [], space.id].sort(),
|
|
916
|
+
vertexIds: vertexIds.sort(),
|
|
917
|
+
fields: {}
|
|
918
|
+
});
|
|
919
|
+
for (const vertexId2 of vertexIds) {
|
|
920
|
+
const vertex = vertices.get(vertexId2);
|
|
921
|
+
vertices.set(vertexId2, {
|
|
922
|
+
id: vertexId2,
|
|
923
|
+
spaceIds: [
|
|
924
|
+
.../* @__PURE__ */ new Set([...vertex?.spaceIds ?? [], space.id])
|
|
925
|
+
].sort(),
|
|
926
|
+
edgeIds: [.../* @__PURE__ */ new Set([...vertex?.edgeIds ?? [], id])].sort(),
|
|
927
|
+
fields: {}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return { edges, vertices, edgeAt, vertexAt };
|
|
933
|
+
}
|
|
934
|
+
function createBoardTopologyCache() {
|
|
935
|
+
const entries = /* @__PURE__ */ new Map();
|
|
936
|
+
return (table, definitions, boardId) => {
|
|
937
|
+
if (!Object.hasOwn(table.boards, boardId))
|
|
938
|
+
throw new BoardTopologyError(`Unknown board '${boardId}'.`);
|
|
939
|
+
const instance = table.boards[boardId];
|
|
940
|
+
const placements = Object.entries(table.tiles).flatMap(([id, tile]) => {
|
|
941
|
+
const location = table.componentLocations[id];
|
|
942
|
+
return location?.type === "OnBoard" && "boardId" in location && location.boardId === boardId ? [
|
|
943
|
+
{
|
|
944
|
+
id,
|
|
945
|
+
tileTypeId: tile.tileTypeId,
|
|
946
|
+
location,
|
|
947
|
+
definition: definitions.tileDefinitions[tile.tileTypeId]
|
|
948
|
+
}
|
|
949
|
+
] : [];
|
|
950
|
+
}).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
951
|
+
const key = encodeCanonicalPluginRuntimeJson({
|
|
952
|
+
instance,
|
|
953
|
+
definition: definitions.boardDefinitions[instance.baseId],
|
|
954
|
+
placements
|
|
955
|
+
});
|
|
956
|
+
const previous = entries.get(boardId);
|
|
957
|
+
if (previous?.key === key) return previous.topology;
|
|
958
|
+
const topology = deriveBoardTopology(table, definitions, boardId);
|
|
959
|
+
if (!entries.has(boardId) && entries.size >= 64)
|
|
960
|
+
entries.delete(entries.keys().next().value);
|
|
961
|
+
entries.set(boardId, { key, topology });
|
|
962
|
+
return topology;
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// src/shared/board-topology-schema.ts
|
|
967
|
+
import * as z6 from "zod";
|
|
968
|
+
var coordinate2 = z6.number().int().min(-MAXIMUM_BOARD_COORDINATE).max(MAXIMUM_BOARD_COORDINATE);
|
|
969
|
+
var fields = z6.record(z6.string(), RuntimeJsonSchema);
|
|
970
|
+
var BoardRelationSchema = z6.strictObject({
|
|
971
|
+
id: z6.string().min(1),
|
|
972
|
+
typeId: z6.string().min(1),
|
|
973
|
+
fromSpaceId: z6.string().min(1),
|
|
974
|
+
toSpaceId: z6.string().min(1),
|
|
975
|
+
directed: z6.boolean(),
|
|
976
|
+
fields
|
|
977
|
+
});
|
|
978
|
+
var BoardSpaceSchema = z6.strictObject({
|
|
979
|
+
id: z6.string().min(1),
|
|
980
|
+
name: z6.string().nullable().optional(),
|
|
981
|
+
typeId: z6.string().min(1).nullable().optional(),
|
|
982
|
+
fields
|
|
983
|
+
});
|
|
984
|
+
var tileCell = BoardSpaceSchema.extend({
|
|
985
|
+
id: z6.custom((value) => parseTileSpaceId(value) !== null),
|
|
986
|
+
tileId: z6.string().min(1),
|
|
987
|
+
localCellId: z6.string().min(1)
|
|
988
|
+
});
|
|
989
|
+
function checkTileCell(cell2, ctx) {
|
|
990
|
+
const identity = parseTileSpaceId(cell2.id);
|
|
991
|
+
if (identity?.tileId !== cell2.tileId || identity?.cellId !== cell2.localCellId)
|
|
992
|
+
ctx.addIssue({
|
|
993
|
+
code: "custom",
|
|
994
|
+
path: ["id"],
|
|
995
|
+
message: "Tile space identity must match tileId and localCellId."
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
var HexSpaceSchema = tileCell.extend({ q: coordinate2, r: coordinate2 }).superRefine(checkTileCell);
|
|
999
|
+
var SquareSpaceSchema = tileCell.extend({ col: coordinate2, row: coordinate2 }).superRefine(checkTileCell);
|
|
1000
|
+
var edgeId = z6.custom(
|
|
1001
|
+
(value) => parseBoardElementId(value)?.kind === "edge"
|
|
1002
|
+
);
|
|
1003
|
+
var vertexId = z6.custom(
|
|
1004
|
+
(value) => parseBoardElementId(value)?.kind === "vertex"
|
|
1005
|
+
);
|
|
1006
|
+
var BoardEdgeSchema = z6.strictObject({
|
|
1007
|
+
id: edgeId,
|
|
1008
|
+
spaceIds: z6.array(z6.string().min(1)).min(1),
|
|
1009
|
+
vertexIds: z6.tuple([vertexId, vertexId]),
|
|
1010
|
+
typeId: z6.string().min(1).optional(),
|
|
1011
|
+
label: z6.string().optional(),
|
|
1012
|
+
fields
|
|
1013
|
+
});
|
|
1014
|
+
var BoardVertexSchema = z6.strictObject({
|
|
1015
|
+
id: vertexId,
|
|
1016
|
+
spaceIds: z6.array(z6.string().min(1)).min(1),
|
|
1017
|
+
edgeIds: z6.array(edgeId).min(1),
|
|
1018
|
+
typeId: z6.string().min(1).optional(),
|
|
1019
|
+
label: z6.string().optional(),
|
|
1020
|
+
fields
|
|
1021
|
+
});
|
|
1022
|
+
function identityRecord(schema) {
|
|
1023
|
+
return z6.record(z6.string().min(1), schema).superRefine((entries, ctx) => {
|
|
1024
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
1025
|
+
if (value.id !== key)
|
|
1026
|
+
ctx.addIssue({
|
|
1027
|
+
code: "custom",
|
|
1028
|
+
path: [key, "id"],
|
|
1029
|
+
message: "Record key must match item identity."
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
var board = z6.strictObject({
|
|
1035
|
+
id: z6.string().min(1),
|
|
1036
|
+
baseId: z6.string().min(1),
|
|
1037
|
+
name: z6.string(),
|
|
1038
|
+
scope: z6.enum(["shared", "perPlayer"]),
|
|
1039
|
+
playerId: z6.string().min(1).optional(),
|
|
1040
|
+
typeId: z6.string().min(1).optional(),
|
|
1041
|
+
fields,
|
|
1042
|
+
relations: z6.array(BoardRelationSchema)
|
|
1043
|
+
});
|
|
1044
|
+
var GenericBoardTopologySchema = board.extend({
|
|
1045
|
+
layout: z6.literal("generic"),
|
|
1046
|
+
spaces: identityRecord(BoardSpaceSchema)
|
|
1047
|
+
});
|
|
1048
|
+
var HexBoardTopologySchema = board.extend({
|
|
1049
|
+
layout: z6.literal("hex"),
|
|
1050
|
+
orientation: z6.enum(["pointy", "flat"]),
|
|
1051
|
+
spaces: identityRecord(HexSpaceSchema),
|
|
1052
|
+
edges: z6.array(BoardEdgeSchema),
|
|
1053
|
+
vertices: z6.array(BoardVertexSchema)
|
|
1054
|
+
});
|
|
1055
|
+
var SquareBoardTopologySchema = board.extend({
|
|
1056
|
+
layout: z6.literal("square"),
|
|
1057
|
+
spaces: identityRecord(SquareSpaceSchema),
|
|
1058
|
+
edges: z6.array(BoardEdgeSchema),
|
|
1059
|
+
vertices: z6.array(BoardVertexSchema)
|
|
1060
|
+
});
|
|
1061
|
+
function validateTopologyGraph(topology, ctx) {
|
|
1062
|
+
const issue = (path, message) => ctx.addIssue({ code: "custom", path, message });
|
|
1063
|
+
const replica = parsePerPlayerInstanceId(topology.id);
|
|
1064
|
+
if (topology.scope === "perPlayer") {
|
|
1065
|
+
if (replica?.family !== "board" || replica.baseId !== topology.baseId || replica.playerId !== topology.playerId)
|
|
1066
|
+
issue(
|
|
1067
|
+
["playerId"],
|
|
1068
|
+
"Per-player board identity must match baseId and playerId."
|
|
1069
|
+
);
|
|
1070
|
+
} else if (topology.playerId !== void 0 || replica !== null || topology.id !== topology.baseId) {
|
|
1071
|
+
issue(
|
|
1072
|
+
["playerId"],
|
|
1073
|
+
"Shared board identity must match baseId and cannot carry a player identity."
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
const spaces = new Set(Object.keys(topology.spaces));
|
|
1077
|
+
const relationIds = /* @__PURE__ */ new Set();
|
|
1078
|
+
for (const [index, relation] of topology.relations.entries()) {
|
|
1079
|
+
if (!spaces.has(relation.fromSpaceId) || !spaces.has(relation.toSpaceId))
|
|
1080
|
+
issue(
|
|
1081
|
+
["relations", index],
|
|
1082
|
+
"Relation endpoints must belong to this topology."
|
|
1083
|
+
);
|
|
1084
|
+
if (relation.id != null) {
|
|
1085
|
+
if (relationIds.has(relation.id))
|
|
1086
|
+
issue(
|
|
1087
|
+
["relations", index, "id"],
|
|
1088
|
+
"Relation identities must be unique."
|
|
1089
|
+
);
|
|
1090
|
+
relationIds.add(relation.id);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (topology.layout === "generic") return;
|
|
1094
|
+
const edges = new Map(topology.edges.map((edge) => [edge.id, edge]));
|
|
1095
|
+
const vertices = new Map(
|
|
1096
|
+
topology.vertices.map((vertex) => [vertex.id, vertex])
|
|
1097
|
+
);
|
|
1098
|
+
if (edges.size !== topology.edges.length)
|
|
1099
|
+
issue(["edges"], "Edge identities must be unique.");
|
|
1100
|
+
if (vertices.size !== topology.vertices.length)
|
|
1101
|
+
issue(["vertices"], "Vertex identities must be unique.");
|
|
1102
|
+
const references = (ids, known, path) => {
|
|
1103
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1104
|
+
for (const [index, id] of ids.entries()) {
|
|
1105
|
+
if (seen.has(id))
|
|
1106
|
+
issue([...path, index], "Incidence references must be unique.");
|
|
1107
|
+
if (!known.has(id))
|
|
1108
|
+
issue(
|
|
1109
|
+
[...path, index],
|
|
1110
|
+
"Incidence reference must belong to this topology."
|
|
1111
|
+
);
|
|
1112
|
+
seen.add(id);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
const edgeIds = new Set(edges.keys());
|
|
1116
|
+
const vertexIds = new Set(vertices.keys());
|
|
1117
|
+
const vertexEdges = new Map(
|
|
1118
|
+
topology.vertices.map((vertex) => [vertex.id, new Set(vertex.edgeIds)])
|
|
1119
|
+
);
|
|
1120
|
+
for (const [index, edge] of topology.edges.entries()) {
|
|
1121
|
+
references(edge.spaceIds, spaces, ["edges", index, "spaceIds"]);
|
|
1122
|
+
references(edge.vertexIds, vertexIds, ["edges", index, "vertexIds"]);
|
|
1123
|
+
for (const vertexId2 of edge.vertexIds) {
|
|
1124
|
+
if (vertices.has(vertexId2) && !vertexEdges.get(vertexId2)?.has(edge.id))
|
|
1125
|
+
issue(
|
|
1126
|
+
["edges", index, "vertexIds"],
|
|
1127
|
+
"Edge and vertex incidence must agree."
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
for (const [index, vertex] of topology.vertices.entries()) {
|
|
1132
|
+
references(vertex.spaceIds, spaces, ["vertices", index, "spaceIds"]);
|
|
1133
|
+
references(vertex.edgeIds, edgeIds, ["vertices", index, "edgeIds"]);
|
|
1134
|
+
for (const edgeId2 of vertex.edgeIds) {
|
|
1135
|
+
const edge = edges.get(edgeId2);
|
|
1136
|
+
if (edge && !edge.vertexIds.includes(vertex.id))
|
|
1137
|
+
issue(
|
|
1138
|
+
["vertices", index, "edgeIds"],
|
|
1139
|
+
"Edge and vertex incidence must agree."
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
for (const [kind, items] of [
|
|
1144
|
+
["edges", topology.edges],
|
|
1145
|
+
["vertices", topology.vertices]
|
|
1146
|
+
]) {
|
|
1147
|
+
for (const [index, item] of items.entries()) {
|
|
1148
|
+
const ids = [
|
|
1149
|
+
item.id,
|
|
1150
|
+
..."vertexIds" in item ? item.vertexIds : item.edgeIds
|
|
1151
|
+
];
|
|
1152
|
+
if (ids.some((id) => {
|
|
1153
|
+
const identity = parseBoardElementId(id);
|
|
1154
|
+
return identity?.boardId !== topology.id || identity?.layout !== topology.layout;
|
|
1155
|
+
}))
|
|
1156
|
+
ctx.addIssue({
|
|
1157
|
+
code: "custom",
|
|
1158
|
+
path: [kind, index],
|
|
1159
|
+
message: "World element identity must match its board and layout."
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
var BoardTopologySchema = z6.discriminatedUnion("layout", [
|
|
1165
|
+
GenericBoardTopologySchema,
|
|
1166
|
+
HexBoardTopologySchema,
|
|
1167
|
+
SquareBoardTopologySchema
|
|
1168
|
+
]).superRefine(validateTopologyGraph);
|
|
1169
|
+
var BoardTopologyMapSchema = identityRecord(BoardTopologySchema);
|
|
1170
|
+
|
|
1171
|
+
// src/shared/domain/player-identity.ts
|
|
1172
|
+
import * as z7 from "zod";
|
|
1173
|
+
function isPlayerIdValue(value) {
|
|
1174
|
+
return typeof value === "string" && value.length > 0 && value !== "__proto__";
|
|
1175
|
+
}
|
|
1176
|
+
var PlayerIdSchema = z7.string().min(1).refine((value) => isPlayerIdValue(value), {
|
|
1177
|
+
error: "Player id '__proto__' is reserved for record/parser interoperability."
|
|
1178
|
+
});
|
|
1179
|
+
var PlayerRosterSchema = z7.array(PlayerIdSchema).refine((ids) => new Set(ids).size === ids.length, {
|
|
1180
|
+
error: "Duplicate player id"
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
// src/shared/domain/seat-reference.ts
|
|
1184
|
+
import * as z8 from "zod";
|
|
1185
|
+
var SeatTileRefSchema = z8.string().regex(/^tile-ref:sha256:[0-9a-f]{64}$/).brand();
|
|
1186
|
+
var SeatSpaceRefSchema = z8.string().regex(/^space-ref:sha256:[0-9a-f]{64}$/).brand();
|
|
1187
|
+
|
|
1188
|
+
// src/shared/tile-appearance.ts
|
|
1189
|
+
function coordinate3(value) {
|
|
1190
|
+
if (!Number.isSafeInteger(value) || Math.abs(value) > MAXIMUM_BOARD_COORDINATE)
|
|
1191
|
+
throw new Error(
|
|
1192
|
+
"Public tile footprint exceeds the board coordinate domain."
|
|
1193
|
+
);
|
|
1194
|
+
return value === 0 ? 0 : value;
|
|
1195
|
+
}
|
|
1196
|
+
function getPublicTileFootprint(appearance, placement2) {
|
|
1197
|
+
if (appearance.layout === "hex" && placement2.layout === "hex")
|
|
1198
|
+
return {
|
|
1199
|
+
layout: "hex",
|
|
1200
|
+
cells: appearance.cells.map((cell2) => {
|
|
1201
|
+
const rotated = rotateHex(cell2.q, cell2.r, placement2.rotation);
|
|
1202
|
+
return {
|
|
1203
|
+
q: coordinate3(rotated.q + placement2.q),
|
|
1204
|
+
r: coordinate3(rotated.r + placement2.r)
|
|
1205
|
+
};
|
|
1206
|
+
})
|
|
1207
|
+
};
|
|
1208
|
+
if (appearance.layout === "square" && placement2.layout === "square")
|
|
1209
|
+
return {
|
|
1210
|
+
layout: "square",
|
|
1211
|
+
cells: appearance.cells.map((cell2) => {
|
|
1212
|
+
const rotated = rotateSquare(cell2.col, cell2.row, placement2.rotation);
|
|
1213
|
+
return {
|
|
1214
|
+
col: coordinate3(rotated.col + placement2.col),
|
|
1215
|
+
row: coordinate3(rotated.row + placement2.row)
|
|
1216
|
+
};
|
|
1217
|
+
})
|
|
1218
|
+
};
|
|
1219
|
+
throw new Error(
|
|
1220
|
+
"Tile public appearance must match its board placement layout."
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// src/shared/seat-topology-schema.ts
|
|
1225
|
+
import * as z11 from "zod";
|
|
1226
|
+
|
|
1227
|
+
// src/shared/domain/tile-disclosure.ts
|
|
1228
|
+
import * as z9 from "zod";
|
|
1229
|
+
var coordinate4 = z9.int().min(-MAXIMUM_BOARD_COORDINATE).max(MAXIMUM_BOARD_COORDINATE);
|
|
1230
|
+
var backImage = z9.string().regex(
|
|
1231
|
+
/^assets\/(?:[\w-][\w.-]*\/)*[\w-][\w.-]*\.(?:avif|gif|jpe?g|png|svg|webp)$/i,
|
|
1232
|
+
{ error: "Tile back image must be an image path under assets/." }
|
|
1233
|
+
);
|
|
1234
|
+
var PublicTileAppearanceSchema = z9.discriminatedUnion("layout", [
|
|
1235
|
+
z9.strictObject({
|
|
1236
|
+
layout: z9.literal("hex"),
|
|
1237
|
+
cells: z9.array(z9.strictObject({ q: coordinate4, r: coordinate4 })).nonempty(),
|
|
1238
|
+
backImage: backImage.optional()
|
|
1239
|
+
}),
|
|
1240
|
+
z9.strictObject({
|
|
1241
|
+
layout: z9.literal("square"),
|
|
1242
|
+
cells: z9.array(z9.strictObject({ col: coordinate4, row: coordinate4 })).nonempty(),
|
|
1243
|
+
backImage: backImage.optional()
|
|
1244
|
+
})
|
|
1245
|
+
]).superRefine((appearance, ctx) => {
|
|
1246
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1247
|
+
for (const [index, cell2] of appearance.cells.entries()) {
|
|
1248
|
+
const key = "q" in cell2 ? `${cell2.q},${cell2.r}` : `${cell2.col},${cell2.row}`;
|
|
1249
|
+
if (seen.has(key))
|
|
1250
|
+
ctx.addIssue({
|
|
1251
|
+
code: "custom",
|
|
1252
|
+
path: ["cells", index],
|
|
1253
|
+
message: "Public footprint coordinates must be unique."
|
|
1254
|
+
});
|
|
1255
|
+
seen.add(key);
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
var publicFace = z9.strictObject({ audience: z9.literal("public") });
|
|
1259
|
+
var ownerFace = z9.strictObject({ audience: z9.literal("owner") });
|
|
1260
|
+
var noFace = z9.strictObject({ audience: z9.literal("none") });
|
|
1261
|
+
var AuthoredTileFaceAudienceSchema = z9.discriminatedUnion("audience", [
|
|
1262
|
+
publicFace,
|
|
1263
|
+
ownerFace,
|
|
1264
|
+
noFace
|
|
1265
|
+
]);
|
|
1266
|
+
var TileFaceAudienceSchema = z9.discriminatedUnion("audience", [
|
|
1267
|
+
publicFace,
|
|
1268
|
+
ownerFace,
|
|
1269
|
+
noFace,
|
|
1270
|
+
z9.strictObject({
|
|
1271
|
+
audience: z9.literal("seats"),
|
|
1272
|
+
playerIds: z9.array(PlayerIdSchema).nonempty()
|
|
1273
|
+
}).superRefine((face, ctx) => {
|
|
1274
|
+
if (new Set(face.playerIds).size !== face.playerIds.length)
|
|
1275
|
+
ctx.addIssue({
|
|
1276
|
+
code: "custom",
|
|
1277
|
+
path: ["playerIds"],
|
|
1278
|
+
message: "Face audience seats must be unique."
|
|
1279
|
+
});
|
|
1280
|
+
})
|
|
1281
|
+
]);
|
|
1282
|
+
var AuthoredTileDisclosureSchema = z9.strictObject({
|
|
1283
|
+
face: AuthoredTileFaceAudienceSchema,
|
|
1284
|
+
appearance: PublicTileAppearanceSchema.optional()
|
|
1285
|
+
});
|
|
1286
|
+
var TileDisclosureSchema = z9.strictObject({
|
|
1287
|
+
face: TileFaceAudienceSchema,
|
|
1288
|
+
appearance: PublicTileAppearanceSchema.optional()
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
// src/shared/domain/tile-placement.ts
|
|
1292
|
+
import * as z10 from "zod";
|
|
1293
|
+
var coordinate5 = z10.number().int().min(-MAXIMUM_BOARD_COORDINATE).max(MAXIMUM_BOARD_COORDINATE);
|
|
1294
|
+
var TilePlacementSchema = z10.discriminatedUnion("layout", [
|
|
1295
|
+
z10.strictObject({
|
|
1296
|
+
type: z10.literal("OnBoard"),
|
|
1297
|
+
layout: z10.literal("hex"),
|
|
1298
|
+
boardId: z10.string().min(1),
|
|
1299
|
+
q: coordinate5,
|
|
1300
|
+
r: coordinate5,
|
|
1301
|
+
rotation: z10.union([
|
|
1302
|
+
z10.literal(0),
|
|
1303
|
+
z10.literal(1),
|
|
1304
|
+
z10.literal(2),
|
|
1305
|
+
z10.literal(3),
|
|
1306
|
+
z10.literal(4),
|
|
1307
|
+
z10.literal(5)
|
|
1308
|
+
])
|
|
1309
|
+
}),
|
|
1310
|
+
z10.strictObject({
|
|
1311
|
+
type: z10.literal("OnBoard"),
|
|
1312
|
+
layout: z10.literal("square"),
|
|
1313
|
+
boardId: z10.string().min(1),
|
|
1314
|
+
col: coordinate5,
|
|
1315
|
+
row: coordinate5,
|
|
1316
|
+
rotation: z10.union([z10.literal(0), z10.literal(1), z10.literal(2), z10.literal(3)])
|
|
1317
|
+
})
|
|
1318
|
+
]);
|
|
1319
|
+
|
|
1320
|
+
// src/shared/seat-topology-schema.ts
|
|
1321
|
+
var coordinate6 = z11.int().min(-MAXIMUM_BOARD_COORDINATE).max(MAXIMUM_BOARD_COORDINATE);
|
|
1322
|
+
var fields2 = z11.record(z11.string(), RuntimeJsonSchema);
|
|
1323
|
+
var ProjectedTileSchema = z11.discriminatedUnion("disclosure", [
|
|
1324
|
+
z11.strictObject({
|
|
1325
|
+
disclosure: z11.literal("visible"),
|
|
1326
|
+
ref: SeatTileRefSchema,
|
|
1327
|
+
tileTypeId: z11.string().min(1),
|
|
1328
|
+
name: z11.string(),
|
|
1329
|
+
frontImage: z11.string().optional(),
|
|
1330
|
+
ownerId: z11.string().min(1).nullable(),
|
|
1331
|
+
fields: fields2,
|
|
1332
|
+
properties: fields2
|
|
1333
|
+
}),
|
|
1334
|
+
z11.strictObject({
|
|
1335
|
+
disclosure: z11.literal("concealed"),
|
|
1336
|
+
ref: SeatTileRefSchema,
|
|
1337
|
+
appearance: PublicTileAppearanceSchema
|
|
1338
|
+
})
|
|
1339
|
+
]);
|
|
1340
|
+
var placement = z11.discriminatedUnion("layout", [
|
|
1341
|
+
TilePlacementSchema.options[0].omit({ type: true, boardId: true }),
|
|
1342
|
+
TilePlacementSchema.options[1].omit({ type: true, boardId: true })
|
|
1343
|
+
]);
|
|
1344
|
+
var placedTile = z11.discriminatedUnion("disclosure", [
|
|
1345
|
+
ProjectedTileSchema.options[0].extend({ placement }),
|
|
1346
|
+
ProjectedTileSchema.options[1].extend({ placement })
|
|
1347
|
+
]);
|
|
1348
|
+
var cell = BoardSpaceSchema.extend({
|
|
1349
|
+
id: SeatSpaceRefSchema,
|
|
1350
|
+
tileRef: SeatTileRefSchema,
|
|
1351
|
+
localCellId: z11.string().min(1)
|
|
1352
|
+
});
|
|
1353
|
+
var SeatHexSpaceSchema = cell.extend({ q: coordinate6, r: coordinate6 });
|
|
1354
|
+
var SeatSquareSpaceSchema = cell.extend({
|
|
1355
|
+
col: coordinate6,
|
|
1356
|
+
row: coordinate6
|
|
1357
|
+
});
|
|
1358
|
+
var SeatBoardTopologySchema = z11.discriminatedUnion("layout", [
|
|
1359
|
+
GenericBoardTopologySchema,
|
|
1360
|
+
HexBoardTopologySchema.extend({
|
|
1361
|
+
spaces: identityRecord(SeatHexSpaceSchema),
|
|
1362
|
+
tiles: z11.array(placedTile)
|
|
1363
|
+
}),
|
|
1364
|
+
SquareBoardTopologySchema.extend({
|
|
1365
|
+
spaces: identityRecord(SeatSquareSpaceSchema),
|
|
1366
|
+
tiles: z11.array(placedTile)
|
|
1367
|
+
})
|
|
1368
|
+
]).superRefine((board2, ctx) => {
|
|
1369
|
+
validateTopologyGraph(board2, ctx);
|
|
1370
|
+
if (board2.layout === "generic") return;
|
|
1371
|
+
const tiles = new Map(board2.tiles.map((tile) => [tile.ref, tile]));
|
|
1372
|
+
if (tiles.size !== board2.tiles.length)
|
|
1373
|
+
ctx.addIssue({
|
|
1374
|
+
code: "custom",
|
|
1375
|
+
path: ["tiles"],
|
|
1376
|
+
message: "Tile references must be unique."
|
|
1377
|
+
});
|
|
1378
|
+
for (const [index, tile] of board2.tiles.entries()) {
|
|
1379
|
+
if (tile.placement.layout !== board2.layout || tile.disclosure === "concealed" && tile.appearance.layout !== board2.layout)
|
|
1380
|
+
ctx.addIssue({
|
|
1381
|
+
code: "custom",
|
|
1382
|
+
path: ["tiles", index],
|
|
1383
|
+
message: "Tile presentation must match its board layout."
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
for (const [index, tile] of board2.tiles.entries()) {
|
|
1387
|
+
if (tile.disclosure !== "concealed") continue;
|
|
1388
|
+
try {
|
|
1389
|
+
getPublicTileFootprint(tile.appearance, tile.placement);
|
|
1390
|
+
} catch (error) {
|
|
1391
|
+
ctx.addIssue({
|
|
1392
|
+
code: "custom",
|
|
1393
|
+
path: ["tiles", index, "appearance"],
|
|
1394
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
const cellsByTile = /* @__PURE__ */ new Map();
|
|
1399
|
+
for (const [id, space] of Object.entries(board2.spaces)) {
|
|
1400
|
+
const cells = cellsByTile.get(space.tileRef) ?? /* @__PURE__ */ new Set();
|
|
1401
|
+
if (cells.has(space.localCellId))
|
|
1402
|
+
ctx.addIssue({
|
|
1403
|
+
code: "custom",
|
|
1404
|
+
path: ["spaces", id],
|
|
1405
|
+
message: "A visible tile cell may be projected only once."
|
|
1406
|
+
});
|
|
1407
|
+
cells.add(space.localCellId);
|
|
1408
|
+
cellsByTile.set(space.tileRef, cells);
|
|
1409
|
+
if (tiles.get(space.tileRef)?.disclosure !== "visible")
|
|
1410
|
+
ctx.addIssue({
|
|
1411
|
+
code: "custom",
|
|
1412
|
+
path: ["spaces", id],
|
|
1413
|
+
message: "A projected cell must belong to a visible tile."
|
|
1414
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
for (const [index, tile] of board2.tiles.entries())
|
|
1417
|
+
if (tile.disclosure === "visible" && !cellsByTile.get(tile.ref)?.size)
|
|
1418
|
+
ctx.addIssue({
|
|
1419
|
+
code: "custom",
|
|
1420
|
+
path: ["tiles", index],
|
|
1421
|
+
message: "A visible tile requires at least one projected cell."
|
|
1422
|
+
});
|
|
1423
|
+
});
|
|
1424
|
+
var BoardProjectionSchema = identityRecord(SeatBoardTopologySchema);
|
|
1425
|
+
|
|
1426
|
+
// src/shared/lookup.ts
|
|
1427
|
+
function requireLookup(value, kind, id) {
|
|
1428
|
+
if (value === void 0) throw new Error(`${kind} "${id}" is not present.`);
|
|
1429
|
+
return value;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// src/shared/board-target.ts
|
|
1433
|
+
import * as z12 from "zod";
|
|
1434
|
+
var BoardSpaceTargetSchema = z12.strictObject({
|
|
1435
|
+
boardId: z12.string(),
|
|
1436
|
+
spaceId: z12.string()
|
|
1437
|
+
});
|
|
1438
|
+
function isBoardSpaceTarget(value) {
|
|
1439
|
+
return typeof value === "object" && value !== null && Object.keys(value).length === 2 && "boardId" in value && typeof value.boardId === "string" && "spaceId" in value && typeof value.spaceId === "string";
|
|
1440
|
+
}
|
|
1441
|
+
function sameBoardSpaceTarget(left, right) {
|
|
1442
|
+
return left.boardId === right.boardId && left.spaceId === right.spaceId;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// src/shared/input-domain.ts
|
|
1446
|
+
function inputDomainErrors(domain, value, selection, options = {}) {
|
|
1447
|
+
if (selection?.mode === "many") {
|
|
1448
|
+
if (!Array.isArray(value)) return ["Expected a list of values."];
|
|
1449
|
+
const errors = cardinalityErrors(
|
|
1450
|
+
value,
|
|
1451
|
+
selection.min,
|
|
1452
|
+
selection.max,
|
|
1453
|
+
"value",
|
|
1454
|
+
options.ignoreMinimum
|
|
1455
|
+
);
|
|
1456
|
+
if (selection.distinct && new Set(value.map(inputValueKey)).size !== value.length)
|
|
1457
|
+
errors.push("Choose each value only once.");
|
|
1458
|
+
if (value.some(
|
|
1459
|
+
(item) => inputDomainErrors(domain, item, void 0, options).length > 0
|
|
1460
|
+
))
|
|
1461
|
+
errors.push(ineligibleMessage(domain));
|
|
1462
|
+
return errors;
|
|
1463
|
+
}
|
|
1464
|
+
switch (domain.type) {
|
|
1465
|
+
case "choice":
|
|
1466
|
+
return inputTargetInDomain(domain, value) ? [] : [ineligibleMessage(domain)];
|
|
1467
|
+
case "choiceList": {
|
|
1468
|
+
if (!Array.isArray(value)) return ["Expected a list of choices."];
|
|
1469
|
+
const errors = cardinalityErrors(
|
|
1470
|
+
value,
|
|
1471
|
+
domain.min ?? 0,
|
|
1472
|
+
domain.max ?? domain.choices.length,
|
|
1473
|
+
"option",
|
|
1474
|
+
options.ignoreMinimum
|
|
1475
|
+
);
|
|
1476
|
+
if (value.some((item) => !inputTargetInDomain(domain, item)))
|
|
1477
|
+
errors.push(ineligibleMessage(domain));
|
|
1478
|
+
return errors;
|
|
1479
|
+
}
|
|
1480
|
+
case "tileTarget":
|
|
1481
|
+
case "cardTarget":
|
|
1482
|
+
case "boardTarget":
|
|
1483
|
+
case "zonePosition":
|
|
1484
|
+
return inputTargetInDomain(domain, value) ? [] : [ineligibleMessage(domain)];
|
|
1485
|
+
case "boundedNumber":
|
|
1486
|
+
return typeof value === "number" && value >= domain.min && value <= domain.max && Math.abs(
|
|
1487
|
+
(value - domain.min) / (domain.step ?? 1) - Math.round((value - domain.min) / (domain.step ?? 1))
|
|
1488
|
+
) < 1e-9 ? [] : ["Value is outside the current input domain."];
|
|
1489
|
+
case "resourceMap":
|
|
1490
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).every(
|
|
1491
|
+
(key) => domain.resources.some((entry) => entry.resourceId === key)
|
|
1492
|
+
) && domain.resources.every((entry) => {
|
|
1493
|
+
const amount = value[entry.resourceId] ?? 0;
|
|
1494
|
+
return typeof amount === "number" && Number.isInteger(amount) && amount >= (entry.min ?? 0) && amount <= (entry.max ?? Infinity);
|
|
1495
|
+
}) ? [] : ["Value is outside the current input domain."];
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
function inputValueInDomain(domain, value, selection, options = {}) {
|
|
1499
|
+
return inputDomainErrors(domain, value, selection, options).length === 0;
|
|
1500
|
+
}
|
|
1501
|
+
function inputTargetInDomain(domain, value) {
|
|
1502
|
+
switch (domain.type) {
|
|
1503
|
+
case "choice":
|
|
1504
|
+
case "choiceList":
|
|
1505
|
+
return domain.choices.some(
|
|
1506
|
+
(choice) => !choice.disabled && Object.is(choice.value, value)
|
|
1507
|
+
);
|
|
1508
|
+
case "tileTarget":
|
|
1509
|
+
case "cardTarget":
|
|
1510
|
+
return typeof value === "string" && domain.eligibleTargets.includes(value);
|
|
1511
|
+
case "boardTarget":
|
|
1512
|
+
return domain.valueKind === "board-space" ? isBoardSpaceTarget(value) && domain.eligibleTargets.some(
|
|
1513
|
+
(candidate) => sameBoardSpaceTarget(candidate, value)
|
|
1514
|
+
) : typeof value === "string" && domain.eligibleTargets.includes(value);
|
|
1515
|
+
case "zonePosition":
|
|
1516
|
+
return isPositionTarget(value) && domain.zones.some(
|
|
1517
|
+
(zone) => zone.zoneId === value.zoneId && zone.hostId === value.hostId && value.index <= zone.size
|
|
1518
|
+
);
|
|
1519
|
+
default:
|
|
1520
|
+
return inputValueInDomain(domain, value);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
function cardinalityErrors(value, min, max, unit, ignoreMinimum = false) {
|
|
1524
|
+
const errors = [];
|
|
1525
|
+
if (!ignoreMinimum && value.length < min)
|
|
1526
|
+
errors.push(`Choose at least ${min} ${pluralize(unit, min)}.`);
|
|
1527
|
+
if (max !== void 0 && value.length > max)
|
|
1528
|
+
errors.push(`Choose at most ${max} ${pluralize(unit, max)}.`);
|
|
1529
|
+
return errors;
|
|
1530
|
+
}
|
|
1531
|
+
function ineligibleMessage(domain) {
|
|
1532
|
+
return domain.type === "choice" || domain.type === "choiceList" ? "Selected choice is not eligible." : domain.type === "cardTarget" || domain.type === "tileTarget" || domain.type === "boardTarget" || domain.type === "zonePosition" ? "Selected target is not eligible." : "Value is outside the current input domain.";
|
|
1533
|
+
}
|
|
1534
|
+
function inputValueKey(value) {
|
|
1535
|
+
if (value === null) return "null";
|
|
1536
|
+
if (isBoardSpaceTarget(value))
|
|
1537
|
+
return `board-space:${JSON.stringify([value.boardId, value.spaceId])}`;
|
|
1538
|
+
if (isPositionTarget(value))
|
|
1539
|
+
return `position:${JSON.stringify([value.zoneId, value.hostId, value.index])}`;
|
|
1540
|
+
switch (typeof value) {
|
|
1541
|
+
case "string":
|
|
1542
|
+
return `string:${value}`;
|
|
1543
|
+
case "number":
|
|
1544
|
+
case "boolean":
|
|
1545
|
+
case "undefined":
|
|
1546
|
+
return `${typeof value}:${String(value)}`;
|
|
1547
|
+
default:
|
|
1548
|
+
return `json:${JSON.stringify(value)}`;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
function pluralize(word, count) {
|
|
1552
|
+
return count === 1 ? word : `${word}s`;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
export {
|
|
1556
|
+
__export,
|
|
1557
|
+
PositionTargetSchema,
|
|
1558
|
+
isPositionTarget,
|
|
1559
|
+
zonePositions,
|
|
1560
|
+
immutableCopy,
|
|
1561
|
+
GENERATED_ID_PREFIX,
|
|
1562
|
+
perPlayerInstanceId,
|
|
1563
|
+
parsePerPlayerInstanceId,
|
|
1564
|
+
perPlayerInstanceSchema,
|
|
1565
|
+
boardEdgeId,
|
|
1566
|
+
boardVertexId,
|
|
1567
|
+
parseBoardElementId,
|
|
1568
|
+
tileSpaceId,
|
|
1569
|
+
parseTileSpaceId,
|
|
1570
|
+
MAXIMUM_BOARD_COORDINATE,
|
|
1571
|
+
createHexTopology,
|
|
1572
|
+
createHexTopologyCache,
|
|
1573
|
+
RuntimeJsonSchema,
|
|
1574
|
+
canonicalizePluginRuntimeJson,
|
|
1575
|
+
encodeCanonicalPluginRuntimeJson,
|
|
1576
|
+
digestPluginRuntimeJson,
|
|
1577
|
+
BoardTopologyError,
|
|
1578
|
+
deriveBoardTopology,
|
|
1579
|
+
createBoardTopologyCache,
|
|
1580
|
+
getPublicTileFootprint,
|
|
1581
|
+
BoardRelationSchema,
|
|
1582
|
+
isPlayerIdValue,
|
|
1583
|
+
PlayerIdSchema,
|
|
1584
|
+
PlayerRosterSchema,
|
|
1585
|
+
AuthoredTileDisclosureSchema,
|
|
1586
|
+
TileDisclosureSchema,
|
|
1587
|
+
SeatTileRefSchema,
|
|
1588
|
+
SeatSpaceRefSchema,
|
|
1589
|
+
TilePlacementSchema,
|
|
1590
|
+
ProjectedTileSchema,
|
|
1591
|
+
SeatBoardTopologySchema,
|
|
1592
|
+
BoardProjectionSchema,
|
|
1593
|
+
requireLookup,
|
|
1594
|
+
BoardSpaceTargetSchema,
|
|
1595
|
+
isBoardSpaceTarget,
|
|
1596
|
+
sameBoardSpaceTarget,
|
|
1597
|
+
inputDomainErrors,
|
|
1598
|
+
inputValueInDomain,
|
|
1599
|
+
inputTargetInDomain,
|
|
1600
|
+
inputValueKey
|
|
1601
|
+
};
|
|
1602
|
+
//# sourceMappingURL=chunk-CZRPUWCT.js.map
|