@dreamboard-games/workspace-codegen 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.
Files changed (73) hide show
  1. package/LICENSE +89 -0
  2. package/NOTICE +1 -0
  3. package/dist/hex-geometry.d.ts +2 -0
  4. package/dist/hex-geometry.js +49 -0
  5. package/dist/index.d.ts +13 -0
  6. package/dist/index.js +22 -0
  7. package/dist/manifest-contract.d.ts +14 -0
  8. package/dist/manifest-contract.js +4897 -0
  9. package/dist/manifest-validation.d.ts +6 -0
  10. package/dist/manifest-validation.js +506 -0
  11. package/dist/ownership.d.ts +31 -0
  12. package/dist/ownership.js +86 -0
  13. package/dist/preset-card-sets.d.ts +5 -0
  14. package/dist/preset-card-sets.js +135 -0
  15. package/dist/seeds.d.ts +6 -0
  16. package/dist/seeds.js +766 -0
  17. package/ownership.json +51 -0
  18. package/package.json +46 -0
  19. package/src/__fixtures__/sdk-types/invalid-card-properties-extra-key.ts +62 -0
  20. package/src/__fixtures__/sdk-types/invalid-card-properties-missing-required.ts +60 -0
  21. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-enum.ts +61 -0
  22. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-nested.ts +61 -0
  23. package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-scalar.ts +61 -0
  24. package/src/__fixtures__/sdk-types/invalid-card-visibility.ts +40 -0
  25. package/src/__fixtures__/sdk-types/invalid-container-card-set-manifest.ts +62 -0
  26. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-array-item.ts +43 -0
  27. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-player-id.ts +43 -0
  28. package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-resource-id.ts +43 -0
  29. package/src/__fixtures__/sdk-types/invalid-die-home-per-player-zone-no-owner.ts +35 -0
  30. package/src/__fixtures__/sdk-types/invalid-die-seed-type-id.ts +31 -0
  31. package/src/__fixtures__/sdk-types/invalid-die-visibility.ts +28 -0
  32. package/src/__fixtures__/sdk-types/invalid-generic-board-edge-id.ts +38 -0
  33. package/src/__fixtures__/sdk-types/invalid-generic-board-nested-field.ts +45 -0
  34. package/src/__fixtures__/sdk-types/invalid-hex-edge-field-edge-id.ts +47 -0
  35. package/src/__fixtures__/sdk-types/invalid-hex-vertex-field-vertex-id.ts +47 -0
  36. package/src/__fixtures__/sdk-types/invalid-manifest.ts +143 -0
  37. package/src/__fixtures__/sdk-types/invalid-piece-fields-extra-key.ts +62 -0
  38. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-card-id.ts +61 -0
  39. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-enum.ts +61 -0
  40. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-scalar.ts +61 -0
  41. package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-zone-id.ts +61 -0
  42. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-container-no-owner.ts +48 -0
  43. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-edge-no-owner.ts +47 -0
  44. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-space-no-owner.ts +42 -0
  45. package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-vertex-no-owner.ts +49 -0
  46. package/src/__fixtures__/sdk-types/invalid-piece-seed-type-id.ts +30 -0
  47. package/src/__fixtures__/sdk-types/invalid-piece-visibility.ts +28 -0
  48. package/src/__fixtures__/sdk-types/invalid-slot-host-manifest.ts +47 -0
  49. package/src/__fixtures__/sdk-types/invalid-slot-id-manifest.ts +49 -0
  50. package/src/__fixtures__/sdk-types/invalid-square-board-edge-id.ts +47 -0
  51. package/src/__fixtures__/sdk-types/invalid-square-board-space-id.ts +47 -0
  52. package/src/__fixtures__/sdk-types/invalid-square-board-vertex-id.ts +49 -0
  53. package/src/__fixtures__/sdk-types/invalid-square-container-field-space-id.ts +59 -0
  54. package/src/__fixtures__/sdk-types/invalid-square-container-host-space-id.ts +49 -0
  55. package/src/__fixtures__/sdk-types/invalid-square-relation-field-scalar.ts +48 -0
  56. package/src/__fixtures__/sdk-types/invalid-square-relation-space-id.ts +48 -0
  57. package/src/__fixtures__/sdk-types/invalid-square-space-fields-enum.ts +44 -0
  58. package/src/__fixtures__/sdk-types/invalid-square-space-fields-extra-key.ts +45 -0
  59. package/src/__fixtures__/sdk-types/valid-die-type-omits-sides.ts +29 -0
  60. package/src/__fixtures__/sdk-types/valid-manifest-omits-board-templates.ts +19 -0
  61. package/src/__fixtures__/sdk-types/valid-manifest.ts +612 -0
  62. package/src/__fixtures__/sdk-types/valid-player-scoped-seed-homes.ts +59 -0
  63. package/src/authoring-benchmark.test.ts +362 -0
  64. package/src/hex-geometry.ts +69 -0
  65. package/src/index.ts +64 -0
  66. package/src/manifest-contract.test.ts +1764 -0
  67. package/src/manifest-contract.ts +6581 -0
  68. package/src/manifest-validation.test.ts +393 -0
  69. package/src/manifest-validation.ts +795 -0
  70. package/src/ownership.ts +127 -0
  71. package/src/preset-card-sets.ts +169 -0
  72. package/src/sdk-types-authoring.test.ts +361 -0
  73. package/src/seeds.ts +800 -0
@@ -0,0 +1,4897 @@
1
+ import { resolveHexVertexGeometryKey } from "./hex-geometry.js";
2
+ import { addStandardDecksIfNeeded, materializeCardSet, } from "./preset-card-sets.js";
3
+ function isCardPropertySchemaVariants(schema) {
4
+ return Boolean(schema && "variants" in schema);
5
+ }
6
+ function mergeSharedCardProperties(schema, cardType) {
7
+ const variant = schema.variants[cardType];
8
+ if (!variant) {
9
+ return null;
10
+ }
11
+ return {
12
+ properties: {
13
+ ...(schema.shared ?? {}),
14
+ ...variant.properties,
15
+ },
16
+ };
17
+ }
18
+ function hasPropertySchemaDefault(property) {
19
+ return Boolean(property && "default" in property);
20
+ }
21
+ function quote(value) {
22
+ return JSON.stringify(value);
23
+ }
24
+ function toPascalCase(input) {
25
+ return input
26
+ .split(/[_-]/g)
27
+ .filter(Boolean)
28
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
29
+ .join("");
30
+ }
31
+ function toHandleKey(input) {
32
+ if (!/[_-]/.test(input)) {
33
+ return input.charAt(0).toLowerCase() + input.slice(1);
34
+ }
35
+ const [first = "", ...rest] = input.split(/[_-]/g).filter(Boolean);
36
+ return [
37
+ first.toLowerCase(),
38
+ ...rest.map((part) => {
39
+ const lower = part.toLowerCase();
40
+ return lower.charAt(0).toUpperCase() + lower.slice(1);
41
+ }),
42
+ ].join("");
43
+ }
44
+ function renderBlocks(blocks) {
45
+ return blocks.filter((block) => Boolean(block)).join("\n\n");
46
+ }
47
+ function dedupeSorted(values) {
48
+ return Array.from(new Set(values)).sort();
49
+ }
50
+ function renderConstArray(values) {
51
+ return `[${values.map((value) => quote(value)).join(", ")}] as const`;
52
+ }
53
+ function renderStringUnion(values, fallback = "never") {
54
+ return values.length > 0
55
+ ? values.map((value) => quote(value)).join(" | ")
56
+ : fallback;
57
+ }
58
+ function renderStringRecord(entries) {
59
+ const lines = Array.from(entries)
60
+ .sort(([left], [right]) => left.localeCompare(right))
61
+ .map(([key, value]) => ` ${quote(key)}: ${quote(value)},`);
62
+ return `{\n${lines.join("\n")}\n} as const`;
63
+ }
64
+ function renderReadonlyArrayRecord(entries) {
65
+ const lines = Array.from(entries)
66
+ .sort(([left], [right]) => left.localeCompare(right))
67
+ .map(([key, values]) => ` ${quote(key)}: [${values.map((value) => quote(value)).join(", ")}] as const,`);
68
+ return `{\n${lines.join("\n")}\n} as const`;
69
+ }
70
+ function renderJsonConst(value) {
71
+ return `${JSON.stringify(value, null, 2)} as const`;
72
+ }
73
+ const GENERATED_ID_FAMILIES = [
74
+ {
75
+ literalKey: "playerIds",
76
+ typeName: "PlayerId",
77
+ guardName: "PlayerId",
78
+ description: "player id",
79
+ },
80
+ {
81
+ literalKey: "boardLayouts",
82
+ typeName: "BoardLayout",
83
+ guardName: "BoardLayout",
84
+ description: "board layout",
85
+ },
86
+ {
87
+ literalKey: "setupOptionIds",
88
+ typeName: "SetupOptionId",
89
+ guardName: "SetupOptionId",
90
+ description: "setup option id",
91
+ },
92
+ {
93
+ literalKey: "setupProfileIds",
94
+ typeName: "SetupProfileId",
95
+ guardName: "SetupProfileId",
96
+ description: "setup profile id",
97
+ },
98
+ {
99
+ literalKey: "cardSetIds",
100
+ typeName: "CardSetId",
101
+ guardName: "CardSetId",
102
+ description: "card set id",
103
+ },
104
+ {
105
+ literalKey: "cardTypes",
106
+ typeName: "CardType",
107
+ guardName: "CardType",
108
+ description: "card type",
109
+ },
110
+ {
111
+ literalKey: "cardIds",
112
+ typeName: "CardId",
113
+ guardName: "CardId",
114
+ description: "card id",
115
+ },
116
+ {
117
+ literalKey: "deckIds",
118
+ typeName: "DeckId",
119
+ guardName: "DeckId",
120
+ description: "deck id",
121
+ },
122
+ {
123
+ literalKey: "handIds",
124
+ typeName: "HandId",
125
+ guardName: "HandId",
126
+ description: "hand id",
127
+ },
128
+ {
129
+ literalKey: "sharedZoneIds",
130
+ typeName: "SharedZoneId",
131
+ guardName: "SharedZoneId",
132
+ description: "shared zone id",
133
+ },
134
+ {
135
+ literalKey: "playerZoneIds",
136
+ typeName: "PlayerZoneId",
137
+ guardName: "PlayerZoneId",
138
+ description: "player zone id",
139
+ },
140
+ {
141
+ literalKey: "zoneIds",
142
+ typeName: "ZoneId",
143
+ guardName: "ZoneId",
144
+ description: "zone id",
145
+ },
146
+ {
147
+ literalKey: "resourceIds",
148
+ typeName: "ResourceId",
149
+ guardName: "ResourceId",
150
+ description: "resource id",
151
+ },
152
+ {
153
+ literalKey: "pieceTypeIds",
154
+ typeName: "PieceTypeId",
155
+ guardName: "PieceTypeId",
156
+ description: "piece type id",
157
+ },
158
+ {
159
+ literalKey: "pieceIds",
160
+ typeName: "PieceId",
161
+ guardName: "PieceId",
162
+ description: "piece id",
163
+ },
164
+ {
165
+ literalKey: "dieTypeIds",
166
+ typeName: "DieTypeId",
167
+ guardName: "DieTypeId",
168
+ description: "die type id",
169
+ },
170
+ {
171
+ literalKey: "dieIds",
172
+ typeName: "DieId",
173
+ guardName: "DieId",
174
+ description: "die id",
175
+ },
176
+ {
177
+ literalKey: "boardTypeIds",
178
+ typeName: "BoardTypeId",
179
+ guardName: "BoardTypeId",
180
+ description: "board type id",
181
+ },
182
+ {
183
+ literalKey: "boardBaseIds",
184
+ typeName: "BoardBaseId",
185
+ guardName: "BoardBaseId",
186
+ description: "board base id",
187
+ },
188
+ {
189
+ literalKey: "boardIds",
190
+ typeName: "BoardId",
191
+ guardName: "BoardId",
192
+ description: "board id",
193
+ },
194
+ {
195
+ literalKey: "boardContainerIds",
196
+ typeName: "BoardContainerId",
197
+ guardName: "BoardContainerId",
198
+ description: "board container id",
199
+ },
200
+ {
201
+ literalKey: "relationTypeIds",
202
+ typeName: "RelationTypeId",
203
+ guardName: "RelationTypeId",
204
+ description: "relation type id",
205
+ },
206
+ {
207
+ literalKey: "edgeIds",
208
+ typeName: "EdgeId",
209
+ guardName: "EdgeId",
210
+ description: "edge id",
211
+ },
212
+ {
213
+ literalKey: "edgeTypeIds",
214
+ typeName: "EdgeTypeId",
215
+ guardName: "EdgeTypeId",
216
+ description: "edge type id",
217
+ },
218
+ {
219
+ literalKey: "vertexIds",
220
+ typeName: "VertexId",
221
+ guardName: "VertexId",
222
+ description: "vertex id",
223
+ },
224
+ {
225
+ literalKey: "vertexTypeIds",
226
+ typeName: "VertexTypeId",
227
+ guardName: "VertexTypeId",
228
+ description: "vertex type id",
229
+ },
230
+ {
231
+ literalKey: "spaceIds",
232
+ typeName: "SpaceId",
233
+ guardName: "SpaceId",
234
+ description: "space id",
235
+ },
236
+ {
237
+ literalKey: "spaceTypeIds",
238
+ typeName: "SpaceTypeId",
239
+ guardName: "SpaceTypeId",
240
+ description: "space type id",
241
+ },
242
+ ];
243
+ // `playerIds` are excluded from the generated `records`/`idGuards` surfaces:
244
+ // `PlayerId` is now an opaque brand whose runtime roster comes from the active
245
+ // session, not `manifest.players.maxPlayers`. Authors must use
246
+ // `perPlayer(runtimeIds, init)` and `asPlayerId` from `@dreamboard/app-sdk/reducer`
247
+ // instead of a `Record<PlayerId, T>` keyed on the max-players set.
248
+ const GENERATED_ID_FAMILIES_WITHOUT_PLAYERS = GENERATED_ID_FAMILIES.filter((family) => family.literalKey !== "playerIds");
249
+ function renderGeneratedRecordsHelpers() {
250
+ return `export const records = {
251
+ ${GENERATED_ID_FAMILIES_WITHOUT_PLAYERS.map(({ literalKey, typeName }) => ` ${literalKey}<Value>(
252
+ initial: Value | ((${literalKey.slice(0, -1)}: ${typeName}) => Value),
253
+ ): Record<${typeName}, Value> {
254
+ return buildTypedRecord(literals.${literalKey}, initial);
255
+ },`).join("\n")}
256
+ } as const;`;
257
+ }
258
+ function renderGeneratedIdGuards() {
259
+ return `export const idGuards = {
260
+ ${GENERATED_ID_FAMILIES_WITHOUT_PLAYERS.map(({ literalKey, typeName, guardName, description, }) => ` is${guardName}(value: string): value is ${typeName} {
261
+ return isTypedId(literals.${literalKey}, value);
262
+ },
263
+ expect${guardName}(value: string): ${typeName} {
264
+ return expectTypedId(literals.${literalKey}, value, ${quote(description)});
265
+ },`).join("\n")}
266
+ } as const;`;
267
+ }
268
+ function renderGeneratedIdHandles(analysis) {
269
+ return `export const cardTypes = ${renderHandleObject("CardType", analysis.cardTypes)};
270
+
271
+ export const zones = ${renderHandleObject("ZoneId", analysis.zoneIds)};`;
272
+ }
273
+ function renderHandleObject(typeName, values) {
274
+ const lines = Array.from(values)
275
+ .sort((left, right) => left.localeCompare(right))
276
+ .map((value) => ` ${quote(toHandleKey(value))}: ${quote(value)},`);
277
+ return `{\n${lines.join("\n")}\n} as const satisfies Record<string, ${typeName}>`;
278
+ }
279
+ function sortedObject(entries) {
280
+ return Object.fromEntries(Array.from(entries).sort(([left], [right]) => left.localeCompare(right)));
281
+ }
282
+ function renderCardInstanceIds(card) {
283
+ if (card.count > 1) {
284
+ return Array.from({ length: card.count }, (_, index) => `${card.type}-${index + 1}`);
285
+ }
286
+ return [card.type];
287
+ }
288
+ function expandSeedIds(seeds) {
289
+ const expanded = [];
290
+ for (const seed of seeds) {
291
+ const count = seed.count ?? 1;
292
+ const baseId = seed.id ?? seed.typeId;
293
+ if (count <= 1) {
294
+ expanded.push(baseId);
295
+ continue;
296
+ }
297
+ for (let index = 1; index <= count; index += 1) {
298
+ expanded.push(`${baseId}-${index}`);
299
+ }
300
+ }
301
+ return expanded;
302
+ }
303
+ function isHexBoardTemplateSpec(boardTemplate) {
304
+ return boardTemplate.layout === "hex";
305
+ }
306
+ function isSquareBoardTemplateSpec(boardTemplate) {
307
+ return boardTemplate.layout === "square";
308
+ }
309
+ function isGenericBoardTemplateSpec(boardTemplate) {
310
+ return boardTemplate.layout === "generic";
311
+ }
312
+ function isHexBoardSpec(board) {
313
+ return board.layout === "hex";
314
+ }
315
+ function isSquareBoardSpec(board) {
316
+ return board.layout === "square";
317
+ }
318
+ const HEX_SIDES = ["e", "ne", "nw", "w", "sw", "se"];
319
+ const HEX_CORNERS = ["ne-e", "e-se", "se-sw", "sw-w", "w-nw", "nw-ne"];
320
+ const HEX_SIDE_OFFSETS = {
321
+ e: [1, 0],
322
+ ne: [1, -1],
323
+ nw: [0, -1],
324
+ w: [-1, 0],
325
+ sw: [-1, 1],
326
+ se: [0, 1],
327
+ };
328
+ const HEX_CORNER_OFFSETS = {
329
+ "ne-e": [2, -1, -1],
330
+ "e-se": [1, -2, 1],
331
+ "se-sw": [-1, -1, 2],
332
+ "sw-w": [-2, 1, 1],
333
+ "w-nw": [-1, 2, -1],
334
+ "nw-ne": [1, 1, -2],
335
+ };
336
+ const HEX_SIDE_CORNERS = {
337
+ e: ["ne-e", "e-se"],
338
+ ne: ["nw-ne", "ne-e"],
339
+ nw: ["w-nw", "nw-ne"],
340
+ w: ["sw-w", "w-nw"],
341
+ sw: ["se-sw", "sw-w"],
342
+ se: ["e-se", "se-sw"],
343
+ };
344
+ const HEX_CORNER_SIDES = {
345
+ "ne-e": ["ne", "e"],
346
+ "e-se": ["e", "se"],
347
+ "se-sw": ["se", "sw"],
348
+ "sw-w": ["sw", "w"],
349
+ "w-nw": ["w", "nw"],
350
+ "nw-ne": ["nw", "ne"],
351
+ };
352
+ function cubeFromAxial(space) {
353
+ const x = space.q;
354
+ const z = space.r;
355
+ return [x, -x - z, z];
356
+ }
357
+ function cornerGeometryKey(space, corner) {
358
+ const [x, y, z] = cubeFromAxial(space);
359
+ const [dx, dy, dz] = HEX_CORNER_OFFSETS[corner];
360
+ return `${3 * x + dx},${3 * y + dy},${3 * z + dz}`;
361
+ }
362
+ function edgeGeometryKey(space, side) {
363
+ const [leftCorner, rightCorner] = HEX_SIDE_CORNERS[side];
364
+ return [
365
+ cornerGeometryKey(space, leftCorner),
366
+ cornerGeometryKey(space, rightCorner),
367
+ ]
368
+ .sort((left, right) => left.localeCompare(right))
369
+ .join("::");
370
+ }
371
+ function edgeIdFromGeometryKey(key) {
372
+ return `hex-edge:${key}`;
373
+ }
374
+ function vertexIdFromGeometryKey(key) {
375
+ return `hex-vertex:${key}`;
376
+ }
377
+ const SQUARE_SIDES = ["north", "east", "south", "west"];
378
+ const SQUARE_CORNERS = ["nw", "ne", "se", "sw"];
379
+ function squareEdgeIdFromGeometryKey(key) {
380
+ return `square-edge:${key}`;
381
+ }
382
+ function squareVertexIdFromGeometryKey(key) {
383
+ return `square-vertex:${key}`;
384
+ }
385
+ function squareCornerGeometryKey(space, corner) {
386
+ switch (corner) {
387
+ case "nw":
388
+ return `${space.col},${space.row}`;
389
+ case "ne":
390
+ return `${space.col + 1},${space.row}`;
391
+ case "se":
392
+ return `${space.col + 1},${space.row + 1}`;
393
+ case "sw":
394
+ return `${space.col},${space.row + 1}`;
395
+ }
396
+ }
397
+ function squareEdgeGeometryKey(space, side) {
398
+ const endpoints = side === "north"
399
+ ? [`${space.col},${space.row}`, `${space.col + 1},${space.row}`]
400
+ : side === "east"
401
+ ? [`${space.col + 1},${space.row}`, `${space.col + 1},${space.row + 1}`]
402
+ : side === "south"
403
+ ? [
404
+ `${space.col},${space.row + 1}`,
405
+ `${space.col + 1},${space.row + 1}`,
406
+ ]
407
+ : [`${space.col},${space.row}`, `${space.col},${space.row + 1}`];
408
+ return endpoints.sort((left, right) => left.localeCompare(right)).join("::");
409
+ }
410
+ function geometryKeyFromSquareEdgeRef(ref, spacesById) {
411
+ const resolvedSpaces = [...ref.spaces]
412
+ .sort((a, b) => a.localeCompare(b))
413
+ .map((spaceId) => {
414
+ const space = spacesById.get(spaceId);
415
+ if (!space) {
416
+ throw new Error(`Square edge ref references unknown space '${spaceId}'.`);
417
+ }
418
+ return space;
419
+ });
420
+ const keyCounts = new Map();
421
+ for (const space of resolvedSpaces) {
422
+ for (const side of SQUARE_SIDES) {
423
+ const key = squareEdgeGeometryKey(space, side);
424
+ keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
425
+ }
426
+ }
427
+ const candidates = [...keyCounts.entries()]
428
+ .filter(([, count]) => count === resolvedSpaces.length)
429
+ .map(([key]) => key)
430
+ .sort((left, right) => left.localeCompare(right));
431
+ if (candidates.length !== 1) {
432
+ throw new Error(`Square edge ref spaces '${ref.spaces.join(", ")}' do not resolve to exactly one shared edge.`);
433
+ }
434
+ const [only] = candidates;
435
+ if (only === undefined) {
436
+ throw new Error("unreachable: candidates.length === 1 but first is undefined");
437
+ }
438
+ return only;
439
+ }
440
+ function geometryKeyFromSquareVertexRef(ref, spacesById) {
441
+ const resolvedSpaces = [...ref.spaces]
442
+ .sort((a, b) => a.localeCompare(b))
443
+ .map((spaceId) => {
444
+ const space = spacesById.get(spaceId);
445
+ if (!space) {
446
+ throw new Error(`Square vertex ref references unknown space '${spaceId}'.`);
447
+ }
448
+ return space;
449
+ });
450
+ const keyCounts = new Map();
451
+ for (const space of resolvedSpaces) {
452
+ for (const corner of SQUARE_CORNERS) {
453
+ const key = squareCornerGeometryKey(space, corner);
454
+ keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
455
+ }
456
+ }
457
+ const candidates = [...keyCounts.entries()]
458
+ .filter(([, count]) => count === resolvedSpaces.length)
459
+ .map(([key]) => key)
460
+ .sort((left, right) => left.localeCompare(right));
461
+ if (candidates.length !== 1) {
462
+ throw new Error(`Square vertex ref spaces '${ref.spaces.join(", ")}' do not resolve to exactly one shared vertex.`);
463
+ }
464
+ const [only] = candidates;
465
+ if (only === undefined) {
466
+ throw new Error("unreachable: candidates.length === 1 but first is undefined");
467
+ }
468
+ return only;
469
+ }
470
+ function geometryKeyFromEdgeRef(ref, spacesById) {
471
+ const [leftId, rightId] = [...ref.spaces].sort((a, b) => a.localeCompare(b));
472
+ const leftSpace = spacesById.get(leftId);
473
+ const rightSpace = spacesById.get(rightId);
474
+ if (!leftSpace || !rightSpace) {
475
+ throw new Error(`Hex edge ref references unknown spaces: ${ref.spaces.join(", ")}.`);
476
+ }
477
+ const [dq, dr] = [rightSpace.q - leftSpace.q, rightSpace.r - leftSpace.r];
478
+ const side = Object.entries(HEX_SIDE_OFFSETS).find(([, [sideQ, sideR]]) => sideQ === dq && sideR === dr)?.[0];
479
+ if (!side) {
480
+ throw new Error(`Hex edge ref spaces '${leftId}' and '${rightId}' are not adjacent.`);
481
+ }
482
+ return edgeGeometryKey(leftSpace, side);
483
+ }
484
+ function geometryKeyFromVertexRef(ref, spacesById) {
485
+ return resolveHexVertexGeometryKey(ref, spacesById);
486
+ }
487
+ function deriveHexEdges(spaces) {
488
+ const spacesByCoordinate = new Map();
489
+ for (const space of spaces) {
490
+ spacesByCoordinate.set(`${space.q},${space.r}`, space);
491
+ }
492
+ const edgeMap = new Map();
493
+ for (const space of spaces) {
494
+ for (const side of HEX_SIDES) {
495
+ const [dq, dr] = HEX_SIDE_OFFSETS[side];
496
+ const neighbor = spacesByCoordinate.get(`${space.q + dq},${space.r + dr}`);
497
+ const geometryKey = edgeGeometryKey(space, side);
498
+ const existing = edgeMap.get(geometryKey);
499
+ const nextSpaceIds = dedupeSorted([
500
+ ...(existing?.spaceIds ?? []),
501
+ space.id,
502
+ ...(neighbor ? [neighbor.id] : []),
503
+ ]);
504
+ edgeMap.set(geometryKey, {
505
+ id: edgeIdFromGeometryKey(geometryKey),
506
+ geometryKey,
507
+ spaceIds: nextSpaceIds,
508
+ typeId: existing?.typeId ?? null,
509
+ label: existing?.label ?? null,
510
+ fields: existing?.fields ?? null,
511
+ });
512
+ }
513
+ }
514
+ return [...edgeMap.values()].sort((left, right) => left.id.localeCompare(right.id));
515
+ }
516
+ function deriveHexVertices(spaces) {
517
+ const spacesByCoordinate = new Map();
518
+ for (const space of spaces) {
519
+ spacesByCoordinate.set(`${space.q},${space.r}`, space);
520
+ }
521
+ const vertexMap = new Map();
522
+ for (const space of spaces) {
523
+ for (const corner of HEX_CORNERS) {
524
+ const [leftSide, rightSide] = HEX_CORNER_SIDES[corner];
525
+ const [leftQ, leftR] = HEX_SIDE_OFFSETS[leftSide];
526
+ const [rightQ, rightR] = HEX_SIDE_OFFSETS[rightSide];
527
+ const leftNeighbor = spacesByCoordinate.get(`${space.q + leftQ},${space.r + leftR}`);
528
+ const rightNeighbor = spacesByCoordinate.get(`${space.q + rightQ},${space.r + rightR}`);
529
+ const geometryKey = cornerGeometryKey(space, corner);
530
+ const existing = vertexMap.get(geometryKey);
531
+ const nextSpaceIds = dedupeSorted([
532
+ ...(existing?.spaceIds ?? []),
533
+ space.id,
534
+ ...(leftNeighbor ? [leftNeighbor.id] : []),
535
+ ...(rightNeighbor ? [rightNeighbor.id] : []),
536
+ ]);
537
+ vertexMap.set(geometryKey, {
538
+ id: vertexIdFromGeometryKey(geometryKey),
539
+ geometryKey,
540
+ spaceIds: nextSpaceIds,
541
+ typeId: existing?.typeId ?? null,
542
+ label: existing?.label ?? null,
543
+ fields: existing?.fields ?? null,
544
+ });
545
+ }
546
+ }
547
+ return [...vertexMap.values()].sort((left, right) => left.id.localeCompare(right.id));
548
+ }
549
+ function mergeBoardSpaces(templateSpaces, boardSpaces) {
550
+ return Array.from([...templateSpaces, ...boardSpaces]
551
+ .reduce((accumulator, space) => {
552
+ accumulator.set(space.id, space);
553
+ return accumulator;
554
+ }, new Map())
555
+ .values()).sort((left, right) => left.id.localeCompare(right.id));
556
+ }
557
+ function mergeBoardContainers(templateContainers, boardContainers) {
558
+ return Array.from([...templateContainers, ...boardContainers]
559
+ .reduce((accumulator, container) => {
560
+ accumulator.set(container.id, container);
561
+ return accumulator;
562
+ }, new Map())
563
+ .values()).sort((left, right) => left.id.localeCompare(right.id));
564
+ }
565
+ function resolveHexSpaces(board, template) {
566
+ if (!template) {
567
+ return [...(board.spaces ?? [])].sort((left, right) => left.id.localeCompare(right.id));
568
+ }
569
+ const templateSpacesById = new Map((template.spaces ?? []).map((space) => [space.id, space]));
570
+ const overridesById = new Map((board.spaces ?? []).map((space) => [space.id, space]));
571
+ for (const overrideId of overridesById.keys()) {
572
+ if (!templateSpacesById.has(overrideId)) {
573
+ throw new Error(`Hex board '${board.id}' overrides unknown space '${overrideId}' from template '${template.id}'.`);
574
+ }
575
+ }
576
+ return (template.spaces ?? [])
577
+ .map((templateSpace) => {
578
+ const override = overridesById.get(templateSpace.id);
579
+ if (!override) {
580
+ return templateSpace;
581
+ }
582
+ const templateMatch = templateSpacesById.get(override.id);
583
+ if (!templateMatch) {
584
+ throw new Error(`Hex board '${board.id}' overrides unknown space '${override.id}' from template '${template.id}'.`);
585
+ }
586
+ if (templateMatch.q !== override.q || templateMatch.r !== override.r) {
587
+ throw new Error(`Hex board '${board.id}' cannot change coordinates for space '${override.id}' from template '${template.id}'.`);
588
+ }
589
+ return {
590
+ ...templateSpace,
591
+ ...override,
592
+ id: templateSpace.id,
593
+ q: templateSpace.q,
594
+ r: templateSpace.r,
595
+ };
596
+ })
597
+ .sort((left, right) => left.id.localeCompare(right.id));
598
+ }
599
+ function indexHexEdgeMetadata(specs, spacesById, ownerLabel) {
600
+ const metadataByKey = new Map();
601
+ for (const spec of specs) {
602
+ const geometryKey = geometryKeyFromEdgeRef(spec.ref, spacesById);
603
+ if (metadataByKey.has(geometryKey)) {
604
+ throw new Error(`${ownerLabel} contains duplicate hex edge refs.`);
605
+ }
606
+ metadataByKey.set(geometryKey, {
607
+ geometryKey,
608
+ typeId: spec.typeId ?? null,
609
+ label: spec.label ?? null,
610
+ fields: spec.fields ?? null,
611
+ });
612
+ }
613
+ return metadataByKey;
614
+ }
615
+ function indexHexVertexMetadata(specs, spacesById, ownerLabel) {
616
+ const metadataByKey = new Map();
617
+ for (const spec of specs) {
618
+ const geometryKey = geometryKeyFromVertexRef(spec.ref, spacesById);
619
+ if (metadataByKey.has(geometryKey)) {
620
+ throw new Error(`${ownerLabel} contains duplicate hex vertex refs.`);
621
+ }
622
+ metadataByKey.set(geometryKey, {
623
+ geometryKey,
624
+ typeId: spec.typeId ?? null,
625
+ label: spec.label ?? null,
626
+ fields: spec.fields ?? null,
627
+ });
628
+ }
629
+ return metadataByKey;
630
+ }
631
+ function resolveAuthoredHexEdges(board, template, spaces) {
632
+ const spacesById = new Map(spaces.map((space) => [space.id, space]));
633
+ const derivedByGeometryKey = new Map(deriveHexEdges(spaces).map((edge) => [edge.geometryKey, edge]));
634
+ const collectSpecsByGeometryKey = (specs, ownerLabel) => {
635
+ const specsByGeometryKey = new Map();
636
+ for (const spec of specs) {
637
+ const geometryKey = geometryKeyFromEdgeRef(spec.ref, spacesById);
638
+ if (specsByGeometryKey.has(geometryKey)) {
639
+ throw new Error(`${ownerLabel} contains duplicate hex edge refs.`);
640
+ }
641
+ specsByGeometryKey.set(geometryKey, spec);
642
+ }
643
+ return specsByGeometryKey;
644
+ };
645
+ const templateSpecsByGeometryKey = template
646
+ ? collectSpecsByGeometryKey(template.edges ?? [], `Hex board template '${template.id}'`)
647
+ : new Map();
648
+ const overrideSpecsByGeometryKey = collectSpecsByGeometryKey(board.edges ?? [], `Hex board '${board.id}'`);
649
+ if (template) {
650
+ for (const overrideKey of overrideSpecsByGeometryKey.keys()) {
651
+ if (!templateSpecsByGeometryKey.has(overrideKey)) {
652
+ throw new Error(`Hex board '${board.id}' overrides unknown edge ref from template '${template.id}'.`);
653
+ }
654
+ }
655
+ }
656
+ const mergedSpecs = template
657
+ ? Array.from(templateSpecsByGeometryKey.entries()).map(([geometryKey, templateSpec]) => [
658
+ geometryKey,
659
+ overrideSpecsByGeometryKey.get(geometryKey) ?? templateSpec,
660
+ ])
661
+ : Array.from(overrideSpecsByGeometryKey.entries());
662
+ return mergedSpecs.map(([geometryKey, spec]) => {
663
+ const derivedEdge = derivedByGeometryKey.get(geometryKey);
664
+ if (!derivedEdge) {
665
+ throw new Error(`Hex edge ref on board '${board.id}' does not resolve to a derived edge.`);
666
+ }
667
+ return {
668
+ id: derivedEdge.id,
669
+ ref: cloneJson(spec.ref),
670
+ typeId: spec.typeId ?? null,
671
+ label: spec.label ?? null,
672
+ fields: spec.fields ?? null,
673
+ };
674
+ });
675
+ }
676
+ function resolveAuthoredHexVertices(board, template, spaces) {
677
+ const spacesById = new Map(spaces.map((space) => [space.id, space]));
678
+ const derivedByGeometryKey = new Map(deriveHexVertices(spaces).map((vertex) => [vertex.geometryKey, vertex]));
679
+ const collectSpecsByGeometryKey = (specs, ownerLabel) => {
680
+ const specsByGeometryKey = new Map();
681
+ for (const spec of specs) {
682
+ const geometryKey = geometryKeyFromVertexRef(spec.ref, spacesById);
683
+ if (specsByGeometryKey.has(geometryKey)) {
684
+ throw new Error(`${ownerLabel} contains duplicate hex vertex refs.`);
685
+ }
686
+ specsByGeometryKey.set(geometryKey, spec);
687
+ }
688
+ return specsByGeometryKey;
689
+ };
690
+ const templateSpecsByGeometryKey = template
691
+ ? collectSpecsByGeometryKey(template.vertices ?? [], `Hex board template '${template.id}'`)
692
+ : new Map();
693
+ const overrideSpecsByGeometryKey = collectSpecsByGeometryKey(board.vertices ?? [], `Hex board '${board.id}'`);
694
+ if (template) {
695
+ for (const overrideKey of overrideSpecsByGeometryKey.keys()) {
696
+ if (!templateSpecsByGeometryKey.has(overrideKey)) {
697
+ throw new Error(`Hex board '${board.id}' overrides unknown vertex ref from template '${template.id}'.`);
698
+ }
699
+ }
700
+ }
701
+ const mergedSpecs = template
702
+ ? Array.from(templateSpecsByGeometryKey.entries()).map(([geometryKey, templateSpec]) => [
703
+ geometryKey,
704
+ overrideSpecsByGeometryKey.get(geometryKey) ?? templateSpec,
705
+ ])
706
+ : Array.from(overrideSpecsByGeometryKey.entries());
707
+ return mergedSpecs.map(([geometryKey, spec]) => {
708
+ const derivedVertex = derivedByGeometryKey.get(geometryKey);
709
+ if (!derivedVertex) {
710
+ throw new Error(`Hex vertex ref on board '${board.id}' does not resolve to a derived vertex.`);
711
+ }
712
+ return {
713
+ id: derivedVertex.id,
714
+ ref: cloneJson(spec.ref),
715
+ typeId: spec.typeId ?? null,
716
+ label: spec.label ?? null,
717
+ fields: spec.fields ?? null,
718
+ };
719
+ });
720
+ }
721
+ function resolveHexEdges(board, template, spaces) {
722
+ const derived = deriveHexEdges(spaces);
723
+ const edgesById = new Map(derived.map((edge) => [edge.id, edge]));
724
+ for (const authoredEdge of resolveAuthoredHexEdges(board, template, spaces)) {
725
+ const edge = edgesById.get(authoredEdge.id);
726
+ if (!edge) {
727
+ throw new Error(`Hex edge ref on board '${board.id}' does not resolve to a derived edge.`);
728
+ }
729
+ edgesById.set(authoredEdge.id, {
730
+ ...edge,
731
+ typeId: authoredEdge.typeId ?? null,
732
+ label: authoredEdge.label ?? null,
733
+ fields: authoredEdge.fields ?? null,
734
+ });
735
+ }
736
+ return [...edgesById.values()].sort((left, right) => left.id.localeCompare(right.id));
737
+ }
738
+ function resolveHexVertices(board, template, spaces) {
739
+ const derived = deriveHexVertices(spaces);
740
+ const verticesById = new Map(derived.map((vertex) => [vertex.id, vertex]));
741
+ for (const authoredVertex of resolveAuthoredHexVertices(board, template, spaces)) {
742
+ const vertex = verticesById.get(authoredVertex.id);
743
+ if (!vertex) {
744
+ throw new Error(`Hex vertex ref on board '${board.id}' does not resolve to a derived vertex.`);
745
+ }
746
+ verticesById.set(authoredVertex.id, {
747
+ ...vertex,
748
+ typeId: authoredVertex.typeId ?? null,
749
+ label: authoredVertex.label ?? null,
750
+ fields: authoredVertex.fields ?? null,
751
+ });
752
+ }
753
+ return [...verticesById.values()].sort((left, right) => left.id.localeCompare(right.id));
754
+ }
755
+ function resolveSquareSpaces(board, template) {
756
+ if (!template) {
757
+ return [...(board.spaces ?? [])].sort((left, right) => left.id.localeCompare(right.id));
758
+ }
759
+ const templateSpacesById = new Map((template.spaces ?? []).map((space) => [space.id, space]));
760
+ const overridesById = new Map((board.spaces ?? []).map((space) => [space.id, space]));
761
+ for (const overrideId of overridesById.keys()) {
762
+ if (!templateSpacesById.has(overrideId)) {
763
+ throw new Error(`Square board '${board.id}' overrides unknown space '${overrideId}' from template '${template.id}'.`);
764
+ }
765
+ }
766
+ return (template.spaces ?? [])
767
+ .map((templateSpace) => {
768
+ const override = overridesById.get(templateSpace.id);
769
+ if (!override) {
770
+ return templateSpace;
771
+ }
772
+ const templateMatch = templateSpacesById.get(override.id);
773
+ if (!templateMatch) {
774
+ throw new Error(`Square board '${board.id}' overrides unknown space '${override.id}' from template '${template.id}'.`);
775
+ }
776
+ if (templateMatch.row !== override.row ||
777
+ templateMatch.col !== override.col) {
778
+ throw new Error(`Square board '${board.id}' cannot change coordinates for space '${override.id}' from template '${template.id}'.`);
779
+ }
780
+ return {
781
+ ...templateSpace,
782
+ ...override,
783
+ id: templateSpace.id,
784
+ row: templateSpace.row,
785
+ col: templateSpace.col,
786
+ };
787
+ })
788
+ .sort((left, right) => left.id.localeCompare(right.id));
789
+ }
790
+ function deriveSquareEdges(spaces) {
791
+ const edgeMap = new Map();
792
+ for (const space of spaces) {
793
+ for (const side of SQUARE_SIDES) {
794
+ const geometryKey = squareEdgeGeometryKey(space, side);
795
+ const existing = edgeMap.get(geometryKey);
796
+ const nextSpaceIds = dedupeSorted([
797
+ ...(existing?.spaceIds ?? []),
798
+ space.id,
799
+ ]);
800
+ edgeMap.set(geometryKey, {
801
+ id: squareEdgeIdFromGeometryKey(geometryKey),
802
+ geometryKey,
803
+ spaceIds: nextSpaceIds,
804
+ typeId: existing?.typeId ?? null,
805
+ label: existing?.label ?? null,
806
+ fields: existing?.fields ?? null,
807
+ });
808
+ }
809
+ }
810
+ return [...edgeMap.values()].sort((left, right) => left.id.localeCompare(right.id));
811
+ }
812
+ function deriveSquareVertices(spaces) {
813
+ const vertexMap = new Map();
814
+ for (const space of spaces) {
815
+ for (const corner of SQUARE_CORNERS) {
816
+ const geometryKey = squareCornerGeometryKey(space, corner);
817
+ const existing = vertexMap.get(geometryKey);
818
+ const nextSpaceIds = dedupeSorted([
819
+ ...(existing?.spaceIds ?? []),
820
+ space.id,
821
+ ]);
822
+ vertexMap.set(geometryKey, {
823
+ id: squareVertexIdFromGeometryKey(geometryKey),
824
+ geometryKey,
825
+ spaceIds: nextSpaceIds,
826
+ typeId: existing?.typeId ?? null,
827
+ label: existing?.label ?? null,
828
+ fields: existing?.fields ?? null,
829
+ });
830
+ }
831
+ }
832
+ return [...vertexMap.values()].sort((left, right) => left.id.localeCompare(right.id));
833
+ }
834
+ function indexSquareEdgeMetadata(specs, spacesById, ownerLabel) {
835
+ const metadataByKey = new Map();
836
+ for (const spec of specs) {
837
+ const geometryKey = geometryKeyFromSquareEdgeRef(spec.ref, spacesById);
838
+ if (metadataByKey.has(geometryKey)) {
839
+ throw new Error(`${ownerLabel} contains duplicate square edge refs.`);
840
+ }
841
+ metadataByKey.set(geometryKey, {
842
+ geometryKey,
843
+ typeId: spec.typeId ?? null,
844
+ label: spec.label ?? null,
845
+ fields: spec.fields ?? null,
846
+ });
847
+ }
848
+ return metadataByKey;
849
+ }
850
+ function indexSquareVertexMetadata(specs, spacesById, ownerLabel) {
851
+ const metadataByKey = new Map();
852
+ for (const spec of specs) {
853
+ const geometryKey = geometryKeyFromSquareVertexRef(spec.ref, spacesById);
854
+ if (metadataByKey.has(geometryKey)) {
855
+ throw new Error(`${ownerLabel} contains duplicate square vertex refs.`);
856
+ }
857
+ metadataByKey.set(geometryKey, {
858
+ geometryKey,
859
+ typeId: spec.typeId ?? null,
860
+ label: spec.label ?? null,
861
+ fields: spec.fields ?? null,
862
+ });
863
+ }
864
+ return metadataByKey;
865
+ }
866
+ function resolveSquareEdges(board, template, spaces) {
867
+ const spacesById = new Map(spaces.map((space) => [space.id, space]));
868
+ const derived = deriveSquareEdges(spaces);
869
+ const edgesByGeometryKey = new Map(derived.map((edge) => [edge.geometryKey, edge]));
870
+ const templateMetadata = template
871
+ ? indexSquareEdgeMetadata(template.edges ?? [], spacesById, `Square board template '${template.id}'`)
872
+ : new Map();
873
+ const overrideMetadata = indexSquareEdgeMetadata(board.edges ?? [], spacesById, `Square board '${board.id}'`);
874
+ if (template) {
875
+ for (const overrideKey of overrideMetadata.keys()) {
876
+ if (!templateMetadata.has(overrideKey)) {
877
+ throw new Error(`Square board '${board.id}' overrides unknown edge ref from template '${template.id}'.`);
878
+ }
879
+ }
880
+ }
881
+ const mergedMetadata = template
882
+ ? new Map([...templateMetadata.entries()].map(([key, metadata]) => [
883
+ key,
884
+ {
885
+ ...metadata,
886
+ ...(overrideMetadata.get(key) ?? {}),
887
+ geometryKey: key,
888
+ },
889
+ ]))
890
+ : overrideMetadata;
891
+ for (const [geometryKey, metadata] of mergedMetadata.entries()) {
892
+ const edge = edgesByGeometryKey.get(geometryKey);
893
+ if (!edge) {
894
+ throw new Error(`Square edge ref on board '${board.id}' does not resolve to a derived edge.`);
895
+ }
896
+ edgesByGeometryKey.set(geometryKey, {
897
+ ...edge,
898
+ typeId: metadata.typeId ?? null,
899
+ label: metadata.label ?? null,
900
+ fields: metadata.fields ?? null,
901
+ });
902
+ }
903
+ return [...edgesByGeometryKey.values()].sort((left, right) => left.id.localeCompare(right.id));
904
+ }
905
+ function resolveSquareVertices(board, template, spaces) {
906
+ const spacesById = new Map(spaces.map((space) => [space.id, space]));
907
+ const derived = deriveSquareVertices(spaces);
908
+ const verticesByGeometryKey = new Map(derived.map((vertex) => [vertex.geometryKey, vertex]));
909
+ const templateMetadata = template
910
+ ? indexSquareVertexMetadata(template.vertices ?? [], spacesById, `Square board template '${template.id}'`)
911
+ : new Map();
912
+ const overrideMetadata = indexSquareVertexMetadata(board.vertices ?? [], spacesById, `Square board '${board.id}'`);
913
+ if (template) {
914
+ for (const overrideKey of overrideMetadata.keys()) {
915
+ if (!templateMetadata.has(overrideKey)) {
916
+ throw new Error(`Square board '${board.id}' overrides unknown vertex ref from template '${template.id}'.`);
917
+ }
918
+ }
919
+ }
920
+ const mergedMetadata = template
921
+ ? new Map([...templateMetadata.entries()].map(([key, metadata]) => [
922
+ key,
923
+ {
924
+ ...metadata,
925
+ ...(overrideMetadata.get(key) ?? {}),
926
+ geometryKey: key,
927
+ },
928
+ ]))
929
+ : overrideMetadata;
930
+ for (const [geometryKey, metadata] of mergedMetadata.entries()) {
931
+ const vertex = verticesByGeometryKey.get(geometryKey);
932
+ if (!vertex) {
933
+ throw new Error(`Square vertex ref on board '${board.id}' does not resolve to a derived vertex.`);
934
+ }
935
+ verticesByGeometryKey.set(geometryKey, {
936
+ ...vertex,
937
+ typeId: metadata.typeId ?? null,
938
+ label: metadata.label ?? null,
939
+ fields: metadata.fields ?? null,
940
+ });
941
+ }
942
+ return [...verticesByGeometryKey.values()].sort((left, right) => left.id.localeCompare(right.id));
943
+ }
944
+ function analyzeBoards(manifest, playerIds) {
945
+ const boardTemplates = manifest.boardTemplates ?? [];
946
+ const genericTemplateById = new Map(boardTemplates
947
+ .filter(isGenericBoardTemplateSpec)
948
+ .map((boardTemplate) => [boardTemplate.id, boardTemplate]));
949
+ const hexTemplateById = new Map(boardTemplates
950
+ .filter(isHexBoardTemplateSpec)
951
+ .map((boardTemplate) => [boardTemplate.id, boardTemplate]));
952
+ const squareTemplateById = new Map(boardTemplates
953
+ .filter(isSquareBoardTemplateSpec)
954
+ .map((boardTemplate) => [boardTemplate.id, boardTemplate]));
955
+ return (manifest.boards ?? []).map((board) => {
956
+ const runtimeBoardIds = board.scope === "perPlayer"
957
+ ? playerIds.map((playerId) => `${board.id}:${playerId}`)
958
+ : [board.id];
959
+ if (isHexBoardSpec(board)) {
960
+ const hexBoard = board;
961
+ const template = board.templateId
962
+ ? hexTemplateById.get(board.templateId)
963
+ : undefined;
964
+ const spaces = resolveHexSpaces(hexBoard, template);
965
+ return {
966
+ layout: "hex",
967
+ board: hexBoard,
968
+ template,
969
+ boardTypeId: hexBoard.typeId ?? template?.typeId,
970
+ runtimeBoardIds,
971
+ boardFieldsSchema: hexBoard.boardFieldsSchema ?? template?.boardFieldsSchema,
972
+ spaceFieldsSchema: hexBoard.spaceFieldsSchema ?? template?.spaceFieldsSchema,
973
+ edgeFieldsSchema: hexBoard.edgeFieldsSchema ?? template?.edgeFieldsSchema,
974
+ vertexFieldsSchema: hexBoard.vertexFieldsSchema ?? template?.vertexFieldsSchema,
975
+ spaces,
976
+ authoredEdges: resolveAuthoredHexEdges(hexBoard, template, spaces),
977
+ authoredVertices: resolveAuthoredHexVertices(hexBoard, template, spaces),
978
+ edges: resolveHexEdges(hexBoard, template, spaces),
979
+ vertices: resolveHexVertices(hexBoard, template, spaces),
980
+ };
981
+ }
982
+ if (isSquareBoardSpec(board)) {
983
+ const squareBoard = board;
984
+ const template = board.templateId
985
+ ? squareTemplateById.get(board.templateId)
986
+ : undefined;
987
+ const spaces = resolveSquareSpaces(squareBoard, template);
988
+ return {
989
+ layout: "square",
990
+ board: squareBoard,
991
+ template,
992
+ boardTypeId: squareBoard.typeId ?? template?.typeId,
993
+ runtimeBoardIds,
994
+ boardFieldsSchema: squareBoard.boardFieldsSchema ?? template?.boardFieldsSchema,
995
+ spaceFieldsSchema: squareBoard.spaceFieldsSchema ?? template?.spaceFieldsSchema,
996
+ relationFieldsSchema: squareBoard.relationFieldsSchema ?? template?.relationFieldsSchema,
997
+ containerFieldsSchema: squareBoard.containerFieldsSchema ?? template?.containerFieldsSchema,
998
+ edgeFieldsSchema: squareBoard.edgeFieldsSchema ?? template?.edgeFieldsSchema,
999
+ vertexFieldsSchema: squareBoard.vertexFieldsSchema ?? template?.vertexFieldsSchema,
1000
+ spaces,
1001
+ relations: [
1002
+ ...(template?.relations ?? []),
1003
+ ...(squareBoard.relations ?? []),
1004
+ ],
1005
+ containers: mergeBoardContainers(template?.containers ?? [], squareBoard.containers ?? []),
1006
+ edges: resolveSquareEdges(squareBoard, template, spaces),
1007
+ vertices: resolveSquareVertices(squareBoard, template, spaces),
1008
+ };
1009
+ }
1010
+ const genericBoard = board;
1011
+ const template = board.templateId
1012
+ ? genericTemplateById.get(board.templateId)
1013
+ : undefined;
1014
+ return {
1015
+ layout: "generic",
1016
+ board: genericBoard,
1017
+ template,
1018
+ boardTypeId: genericBoard.typeId ?? template?.typeId,
1019
+ runtimeBoardIds,
1020
+ boardFieldsSchema: genericBoard.boardFieldsSchema ?? template?.boardFieldsSchema,
1021
+ spaceFieldsSchema: genericBoard.spaceFieldsSchema ?? template?.spaceFieldsSchema,
1022
+ relationFieldsSchema: genericBoard.relationFieldsSchema ?? template?.relationFieldsSchema,
1023
+ containerFieldsSchema: genericBoard.containerFieldsSchema ?? template?.containerFieldsSchema,
1024
+ spaces: mergeBoardSpaces(template?.spaces ?? [], genericBoard.spaces ?? []),
1025
+ relations: [
1026
+ ...(template?.relations ?? []),
1027
+ ...(genericBoard.relations ?? []),
1028
+ ],
1029
+ containers: mergeBoardContainers(template?.containers ?? [], genericBoard.containers ?? []),
1030
+ };
1031
+ });
1032
+ }
1033
+ function isSingletonExplicitSeed(seed) {
1034
+ return (typeof seed.id === "string" && seed.id.length > 0 && (seed.count ?? 1) === 1);
1035
+ }
1036
+ function renderStrictSlotLocationSchema(hosts) {
1037
+ const branches = hosts.flatMap((host) => host.slotIds.map((slotId) => `z.object({
1038
+ type: z.literal("InSlot"),
1039
+ host: z.object({
1040
+ kind: z.literal(${quote(host.kind)}),
1041
+ id: z.literal(${quote(host.id)}),
1042
+ }),
1043
+ slotId: z.literal(${quote(slotId)}),
1044
+ position: z.number().int().nullable().optional(),
1045
+ })`));
1046
+ if (branches.length === 0) {
1047
+ return `z.object({
1048
+ type: z.literal("InSlot"),
1049
+ host: z.never(),
1050
+ slotId: z.never(),
1051
+ position: z.number().int().nullable().optional(),
1052
+ })`;
1053
+ }
1054
+ return branches.join(",\n ");
1055
+ }
1056
+ function analyzeManifest(inputManifest) {
1057
+ const manifest = addStandardDecksIfNeeded(inputManifest);
1058
+ const playerIds = Array.from({ length: manifest.players.maxPlayers }, (_, index) => `player-${index + 1}`);
1059
+ const sharedZones = (manifest.zones ?? []).filter((zone) => zone.scope === "shared");
1060
+ const playerZones = (manifest.zones ?? []).filter((zone) => zone.scope === "perPlayer");
1061
+ const zoneIds = dedupeSorted((manifest.zones ?? []).map((zone) => zone.id));
1062
+ const cardSets = manifest.cardSets.map(materializeCardSet);
1063
+ const cardSetIds = dedupeSorted(cardSets.map((cardSet) => cardSet.id));
1064
+ const cardTypes = dedupeSorted(cardSets.flatMap((cardSet) => cardSet.cards.map((card) => card.type)));
1065
+ const cardIds = dedupeSorted(cardSets.flatMap((cardSet) => cardSet.cards.flatMap(renderCardInstanceIds)));
1066
+ const cardSetIdByCardId = new Map();
1067
+ const cardTypeByCardId = new Map();
1068
+ for (const cardSet of cardSets) {
1069
+ for (const card of cardSet.cards) {
1070
+ for (const cardId of renderCardInstanceIds(card)) {
1071
+ cardSetIdByCardId.set(cardId, cardSet.id);
1072
+ cardTypeByCardId.set(cardId, card.cardType ?? card.type);
1073
+ }
1074
+ }
1075
+ }
1076
+ const sharedZoneCardSetIds = new Map();
1077
+ for (const zone of sharedZones) {
1078
+ sharedZoneCardSetIds.set(zone.id, dedupeSorted(zone.allowedCardSetIds ?? []));
1079
+ }
1080
+ const playerZoneCardSetIds = new Map();
1081
+ for (const zone of playerZones) {
1082
+ playerZoneCardSetIds.set(zone.id, dedupeSorted(zone.allowedCardSetIds ?? []));
1083
+ }
1084
+ const zoneCardSetIdsById = new Map();
1085
+ for (const [zoneId, cardSetIds] of sharedZoneCardSetIds.entries()) {
1086
+ zoneCardSetIdsById.set(zoneId, cardSetIds);
1087
+ }
1088
+ for (const [zoneId, cardSetIds] of playerZoneCardSetIds.entries()) {
1089
+ zoneCardSetIdsById.set(zoneId, cardSetIds);
1090
+ }
1091
+ const sharedZoneIdsByCardSetId = new Map(cardSetIds.map((cardSetId) => [cardSetId, []]));
1092
+ for (const [zoneId, allowedCardSetIds] of sharedZoneCardSetIds.entries()) {
1093
+ for (const cardSetId of allowedCardSetIds) {
1094
+ const zoneIds = sharedZoneIdsByCardSetId.get(cardSetId) ?? [];
1095
+ zoneIds.push(zoneId);
1096
+ sharedZoneIdsByCardSetId.set(cardSetId, zoneIds);
1097
+ }
1098
+ }
1099
+ for (const [cardSetId, zoneIds] of sharedZoneIdsByCardSetId.entries()) {
1100
+ sharedZoneIdsByCardSetId.set(cardSetId, dedupeSorted(zoneIds));
1101
+ }
1102
+ const sharedZoneIdSet = new Set(sharedZones.map((zone) => zone.id));
1103
+ const homeSharedZoneIdsByCardType = new Map(cardTypes.map((cardType) => [cardType, []]));
1104
+ for (const cardSet of cardSets) {
1105
+ for (const card of cardSet.cards) {
1106
+ if (card.home?.type !== "zone" ||
1107
+ !sharedZoneIdSet.has(card.home.zoneId)) {
1108
+ continue;
1109
+ }
1110
+ const zoneIds = homeSharedZoneIdsByCardType.get(card.type) ?? [];
1111
+ zoneIds.push(card.home.zoneId);
1112
+ homeSharedZoneIdsByCardType.set(card.type, zoneIds);
1113
+ }
1114
+ }
1115
+ const homeSharedZoneIdByCardType = new Map();
1116
+ for (const [cardType, zoneIds] of homeSharedZoneIdsByCardType.entries()) {
1117
+ const uniqueZoneIds = dedupeSorted(zoneIds);
1118
+ homeSharedZoneIdsByCardType.set(cardType, uniqueZoneIds);
1119
+ const [homeZoneId] = uniqueZoneIds;
1120
+ if (uniqueZoneIds.length === 1 && homeZoneId !== undefined) {
1121
+ homeSharedZoneIdByCardType.set(cardType, homeZoneId);
1122
+ }
1123
+ }
1124
+ const zoneVisibilityById = new Map((manifest.zones ?? []).map((zone) => [
1125
+ zone.id,
1126
+ zone.visibility ?? "public",
1127
+ ]));
1128
+ const resourceIds = dedupeSorted((manifest.resources ?? []).map((resource) => resource.id));
1129
+ const resourcePresentationById = sortedObject((manifest.resources ?? []).map((resource) => [
1130
+ resource.id,
1131
+ {
1132
+ label: resource.name,
1133
+ ...(resource.icon ? { icon: resource.icon } : {}),
1134
+ },
1135
+ ]));
1136
+ const setupOptionIds = dedupeSorted((manifest.setupOptions ?? []).map((option) => option.id));
1137
+ const setupProfileIds = dedupeSorted((manifest.setupProfiles ?? []).map((profile) => profile.id));
1138
+ const setupChoiceIdsByOptionId = new Map((manifest.setupOptions ?? []).map((option) => [
1139
+ option.id,
1140
+ dedupeSorted((option.choices ?? []).map((choice) => choice.id)),
1141
+ ]));
1142
+ const setupOptionsById = Object.fromEntries((manifest.setupOptions ?? [])
1143
+ .slice()
1144
+ .sort((left, right) => left.id.localeCompare(right.id))
1145
+ .map((option) => [
1146
+ option.id,
1147
+ {
1148
+ id: option.id,
1149
+ name: option.name,
1150
+ description: option.description ?? null,
1151
+ choices: (option.choices ?? []).map((choice) => ({
1152
+ id: choice.id,
1153
+ label: choice.label,
1154
+ description: choice.description ?? null,
1155
+ })),
1156
+ },
1157
+ ]));
1158
+ const setupProfilesById = Object.fromEntries((manifest.setupProfiles ?? [])
1159
+ .slice()
1160
+ .sort((left, right) => left.id.localeCompare(right.id))
1161
+ .map((profile) => [
1162
+ profile.id,
1163
+ {
1164
+ id: profile.id,
1165
+ name: profile.name,
1166
+ description: profile.description ?? null,
1167
+ optionValues: profile.optionValues
1168
+ ? Object.fromEntries(Object.entries(profile.optionValues).filter((entry) => typeof entry[1] === "string"))
1169
+ : null,
1170
+ },
1171
+ ]));
1172
+ const pieceTypeIds = dedupeSorted((manifest.pieceTypes ?? []).map((pieceType) => pieceType.id));
1173
+ const pieceTypeSchemasById = new Map((manifest.pieceTypes ?? []).map((pieceType) => [
1174
+ pieceType.id,
1175
+ pieceType.fieldsSchema,
1176
+ ]));
1177
+ const pieceTypeSlotIdsById = new Map((manifest.pieceTypes ?? []).map((pieceType) => [
1178
+ pieceType.id,
1179
+ dedupeSorted((pieceType.slots ?? []).map((slot) => slot.id)),
1180
+ ]));
1181
+ const pieceTypeIdByPieceId = new Map();
1182
+ for (const seed of manifest.pieceSeeds ?? []) {
1183
+ for (const pieceId of expandSeedIds([seed])) {
1184
+ pieceTypeIdByPieceId.set(pieceId, seed.typeId);
1185
+ }
1186
+ }
1187
+ const pieceIds = dedupeSorted(pieceTypeIdByPieceId.keys());
1188
+ const dieTypeIds = dedupeSorted((manifest.dieTypes ?? []).map((dieType) => dieType.id));
1189
+ const dieTypeSchemasById = new Map((manifest.dieTypes ?? []).map((dieType) => [
1190
+ dieType.id,
1191
+ dieType.fieldsSchema,
1192
+ ]));
1193
+ const dieTypeSlotIdsById = new Map((manifest.dieTypes ?? []).map((dieType) => [
1194
+ dieType.id,
1195
+ dedupeSorted((dieType.slots ?? []).map((slot) => slot.id)),
1196
+ ]));
1197
+ const dieTypeIdByDieId = new Map();
1198
+ for (const seed of manifest.dieSeeds ?? []) {
1199
+ for (const dieId of expandSeedIds([seed])) {
1200
+ dieTypeIdByDieId.set(dieId, seed.typeId);
1201
+ }
1202
+ }
1203
+ const dieIds = dedupeSorted(dieTypeIdByDieId.keys());
1204
+ const strictSlotHosts = [
1205
+ ...(manifest.pieceSeeds ?? []).flatMap((seed) => {
1206
+ if (!isSingletonExplicitSeed(seed)) {
1207
+ return [];
1208
+ }
1209
+ const slotIds = pieceTypeSlotIdsById.get(seed.typeId) ?? [];
1210
+ return slotIds.length > 0
1211
+ ? [
1212
+ {
1213
+ kind: "piece",
1214
+ id: seed.id,
1215
+ slotIds,
1216
+ },
1217
+ ]
1218
+ : [];
1219
+ }),
1220
+ ...(manifest.dieSeeds ?? []).flatMap((seed) => {
1221
+ if (!isSingletonExplicitSeed(seed)) {
1222
+ return [];
1223
+ }
1224
+ const slotIds = dieTypeSlotIdsById.get(seed.typeId) ?? [];
1225
+ return slotIds.length > 0
1226
+ ? [
1227
+ {
1228
+ kind: "die",
1229
+ id: seed.id,
1230
+ slotIds,
1231
+ },
1232
+ ]
1233
+ : [];
1234
+ }),
1235
+ ].sort((left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id));
1236
+ const analyzedBoards = analyzeBoards(manifest, playerIds);
1237
+ const boardTemplateIds = dedupeSorted((manifest.boardTemplates ?? []).map((boardTemplate) => boardTemplate.id));
1238
+ const boardBaseIds = dedupeSorted(analyzedBoards.map(({ board }) => board.id));
1239
+ const boardIds = dedupeSorted(analyzedBoards.flatMap((entry) => entry.runtimeBoardIds));
1240
+ const boardTypeIds = dedupeSorted(analyzedBoards
1241
+ .map((board) => board.boardTypeId)
1242
+ .filter((typeId) => typeof typeId === "string"));
1243
+ const boardLayoutById = new Map();
1244
+ const boardIdsByLayout = new Map();
1245
+ const boardBaseIdsByLayout = new Map();
1246
+ const boardIdsByBaseId = new Map();
1247
+ const boardBaseIdsByTemplateId = new Map();
1248
+ const boardTemplateLayoutById = new Map();
1249
+ const boardIdsByTypeId = new Map();
1250
+ const spaceIdsByBoardId = new Map();
1251
+ const spaceTypeIdByBoardId = new Map();
1252
+ const spaceIdsByTypeId = new Map();
1253
+ const containerIdsByBoardId = new Map();
1254
+ const containerHostByBoardId = new Map();
1255
+ const relationTypeIdsByBoardId = new Map();
1256
+ const edgeIdsByTypeId = new Map();
1257
+ const edgeIdsByBoardIdAndTypeId = new Map();
1258
+ const vertexIdsByTypeId = new Map();
1259
+ const vertexIdsByBoardIdAndTypeId = new Map();
1260
+ for (const boardTemplate of manifest.boardTemplates ?? []) {
1261
+ boardTemplateLayoutById.set(boardTemplate.id, boardTemplate.layout);
1262
+ boardBaseIdsByLayout.set(boardTemplate.layout, dedupeSorted([...(boardBaseIdsByLayout.get(boardTemplate.layout) ?? [])]));
1263
+ }
1264
+ for (const analyzedBoard of analyzedBoards) {
1265
+ boardIdsByBaseId.set(analyzedBoard.board.id, analyzedBoard.runtimeBoardIds);
1266
+ boardIdsByLayout.set(analyzedBoard.board.layout, dedupeSorted([
1267
+ ...(boardIdsByLayout.get(analyzedBoard.board.layout) ?? []),
1268
+ ...analyzedBoard.runtimeBoardIds,
1269
+ ]));
1270
+ boardBaseIdsByLayout.set(analyzedBoard.board.layout, dedupeSorted([
1271
+ ...(boardBaseIdsByLayout.get(analyzedBoard.board.layout) ?? []),
1272
+ analyzedBoard.board.id,
1273
+ ]));
1274
+ if (analyzedBoard.board.templateId) {
1275
+ boardBaseIdsByTemplateId.set(analyzedBoard.board.templateId, dedupeSorted([
1276
+ ...(boardBaseIdsByTemplateId.get(analyzedBoard.board.templateId) ??
1277
+ []),
1278
+ analyzedBoard.board.id,
1279
+ ]));
1280
+ }
1281
+ const runtimeSpaceIds = analyzedBoard.spaces.map((space) => space.id);
1282
+ const runtimeSpaceTypeIds = sortedObject(analyzedBoard.spaces.map((space) => [space.id, space.typeId ?? null]));
1283
+ const runtimeContainerIds = analyzedBoard.layout === "hex"
1284
+ ? []
1285
+ : analyzedBoard.containers.map((container) => container.id);
1286
+ const runtimeContainerHosts = analyzedBoard.layout === "hex"
1287
+ ? {}
1288
+ : sortedObject(analyzedBoard.containers.map((container) => [
1289
+ container.id,
1290
+ container.host.type === "space"
1291
+ ? { type: "space", spaceId: container.host.spaceId }
1292
+ : { type: "board" },
1293
+ ]));
1294
+ const runtimeRelationTypeIds = analyzedBoard.layout === "hex"
1295
+ ? ["adjacent"]
1296
+ : analyzedBoard.layout === "square"
1297
+ ? dedupeSorted([
1298
+ "adjacent",
1299
+ ...analyzedBoard.relations.map((relation) => relation.typeId),
1300
+ ])
1301
+ : dedupeSorted(analyzedBoard.relations.map((relation) => relation.typeId));
1302
+ for (const runtimeBoardId of analyzedBoard.runtimeBoardIds) {
1303
+ boardLayoutById.set(runtimeBoardId, analyzedBoard.board.layout);
1304
+ spaceIdsByBoardId.set(runtimeBoardId, runtimeSpaceIds);
1305
+ spaceTypeIdByBoardId.set(runtimeBoardId, runtimeSpaceTypeIds);
1306
+ containerIdsByBoardId.set(runtimeBoardId, runtimeContainerIds);
1307
+ containerHostByBoardId.set(runtimeBoardId, runtimeContainerHosts);
1308
+ relationTypeIdsByBoardId.set(runtimeBoardId, runtimeRelationTypeIds);
1309
+ }
1310
+ if (analyzedBoard.boardTypeId) {
1311
+ boardIdsByTypeId.set(analyzedBoard.boardTypeId, dedupeSorted([
1312
+ ...(boardIdsByTypeId.get(analyzedBoard.boardTypeId) ?? []),
1313
+ ...analyzedBoard.runtimeBoardIds,
1314
+ ]));
1315
+ }
1316
+ for (const space of analyzedBoard.spaces) {
1317
+ if (!space.typeId) {
1318
+ continue;
1319
+ }
1320
+ spaceIdsByTypeId.set(space.typeId, dedupeSorted([...(spaceIdsByTypeId.get(space.typeId) ?? []), space.id]));
1321
+ }
1322
+ if (analyzedBoard.layout !== "generic") {
1323
+ const edgeIdsForBoardByType = {};
1324
+ for (const edge of analyzedBoard.edges) {
1325
+ if (!edge.typeId) {
1326
+ continue;
1327
+ }
1328
+ edgeIdsByTypeId.set(edge.typeId, dedupeSorted([...(edgeIdsByTypeId.get(edge.typeId) ?? []), edge.id]));
1329
+ edgeIdsForBoardByType[edge.typeId] = dedupeSorted([
1330
+ ...(edgeIdsForBoardByType[edge.typeId] ?? []),
1331
+ edge.id,
1332
+ ]);
1333
+ }
1334
+ const vertexIdsForBoardByType = {};
1335
+ for (const vertex of analyzedBoard.vertices) {
1336
+ if (!vertex.typeId) {
1337
+ continue;
1338
+ }
1339
+ vertexIdsByTypeId.set(vertex.typeId, dedupeSorted([
1340
+ ...(vertexIdsByTypeId.get(vertex.typeId) ?? []),
1341
+ vertex.id,
1342
+ ]));
1343
+ vertexIdsForBoardByType[vertex.typeId] = dedupeSorted([
1344
+ ...(vertexIdsForBoardByType[vertex.typeId] ?? []),
1345
+ vertex.id,
1346
+ ]);
1347
+ }
1348
+ for (const runtimeBoardId of analyzedBoard.runtimeBoardIds) {
1349
+ edgeIdsByBoardIdAndTypeId.set(runtimeBoardId, edgeIdsForBoardByType);
1350
+ vertexIdsByBoardIdAndTypeId.set(runtimeBoardId, vertexIdsForBoardByType);
1351
+ }
1352
+ }
1353
+ }
1354
+ const relationTypeIds = dedupeSorted(Array.from(relationTypeIdsByBoardId.values()).flat());
1355
+ const edgeIds = dedupeSorted(analyzedBoards.flatMap((board) => board.layout === "generic" ? [] : board.edges.map((edge) => edge.id)));
1356
+ const edgeTypeIds = dedupeSorted(edgeIdsByTypeId.keys());
1357
+ const vertexIds = dedupeSorted(analyzedBoards.flatMap((board) => board.layout === "generic"
1358
+ ? []
1359
+ : board.vertices.map((vertex) => vertex.id)));
1360
+ const vertexTypeIds = dedupeSorted(vertexIdsByTypeId.keys());
1361
+ const boardContainerIds = dedupeSorted(analyzedBoards.flatMap((board) => board.layout === "hex"
1362
+ ? []
1363
+ : board.containers.map((container) => container.id)));
1364
+ const spaceIds = dedupeSorted(analyzedBoards.flatMap((board) => board.spaces.map((space) => space.id)));
1365
+ const spaceTypeIds = dedupeSorted(analyzedBoards.flatMap((board) => board.spaces
1366
+ .map((space) => space.typeId)
1367
+ .filter((typeId) => typeof typeId === "string")));
1368
+ return {
1369
+ manifest,
1370
+ playerIds,
1371
+ sharedZones,
1372
+ playerZones,
1373
+ zoneIds,
1374
+ cardSets,
1375
+ cardSetIds,
1376
+ cardTypes,
1377
+ cardIds,
1378
+ cardSetIdByCardId,
1379
+ cardTypeByCardId,
1380
+ sharedZoneCardSetIds,
1381
+ sharedZoneIdsByCardSetId,
1382
+ homeSharedZoneIdsByCardType,
1383
+ homeSharedZoneIdByCardType,
1384
+ playerZoneCardSetIds,
1385
+ zoneCardSetIdsById,
1386
+ zoneVisibilityById,
1387
+ resourceIds,
1388
+ resourcePresentationById,
1389
+ setupOptionIds,
1390
+ setupProfileIds,
1391
+ setupChoiceIdsByOptionId,
1392
+ setupOptionsById,
1393
+ setupProfilesById,
1394
+ pieceTypeIds,
1395
+ pieceIds,
1396
+ pieceTypeIdByPieceId,
1397
+ dieTypeIds,
1398
+ dieIds,
1399
+ dieTypeIdByDieId,
1400
+ strictSlotHosts,
1401
+ boardTemplateIds,
1402
+ boardBaseIds,
1403
+ boardIds,
1404
+ boardContainerIds,
1405
+ boardTypeIds,
1406
+ boardLayoutById,
1407
+ boardIdsByLayout,
1408
+ boardBaseIdsByLayout,
1409
+ boardIdsByBaseId,
1410
+ boardBaseIdsByTemplateId,
1411
+ boardTemplateLayoutById,
1412
+ boardIdsByTypeId,
1413
+ spaceIdsByBoardId,
1414
+ spaceTypeIdByBoardId,
1415
+ spaceIdsByTypeId,
1416
+ containerIdsByBoardId,
1417
+ containerHostByBoardId,
1418
+ relationTypeIds,
1419
+ relationTypeIdsByBoardId,
1420
+ edgeIds,
1421
+ edgeTypeIds,
1422
+ edgeIdsByTypeId,
1423
+ edgeIdsByBoardIdAndTypeId,
1424
+ vertexIds,
1425
+ vertexTypeIds,
1426
+ vertexIdsByTypeId,
1427
+ vertexIdsByBoardIdAndTypeId,
1428
+ spaceIds,
1429
+ spaceTypeIds,
1430
+ analyzedBoards,
1431
+ pieceTypeSchemasById,
1432
+ dieTypeSchemasById,
1433
+ };
1434
+ }
1435
+ function cloneJson(value) {
1436
+ return JSON.parse(JSON.stringify(value));
1437
+ }
1438
+ function boardSpaceRefKey(spaceIds) {
1439
+ return [...spaceIds]
1440
+ .sort((left, right) => left.localeCompare(right))
1441
+ .join("$$");
1442
+ }
1443
+ function materializePropertySchemaDefault(property, analysis, runtimeBoardId) {
1444
+ if (hasPropertySchemaDefault(property)) {
1445
+ return cloneJson(property.default);
1446
+ }
1447
+ if (property.optional) {
1448
+ return undefined;
1449
+ }
1450
+ if (property.nullable) {
1451
+ return null;
1452
+ }
1453
+ switch (property.type) {
1454
+ case "string":
1455
+ return "";
1456
+ case "integer":
1457
+ case "number":
1458
+ return 0;
1459
+ case "boolean":
1460
+ return false;
1461
+ case "enum":
1462
+ return property.enums?.[0] ?? "";
1463
+ case "array":
1464
+ return [];
1465
+ case "object":
1466
+ return materializeObjectSchemaDefaults(property.properties ? { properties: property.properties } : undefined, analysis, runtimeBoardId);
1467
+ case "record":
1468
+ return {};
1469
+ case "zoneId":
1470
+ return analysis.zoneIds[0] ?? "";
1471
+ case "cardId":
1472
+ return analysis.cardIds[0] ?? "";
1473
+ case "playerId":
1474
+ return analysis.playerIds[0] ?? "";
1475
+ case "boardId":
1476
+ return runtimeBoardId ?? analysis.boardIds[0] ?? "";
1477
+ case "edgeId":
1478
+ return analysis.edgeIds[0] ?? "";
1479
+ case "vertexId":
1480
+ return analysis.vertexIds[0] ?? "";
1481
+ case "spaceId":
1482
+ return ((runtimeBoardId
1483
+ ? analysis.spaceIdsByBoardId.get(runtimeBoardId)?.[0]
1484
+ : undefined) ??
1485
+ analysis.spaceIds[0] ??
1486
+ "");
1487
+ case "pieceId":
1488
+ return analysis.pieceIds[0] ?? "";
1489
+ case "dieId":
1490
+ return analysis.dieIds[0] ?? "";
1491
+ case "resourceId":
1492
+ return analysis.resourceIds[0] ?? "";
1493
+ }
1494
+ }
1495
+ function materializeObjectSchemaDefaults(schema, analysis, runtimeBoardId) {
1496
+ if (!schema?.properties) {
1497
+ return {};
1498
+ }
1499
+ return Object.fromEntries(Object.entries(schema.properties).flatMap(([key, property]) => {
1500
+ const value = materializePropertySchemaDefault(property, analysis, runtimeBoardId);
1501
+ return value === undefined ? [] : [[key, value]];
1502
+ }));
1503
+ }
1504
+ function materializeCardPropertiesDefaults(schema, cardType, analysis) {
1505
+ const objectSchema = isCardPropertySchemaVariants(schema)
1506
+ ? mergeSharedCardProperties(schema, cardType)
1507
+ : schema;
1508
+ return materializeObjectSchemaDefaults(objectSchema, analysis);
1509
+ }
1510
+ export function materializeManifestTable(options) {
1511
+ const analysis = analyzeManifest(options.manifest);
1512
+ const manifest = analysis.manifest;
1513
+ const playerIds = [...options.playerIds];
1514
+ const cards = {};
1515
+ const pieces = {};
1516
+ const dice = {};
1517
+ const componentLocations = {};
1518
+ const locationOrder = new Map();
1519
+ const cardIdsByCardSetId = new Map();
1520
+ const boardAnalysisByBaseId = new Map(analysis.analyzedBoards.map((board) => [board.board.id, board]));
1521
+ const edgeIdByBoardBaseIdAndSpaces = new Map();
1522
+ const vertexIdByBoardBaseIdAndSpaces = new Map();
1523
+ for (const analyzedBoard of analysis.analyzedBoards) {
1524
+ if (analyzedBoard.layout === "generic") {
1525
+ continue;
1526
+ }
1527
+ edgeIdByBoardBaseIdAndSpaces.set(analyzedBoard.board.id, new Map(analyzedBoard.edges.map((edge) => [
1528
+ boardSpaceRefKey(edge.spaceIds),
1529
+ edge.id,
1530
+ ])));
1531
+ vertexIdByBoardBaseIdAndSpaces.set(analyzedBoard.board.id, new Map(analyzedBoard.vertices.map((vertex) => [
1532
+ boardSpaceRefKey(vertex.spaceIds),
1533
+ vertex.id,
1534
+ ])));
1535
+ }
1536
+ const nextLocationPosition = (location) => {
1537
+ const key = JSON.stringify(location);
1538
+ const position = locationOrder.get(key) ?? 0;
1539
+ locationOrder.set(key, position + 1);
1540
+ return position;
1541
+ };
1542
+ const zoneScopeById = new Map([
1543
+ ...analysis.sharedZones.map((zone) => [zone.id, "shared"]),
1544
+ ...analysis.playerZones.map((zone) => [zone.id, "perPlayer"]),
1545
+ ]);
1546
+ const nextDeckPositionByZoneId = new Map();
1547
+ const resolveRuntimeBoardId = (boardBaseId, ownerId, context) => {
1548
+ const analyzedBoard = boardAnalysisByBaseId.get(boardBaseId);
1549
+ if (analyzedBoard?.board.scope === "perPlayer") {
1550
+ if (!ownerId) {
1551
+ throw new Error(`${context ?? `Home on board '${boardBaseId}'`} requires ownerId because board '${boardBaseId}' has scope 'perPlayer'. Add ownerId to resolve the player-scoped destination.`);
1552
+ }
1553
+ return `${boardBaseId}:${ownerId}`;
1554
+ }
1555
+ return boardBaseId;
1556
+ };
1557
+ const resolveBoardEdgeId = (boardBaseId, spaceIds) => edgeIdByBoardBaseIdAndSpaces
1558
+ .get(boardBaseId)
1559
+ ?.get(boardSpaceRefKey(spaceIds)) ?? boardSpaceRefKey(spaceIds);
1560
+ const resolveBoardVertexId = (boardBaseId, spaceIds) => vertexIdByBoardBaseIdAndSpaces
1561
+ .get(boardBaseId)
1562
+ ?.get(boardSpaceRefKey(spaceIds)) ?? boardSpaceRefKey(spaceIds);
1563
+ for (const cardSet of manifest.cardSets) {
1564
+ const materializedCardSet = materializeCardSet(cardSet);
1565
+ const cardIds = [];
1566
+ for (const card of materializedCardSet.cards) {
1567
+ const cardInstanceIds = renderCardInstanceIds(card);
1568
+ for (const cardId of cardInstanceIds) {
1569
+ cardIds.push(cardId);
1570
+ cards[cardId] = {
1571
+ id: cardId,
1572
+ cardSetId: materializedCardSet.id,
1573
+ cardType: card.type,
1574
+ name: card.name,
1575
+ text: card.text,
1576
+ properties: {
1577
+ ...materializeCardPropertiesDefaults(materializedCardSet.cardSchema, card.type, analysis),
1578
+ ...(card.properties ?? {}),
1579
+ },
1580
+ };
1581
+ if (card.home?.type === "zone") {
1582
+ const nextPosition = nextDeckPositionByZoneId.get(card.home.zoneId) ?? 0;
1583
+ nextDeckPositionByZoneId.set(card.home.zoneId, nextPosition + 1);
1584
+ componentLocations[cardId] = {
1585
+ type: "InDeck",
1586
+ deckId: card.home.zoneId,
1587
+ playedBy: null,
1588
+ position: nextPosition,
1589
+ };
1590
+ }
1591
+ else if (card.home?.type === "detached") {
1592
+ componentLocations[cardId] = {
1593
+ type: "Detached",
1594
+ position: nextLocationPosition({ type: "Detached" }),
1595
+ };
1596
+ }
1597
+ }
1598
+ }
1599
+ cardIdsByCardSetId.set(materializedCardSet.id, cardIds);
1600
+ }
1601
+ for (const zone of analysis.sharedZones) {
1602
+ const primaryCardSetId = zone.allowedCardSetIds?.[0];
1603
+ if (!primaryCardSetId) {
1604
+ continue;
1605
+ }
1606
+ const shuffled = options.shuffleItems((cardIdsByCardSetId.get(primaryCardSetId) ?? []).filter((cardId) => componentLocations[cardId] === undefined));
1607
+ shuffled.forEach((cardId, index) => {
1608
+ componentLocations[cardId] = {
1609
+ type: "InDeck",
1610
+ deckId: zone.id,
1611
+ playedBy: null,
1612
+ position: index,
1613
+ };
1614
+ });
1615
+ }
1616
+ const pieceTypesById = new Map((manifest.pieceTypes ?? []).map((pieceType) => [pieceType.id, pieceType]));
1617
+ const dieTypesById = new Map((manifest.dieTypes ?? []).map((dieType) => [dieType.id, dieType]));
1618
+ const assignSeedLocation = (componentId, ownerId, home, context) => {
1619
+ if (!home) {
1620
+ componentLocations[componentId] = { type: "Detached" };
1621
+ return;
1622
+ }
1623
+ if (home.type === "slot") {
1624
+ componentLocations[componentId] = {
1625
+ type: "InSlot",
1626
+ host: home.host,
1627
+ slotId: home.slotId,
1628
+ position: nextLocationPosition({
1629
+ type: "InSlot",
1630
+ host: home.host,
1631
+ slotId: home.slotId,
1632
+ }),
1633
+ };
1634
+ return;
1635
+ }
1636
+ if (home.type === "zone") {
1637
+ if (zoneScopeById.get(home.zoneId) === "perPlayer") {
1638
+ if (!ownerId) {
1639
+ throw new Error(`${context.path}.zoneId: ${context.label} requires ownerId because zone '${home.zoneId}' has scope 'perPlayer'. Add ownerId to resolve the player-scoped destination.`);
1640
+ }
1641
+ componentLocations[componentId] = {
1642
+ type: "InHand",
1643
+ handId: home.zoneId,
1644
+ playerId: ownerId,
1645
+ position: nextLocationPosition({
1646
+ type: "InHand",
1647
+ handId: home.zoneId,
1648
+ playerId: ownerId,
1649
+ }),
1650
+ };
1651
+ return;
1652
+ }
1653
+ componentLocations[componentId] = {
1654
+ type: "InZone",
1655
+ zoneId: home.zoneId,
1656
+ playedBy: null,
1657
+ position: nextLocationPosition({
1658
+ type: "InZone",
1659
+ zoneId: home.zoneId,
1660
+ }),
1661
+ };
1662
+ return;
1663
+ }
1664
+ if (home.type === "space") {
1665
+ const boardId = resolveRuntimeBoardId(home.boardId, ownerId, `${context.path}.boardId: ${context.label}`);
1666
+ componentLocations[componentId] = {
1667
+ type: "OnSpace",
1668
+ boardId,
1669
+ spaceId: home.spaceId,
1670
+ position: nextLocationPosition({
1671
+ type: "OnSpace",
1672
+ boardId,
1673
+ spaceId: home.spaceId,
1674
+ }),
1675
+ };
1676
+ return;
1677
+ }
1678
+ if (home.type === "container") {
1679
+ const boardId = resolveRuntimeBoardId(home.boardId, ownerId, `${context.path}.boardId: ${context.label}`);
1680
+ componentLocations[componentId] = {
1681
+ type: "InContainer",
1682
+ boardId,
1683
+ containerId: home.containerId,
1684
+ position: nextLocationPosition({
1685
+ type: "InContainer",
1686
+ boardId,
1687
+ containerId: home.containerId,
1688
+ }),
1689
+ };
1690
+ return;
1691
+ }
1692
+ if (home.type === "edge") {
1693
+ const boardId = resolveRuntimeBoardId(home.boardId, ownerId, `${context.path}.boardId: ${context.label}`);
1694
+ const edgeId = resolveBoardEdgeId(home.boardId, home.ref.spaces);
1695
+ componentLocations[componentId] = {
1696
+ type: "OnEdge",
1697
+ boardId,
1698
+ edgeId,
1699
+ position: nextLocationPosition({
1700
+ type: "OnEdge",
1701
+ boardId,
1702
+ edgeId,
1703
+ }),
1704
+ };
1705
+ return;
1706
+ }
1707
+ if (home.type === "vertex") {
1708
+ const boardId = resolveRuntimeBoardId(home.boardId, ownerId, `${context.path}.boardId: ${context.label}`);
1709
+ const vertexId = resolveBoardVertexId(home.boardId, home.ref.spaces);
1710
+ componentLocations[componentId] = {
1711
+ type: "OnVertex",
1712
+ boardId,
1713
+ vertexId,
1714
+ position: nextLocationPosition({
1715
+ type: "OnVertex",
1716
+ boardId,
1717
+ vertexId,
1718
+ }),
1719
+ };
1720
+ return;
1721
+ }
1722
+ componentLocations[componentId] = { type: "Detached" };
1723
+ };
1724
+ for (const [seedIndex, seed] of (manifest.pieceSeeds ?? []).entries()) {
1725
+ const pieceType = pieceTypesById.get(seed.typeId);
1726
+ for (const componentId of expandSeedIds([seed])) {
1727
+ pieces[componentId] = {
1728
+ componentType: "piece",
1729
+ id: componentId,
1730
+ pieceTypeId: seed.typeId,
1731
+ pieceName: pieceType?.name ?? seed.typeId,
1732
+ ownerId: seed.ownerId ?? null,
1733
+ properties: {
1734
+ ...materializeObjectSchemaDefaults(analysis.pieceTypeSchemasById.get(seed.typeId), analysis),
1735
+ ...(seed.fields ?? {}),
1736
+ },
1737
+ };
1738
+ assignSeedLocation(componentId, seed.ownerId, seed.home, {
1739
+ path: `manifest.pieceSeeds[${seedIndex}].home`,
1740
+ label: `Piece seed '${seed.id ?? seed.typeId}'`,
1741
+ });
1742
+ }
1743
+ }
1744
+ for (const [seedIndex, seed] of (manifest.dieSeeds ?? []).entries()) {
1745
+ const dieType = dieTypesById.get(seed.typeId);
1746
+ for (const componentId of expandSeedIds([seed])) {
1747
+ dice[componentId] = {
1748
+ componentType: "die",
1749
+ id: componentId,
1750
+ dieTypeId: seed.typeId,
1751
+ dieName: dieType?.name ?? seed.typeId,
1752
+ ownerId: seed.ownerId ?? null,
1753
+ sides: dieType?.sides ?? 6,
1754
+ value: null,
1755
+ properties: {
1756
+ ...materializeObjectSchemaDefaults(analysis.dieTypeSchemasById.get(seed.typeId), analysis),
1757
+ ...(seed.fields ?? {}),
1758
+ },
1759
+ };
1760
+ assignSeedLocation(componentId, seed.ownerId, seed.home, {
1761
+ path: `manifest.dieSeeds[${seedIndex}].home`,
1762
+ label: `Die seed '${seed.id ?? seed.typeId}'`,
1763
+ });
1764
+ }
1765
+ }
1766
+ const boardStatesById = {};
1767
+ const hexBoardStatesById = {};
1768
+ const squareBoardStatesById = {};
1769
+ for (const analyzedBoard of analysis.analyzedBoards) {
1770
+ const sharedBoardState = {
1771
+ baseId: analyzedBoard.board.id,
1772
+ layout: analyzedBoard.layout,
1773
+ typeId: analyzedBoard.boardTypeId ?? null,
1774
+ scope: analyzedBoard.board.scope,
1775
+ templateId: analyzedBoard.board.templateId ?? null,
1776
+ fields: {
1777
+ ...materializeObjectSchemaDefaults(analyzedBoard.boardFieldsSchema, analysis),
1778
+ ...(analyzedBoard.board.fields ?? {}),
1779
+ },
1780
+ };
1781
+ const buildSpaces = () => Object.fromEntries(analysis.spaceIds.map((spaceId) => {
1782
+ const space = analyzedBoard.spaces.find((entry) => entry.id === spaceId);
1783
+ const baseSpaceState = {
1784
+ id: spaceId,
1785
+ name: space && "name" in space ? (space.name ?? null) : null,
1786
+ typeId: space?.typeId ?? null,
1787
+ fields: {
1788
+ ...materializeObjectSchemaDefaults(analyzedBoard.spaceFieldsSchema, analysis),
1789
+ ...(space?.fields ?? {}),
1790
+ },
1791
+ zoneId: space && "zoneId" in space ? (space.zoneId ?? null) : null,
1792
+ };
1793
+ if (analyzedBoard.layout === "hex") {
1794
+ const hexSpace = space;
1795
+ return [
1796
+ spaceId,
1797
+ {
1798
+ ...baseSpaceState,
1799
+ q: hexSpace?.q ?? 0,
1800
+ r: hexSpace?.r ?? 0,
1801
+ },
1802
+ ];
1803
+ }
1804
+ if (analyzedBoard.layout === "square") {
1805
+ const squareSpace = space;
1806
+ return [
1807
+ spaceId,
1808
+ {
1809
+ ...baseSpaceState,
1810
+ row: squareSpace?.row ?? 0,
1811
+ col: squareSpace?.col ?? 0,
1812
+ },
1813
+ ];
1814
+ }
1815
+ return [spaceId, baseSpaceState];
1816
+ }));
1817
+ const relations = analyzedBoard.layout === "hex"
1818
+ ? []
1819
+ : analyzedBoard.relations.map((relation) => ({
1820
+ id: relation.id ?? null,
1821
+ typeId: relation.typeId,
1822
+ fromSpaceId: relation.fromSpaceId,
1823
+ toSpaceId: relation.toSpaceId,
1824
+ directed: relation.directed ?? false,
1825
+ fields: {
1826
+ ...materializeObjectSchemaDefaults(analyzedBoard.relationFieldsSchema, analysis),
1827
+ ...(relation.fields ?? {}),
1828
+ },
1829
+ }));
1830
+ const buildContainers = (runtimeBoardId) => analyzedBoard.layout === "hex"
1831
+ ? {}
1832
+ : Object.fromEntries(analysis.boardContainerIds.map((containerId) => {
1833
+ const container = analyzedBoard.containers.find((entry) => entry.id === containerId);
1834
+ return [
1835
+ containerId,
1836
+ {
1837
+ id: containerId,
1838
+ name: container?.name ?? containerId,
1839
+ host: container?.host.type === "space"
1840
+ ? { type: "space", spaceId: container.host.spaceId }
1841
+ : { type: "board" },
1842
+ allowedCardSetIds: container?.allowedCardSetIds,
1843
+ zoneId: `board:${runtimeBoardId}:container:${containerId}`,
1844
+ fields: {
1845
+ ...materializeObjectSchemaDefaults(analyzedBoard.containerFieldsSchema, analysis, runtimeBoardId),
1846
+ ...(container?.fields ?? {}),
1847
+ },
1848
+ },
1849
+ ];
1850
+ }));
1851
+ const edges = analyzedBoard.layout === "generic"
1852
+ ? []
1853
+ : analyzedBoard.edges.map((edge) => ({
1854
+ id: edge.id,
1855
+ spaceIds: [...edge.spaceIds],
1856
+ typeId: edge.typeId ?? null,
1857
+ label: edge.label ?? null,
1858
+ ownerId: null,
1859
+ fields: {
1860
+ ...materializeObjectSchemaDefaults(analyzedBoard.edgeFieldsSchema, analysis),
1861
+ ...(edge.fields ?? {}),
1862
+ },
1863
+ }));
1864
+ const vertices = analyzedBoard.layout === "generic"
1865
+ ? []
1866
+ : analyzedBoard.vertices.map((vertex) => ({
1867
+ id: vertex.id,
1868
+ spaceIds: [...vertex.spaceIds],
1869
+ typeId: vertex.typeId ?? null,
1870
+ label: vertex.label ?? null,
1871
+ ownerId: null,
1872
+ fields: {
1873
+ ...materializeObjectSchemaDefaults(analyzedBoard.vertexFieldsSchema, analysis),
1874
+ ...(vertex.fields ?? {}),
1875
+ },
1876
+ }));
1877
+ for (const runtimeBoardId of analyzedBoard.runtimeBoardIds) {
1878
+ const playerId = analyzedBoard.board.scope === "perPlayer"
1879
+ ? runtimeBoardId.slice(analyzedBoard.board.id.length + 1)
1880
+ : null;
1881
+ const boardState = {
1882
+ id: runtimeBoardId,
1883
+ ...sharedBoardState,
1884
+ playerId,
1885
+ spaces: cloneJson(buildSpaces()),
1886
+ relations: cloneJson(relations),
1887
+ containers: cloneJson(buildContainers(runtimeBoardId)),
1888
+ ...(analyzedBoard.layout === "hex"
1889
+ ? {
1890
+ orientation: analyzedBoard.board.orientation ?? "pointy-top",
1891
+ edges: cloneJson(edges),
1892
+ vertices: cloneJson(vertices),
1893
+ }
1894
+ : analyzedBoard.layout === "square"
1895
+ ? {
1896
+ edges: cloneJson(edges),
1897
+ vertices: cloneJson(vertices),
1898
+ }
1899
+ : {}),
1900
+ };
1901
+ boardStatesById[runtimeBoardId] = boardState;
1902
+ if (analyzedBoard.layout === "hex") {
1903
+ hexBoardStatesById[runtimeBoardId] = boardState;
1904
+ }
1905
+ if (analyzedBoard.layout === "square") {
1906
+ squareBoardStatesById[runtimeBoardId] = boardState;
1907
+ }
1908
+ }
1909
+ }
1910
+ const sharedZones = Object.fromEntries(analysis.sharedZones.map((zone) => [zone.id, []]));
1911
+ const perPlayerZones = Object.fromEntries(analysis.playerZones.map((zone) => [
1912
+ zone.id,
1913
+ Object.fromEntries(playerIds.map((playerId) => [playerId, []])),
1914
+ ]));
1915
+ const zoneVisibility = Object.fromEntries((manifest.zones ?? []).map((zone) => [
1916
+ zone.id,
1917
+ zone.visibility ?? "public",
1918
+ ]));
1919
+ const zoneCardSetIdsByZoneId = Object.fromEntries(Array.from(analysis.zoneCardSetIdsById.entries()).sort(([left], [right]) => left.localeCompare(right)));
1920
+ const handVisibility = Object.fromEntries(analysis.playerZones.map((zone) => [
1921
+ zone.id,
1922
+ zone.visibility ?? "ownerOnly",
1923
+ ]));
1924
+ const componentSortPosition = (position) => typeof position === "number" ? position : Number.MAX_SAFE_INTEGER;
1925
+ const pushSharedComponent = (zoneId, componentId, position) => {
1926
+ const zone = sharedZones[zoneId];
1927
+ if (!zone) {
1928
+ return;
1929
+ }
1930
+ zone.push(componentId);
1931
+ zone.sort((left, right) => componentSortPosition(componentLocations[left]?.position) -
1932
+ componentSortPosition(componentLocations[right]?.position));
1933
+ };
1934
+ const pushPlayerComponent = (zoneId, playerId, componentId, position) => {
1935
+ const zone = perPlayerZones[zoneId]?.[playerId];
1936
+ if (!zone) {
1937
+ return;
1938
+ }
1939
+ zone.push(componentId);
1940
+ zone.sort((left, right) => componentSortPosition(componentLocations[left]?.position) -
1941
+ componentSortPosition(componentLocations[right]?.position));
1942
+ };
1943
+ for (const [componentId, location] of Object.entries(componentLocations)) {
1944
+ switch (location.type) {
1945
+ case "InDeck":
1946
+ pushSharedComponent(location.deckId, componentId, location.position);
1947
+ break;
1948
+ case "InHand":
1949
+ pushPlayerComponent(location.handId, location.playerId, componentId, location.position);
1950
+ break;
1951
+ case "InZone":
1952
+ pushSharedComponent(location.zoneId, componentId, location.position);
1953
+ break;
1954
+ default:
1955
+ break;
1956
+ }
1957
+ }
1958
+ const ownerOfCard = Object.fromEntries(Object.keys(cards).map((cardId) => [cardId, null]));
1959
+ const visibility = Object.fromEntries(Object.keys(cards).map((cardId) => [cardId, { faceUp: true }]));
1960
+ const resourcesByPlayer = Object.fromEntries(playerIds.map((playerId) => [
1961
+ playerId,
1962
+ Object.fromEntries(analysis.resourceIds.map((resourceId) => [resourceId, 0])),
1963
+ ]));
1964
+ // PerPlayer<T> wire shape only: { __perPlayer: true, entries: [[playerId, value], ...] }.
1965
+ const toPerPlayerWireFormat = (entries) => ({
1966
+ __perPlayer: true,
1967
+ entries: playerIds.map((playerId) => [playerId, entries[playerId]]),
1968
+ });
1969
+ const perPlayerZonesWire = Object.fromEntries(Object.entries(perPlayerZones).map(([zoneId, entries]) => [
1970
+ zoneId,
1971
+ toPerPlayerWireFormat(entries),
1972
+ ]));
1973
+ const resourcesWire = toPerPlayerWireFormat(resourcesByPlayer);
1974
+ return {
1975
+ playerOrder: playerIds,
1976
+ zones: {
1977
+ shared: sharedZones,
1978
+ perPlayer: perPlayerZonesWire,
1979
+ visibility: zoneVisibility,
1980
+ cardSetIdsByZoneId: zoneCardSetIdsByZoneId,
1981
+ },
1982
+ decks: cloneJson(sharedZones),
1983
+ hands: cloneJson(perPlayerZonesWire),
1984
+ handVisibility,
1985
+ cards,
1986
+ pieces,
1987
+ componentLocations,
1988
+ ownerOfCard,
1989
+ visibility,
1990
+ resources: resourcesWire,
1991
+ boards: {
1992
+ byId: boardStatesById,
1993
+ hex: hexBoardStatesById,
1994
+ square: squareBoardStatesById,
1995
+ network: {},
1996
+ track: {},
1997
+ },
1998
+ dice,
1999
+ };
2000
+ }
2001
+ function renderTypeForPropertySchema(schema) {
2002
+ if (!schema) {
2003
+ return "RuntimePayload";
2004
+ }
2005
+ let rendered;
2006
+ switch (schema.type) {
2007
+ case "string":
2008
+ rendered = "string";
2009
+ break;
2010
+ case "integer":
2011
+ case "number":
2012
+ rendered = "number";
2013
+ break;
2014
+ case "boolean":
2015
+ rendered = "boolean";
2016
+ break;
2017
+ case "cardId":
2018
+ rendered = "CardId";
2019
+ break;
2020
+ case "playerId":
2021
+ rendered = "PlayerId";
2022
+ break;
2023
+ case "zoneId":
2024
+ rendered = "ZoneId";
2025
+ break;
2026
+ case "boardId":
2027
+ rendered = "BoardId";
2028
+ break;
2029
+ case "edgeId":
2030
+ rendered = "EdgeId";
2031
+ break;
2032
+ case "vertexId":
2033
+ rendered = "VertexId";
2034
+ break;
2035
+ case "spaceId":
2036
+ rendered = "SpaceId";
2037
+ break;
2038
+ case "pieceId":
2039
+ rendered = "PieceId";
2040
+ break;
2041
+ case "dieId":
2042
+ rendered = "DieId";
2043
+ break;
2044
+ case "resourceId":
2045
+ rendered = "ResourceId";
2046
+ break;
2047
+ case "enum":
2048
+ rendered =
2049
+ schema.enums && schema.enums.length > 0
2050
+ ? schema.enums.map((value) => quote(value)).join(" | ")
2051
+ : "string";
2052
+ break;
2053
+ case "array":
2054
+ rendered = `Array<${renderTypeForPropertySchema(schema.items)}>`;
2055
+ break;
2056
+ case "object":
2057
+ rendered = renderTypeForObjectSchema(schema.properties
2058
+ ? {
2059
+ properties: schema.properties,
2060
+ }
2061
+ : null);
2062
+ break;
2063
+ case "record":
2064
+ rendered = `Record<string, ${renderTypeForPropertySchema(schema.values)}>`;
2065
+ break;
2066
+ default:
2067
+ rendered = "RuntimePayload";
2068
+ break;
2069
+ }
2070
+ return schema.nullable ? `${rendered} | null` : rendered;
2071
+ }
2072
+ function renderZodForPropertySchema(schema) {
2073
+ if (!schema) {
2074
+ return "z.unknown()";
2075
+ }
2076
+ let rendered;
2077
+ switch (schema.type) {
2078
+ case "string":
2079
+ rendered = "z.string()";
2080
+ break;
2081
+ case "integer":
2082
+ rendered = "z.number().int()";
2083
+ break;
2084
+ case "number":
2085
+ rendered = "z.number()";
2086
+ break;
2087
+ case "boolean":
2088
+ rendered = "z.boolean()";
2089
+ break;
2090
+ case "cardId":
2091
+ rendered = "ids.cardId";
2092
+ break;
2093
+ case "playerId":
2094
+ rendered = "ids.playerId";
2095
+ break;
2096
+ case "zoneId":
2097
+ rendered = "ids.zoneId";
2098
+ break;
2099
+ case "boardId":
2100
+ rendered = "ids.boardId";
2101
+ break;
2102
+ case "edgeId":
2103
+ rendered = "ids.edgeId";
2104
+ break;
2105
+ case "vertexId":
2106
+ rendered = "ids.vertexId";
2107
+ break;
2108
+ case "spaceId":
2109
+ rendered = "ids.spaceId";
2110
+ break;
2111
+ case "pieceId":
2112
+ rendered = "ids.pieceId";
2113
+ break;
2114
+ case "dieId":
2115
+ rendered = "ids.dieId";
2116
+ break;
2117
+ case "resourceId":
2118
+ rendered = "ids.resourceId";
2119
+ break;
2120
+ case "enum":
2121
+ rendered =
2122
+ schema.enums && schema.enums.length > 0
2123
+ ? `z.enum([${schema.enums.map((value) => quote(value)).join(", ")}])`
2124
+ : "z.string()";
2125
+ break;
2126
+ case "array":
2127
+ rendered = `z.array(${renderZodForPropertySchema(schema.items)})`;
2128
+ break;
2129
+ case "object":
2130
+ rendered = renderZodForObjectSchema(schema.properties
2131
+ ? {
2132
+ properties: schema.properties,
2133
+ }
2134
+ : null);
2135
+ break;
2136
+ case "record":
2137
+ rendered = `z.record(z.string(), ${renderZodForPropertySchema(schema.values)})`;
2138
+ break;
2139
+ default:
2140
+ rendered = "z.unknown()";
2141
+ break;
2142
+ }
2143
+ if (schema.nullable) {
2144
+ rendered = `${rendered}.nullable()`;
2145
+ }
2146
+ if (schema.optional) {
2147
+ rendered = `${rendered}.optional()`;
2148
+ }
2149
+ if (hasPropertySchemaDefault(schema)) {
2150
+ rendered = `${rendered}.default(${JSON.stringify(schema.default)})`;
2151
+ }
2152
+ return rendered;
2153
+ }
2154
+ function renderTypeForObjectSchema(schema) {
2155
+ if (!schema || Object.keys(schema.properties).length === 0) {
2156
+ return "RuntimeRecord";
2157
+ }
2158
+ return `{\n${Object.entries(schema.properties)
2159
+ .map(([key, value]) => {
2160
+ const optional = value?.optional && !hasPropertySchemaDefault(value) ? "?" : "";
2161
+ return ` ${quote(key)}${optional}: ${renderTypeForPropertySchema(value)};`;
2162
+ })
2163
+ .join("\n")}\n}`;
2164
+ }
2165
+ function renderZodForObjectSchema(schema) {
2166
+ if (!schema || Object.keys(schema.properties).length === 0) {
2167
+ return "z.record(z.string(), z.unknown())";
2168
+ }
2169
+ return `z.object({\n${Object.entries(schema.properties)
2170
+ .map(([key, value]) => ` ${quote(key)}: ${renderZodForPropertySchema(value)},`)
2171
+ .join("\n")}\n})`;
2172
+ }
2173
+ function renderCardSetPropertySections(cardSets) {
2174
+ return cardSets
2175
+ .map((cardSet) => {
2176
+ const typeName = `${toPascalCase(cardSet.id)}CardProperties`;
2177
+ const schemaName = `${typeName}Schema`;
2178
+ if (isCardPropertySchemaVariants(cardSet.cardSchema)) {
2179
+ const cardSchema = cardSet.cardSchema;
2180
+ const variantBlocks = Object.entries(cardSchema.variants).map(([cardType]) => {
2181
+ const variantTypeName = cardPropertiesTypeName(cardSet, cardType);
2182
+ const variantSchemaName = cardPropertiesSchemaName(cardSet, cardType);
2183
+ const schema = mergeSharedCardProperties(cardSchema, cardType);
2184
+ return renderBlocks([
2185
+ `export type ${variantTypeName} = ${renderTypeForObjectSchema(schema)};`,
2186
+ `export const ${variantSchemaName} = ${renderZodForObjectSchema(schema)};`,
2187
+ ]);
2188
+ });
2189
+ const variantTypeNames = Object.keys(cardSchema.variants).map((cardType) => cardPropertiesTypeName(cardSet, cardType));
2190
+ const variantSchemaNames = Object.keys(cardSchema.variants).map((cardType) => cardPropertiesSchemaName(cardSet, cardType));
2191
+ const unionSchema = variantSchemaNames.length === 0
2192
+ ? "z.record(z.string(), z.unknown())"
2193
+ : variantSchemaNames.length === 1
2194
+ ? variantSchemaNames[0]
2195
+ : `z.union([${variantSchemaNames.join(", ")}])`;
2196
+ return renderBlocks([
2197
+ ...variantBlocks,
2198
+ `export type ${typeName} = ${variantTypeNames.join(" | ") || "RuntimeRecord"};`,
2199
+ `export const ${schemaName} = ${unionSchema};`,
2200
+ `export type ${toPascalCase(cardSet.id)}CardId = ${cardSet.cards
2201
+ .flatMap(renderCardInstanceIds)
2202
+ .map((cardId) => quote(cardId))
2203
+ .join(" | ") || "never"};`,
2204
+ ]);
2205
+ }
2206
+ return renderBlocks([
2207
+ `export type ${typeName} = ${renderTypeForObjectSchema(cardSet.cardSchema)};`,
2208
+ `export const ${schemaName} = ${renderZodForObjectSchema(cardSet.cardSchema)};`,
2209
+ `export type ${toPascalCase(cardSet.id)}CardId = ${cardSet.cards
2210
+ .flatMap(renderCardInstanceIds)
2211
+ .map((cardId) => quote(cardId))
2212
+ .join(" | ") || "never"};`,
2213
+ ]);
2214
+ })
2215
+ .join("\n\n");
2216
+ }
2217
+ function cardPropertiesTypeName(cardSet, cardType) {
2218
+ if (!isCardPropertySchemaVariants(cardSet.cardSchema)) {
2219
+ return `${toPascalCase(cardSet.id)}CardProperties`;
2220
+ }
2221
+ return `${toPascalCase(cardSet.id)}${toPascalCase(cardType)}CardProperties`;
2222
+ }
2223
+ function cardPropertiesSchemaName(cardSet, cardType) {
2224
+ if (!isCardPropertySchemaVariants(cardSet.cardSchema)) {
2225
+ return `${toPascalCase(cardSet.id)}CardPropertiesSchema`;
2226
+ }
2227
+ return `${cardPropertiesTypeName(cardSet, cardType)}Schema`;
2228
+ }
2229
+ function renderCardPropertiesSchemaByCardSetId(cardSets) {
2230
+ const entries = cardSets.flatMap((cardSet) => {
2231
+ if (isCardPropertySchemaVariants(cardSet.cardSchema)) {
2232
+ return Object.keys(cardSet.cardSchema.variants).map((cardType) => ` ${quote(`${cardSet.id}:${cardType}`)}: ${cardPropertiesSchemaName(cardSet, cardType)},`);
2233
+ }
2234
+ return [
2235
+ ` ${quote(cardSet.id)}: ${toPascalCase(cardSet.id)}CardPropertiesSchema,`,
2236
+ ];
2237
+ });
2238
+ return `const cardPropertiesSchemaByCardSetId: Record<string, z.ZodTypeAny> = {\n${entries.join("\n")}\n};`;
2239
+ }
2240
+ function renderObjectSchemaSection(typeName, schemaName, schema) {
2241
+ return renderBlocks([
2242
+ `export type ${typeName} = ${renderTypeForObjectSchema(schema)};`,
2243
+ `export const ${schemaName} = ${renderZodForObjectSchema(schema)};`,
2244
+ ]);
2245
+ }
2246
+ function boardPrefix(boardId) {
2247
+ return toPascalCase(boardId);
2248
+ }
2249
+ function boardFieldsTypeName(boardId) {
2250
+ return `${boardPrefix(boardId)}BoardFields`;
2251
+ }
2252
+ function boardSpaceFieldsTypeName(boardId) {
2253
+ return `${boardPrefix(boardId)}SpaceFields`;
2254
+ }
2255
+ function boardRelationFieldsTypeName(boardId) {
2256
+ return `${boardPrefix(boardId)}RelationFields`;
2257
+ }
2258
+ function boardContainerFieldsTypeName(boardId) {
2259
+ return `${boardPrefix(boardId)}ContainerFields`;
2260
+ }
2261
+ function hexEdgeFieldsTypeName(boardId) {
2262
+ return `${boardPrefix(boardId)}EdgeFields`;
2263
+ }
2264
+ function hexVertexFieldsTypeName(boardId) {
2265
+ return `${boardPrefix(boardId)}VertexFields`;
2266
+ }
2267
+ function pieceFieldsTypeName(typeId) {
2268
+ return `${toPascalCase(typeId)}PieceFields`;
2269
+ }
2270
+ function dieFieldsTypeName(typeId) {
2271
+ return `${toPascalCase(typeId)}DieFields`;
2272
+ }
2273
+ function renderTopologyFieldSections(analysis) {
2274
+ const sections = [];
2275
+ for (const board of analysis.analyzedBoards) {
2276
+ sections.push(renderObjectSchemaSection(boardFieldsTypeName(board.board.id), `${boardFieldsTypeName(board.board.id)}Schema`, board.boardFieldsSchema));
2277
+ sections.push(renderObjectSchemaSection(boardSpaceFieldsTypeName(board.board.id), `${boardSpaceFieldsTypeName(board.board.id)}Schema`, board.spaceFieldsSchema));
2278
+ if (board.layout !== "hex") {
2279
+ sections.push(renderObjectSchemaSection(boardRelationFieldsTypeName(board.board.id), `${boardRelationFieldsTypeName(board.board.id)}Schema`, board.relationFieldsSchema), renderObjectSchemaSection(boardContainerFieldsTypeName(board.board.id), `${boardContainerFieldsTypeName(board.board.id)}Schema`, board.containerFieldsSchema));
2280
+ }
2281
+ if (board.layout !== "generic") {
2282
+ sections.push(renderObjectSchemaSection(hexEdgeFieldsTypeName(board.board.id), `${hexEdgeFieldsTypeName(board.board.id)}Schema`, board.edgeFieldsSchema), renderObjectSchemaSection(hexVertexFieldsTypeName(board.board.id), `${hexVertexFieldsTypeName(board.board.id)}Schema`, board.vertexFieldsSchema));
2283
+ }
2284
+ }
2285
+ for (const pieceTypeId of analysis.pieceTypeIds) {
2286
+ sections.push(renderObjectSchemaSection(pieceFieldsTypeName(pieceTypeId), `${pieceFieldsTypeName(pieceTypeId)}Schema`, analysis.pieceTypeSchemasById.get(pieceTypeId)));
2287
+ }
2288
+ for (const dieTypeId of analysis.dieTypeIds) {
2289
+ sections.push(renderObjectSchemaSection(dieFieldsTypeName(dieTypeId), `${dieFieldsTypeName(dieTypeId)}Schema`, analysis.dieTypeSchemasById.get(dieTypeId)));
2290
+ }
2291
+ return sections.join("\n\n");
2292
+ }
2293
+ function renderBoardFieldMapTypes(analysis) {
2294
+ const boardEntries = analysis.analyzedBoards
2295
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${boardFieldsTypeName(board.board.id)};`))
2296
+ .join("\n");
2297
+ const spaceEntries = analysis.analyzedBoards
2298
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${boardSpaceFieldsTypeName(board.board.id)};`))
2299
+ .join("\n");
2300
+ const relationEntries = analysis.analyzedBoards
2301
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${board.layout === "hex"
2302
+ ? "RuntimeRecord"
2303
+ : boardRelationFieldsTypeName(board.board.id)};`))
2304
+ .join("\n");
2305
+ const containerEntries = analysis.analyzedBoards
2306
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${board.layout === "hex"
2307
+ ? "RuntimeRecord"
2308
+ : boardContainerFieldsTypeName(board.board.id)};`))
2309
+ .join("\n");
2310
+ const edgeEntries = analysis.analyzedBoards
2311
+ .filter((board) => board.layout === "hex")
2312
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${hexEdgeFieldsTypeName(board.board.id)};`))
2313
+ .join("\n");
2314
+ const squareEdgeEntries = analysis.analyzedBoards
2315
+ .filter((board) => board.layout === "square")
2316
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${hexEdgeFieldsTypeName(board.board.id)};`))
2317
+ .join("\n");
2318
+ const tiledEdgeEntries = analysis.analyzedBoards
2319
+ .filter((board) => board.layout !== "generic")
2320
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${hexEdgeFieldsTypeName(board.board.id)};`))
2321
+ .join("\n");
2322
+ const vertexEntries = analysis.analyzedBoards
2323
+ .filter((board) => board.layout === "hex")
2324
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${hexVertexFieldsTypeName(board.board.id)};`))
2325
+ .join("\n");
2326
+ const squareVertexEntries = analysis.analyzedBoards
2327
+ .filter((board) => board.layout === "square")
2328
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${hexVertexFieldsTypeName(board.board.id)};`))
2329
+ .join("\n");
2330
+ const tiledVertexEntries = analysis.analyzedBoards
2331
+ .filter((board) => board.layout !== "generic")
2332
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${hexVertexFieldsTypeName(board.board.id)};`))
2333
+ .join("\n");
2334
+ const pieceEntries = analysis.pieceTypeIds
2335
+ .map((typeId) => ` ${quote(typeId)}: ${pieceFieldsTypeName(typeId)};`)
2336
+ .join("\n");
2337
+ const dieEntries = analysis.dieTypeIds
2338
+ .map((typeId) => ` ${quote(typeId)}: ${dieFieldsTypeName(typeId)};`)
2339
+ .join("\n");
2340
+ return renderBlocks([
2341
+ `export type BoardFieldsByBoardId = {\n${boardEntries}\n};`,
2342
+ `export type BoardSpaceFieldsByBoardId = {\n${spaceEntries}\n};`,
2343
+ `export type BoardRelationFieldsByBoardId = {\n${relationEntries}\n};`,
2344
+ `export type BoardContainerFieldsByBoardId = {\n${containerEntries}\n};`,
2345
+ `export type HexEdgeFieldsByBoardId = ${edgeEntries.length > 0 ? `{\n${edgeEntries}\n}` : "Record<string, never>"};`,
2346
+ `export type HexVertexFieldsByBoardId = ${vertexEntries.length > 0
2347
+ ? `{\n${vertexEntries}\n}`
2348
+ : "Record<string, never>"};`,
2349
+ `export type SquareEdgeFieldsByBoardId = ${squareEdgeEntries.length > 0
2350
+ ? `{\n${squareEdgeEntries}\n}`
2351
+ : "Record<string, never>"};`,
2352
+ `export type SquareVertexFieldsByBoardId = ${squareVertexEntries.length > 0
2353
+ ? `{\n${squareVertexEntries}\n}`
2354
+ : "Record<string, never>"};`,
2355
+ `export type TiledEdgeFieldsByBoardId = ${tiledEdgeEntries.length > 0
2356
+ ? `{\n${tiledEdgeEntries}\n}`
2357
+ : "Record<string, never>"};`,
2358
+ `export type TiledVertexFieldsByBoardId = ${tiledVertexEntries.length > 0
2359
+ ? `{\n${tiledVertexEntries}\n}`
2360
+ : "Record<string, never>"};`,
2361
+ `export type PieceFieldsByTypeId = ${pieceEntries.length > 0
2362
+ ? `{\n${pieceEntries}\n}`
2363
+ : "Record<string, RuntimeRecord>"};`,
2364
+ `export type DieFieldsByTypeId = ${dieEntries.length > 0
2365
+ ? `{\n${dieEntries}\n}`
2366
+ : "Record<string, RuntimeRecord>"};`,
2367
+ ]);
2368
+ }
2369
+ function renderCardStateSchemaById(analysis) {
2370
+ if (analysis.cardIds.length === 0) {
2371
+ return "z.object({})";
2372
+ }
2373
+ return `z.object(
2374
+ Object.fromEntries(
2375
+ literals.cardIds.map((cardId) => [cardId, createCardStateSchema(cardId)]),
2376
+ ) as Record<CardId, z.ZodTypeAny>,
2377
+ )`;
2378
+ }
2379
+ function renderCardStateSchemaFactory(analysis) {
2380
+ if (analysis.cardIds.length === 0) {
2381
+ return "";
2382
+ }
2383
+ return `function createCardStateSchema<CardIdValue extends CardId>(
2384
+ cardId: CardIdValue,
2385
+ ): z.ZodType<CardStateById[CardIdValue]> {
2386
+ const cardSetId = literals.cardSetIdByCardId[cardId];
2387
+ const cardType = literals.cardTypeByCardId[cardId];
2388
+ const cardPropertiesSchema =
2389
+ cardPropertiesSchemaByCardSetId[cardSetId + ":" + cardType] ??
2390
+ cardPropertiesSchemaByCardSetId[cardSetId] ??
2391
+ unknownRecordSchema;
2392
+ return assumeManifestSchema<CardStateById[CardIdValue]>(
2393
+ cardStateSchema.extend({
2394
+ id: z.literal(cardId),
2395
+ cardSetId: z.literal(cardSetId),
2396
+ cardType: z.literal(cardType),
2397
+ properties: cardPropertiesSchema,
2398
+ }),
2399
+ );
2400
+ }`;
2401
+ }
2402
+ function renderPieceStateSchemaById(analysis) {
2403
+ if (analysis.pieceIds.length === 0) {
2404
+ return "z.object({})";
2405
+ }
2406
+ return `z.object({\n${analysis.pieceIds
2407
+ .map((pieceId) => {
2408
+ const pieceTypeId = analysis.pieceTypeIdByPieceId.get(pieceId) ?? "";
2409
+ return ` ${quote(pieceId)}: z.object({
2410
+ componentType: z.string().optional(),
2411
+ id: z.literal(${quote(pieceId)}),
2412
+ pieceTypeId: z.literal(${quote(pieceTypeId)}),
2413
+ pieceName: z.string().nullable().optional(),
2414
+ ownerId: ids.playerId.nullable().optional(),
2415
+ properties: ${pieceTypeId
2416
+ ? `${pieceFieldsTypeName(pieceTypeId)}Schema`
2417
+ : "unknownRecordSchema"},
2418
+ }),`;
2419
+ })
2420
+ .join("\n")}\n})`;
2421
+ }
2422
+ function renderDieStateSchemaById(analysis) {
2423
+ if (analysis.dieIds.length === 0) {
2424
+ return "z.object({})";
2425
+ }
2426
+ return `z.object({\n${analysis.dieIds
2427
+ .map((dieId) => {
2428
+ const dieTypeId = analysis.dieTypeIdByDieId.get(dieId) ?? "";
2429
+ const dieType = (analysis.manifest.dieTypes ?? []).find((candidate) => candidate.id === dieTypeId);
2430
+ return ` ${quote(dieId)}: z.object({
2431
+ componentType: z.string().optional(),
2432
+ id: z.literal(${quote(dieId)}),
2433
+ dieTypeId: z.literal(${quote(dieTypeId)}),
2434
+ dieName: z.string().nullable().optional(),
2435
+ ownerId: ids.playerId.nullable().optional(),
2436
+ sides: z.literal(${dieType?.sides ?? 6}),
2437
+ value: z.number().int().nullable().optional(),
2438
+ properties: ${dieTypeId
2439
+ ? `${dieFieldsTypeName(dieTypeId)}Schema`
2440
+ : "unknownRecordSchema"},
2441
+ }),`;
2442
+ })
2443
+ .join("\n")}\n})`;
2444
+ }
2445
+ function renderGenericBoardStateSchema(board, runtimeBoardId) {
2446
+ const playerId = board.board.scope === "perPlayer"
2447
+ ? runtimeBoardId.slice(board.board.id.length + 1)
2448
+ : null;
2449
+ return `z.object({
2450
+ id: z.literal(${quote(runtimeBoardId)}),
2451
+ baseId: z.literal(${quote(board.board.id)}),
2452
+ layout: z.literal("generic"),
2453
+ typeId: ${board.boardTypeId
2454
+ ? `z.literal(${quote(board.boardTypeId)})`
2455
+ : "ids.boardTypeId.nullable().optional()"},
2456
+ scope: z.literal(${quote(board.board.scope)}),
2457
+ playerId: ${playerId
2458
+ ? `z.literal(${quote(playerId)})`
2459
+ : "ids.playerId.nullable().optional()"},
2460
+ templateId: z.string().nullable().optional(),
2461
+ fields: ${`${boardFieldsTypeName(board.board.id)}Schema`},
2462
+ spaces: z.record(
2463
+ ids.spaceId,
2464
+ z.object({
2465
+ id: ids.spaceId,
2466
+ name: z.string().nullable().optional(),
2467
+ typeId: ids.spaceTypeId.nullable().optional(),
2468
+ fields: ${`${boardSpaceFieldsTypeName(board.board.id)}Schema`},
2469
+ zoneId: z.string().nullable().optional(),
2470
+ }),
2471
+ ),
2472
+ relations: z.array(
2473
+ z.object({
2474
+ id: z.string().nullable().optional(),
2475
+ typeId: ids.relationTypeId,
2476
+ fromSpaceId: ids.spaceId,
2477
+ toSpaceId: ids.spaceId,
2478
+ directed: z.boolean(),
2479
+ fields: ${`${boardRelationFieldsTypeName(board.board.id)}Schema`},
2480
+ }),
2481
+ ),
2482
+ containers: z.record(
2483
+ ids.boardContainerId,
2484
+ z.object({
2485
+ id: ids.boardContainerId,
2486
+ name: z.string(),
2487
+ host: z.discriminatedUnion("type", [
2488
+ z.object({ type: z.literal("board") }),
2489
+ z.object({ type: z.literal("space"), spaceId: ids.spaceId }),
2490
+ ]),
2491
+ allowedCardSetIds: z.array(ids.cardSetId).optional(),
2492
+ zoneId: z.string(),
2493
+ fields: ${`${boardContainerFieldsTypeName(board.board.id)}Schema`},
2494
+ }),
2495
+ ),
2496
+ })`;
2497
+ }
2498
+ function renderHexBoardStateSchema(board, runtimeBoardId) {
2499
+ const playerId = board.board.scope === "perPlayer"
2500
+ ? runtimeBoardId.slice(board.board.id.length + 1)
2501
+ : null;
2502
+ return `z.object({
2503
+ id: z.literal(${quote(runtimeBoardId)}),
2504
+ baseId: z.literal(${quote(board.board.id)}),
2505
+ layout: z.literal("hex"),
2506
+ typeId: ${board.boardTypeId
2507
+ ? `z.literal(${quote(board.boardTypeId)})`
2508
+ : "ids.boardTypeId.nullable().optional()"},
2509
+ scope: z.literal(${quote(board.board.scope)}),
2510
+ playerId: ${playerId
2511
+ ? `z.literal(${quote(playerId)})`
2512
+ : "ids.playerId.nullable().optional()"},
2513
+ templateId: z.string().nullable().optional(),
2514
+ fields: ${`${boardFieldsTypeName(board.board.id)}Schema`},
2515
+ spaces: z.record(
2516
+ ids.spaceId,
2517
+ z.object({
2518
+ id: ids.spaceId,
2519
+ name: z.string().nullable().optional(),
2520
+ typeId: ids.spaceTypeId.nullable().optional(),
2521
+ q: z.number().int(),
2522
+ r: z.number().int(),
2523
+ fields: ${`${boardSpaceFieldsTypeName(board.board.id)}Schema`},
2524
+ zoneId: z.string().nullable().optional(),
2525
+ }),
2526
+ ),
2527
+ relations: z.array(
2528
+ z.object({
2529
+ id: z.string().nullable().optional(),
2530
+ typeId: z.literal("adjacent"),
2531
+ fromSpaceId: ids.spaceId,
2532
+ toSpaceId: ids.spaceId,
2533
+ directed: z.boolean(),
2534
+ fields: unknownRecordSchema,
2535
+ }),
2536
+ ),
2537
+ containers: z.object({}),
2538
+ orientation: z.enum(["pointy-top", "flat-top"]),
2539
+ edges: z.array(
2540
+ z.object({
2541
+ id: ids.edgeId,
2542
+ spaceIds: z.array(ids.spaceId).min(1).max(2),
2543
+ typeId: ids.edgeTypeId.nullable().optional(),
2544
+ label: z.string().nullable().optional(),
2545
+ ownerId: ids.playerId.nullable().optional(),
2546
+ fields: ${`${hexEdgeFieldsTypeName(board.board.id)}Schema`},
2547
+ }),
2548
+ ),
2549
+ vertices: z.array(
2550
+ z.object({
2551
+ id: ids.vertexId,
2552
+ spaceIds: z.array(ids.spaceId).min(1).max(3),
2553
+ typeId: ids.vertexTypeId.nullable().optional(),
2554
+ label: z.string().nullable().optional(),
2555
+ ownerId: ids.playerId.nullable().optional(),
2556
+ fields: ${`${hexVertexFieldsTypeName(board.board.id)}Schema`},
2557
+ }),
2558
+ ),
2559
+ })`;
2560
+ }
2561
+ function renderSquareBoardStateSchema(board, runtimeBoardId) {
2562
+ const playerId = board.board.scope === "perPlayer"
2563
+ ? runtimeBoardId.slice(board.board.id.length + 1)
2564
+ : null;
2565
+ return `z.object({
2566
+ id: z.literal(${quote(runtimeBoardId)}),
2567
+ baseId: z.literal(${quote(board.board.id)}),
2568
+ layout: z.literal("square"),
2569
+ typeId: ${board.boardTypeId
2570
+ ? `z.literal(${quote(board.boardTypeId)})`
2571
+ : "ids.boardTypeId.nullable().optional()"},
2572
+ scope: z.literal(${quote(board.board.scope)}),
2573
+ playerId: ${playerId
2574
+ ? `z.literal(${quote(playerId)})`
2575
+ : "ids.playerId.nullable().optional()"},
2576
+ templateId: z.string().nullable().optional(),
2577
+ fields: ${`${boardFieldsTypeName(board.board.id)}Schema`},
2578
+ spaces: z.record(
2579
+ ids.spaceId,
2580
+ z.object({
2581
+ id: ids.spaceId,
2582
+ name: z.string().nullable().optional(),
2583
+ typeId: ids.spaceTypeId.nullable().optional(),
2584
+ row: z.number().int(),
2585
+ col: z.number().int(),
2586
+ fields: ${`${boardSpaceFieldsTypeName(board.board.id)}Schema`},
2587
+ zoneId: z.string().nullable().optional(),
2588
+ }),
2589
+ ),
2590
+ relations: z.array(
2591
+ z.object({
2592
+ id: z.string().nullable().optional(),
2593
+ typeId: ids.relationTypeId,
2594
+ fromSpaceId: ids.spaceId,
2595
+ toSpaceId: ids.spaceId,
2596
+ directed: z.boolean(),
2597
+ fields: ${`${boardRelationFieldsTypeName(board.board.id)}Schema`},
2598
+ }),
2599
+ ),
2600
+ containers: z.record(
2601
+ ids.boardContainerId,
2602
+ z.object({
2603
+ id: ids.boardContainerId,
2604
+ name: z.string(),
2605
+ host: z.discriminatedUnion("type", [
2606
+ z.object({ type: z.literal("board") }),
2607
+ z.object({ type: z.literal("space"), spaceId: ids.spaceId }),
2608
+ ]),
2609
+ allowedCardSetIds: z.array(ids.cardSetId).optional(),
2610
+ zoneId: z.string(),
2611
+ fields: ${`${boardContainerFieldsTypeName(board.board.id)}Schema`},
2612
+ }),
2613
+ ),
2614
+ edges: z.array(
2615
+ z.object({
2616
+ id: ids.edgeId,
2617
+ spaceIds: z.array(ids.spaceId).min(1).max(2),
2618
+ typeId: ids.edgeTypeId.nullable().optional(),
2619
+ label: z.string().nullable().optional(),
2620
+ ownerId: ids.playerId.nullable().optional(),
2621
+ fields: ${`${hexEdgeFieldsTypeName(board.board.id)}Schema`},
2622
+ }),
2623
+ ),
2624
+ vertices: z.array(
2625
+ z.object({
2626
+ id: ids.vertexId,
2627
+ spaceIds: z.array(ids.spaceId).min(1).max(4),
2628
+ typeId: ids.vertexTypeId.nullable().optional(),
2629
+ label: z.string().nullable().optional(),
2630
+ ownerId: ids.playerId.nullable().optional(),
2631
+ fields: ${`${hexVertexFieldsTypeName(board.board.id)}Schema`},
2632
+ }),
2633
+ ),
2634
+ })`;
2635
+ }
2636
+ function renderBoardStateSchemaById(analysis) {
2637
+ if (analysis.boardIds.length === 0) {
2638
+ return "z.object({})";
2639
+ }
2640
+ return `z.object({\n${analysis.analyzedBoards
2641
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => {
2642
+ const schema = board.layout === "hex"
2643
+ ? renderHexBoardStateSchema(board, runtimeBoardId)
2644
+ : board.layout === "square"
2645
+ ? renderSquareBoardStateSchema(board, runtimeBoardId)
2646
+ : renderGenericBoardStateSchema(board, runtimeBoardId);
2647
+ return ` ${quote(runtimeBoardId)}: ${schema},`;
2648
+ }))
2649
+ .join("\n")}\n})`;
2650
+ }
2651
+ function renderHexBoardStateSchemaById(analysis) {
2652
+ const hexBoards = analysis.analyzedBoards.filter((board) => board.layout === "hex");
2653
+ if (hexBoards.length === 0) {
2654
+ return "z.object({})";
2655
+ }
2656
+ return `z.object({\n${hexBoards
2657
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${renderHexBoardStateSchema(board, runtimeBoardId)},`))
2658
+ .join("\n")}\n})`;
2659
+ }
2660
+ function renderSquareBoardStateSchemaById(analysis) {
2661
+ const squareBoards = analysis.analyzedBoards.filter((board) => board.layout === "square");
2662
+ if (squareBoards.length === 0) {
2663
+ return "z.object({})";
2664
+ }
2665
+ return `z.object({\n${squareBoards
2666
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: ${renderSquareBoardStateSchema(board, runtimeBoardId)},`))
2667
+ .join("\n")}\n})`;
2668
+ }
2669
+ function renderBoardLiteralHelpers(analysis) {
2670
+ const boardIdsByLayout = renderReadonlyArrayRecord(analysis.boardIdsByLayout);
2671
+ const boardBaseIdsByLayout = renderReadonlyArrayRecord(analysis.boardBaseIdsByLayout);
2672
+ const boardIdsByBaseId = renderReadonlyArrayRecord(analysis.boardIdsByBaseId);
2673
+ const boardBaseIdsByTemplateId = renderReadonlyArrayRecord(analysis.boardBaseIdsByTemplateId);
2674
+ const boardLayoutById = renderStringRecord(analysis.boardLayoutById);
2675
+ const boardTemplateLayoutById = renderStringRecord(analysis.boardTemplateLayoutById);
2676
+ const boardIdsByTypeId = renderReadonlyArrayRecord(analysis.boardIdsByTypeId);
2677
+ const spaceIdsByBoardId = renderReadonlyArrayRecord(analysis.spaceIdsByBoardId);
2678
+ const spaceTypeIdByBoardId = renderJsonConst(sortedObject(analysis.spaceTypeIdByBoardId));
2679
+ const spaceIdsByTypeId = renderReadonlyArrayRecord(analysis.spaceIdsByTypeId);
2680
+ const containerIdsByBoardId = renderReadonlyArrayRecord(analysis.containerIdsByBoardId);
2681
+ const containerHostByBoardId = renderJsonConst(sortedObject(analysis.containerHostByBoardId));
2682
+ const relationTypeIdsByBoardId = renderReadonlyArrayRecord(analysis.relationTypeIdsByBoardId);
2683
+ const edgeIdsByTypeId = renderReadonlyArrayRecord(analysis.edgeIdsByTypeId);
2684
+ const edgeIdsByBoardIdAndTypeId = renderJsonConst(sortedObject(analysis.edgeIdsByBoardIdAndTypeId));
2685
+ const vertexIdsByTypeId = renderReadonlyArrayRecord(analysis.vertexIdsByTypeId);
2686
+ const vertexIdsByBoardIdAndTypeId = renderJsonConst(sortedObject(analysis.vertexIdsByBoardIdAndTypeId));
2687
+ const authoredHexEdgesByBoardId = renderJsonConst(sortedObject(analysis.analyzedBoards
2688
+ .filter((board) => board.layout === "hex")
2689
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => [
2690
+ runtimeBoardId,
2691
+ board.authoredEdges.map((edge) => {
2692
+ const renderedSite = {
2693
+ ref: cloneJson(edge.ref),
2694
+ fields: {
2695
+ ...materializeObjectSchemaDefaults(board.edgeFieldsSchema, analysis),
2696
+ ...(edge.fields ?? {}),
2697
+ },
2698
+ };
2699
+ if (edge.typeId !== null && edge.typeId !== undefined) {
2700
+ renderedSite.typeId = edge.typeId;
2701
+ }
2702
+ if (edge.label !== null && edge.label !== undefined) {
2703
+ renderedSite.label = edge.label;
2704
+ }
2705
+ return renderedSite;
2706
+ }),
2707
+ ]))));
2708
+ const authoredHexVerticesByBoardId = renderJsonConst(sortedObject(analysis.analyzedBoards
2709
+ .filter((board) => board.layout === "hex")
2710
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => [
2711
+ runtimeBoardId,
2712
+ board.authoredVertices.map((vertex) => {
2713
+ const renderedSite = {
2714
+ ref: cloneJson(vertex.ref),
2715
+ fields: {
2716
+ ...materializeObjectSchemaDefaults(board.vertexFieldsSchema, analysis),
2717
+ ...(vertex.fields ?? {}),
2718
+ },
2719
+ };
2720
+ if (vertex.typeId !== null && vertex.typeId !== undefined) {
2721
+ renderedSite.typeId = vertex.typeId;
2722
+ }
2723
+ if (vertex.label !== null && vertex.label !== undefined) {
2724
+ renderedSite.label = vertex.label;
2725
+ }
2726
+ return renderedSite;
2727
+ }),
2728
+ ]))));
2729
+ const authoredHexEdgeIdsByBoardIdAndRef = renderJsonConst(sortedObject(analysis.analyzedBoards
2730
+ .filter((board) => board.layout === "hex")
2731
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => [
2732
+ runtimeBoardId,
2733
+ sortedObject(board.authoredEdges.map((edge) => [
2734
+ boardSpaceRefKey(edge.ref.spaces),
2735
+ edge.id,
2736
+ ])),
2737
+ ]))));
2738
+ const authoredHexVertexIdsByBoardIdAndRef = renderJsonConst(sortedObject(analysis.analyzedBoards
2739
+ .filter((board) => board.layout === "hex")
2740
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => [
2741
+ runtimeBoardId,
2742
+ sortedObject(board.authoredVertices.map((vertex) => [
2743
+ boardSpaceRefKey(vertex.ref.spaces),
2744
+ vertex.id,
2745
+ ])),
2746
+ ]))));
2747
+ // `perPlayerBoardIdsByBaseIdAndPlayerIdLookup` has been retired. The old
2748
+ // static lookup keyed on the max-players roster at generate time, which is
2749
+ // incompatible with the PerPlayer model where the runtime roster decides
2750
+ // which seats exist. `boardRefForPlayer(baseId, playerId)` now produces a
2751
+ // `PerPlayerBoardRef` at runtime using the active session's seats.
2752
+ return `const boardIdsByLayoutLookup = ${boardIdsByLayout};
2753
+ const boardBaseIdsByLayoutLookup = ${boardBaseIdsByLayout};
2754
+ const boardIdsByBaseIdLookup = ${boardIdsByBaseId};
2755
+ const boardBaseIdsByTemplateIdLookup = ${boardBaseIdsByTemplateId};
2756
+ const boardLayoutByIdLookup = ${boardLayoutById};
2757
+ const boardTemplateLayoutByIdLookup = ${boardTemplateLayoutById};
2758
+ const boardIdsByTypeIdLookup = ${boardIdsByTypeId};
2759
+ const spaceIdsByBoardIdLookup = ${spaceIdsByBoardId};
2760
+ const spaceTypeIdByBoardIdLookup = ${spaceTypeIdByBoardId};
2761
+ const spaceIdsByTypeIdLookup = ${spaceIdsByTypeId};
2762
+ const containerIdsByBoardIdLookup = ${containerIdsByBoardId};
2763
+ const containerHostByBoardIdLookup = ${containerHostByBoardId};
2764
+ const relationTypeIdsByBoardIdLookup = ${relationTypeIdsByBoardId};
2765
+ const edgeIdsByTypeIdLookup = ${edgeIdsByTypeId};
2766
+ const edgeIdsByBoardIdAndTypeIdLookup = ${edgeIdsByBoardIdAndTypeId};
2767
+ const vertexIdsByTypeIdLookup = ${vertexIdsByTypeId};
2768
+ const vertexIdsByBoardIdAndTypeIdLookup = ${vertexIdsByBoardIdAndTypeId};
2769
+ const authoredHexEdgesByBoardIdLookup = ${authoredHexEdgesByBoardId};
2770
+ const authoredHexVerticesByBoardIdLookup = ${authoredHexVerticesByBoardId};
2771
+ const authoredHexEdgeIdsByBoardIdAndRefLookup = ${authoredHexEdgeIdsByBoardIdAndRef};
2772
+ const authoredHexVertexIdsByBoardIdAndRefLookup = ${authoredHexVertexIdsByBoardIdAndRef};
2773
+
2774
+ function authoredHexRefKey(spaceIds: readonly string[]): string {
2775
+ return [...spaceIds]
2776
+ .sort((left, right) => left.localeCompare(right))
2777
+ .join("$$");
2778
+ }
2779
+
2780
+ type BoardLookupIdValue<
2781
+ Lookup extends Record<string, Record<string, readonly string[]>>,
2782
+ Key extends keyof Lookup,
2783
+ > = Extract<Lookup[Key][keyof Lookup[Key]], readonly string[]>[number];
2784
+
2785
+ function flattenBoardScopedIds<
2786
+ Lookup extends Record<string, Record<string, readonly string[]>>,
2787
+ Key extends keyof Lookup,
2788
+ >(lookup: Lookup, key: Key): ReadonlyArray<BoardLookupIdValue<Lookup, Key>> {
2789
+ return Object.values(lookup[key] ?? {}).flat() as ReadonlyArray<
2790
+ BoardLookupIdValue<Lookup, Key>
2791
+ >;
2792
+ }
2793
+
2794
+ export const boardHelpers = {
2795
+ boardIdsForLayout<
2796
+ LayoutValue extends keyof typeof boardIdsByLayoutLookup,
2797
+ >(layout: LayoutValue): (typeof boardIdsByLayoutLookup)[LayoutValue] {
2798
+ return boardIdsByLayoutLookup[layout];
2799
+ },
2800
+ boardBaseIdsForLayout<
2801
+ LayoutValue extends keyof typeof boardBaseIdsByLayoutLookup,
2802
+ >(layout: LayoutValue): (typeof boardBaseIdsByLayoutLookup)[LayoutValue] {
2803
+ return boardBaseIdsByLayoutLookup[layout];
2804
+ },
2805
+ boardIdsForBase<
2806
+ BoardBaseIdValue extends keyof typeof boardIdsByBaseIdLookup,
2807
+ >(
2808
+ boardBaseId: BoardBaseIdValue,
2809
+ ): (typeof boardIdsByBaseIdLookup)[BoardBaseIdValue] {
2810
+ return boardIdsByBaseIdLookup[boardBaseId];
2811
+ },
2812
+ boardBaseIdsForTemplate<
2813
+ TemplateIdValue extends keyof typeof boardBaseIdsByTemplateIdLookup,
2814
+ >(
2815
+ templateId: TemplateIdValue,
2816
+ ): (typeof boardBaseIdsByTemplateIdLookup)[TemplateIdValue] {
2817
+ return boardBaseIdsByTemplateIdLookup[templateId];
2818
+ },
2819
+ boardIdsForType<TypeIdValue extends keyof typeof boardIdsByTypeIdLookup>(
2820
+ typeId: TypeIdValue,
2821
+ ): (typeof boardIdsByTypeIdLookup)[TypeIdValue] {
2822
+ return boardIdsByTypeIdLookup[typeId];
2823
+ },
2824
+ boardLayout<BoardIdValue extends keyof typeof boardLayoutByIdLookup>(
2825
+ boardId: BoardIdValue,
2826
+ ): (typeof boardLayoutByIdLookup)[BoardIdValue] {
2827
+ return boardLayoutByIdLookup[boardId];
2828
+ },
2829
+ boardTemplateLayout<
2830
+ TemplateIdValue extends keyof typeof boardTemplateLayoutByIdLookup,
2831
+ >(
2832
+ templateId: TemplateIdValue,
2833
+ ): (typeof boardTemplateLayoutByIdLookup)[TemplateIdValue] {
2834
+ return boardTemplateLayoutByIdLookup[templateId];
2835
+ },
2836
+ spaceIds<BoardIdValue extends keyof typeof spaceIdsByBoardIdLookup>(
2837
+ boardId: BoardIdValue,
2838
+ ): (typeof spaceIdsByBoardIdLookup)[BoardIdValue] {
2839
+ return spaceIdsByBoardIdLookup[boardId];
2840
+ },
2841
+ spaceRecord<
2842
+ BoardIdValue extends keyof typeof spaceIdsByBoardIdLookup,
2843
+ Value,
2844
+ >(
2845
+ boardId: BoardIdValue,
2846
+ initial:
2847
+ | Value
2848
+ | ((
2849
+ spaceId: (typeof spaceIdsByBoardIdLookup)[BoardIdValue][number],
2850
+ ) => Value),
2851
+ ): Record<(typeof spaceIdsByBoardIdLookup)[BoardIdValue][number], Value> {
2852
+ const spaceIds = spaceIdsByBoardIdLookup[boardId];
2853
+ if (!spaceIds) {
2854
+ throw new Error(\`Unknown board '\${String(boardId)}'.\`);
2855
+ }
2856
+ return buildTypedRecord(spaceIds, initial) as Record<
2857
+ (typeof spaceIdsByBoardIdLookup)[BoardIdValue][number],
2858
+ Value
2859
+ >;
2860
+ },
2861
+ isSpaceId<BoardIdValue extends keyof typeof spaceIdsByBoardIdLookup>(
2862
+ boardId: BoardIdValue,
2863
+ value: string,
2864
+ ): value is (typeof spaceIdsByBoardIdLookup)[BoardIdValue][number] {
2865
+ const spaceIds = spaceIdsByBoardIdLookup[boardId];
2866
+ return spaceIds ? isTypedId(spaceIds, value) : false;
2867
+ },
2868
+ expectSpaceId<BoardIdValue extends keyof typeof spaceIdsByBoardIdLookup>(
2869
+ boardId: BoardIdValue,
2870
+ value: string,
2871
+ ): (typeof spaceIdsByBoardIdLookup)[BoardIdValue][number] {
2872
+ const spaceIds = spaceIdsByBoardIdLookup[boardId];
2873
+ if (!spaceIds || !isTypedId(spaceIds, value)) {
2874
+ throw new Error(
2875
+ \`Unknown space id '\${value}' on board '\${String(boardId)}'.\`,
2876
+ );
2877
+ }
2878
+ return value as (typeof spaceIdsByBoardIdLookup)[BoardIdValue][number];
2879
+ },
2880
+ spaceKinds<BoardIdValue extends keyof typeof spaceTypeIdByBoardIdLookup>(
2881
+ boardId: BoardIdValue,
2882
+ ): (typeof spaceTypeIdByBoardIdLookup)[BoardIdValue] {
2883
+ return spaceTypeIdByBoardIdLookup[boardId];
2884
+ },
2885
+ spaceIdsForType<TypeIdValue extends keyof typeof spaceIdsByTypeIdLookup>(
2886
+ typeId: TypeIdValue,
2887
+ ): (typeof spaceIdsByTypeIdLookup)[TypeIdValue] {
2888
+ return spaceIdsByTypeIdLookup[typeId];
2889
+ },
2890
+ containerIds<BoardIdValue extends keyof typeof containerIdsByBoardIdLookup>(
2891
+ boardId: BoardIdValue,
2892
+ ): (typeof containerIdsByBoardIdLookup)[BoardIdValue] {
2893
+ return containerIdsByBoardIdLookup[boardId];
2894
+ },
2895
+ containerRecord<
2896
+ BoardIdValue extends keyof typeof containerIdsByBoardIdLookup,
2897
+ Value,
2898
+ >(
2899
+ boardId: BoardIdValue,
2900
+ initial:
2901
+ | Value
2902
+ | ((
2903
+ containerId: (typeof containerIdsByBoardIdLookup)[BoardIdValue][number],
2904
+ ) => Value),
2905
+ ): Record<
2906
+ (typeof containerIdsByBoardIdLookup)[BoardIdValue][number],
2907
+ Value
2908
+ > {
2909
+ const containerIds = containerIdsByBoardIdLookup[boardId];
2910
+ if (!containerIds) {
2911
+ throw new Error(\`Unknown board '\${String(boardId)}'.\`);
2912
+ }
2913
+ return buildTypedRecord(containerIds, initial) as Record<
2914
+ (typeof containerIdsByBoardIdLookup)[BoardIdValue][number],
2915
+ Value
2916
+ >;
2917
+ },
2918
+ isContainerId<
2919
+ BoardIdValue extends keyof typeof containerIdsByBoardIdLookup,
2920
+ >(
2921
+ boardId: BoardIdValue,
2922
+ value: string,
2923
+ ): value is (typeof containerIdsByBoardIdLookup)[BoardIdValue][number] {
2924
+ const containerIds = containerIdsByBoardIdLookup[boardId];
2925
+ return containerIds ? isTypedId(containerIds, value) : false;
2926
+ },
2927
+ expectContainerId<
2928
+ BoardIdValue extends keyof typeof containerIdsByBoardIdLookup,
2929
+ >(
2930
+ boardId: BoardIdValue,
2931
+ value: string,
2932
+ ): (typeof containerIdsByBoardIdLookup)[BoardIdValue][number] {
2933
+ const containerIds = containerIdsByBoardIdLookup[boardId];
2934
+ if (!containerIds || !isTypedId(containerIds, value)) {
2935
+ throw new Error(
2936
+ \`Unknown container id '\${value}' on board '\${String(boardId)}'.\`,
2937
+ );
2938
+ }
2939
+ return value as (typeof containerIdsByBoardIdLookup)[BoardIdValue][number];
2940
+ },
2941
+ containerHost<
2942
+ BoardIdValue extends keyof typeof containerHostByBoardIdLookup,
2943
+ ContainerIdValue extends keyof (typeof containerHostByBoardIdLookup)[BoardIdValue],
2944
+ >(
2945
+ boardId: BoardIdValue,
2946
+ containerId: ContainerIdValue,
2947
+ ): (typeof containerHostByBoardIdLookup)[BoardIdValue][ContainerIdValue] {
2948
+ const containers = containerHostByBoardIdLookup[boardId];
2949
+ const containerHost = containers?.[containerId];
2950
+ if (!containerHost) {
2951
+ throw new Error(
2952
+ \`Unknown container '\${String(containerId)}' on board '\${String(boardId)}'.\`,
2953
+ );
2954
+ }
2955
+ return containerHost as (typeof containerHostByBoardIdLookup)[BoardIdValue][ContainerIdValue];
2956
+ },
2957
+ relationTypeIds<
2958
+ BoardIdValue extends keyof typeof relationTypeIdsByBoardIdLookup,
2959
+ >(
2960
+ boardId: BoardIdValue,
2961
+ ): (typeof relationTypeIdsByBoardIdLookup)[BoardIdValue] {
2962
+ return relationTypeIdsByBoardIdLookup[boardId];
2963
+ },
2964
+ relationTypeRecord<
2965
+ BoardIdValue extends keyof typeof relationTypeIdsByBoardIdLookup,
2966
+ Value,
2967
+ >(
2968
+ boardId: BoardIdValue,
2969
+ initial:
2970
+ | Value
2971
+ | ((
2972
+ relationTypeId: (typeof relationTypeIdsByBoardIdLookup)[BoardIdValue][number],
2973
+ ) => Value),
2974
+ ): Record<
2975
+ (typeof relationTypeIdsByBoardIdLookup)[BoardIdValue][number],
2976
+ Value
2977
+ > {
2978
+ const relationTypeIds = relationTypeIdsByBoardIdLookup[boardId];
2979
+ if (!relationTypeIds) {
2980
+ throw new Error(\`Unknown board '\${String(boardId)}'.\`);
2981
+ }
2982
+ return buildTypedRecord(relationTypeIds, initial) as Record<
2983
+ (typeof relationTypeIdsByBoardIdLookup)[BoardIdValue][number],
2984
+ Value
2985
+ >;
2986
+ },
2987
+ isRelationTypeId<
2988
+ BoardIdValue extends keyof typeof relationTypeIdsByBoardIdLookup,
2989
+ >(
2990
+ boardId: BoardIdValue,
2991
+ value: string,
2992
+ ): value is (typeof relationTypeIdsByBoardIdLookup)[BoardIdValue][number] {
2993
+ const relationTypeIds = relationTypeIdsByBoardIdLookup[boardId];
2994
+ return relationTypeIds ? isTypedId(relationTypeIds, value) : false;
2995
+ },
2996
+ expectRelationTypeId<
2997
+ BoardIdValue extends keyof typeof relationTypeIdsByBoardIdLookup,
2998
+ >(
2999
+ boardId: BoardIdValue,
3000
+ value: string,
3001
+ ): (typeof relationTypeIdsByBoardIdLookup)[BoardIdValue][number] {
3002
+ const relationTypeIds = relationTypeIdsByBoardIdLookup[boardId];
3003
+ if (!relationTypeIds || !isTypedId(relationTypeIds, value)) {
3004
+ throw new Error(
3005
+ \`Unknown relation type id '\${value}' on board '\${String(boardId)}'.\`,
3006
+ );
3007
+ }
3008
+ return value as (typeof relationTypeIdsByBoardIdLookup)[BoardIdValue][number];
3009
+ },
3010
+ authoredHexEdges<
3011
+ BoardIdValue extends keyof typeof authoredHexEdgesByBoardIdLookup,
3012
+ >(
3013
+ boardId: BoardIdValue,
3014
+ ): (typeof authoredHexEdgesByBoardIdLookup)[BoardIdValue] {
3015
+ const authoredHexEdges = authoredHexEdgesByBoardIdLookup[boardId];
3016
+ if (!authoredHexEdges) {
3017
+ throw new Error(\`Unknown hex board '\${String(boardId)}'.\`);
3018
+ }
3019
+ return authoredHexEdges;
3020
+ },
3021
+ authoredHexVertices<
3022
+ BoardIdValue extends keyof typeof authoredHexVerticesByBoardIdLookup,
3023
+ >(
3024
+ boardId: BoardIdValue,
3025
+ ): (typeof authoredHexVerticesByBoardIdLookup)[BoardIdValue] {
3026
+ const authoredHexVertices = authoredHexVerticesByBoardIdLookup[boardId];
3027
+ if (!authoredHexVertices) {
3028
+ throw new Error(\`Unknown hex board '\${String(boardId)}'.\`);
3029
+ }
3030
+ return authoredHexVertices;
3031
+ },
3032
+ resolveHexEdgeId<
3033
+ BoardIdValue extends keyof typeof authoredHexEdgeIdsByBoardIdAndRefLookup,
3034
+ >(
3035
+ boardId: BoardIdValue,
3036
+ ref: HexAuthoredEdgeRef<BoardIdValue>,
3037
+ ): HexEdgeState<BoardIdValue>["id"] {
3038
+ const boardEdges = authoredHexEdgeIdsByBoardIdAndRefLookup[boardId];
3039
+ if (!boardEdges) {
3040
+ throw new Error(\`Unknown hex board '\${String(boardId)}'.\`);
3041
+ }
3042
+ const edgeRef = ref as { spaces: readonly string[] };
3043
+ const edgeId = (boardEdges as Record<string, HexEdgeState<BoardIdValue>["id"]>)[
3044
+ authoredHexRefKey(edgeRef.spaces)
3045
+ ];
3046
+ if (!edgeId) {
3047
+ throw new Error(
3048
+ \`Unknown authored hex edge ref '\${edgeRef.spaces.join(", ")}' on board '\${String(boardId)}'.\`,
3049
+ );
3050
+ }
3051
+ return edgeId as HexEdgeState<BoardIdValue>["id"];
3052
+ },
3053
+ resolveHexVertexId<
3054
+ BoardIdValue extends keyof typeof authoredHexVertexIdsByBoardIdAndRefLookup,
3055
+ >(
3056
+ boardId: BoardIdValue,
3057
+ ref: HexAuthoredVertexRef<BoardIdValue>,
3058
+ ): HexVertexState<BoardIdValue>["id"] {
3059
+ const boardVertices = authoredHexVertexIdsByBoardIdAndRefLookup[boardId];
3060
+ if (!boardVertices) {
3061
+ throw new Error(\`Unknown hex board '\${String(boardId)}'.\`);
3062
+ }
3063
+ const vertexRef = ref as { spaces: readonly string[] };
3064
+ const vertexId = (
3065
+ boardVertices as Record<string, HexVertexState<BoardIdValue>["id"]>
3066
+ )[authoredHexRefKey(vertexRef.spaces)];
3067
+ if (!vertexId) {
3068
+ throw new Error(
3069
+ \`Unknown authored hex vertex ref '\${vertexRef.spaces.join(", ")}' on board '\${String(boardId)}'.\`,
3070
+ );
3071
+ }
3072
+ return vertexId as HexVertexState<BoardIdValue>["id"];
3073
+ },
3074
+ edgeIdsForType<TypeIdValue extends keyof typeof edgeIdsByTypeIdLookup>(
3075
+ typeId: TypeIdValue,
3076
+ ): (typeof edgeIdsByTypeIdLookup)[TypeIdValue] {
3077
+ return edgeIdsByTypeIdLookup[typeId];
3078
+ },
3079
+ edgeRecord<
3080
+ BoardIdValue extends keyof typeof edgeIdsByBoardIdAndTypeIdLookup,
3081
+ Value,
3082
+ >(
3083
+ boardId: BoardIdValue,
3084
+ initial:
3085
+ | Value
3086
+ | ((
3087
+ edgeId: BoardLookupIdValue<
3088
+ typeof edgeIdsByBoardIdAndTypeIdLookup,
3089
+ BoardIdValue
3090
+ >,
3091
+ ) => Value),
3092
+ ): Record<
3093
+ BoardLookupIdValue<typeof edgeIdsByBoardIdAndTypeIdLookup, BoardIdValue>,
3094
+ Value
3095
+ > {
3096
+ const boardEdges = edgeIdsByBoardIdAndTypeIdLookup[boardId];
3097
+ const edgeIds = boardEdges
3098
+ ? flattenBoardScopedIds(edgeIdsByBoardIdAndTypeIdLookup, boardId)
3099
+ : undefined;
3100
+ if (!edgeIds) {
3101
+ throw new Error(\`Unknown board '\${String(boardId)}'.\`);
3102
+ }
3103
+ return buildTypedRecord(edgeIds, initial) as Record<
3104
+ BoardLookupIdValue<typeof edgeIdsByBoardIdAndTypeIdLookup, BoardIdValue>,
3105
+ Value
3106
+ >;
3107
+ },
3108
+ isEdgeId<BoardIdValue extends keyof typeof edgeIdsByBoardIdAndTypeIdLookup>(
3109
+ boardId: BoardIdValue,
3110
+ value: string,
3111
+ ): value is BoardLookupIdValue<
3112
+ typeof edgeIdsByBoardIdAndTypeIdLookup,
3113
+ BoardIdValue
3114
+ > {
3115
+ const boardEdges = edgeIdsByBoardIdAndTypeIdLookup[boardId];
3116
+ const edgeIds = boardEdges
3117
+ ? flattenBoardScopedIds(edgeIdsByBoardIdAndTypeIdLookup, boardId)
3118
+ : undefined;
3119
+ return edgeIds ? isTypedId(edgeIds, value) : false;
3120
+ },
3121
+ expectEdgeId<
3122
+ BoardIdValue extends keyof typeof edgeIdsByBoardIdAndTypeIdLookup,
3123
+ >(
3124
+ boardId: BoardIdValue,
3125
+ value: string,
3126
+ ): BoardLookupIdValue<
3127
+ typeof edgeIdsByBoardIdAndTypeIdLookup,
3128
+ BoardIdValue
3129
+ > {
3130
+ const boardEdges = edgeIdsByBoardIdAndTypeIdLookup[boardId];
3131
+ const edgeIds = boardEdges
3132
+ ? flattenBoardScopedIds(edgeIdsByBoardIdAndTypeIdLookup, boardId)
3133
+ : undefined;
3134
+ if (!edgeIds || !isTypedId(edgeIds, value)) {
3135
+ throw new Error(
3136
+ \`Unknown edge id '\${value}' on board '\${String(boardId)}'.\`,
3137
+ );
3138
+ }
3139
+ return value as BoardLookupIdValue<
3140
+ typeof edgeIdsByBoardIdAndTypeIdLookup,
3141
+ BoardIdValue
3142
+ >;
3143
+ },
3144
+ edgeIds<
3145
+ BoardIdValue extends keyof typeof edgeIdsByBoardIdAndTypeIdLookup,
3146
+ TypeIdValue extends keyof (typeof edgeIdsByBoardIdAndTypeIdLookup)[BoardIdValue],
3147
+ >(
3148
+ boardId: BoardIdValue,
3149
+ typeId: TypeIdValue,
3150
+ ): (typeof edgeIdsByBoardIdAndTypeIdLookup)[BoardIdValue][TypeIdValue] {
3151
+ const boardEdges = edgeIdsByBoardIdAndTypeIdLookup[boardId];
3152
+ const edgeIds = boardEdges?.[typeId];
3153
+ if (!edgeIds) {
3154
+ throw new Error(
3155
+ \`Unknown edge type '\${String(typeId)}' on board '\${String(boardId)}'.\`,
3156
+ );
3157
+ }
3158
+ return edgeIds as (typeof edgeIdsByBoardIdAndTypeIdLookup)[BoardIdValue][TypeIdValue];
3159
+ },
3160
+ vertexIdsForType<TypeIdValue extends keyof typeof vertexIdsByTypeIdLookup>(
3161
+ typeId: TypeIdValue,
3162
+ ): (typeof vertexIdsByTypeIdLookup)[TypeIdValue] {
3163
+ return vertexIdsByTypeIdLookup[typeId];
3164
+ },
3165
+ vertexRecord<
3166
+ BoardIdValue extends keyof typeof vertexIdsByBoardIdAndTypeIdLookup,
3167
+ Value,
3168
+ >(
3169
+ boardId: BoardIdValue,
3170
+ initial:
3171
+ | Value
3172
+ | ((
3173
+ vertexId: BoardLookupIdValue<
3174
+ typeof vertexIdsByBoardIdAndTypeIdLookup,
3175
+ BoardIdValue
3176
+ >,
3177
+ ) => Value),
3178
+ ): Record<
3179
+ BoardLookupIdValue<typeof vertexIdsByBoardIdAndTypeIdLookup, BoardIdValue>,
3180
+ Value
3181
+ > {
3182
+ const boardVertices = vertexIdsByBoardIdAndTypeIdLookup[boardId];
3183
+ const vertexIds = boardVertices
3184
+ ? flattenBoardScopedIds(vertexIdsByBoardIdAndTypeIdLookup, boardId)
3185
+ : undefined;
3186
+ if (!vertexIds) {
3187
+ throw new Error(\`Unknown board '\${String(boardId)}'.\`);
3188
+ }
3189
+ return buildTypedRecord(vertexIds, initial) as Record<
3190
+ BoardLookupIdValue<typeof vertexIdsByBoardIdAndTypeIdLookup, BoardIdValue>,
3191
+ Value
3192
+ >;
3193
+ },
3194
+ isVertexId<
3195
+ BoardIdValue extends keyof typeof vertexIdsByBoardIdAndTypeIdLookup,
3196
+ >(
3197
+ boardId: BoardIdValue,
3198
+ value: string,
3199
+ ): value is BoardLookupIdValue<
3200
+ typeof vertexIdsByBoardIdAndTypeIdLookup,
3201
+ BoardIdValue
3202
+ > {
3203
+ const boardVertices = vertexIdsByBoardIdAndTypeIdLookup[boardId];
3204
+ const vertexIds = boardVertices
3205
+ ? flattenBoardScopedIds(vertexIdsByBoardIdAndTypeIdLookup, boardId)
3206
+ : undefined;
3207
+ return vertexIds ? isTypedId(vertexIds, value) : false;
3208
+ },
3209
+ expectVertexId<
3210
+ BoardIdValue extends keyof typeof vertexIdsByBoardIdAndTypeIdLookup,
3211
+ >(
3212
+ boardId: BoardIdValue,
3213
+ value: string,
3214
+ ): BoardLookupIdValue<
3215
+ typeof vertexIdsByBoardIdAndTypeIdLookup,
3216
+ BoardIdValue
3217
+ > {
3218
+ const boardVertices = vertexIdsByBoardIdAndTypeIdLookup[boardId];
3219
+ const vertexIds = boardVertices
3220
+ ? flattenBoardScopedIds(vertexIdsByBoardIdAndTypeIdLookup, boardId)
3221
+ : undefined;
3222
+ if (!vertexIds || !isTypedId(vertexIds, value)) {
3223
+ throw new Error(
3224
+ \`Unknown vertex id '\${value}' on board '\${String(boardId)}'.\`,
3225
+ );
3226
+ }
3227
+ return value as BoardLookupIdValue<
3228
+ typeof vertexIdsByBoardIdAndTypeIdLookup,
3229
+ BoardIdValue
3230
+ >;
3231
+ },
3232
+ vertexIds<
3233
+ BoardIdValue extends keyof typeof vertexIdsByBoardIdAndTypeIdLookup,
3234
+ TypeIdValue extends keyof (typeof vertexIdsByBoardIdAndTypeIdLookup)[BoardIdValue],
3235
+ >(
3236
+ boardId: BoardIdValue,
3237
+ typeId: TypeIdValue,
3238
+ ): (typeof vertexIdsByBoardIdAndTypeIdLookup)[BoardIdValue][TypeIdValue] {
3239
+ const boardVertices = vertexIdsByBoardIdAndTypeIdLookup[boardId];
3240
+ const vertexIds = boardVertices?.[typeId];
3241
+ if (!vertexIds) {
3242
+ throw new Error(
3243
+ \`Unknown vertex type '\${String(typeId)}' on board '\${String(boardId)}'.\`,
3244
+ );
3245
+ }
3246
+ return vertexIds as (typeof vertexIdsByBoardIdAndTypeIdLookup)[BoardIdValue][TypeIdValue];
3247
+ },
3248
+ // Returns a \`BoardRef\` describing a per-player board scoped to the supplied
3249
+ // seat. The old \`boardIdForPlayer\` returned a concrete runtime-board-id
3250
+ // string, which encoded the "one board per maxPlayers seat" assumption in
3251
+ // static data. Under the PerPlayer model the runtime roster is not known at
3252
+ // generate time, so consumers deal with \`BoardRef\` and let the runtime
3253
+ // resolve the actual owner seat.
3254
+ boardRefForPlayer(
3255
+ boardBaseId: BoardBaseId,
3256
+ playerId: PlayerId,
3257
+ ): PerPlayerBoardRef<BoardBaseId, PlayerId> {
3258
+ return boardRef(boardBaseId, playerId) as PerPlayerBoardRef<
3259
+ BoardBaseId,
3260
+ PlayerId
3261
+ >;
3262
+ },
3263
+ sharedBoardRef(
3264
+ boardBaseId: BoardBaseId,
3265
+ ): SharedBoardRef<BoardBaseId> {
3266
+ return boardRef(boardBaseId) as SharedBoardRef<BoardBaseId>;
3267
+ },
3268
+ } as const;`;
3269
+ }
3270
+ function renderManifestContractSource(manifest) {
3271
+ const analysis = analyzeManifest(manifest);
3272
+ const initialTableTemplate = materializeManifestTable({
3273
+ manifest,
3274
+ playerIds: analysis.playerIds,
3275
+ shuffleItems: (values) => [...values],
3276
+ });
3277
+ const initialTableBoards = initialTableTemplate.boards;
3278
+ const staticBoardsTemplate = {
3279
+ byId: initialTableBoards.byId,
3280
+ hex: initialTableBoards.hex,
3281
+ square: initialTableBoards.square,
3282
+ };
3283
+ const sharedZoneIds = analysis.sharedZones.map((zone) => zone.id).sort();
3284
+ const playerZoneIds = analysis.playerZones.map((zone) => zone.id).sort();
3285
+ const zoneVisibilityById = Object.fromEntries(Array.from(analysis.zoneVisibilityById.entries()).sort(([left], [right]) => left.localeCompare(right)));
3286
+ const emptySharedZonesTemplate = Object.fromEntries(sharedZoneIds.map((zoneId) => [zoneId, []]));
3287
+ const defaultVisibilityTemplate = Object.fromEntries(analysis.cardIds.map((cardId) => [cardId, { faceUp: true }]));
3288
+ const defaultOwnerTemplate = Object.fromEntries(analysis.cardIds.map((cardId) => [cardId, null]));
3289
+ const cardIdsByCardSetId = new Map();
3290
+ for (const cardId of analysis.cardIds) {
3291
+ const cardSetId = analysis.cardSetIdByCardId.get(cardId);
3292
+ if (!cardSetId) {
3293
+ continue;
3294
+ }
3295
+ const cardIds = cardIdsByCardSetId.get(cardSetId) ?? [];
3296
+ cardIds.push(cardId);
3297
+ cardIdsByCardSetId.set(cardSetId, cardIds);
3298
+ }
3299
+ const renderZoneCardIdArrayType = (cardSetIds) => `Array<${renderStringUnion(cardSetIds.flatMap((cardSetId) => cardIdsByCardSetId.get(cardSetId) ?? []))}>`;
3300
+ const sharedZoneCardIdEntries = sharedZoneIds
3301
+ .map((zoneId) => {
3302
+ const allowedCardSetIds = analysis.sharedZoneCardSetIds.get(zoneId) ?? [];
3303
+ return ` ${quote(zoneId)}: ${renderZoneCardIdArrayType(allowedCardSetIds)};`;
3304
+ })
3305
+ .join("\n");
3306
+ const playerZoneCardIdEntries = playerZoneIds
3307
+ .map((zoneId) => {
3308
+ const allowedCardSetIds = analysis.playerZoneCardSetIds.get(zoneId) ?? [];
3309
+ return ` ${quote(zoneId)}: PerPlayer<${renderZoneCardIdArrayType(allowedCardSetIds)}>;`;
3310
+ })
3311
+ .join("\n");
3312
+ const cardSetById = new Map(analysis.cardSets.map((cardSet) => [cardSet.id, cardSet]));
3313
+ const perCardStateEntries = analysis.cardIds
3314
+ .map((cardId) => {
3315
+ const cardSetId = analysis.cardSetIdByCardId.get(cardId) ?? "";
3316
+ const cardType = analysis.cardTypeByCardId.get(cardId) ?? cardId;
3317
+ const cardSet = cardSetById.get(cardSetId);
3318
+ const propertiesType = cardSet
3319
+ ? cardPropertiesTypeName(cardSet, cardType)
3320
+ : cardSetId
3321
+ ? `${toPascalCase(cardSetId)}CardProperties`
3322
+ : "RuntimeRecord";
3323
+ return ` ${quote(cardId)}: CardStateRecord<${quote(cardId)}, ${quote(cardSetId)}, ${quote(cardType)}, ${propertiesType}>;`;
3324
+ })
3325
+ .join("\n");
3326
+ const perPieceStateEntries = analysis.pieceIds
3327
+ .map((pieceId) => {
3328
+ const pieceTypeId = analysis.pieceTypeIdByPieceId.get(pieceId) ?? "";
3329
+ return ` ${quote(pieceId)}: PieceStateRecord<${quote(pieceId)}, ${quote(pieceTypeId)}, ${pieceTypeId ? pieceFieldsTypeName(pieceTypeId) : "RuntimeRecord"}>;`;
3330
+ })
3331
+ .join("\n");
3332
+ const perDieStateEntries = analysis.dieIds
3333
+ .map((dieId) => {
3334
+ const dieTypeId = analysis.dieTypeIdByDieId.get(dieId) ?? "";
3335
+ return ` ${quote(dieId)}: DieStateRecord<${quote(dieId)}, ${quote(dieTypeId)}, ${dieTypeId ? dieFieldsTypeName(dieTypeId) : "RuntimeRecord"}>;`;
3336
+ })
3337
+ .join("\n");
3338
+ const perBoardStateEntries = analysis.analyzedBoards
3339
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => {
3340
+ if (board.layout === "hex") {
3341
+ return ` ${quote(runtimeBoardId)}: HexBoardStateRecord<${quote(runtimeBoardId)}, ${renderStringUnion(board.spaces.map((space) => space.id))}, ${renderStringUnion(board.edges.map((edge) => edge.id))}, ${renderStringUnion(board.vertices.map((vertex) => vertex.id))}, ${boardFieldsTypeName(board.board.id)}, ${boardSpaceFieldsTypeName(board.board.id)}, ${hexEdgeFieldsTypeName(board.board.id)}, ${hexVertexFieldsTypeName(board.board.id)}>;`;
3342
+ }
3343
+ if (board.layout === "square") {
3344
+ return ` ${quote(runtimeBoardId)}: SquareBoardStateRecord<${quote(runtimeBoardId)}, ${renderStringUnion(board.spaces.map((space) => space.id))}, ${renderStringUnion(board.containers.map((container) => container.id))}, ${renderStringUnion(board.edges.map((edge) => edge.id))}, ${renderStringUnion(board.vertices.map((vertex) => vertex.id))}, ${boardFieldsTypeName(board.board.id)}, ${boardSpaceFieldsTypeName(board.board.id)}, ${boardRelationFieldsTypeName(board.board.id)}, ${boardContainerFieldsTypeName(board.board.id)}, ${hexEdgeFieldsTypeName(board.board.id)}, ${hexVertexFieldsTypeName(board.board.id)}>;`;
3345
+ }
3346
+ return ` ${quote(runtimeBoardId)}: GenericBoardStateRecord<${quote(runtimeBoardId)}, ${renderStringUnion(board.spaces.map((space) => space.id))}, ${renderStringUnion(board.containers.map((container) => container.id))}, ${boardFieldsTypeName(board.board.id)}, ${boardSpaceFieldsTypeName(board.board.id)}, ${boardRelationFieldsTypeName(board.board.id)}, ${boardContainerFieldsTypeName(board.board.id)}>;`;
3347
+ }))
3348
+ .join("\n");
3349
+ const perHexBoardStateEntries = analysis.analyzedBoards
3350
+ .filter((board) => board.layout === "hex")
3351
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: BoardStateById[${quote(runtimeBoardId)}];`))
3352
+ .join("\n");
3353
+ const perSquareBoardStateEntries = analysis.analyzedBoards
3354
+ .filter((board) => board.layout === "square")
3355
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => ` ${quote(runtimeBoardId)}: BoardStateById[${quote(runtimeBoardId)}];`))
3356
+ .join("\n");
3357
+ return `/**
3358
+ * Generated file.
3359
+ * Do not edit directly.
3360
+ */
3361
+
3362
+ import { z } from "zod";
3363
+ import {
3364
+ buildTypedRecord,
3365
+ expectTypedId,
3366
+ isTypedId,
3367
+ } from "@dreamboard/sdk-types";
3368
+ import {
3369
+ asPlayerId,
3370
+ assumeManifestSchema,
3371
+ boardRef,
3372
+ boardRefKey,
3373
+ boardRefSchema,
3374
+ cloneManifestDefault,
3375
+ createManifestGameStateSchema,
3376
+ createManifestRuntimeSchema,
3377
+ createManifestStringLiteralSchema,
3378
+ dealToPlayerBoardContainer as createDealToPlayerBoardContainerStep,
3379
+ dealToPlayerZone as createDealToPlayerZoneStep,
3380
+ perPlayer,
3381
+ perPlayerEntries,
3382
+ perPlayerGet,
3383
+ perPlayerHas,
3384
+ perPlayerKeys,
3385
+ perPlayerSchema,
3386
+ markManifestScopedSchema,
3387
+ resolveManifestPlayerIds,
3388
+ seedSharedBoardContainer as createSeedSharedBoardContainerStep,
3389
+ seedSharedBoardSpace as createSeedSharedBoardSpaceStep,
3390
+ shuffle as createShuffleStep,
3391
+ type CardIdOfManifest,
3392
+ type DieIdOfManifest,
3393
+ type PieceIdOfManifest,
3394
+ type BoardRef,
3395
+ type PerPlayer,
3396
+ type PerPlayerBoardRef,
3397
+ type PlayerId,
3398
+ type ReducerManifestContract,
3399
+ type RuntimeCardData,
3400
+ type RuntimeCardVisibility,
3401
+ type RuntimeComponentLocation,
3402
+ type RuntimeDieData,
3403
+ type RuntimeHandVisibilityMode,
3404
+ type RuntimePieceData,
3405
+ type RuntimeRecord,
3406
+ type RuntimeTableRecord,
3407
+ type SetupBootstrapContainerRef,
3408
+ type SetupBootstrapDestinationRef,
3409
+ type SetupBootstrapPerPlayerContainerTemplateRef,
3410
+ type SetupBootstrapStep,
3411
+ type SetupProfileDefinition,
3412
+ type SharedBoardRef,
3413
+ type StaticBoards,
3414
+ } from "@dreamboard/app-sdk/reducer";
3415
+
3416
+ const unknownRecordSchema = assumeManifestSchema<RuntimeRecord>(
3417
+ z.record(z.string(), z.unknown()),
3418
+ );
3419
+
3420
+ function resolveDefaultPlayerIds(
3421
+ playerIds: readonly string[] | undefined,
3422
+ ): readonly PlayerId[] {
3423
+ return resolveManifestPlayerIds(
3424
+ literals.playerIds as unknown as readonly PlayerId[],
3425
+ playerIds,
3426
+ );
3427
+ }
3428
+
3429
+ export const literals = {
3430
+ // literals satisfy \`ManifestLiterals<PlayerId, ...>\`. The cast is safe
3431
+ // because the runtime values are the exact player-id strings the manifest
3432
+ // authored; branding is purely a type-level discipline.
3433
+ playerIds: ${renderConstArray(analysis.playerIds)} as unknown as readonly PlayerId[],
3434
+ phaseNames: [] as readonly string[],
3435
+ boardLayouts: ["generic", "hex", "square"] as const,
3436
+ setupOptionIds: ${renderConstArray(analysis.setupOptionIds)},
3437
+ setupProfileIds: ${renderConstArray(analysis.setupProfileIds)},
3438
+ cardSetIds: ${renderConstArray(analysis.cardSetIds)},
3439
+ cardTypes: ${renderConstArray(analysis.cardTypes)},
3440
+ cardIds: ${renderConstArray(analysis.cardIds)},
3441
+ deckIds: ${renderConstArray(sharedZoneIds)},
3442
+ handIds: ${renderConstArray(playerZoneIds)},
3443
+ sharedZoneIds: ${renderConstArray(sharedZoneIds)},
3444
+ playerZoneIds: ${renderConstArray(playerZoneIds)},
3445
+ zoneIds: ${renderConstArray(analysis.zoneIds)},
3446
+ resourceIds: ${renderConstArray(analysis.resourceIds)},
3447
+ resourcePresentationById: ${renderJsonConst(analysis.resourcePresentationById)},
3448
+ pieceTypeIds: ${renderConstArray(analysis.pieceTypeIds)},
3449
+ pieceIds: ${renderConstArray(analysis.pieceIds)},
3450
+ dieTypeIds: ${renderConstArray(analysis.dieTypeIds)},
3451
+ dieIds: ${renderConstArray(analysis.dieIds)},
3452
+ boardTemplateIds: ${renderConstArray(analysis.boardTemplateIds)},
3453
+ boardTypeIds: ${renderConstArray(analysis.boardTypeIds)},
3454
+ boardBaseIds: ${renderConstArray(analysis.boardBaseIds)},
3455
+ boardIds: ${renderConstArray(analysis.boardIds)},
3456
+ boardContainerIds: ${renderConstArray(analysis.boardContainerIds)},
3457
+ relationTypeIds: ${renderConstArray(analysis.relationTypeIds)},
3458
+ edgeIds: ${renderConstArray(analysis.edgeIds)},
3459
+ edgeTypeIds: ${renderConstArray(analysis.edgeTypeIds)},
3460
+ vertexIds: ${renderConstArray(analysis.vertexIds)},
3461
+ vertexTypeIds: ${renderConstArray(analysis.vertexTypeIds)},
3462
+ spaceIds: ${renderConstArray(analysis.spaceIds)},
3463
+ spaceTypeIds: ${renderConstArray(analysis.spaceTypeIds)},
3464
+ handVisibilityById: ${renderStringRecord(playerZoneIds.map((zoneId) => [zoneId, zoneVisibilityById[zoneId] ?? "ownerOnly"]))},
3465
+ zoneVisibilityById: ${renderStringRecord(analysis.zoneVisibilityById)},
3466
+ cardSetIdByCardId: ${renderStringRecord(analysis.cardSetIdByCardId)},
3467
+ cardTypeByCardId: ${renderStringRecord(analysis.cardTypeByCardId)},
3468
+ setupChoiceIdsByOptionId: ${renderReadonlyArrayRecord(analysis.setupChoiceIdsByOptionId)},
3469
+ cardSetIdsBySharedZoneId: ${renderReadonlyArrayRecord(analysis.sharedZoneCardSetIds)},
3470
+ sharedZoneIdsByCardSetId: ${renderReadonlyArrayRecord(analysis.sharedZoneIdsByCardSetId)},
3471
+ homeSharedZoneIdsByCardType: ${renderReadonlyArrayRecord(analysis.homeSharedZoneIdsByCardType)},
3472
+ homeSharedZoneIdByCardType: ${renderStringRecord(analysis.homeSharedZoneIdByCardType)},
3473
+ cardSetIdsByPlayerZoneId: ${renderReadonlyArrayRecord(analysis.playerZoneCardSetIds)},
3474
+ } as const;
3475
+
3476
+ // PlayerId is an opaque brand imported from @dreamboard/app-sdk/reducer.
3477
+ // We intentionally do NOT enumerate the manifest's max-players roster here:
3478
+ // the runtime session may have fewer active seats than the manifest declares,
3479
+ // and requiring ingress to pick a literal from the max-players set reintroduces
3480
+ // the "total-record" assumption the refactor is meant to eliminate. Runtime
3481
+ // roster validation is done through perPlayerSchema(runtimePlayerIds, ...)
3482
+ // instead, which can be bound to the actual active roster.
3483
+ const playerIdSchema = markManifestScopedSchema(
3484
+ z
3485
+ .string()
3486
+ .min(1)
3487
+ .transform((value) => asPlayerId(value)),
3488
+ );
3489
+ const phaseNameSchema = markManifestScopedSchema(z.string());
3490
+ const boardLayoutSchema = createManifestStringLiteralSchema(literals.boardLayouts);
3491
+ const setupOptionIdSchema = createManifestStringLiteralSchema(literals.setupOptionIds);
3492
+ const setupProfileIdSchema = createManifestStringLiteralSchema(
3493
+ literals.setupProfileIds,
3494
+ );
3495
+ const cardSetIdSchema = createManifestStringLiteralSchema(literals.cardSetIds);
3496
+ const cardTypeSchema = createManifestStringLiteralSchema(literals.cardTypes);
3497
+ const cardIdSchema = createManifestStringLiteralSchema(literals.cardIds);
3498
+ const deckIdSchema = createManifestStringLiteralSchema(literals.deckIds);
3499
+ const handIdSchema = createManifestStringLiteralSchema(literals.handIds);
3500
+ const sharedZoneIdSchema = createManifestStringLiteralSchema(literals.sharedZoneIds);
3501
+ const playerZoneIdSchema = createManifestStringLiteralSchema(literals.playerZoneIds);
3502
+ const zoneIdSchema = createManifestStringLiteralSchema(literals.zoneIds);
3503
+ const resourceIdSchema = createManifestStringLiteralSchema(literals.resourceIds);
3504
+ const pieceTypeIdSchema = createManifestStringLiteralSchema(literals.pieceTypeIds);
3505
+ const pieceIdSchema = createManifestStringLiteralSchema(literals.pieceIds);
3506
+ const dieTypeIdSchema = createManifestStringLiteralSchema(literals.dieTypeIds);
3507
+ const dieIdSchema = createManifestStringLiteralSchema(literals.dieIds);
3508
+ const boardTypeIdSchema = createManifestStringLiteralSchema(literals.boardTypeIds);
3509
+ const boardBaseIdSchema = createManifestStringLiteralSchema(literals.boardBaseIds);
3510
+ const boardIdSchema = createManifestStringLiteralSchema(literals.boardIds);
3511
+ const boardContainerIdSchema = createManifestStringLiteralSchema(
3512
+ literals.boardContainerIds,
3513
+ );
3514
+ const relationTypeIdSchema = createManifestStringLiteralSchema(literals.relationTypeIds);
3515
+ const edgeIdSchema = createManifestStringLiteralSchema(literals.edgeIds);
3516
+ const edgeTypeIdSchema = createManifestStringLiteralSchema(literals.edgeTypeIds);
3517
+ const vertexIdSchema = createManifestStringLiteralSchema(literals.vertexIds);
3518
+ const vertexTypeIdSchema = createManifestStringLiteralSchema(literals.vertexTypeIds);
3519
+ const spaceIdSchema = createManifestStringLiteralSchema(literals.spaceIds);
3520
+ const spaceTypeIdSchema = createManifestStringLiteralSchema(literals.spaceTypeIds);
3521
+
3522
+ export const ids = {
3523
+ playerId: playerIdSchema,
3524
+ phaseName: phaseNameSchema,
3525
+ boardLayout: boardLayoutSchema,
3526
+ setupOptionId: setupOptionIdSchema,
3527
+ setupProfileId: setupProfileIdSchema,
3528
+ cardSetId: cardSetIdSchema,
3529
+ cardType: cardTypeSchema,
3530
+ cardId: cardIdSchema as unknown as z.ZodType<CardId>,
3531
+ deckId: deckIdSchema as unknown as z.ZodType<DeckId>,
3532
+ handId: handIdSchema as unknown as z.ZodType<HandId>,
3533
+ sharedZoneId: sharedZoneIdSchema,
3534
+ playerZoneId: playerZoneIdSchema,
3535
+ zoneId: zoneIdSchema,
3536
+ resourceId: resourceIdSchema,
3537
+ pieceTypeId: pieceTypeIdSchema,
3538
+ pieceId: pieceIdSchema,
3539
+ dieTypeId: dieTypeIdSchema,
3540
+ dieId: dieIdSchema,
3541
+ boardTypeId: boardTypeIdSchema,
3542
+ boardBaseId: boardBaseIdSchema,
3543
+ boardId: boardIdSchema,
3544
+ boardContainerId: boardContainerIdSchema,
3545
+ relationTypeId: relationTypeIdSchema,
3546
+ edgeId: edgeIdSchema,
3547
+ edgeTypeId: edgeTypeIdSchema,
3548
+ vertexId: vertexIdSchema,
3549
+ vertexTypeId: vertexTypeIdSchema,
3550
+ spaceId: spaceIdSchema,
3551
+ spaceTypeId: spaceTypeIdSchema,
3552
+ } as const;
3553
+
3554
+ export type { PlayerId };
3555
+ export type PhaseName = string;
3556
+ export type BoardLayout = (typeof literals.boardLayouts)[number];
3557
+ export type SetupOptionId = (typeof literals.setupOptionIds)[number];
3558
+ export type SetupProfileId = (typeof literals.setupProfileIds)[number];
3559
+ export type CardSetId = (typeof literals.cardSetIds)[number];
3560
+ export type CardType = (typeof literals.cardTypes)[number];
3561
+ export type CardId = (typeof literals.cardIds)[number];
3562
+ export type DeckId = (typeof literals.deckIds)[number];
3563
+ export type HandId = (typeof literals.handIds)[number];
3564
+ export type SharedZoneId = (typeof literals.sharedZoneIds)[number];
3565
+ export type PlayerZoneId = (typeof literals.playerZoneIds)[number];
3566
+ export type ZoneId = (typeof literals.zoneIds)[number];
3567
+ export type ResourceId = (typeof literals.resourceIds)[number];
3568
+ export type PieceTypeId = (typeof literals.pieceTypeIds)[number];
3569
+ export type PieceId = (typeof literals.pieceIds)[number];
3570
+ export type DieTypeId = (typeof literals.dieTypeIds)[number];
3571
+ export type DieId = (typeof literals.dieIds)[number];
3572
+ export type BoardTypeId = (typeof literals.boardTypeIds)[number];
3573
+ export type BoardBaseId = (typeof literals.boardBaseIds)[number];
3574
+ export type BoardId = (typeof literals.boardIds)[number];
3575
+ export type BoardContainerId = (typeof literals.boardContainerIds)[number];
3576
+ export type RelationTypeId = (typeof literals.relationTypeIds)[number];
3577
+ export type EdgeId = (typeof literals.edgeIds)[number];
3578
+ export type EdgeTypeId = (typeof literals.edgeTypeIds)[number];
3579
+ export type VertexId = (typeof literals.vertexIds)[number];
3580
+ export type VertexTypeId = (typeof literals.vertexTypeIds)[number];
3581
+ export type SpaceId = (typeof literals.spaceIds)[number];
3582
+ export type SpaceTypeId = (typeof literals.spaceTypeIds)[number];
3583
+
3584
+ ${renderGeneratedIdHandles(analysis)}
3585
+
3586
+ ${renderGeneratedRecordsHelpers()}
3587
+
3588
+ ${renderGeneratedIdGuards()}
3589
+
3590
+ // Historically this emitted PlayerRecord<T> = Record<PlayerId, T>, but that
3591
+ // type reified the "total roster" assumption (one entry per max-player). It has
3592
+ // been replaced throughout the generated contract with PerPlayer<T> from
3593
+ // @dreamboard/app-sdk/reducer, whose entries array matches the
3594
+ // actual runtime seat list.
3595
+ export type SharedZoneRecord<T> = Record<SharedZoneId, T>;
3596
+ export type PlayerZoneRecord<T> = Record<PlayerZoneId, PerPlayer<T>>;
3597
+ export type ComponentId = CardId | PieceId | DieId;
3598
+ export type ComponentIdsBySharedZoneId = {
3599
+ ${sharedZoneIds.map((zoneId) => ` ${quote(zoneId)}: ComponentId[];`).join("\n")}
3600
+ };
3601
+ export type ComponentIdsByPlayerZoneId = {
3602
+ ${playerZoneIds.map((zoneId) => ` ${quote(zoneId)}: PerPlayer<ComponentId[]>;`).join("\n")}
3603
+ };
3604
+ export type SetupOptionChoice = {
3605
+ id: string;
3606
+ label: string;
3607
+ description?: string | null;
3608
+ };
3609
+ export type SetupOption = {
3610
+ id: SetupOptionId;
3611
+ name: string;
3612
+ description?: string | null;
3613
+ choices: readonly SetupOptionChoice[];
3614
+ };
3615
+ export type SetupProfile = {
3616
+ id: SetupProfileId;
3617
+ name: string;
3618
+ description?: string | null;
3619
+ optionValues?: Partial<Record<SetupOptionId, string>> | null;
3620
+ };
3621
+ export const setupOptionsById = ${renderJsonConst(analysis.setupOptionsById)};
3622
+ export const setupChoiceIdsByOptionId = ${renderReadonlyArrayRecord(analysis.setupChoiceIdsByOptionId)};
3623
+ export const setupProfilesById = ${renderJsonConst(analysis.setupProfilesById)};
3624
+
3625
+ ${renderCardSetPropertySections(analysis.cardSets)}
3626
+
3627
+ ${renderTopologyFieldSections(analysis)}
3628
+
3629
+ ${renderBoardFieldMapTypes(analysis)}
3630
+
3631
+ export type CardProperties = ${analysis.cardSets.length > 0
3632
+ ? analysis.cardSets
3633
+ .map((cardSet) => `${toPascalCase(cardSet.id)}CardProperties`)
3634
+ .join(" | ")
3635
+ : "RuntimeRecord"};
3636
+
3637
+ export type CardStateRecord<
3638
+ CardIdValue extends CardId = CardId,
3639
+ CardSetIdValue extends CardSetId = CardSetId,
3640
+ CardTypeValue extends CardType = CardType,
3641
+ Properties = RuntimeRecord,
3642
+ > = Omit<RuntimeCardData, "id" | "cardSetId" | "cardType" | "properties"> & {
3643
+ id: CardIdValue;
3644
+ cardSetId: CardSetIdValue;
3645
+ cardType: CardTypeValue;
3646
+ properties: Properties;
3647
+ };
3648
+
3649
+ export type CardStateById = ${perCardStateEntries.length > 0
3650
+ ? `{\n${perCardStateEntries}\n}`
3651
+ : "Record<string, never>"};
3652
+
3653
+ export type PieceStateRecord<
3654
+ PieceIdValue extends PieceId = PieceId,
3655
+ PieceTypeIdValue extends PieceTypeId = PieceTypeId,
3656
+ Fields = RuntimeRecord,
3657
+ > = Omit<RuntimePieceData, "id" | "pieceTypeId" | "properties"> & {
3658
+ id: PieceIdValue;
3659
+ pieceTypeId: PieceTypeIdValue;
3660
+ properties: Fields;
3661
+ };
3662
+
3663
+ export type DieStateRecord<
3664
+ DieIdValue extends DieId = DieId,
3665
+ DieTypeIdValue extends DieTypeId = DieTypeId,
3666
+ Fields = RuntimeRecord,
3667
+ > = Omit<RuntimeDieData, "id" | "dieTypeId" | "properties"> & {
3668
+ id: DieIdValue;
3669
+ dieTypeId: DieTypeIdValue;
3670
+ properties: Fields;
3671
+ };
3672
+
3673
+ export type PieceStateById = ${perPieceStateEntries.length > 0
3674
+ ? `{\n${perPieceStateEntries}\n}`
3675
+ : "Record<string, never>"};
3676
+
3677
+ export type DieStateById = ${perDieStateEntries.length > 0
3678
+ ? `{\n${perDieStateEntries}\n}`
3679
+ : "Record<string, never>"};
3680
+ export type CardIdsBySharedZoneId = {
3681
+ ${sharedZoneCardIdEntries}
3682
+ };
3683
+ export type CardIdsByPlayerZoneId = {
3684
+ ${playerZoneCardIdEntries}
3685
+ };
3686
+ export type CardIdsByDeckId = CardIdsBySharedZoneId;
3687
+
3688
+ export interface BoardSpaceStateRecord<
3689
+ SpaceIdValue extends SpaceId = SpaceId,
3690
+ Fields = RuntimeRecord,
3691
+ > {
3692
+ id: SpaceIdValue;
3693
+ name?: string | null;
3694
+ typeId?: SpaceTypeId | null;
3695
+ fields: Fields;
3696
+ zoneId?: string | null;
3697
+ }
3698
+
3699
+ export interface BoardRelationStateRecord<
3700
+ SpaceIdValue extends SpaceId = SpaceId,
3701
+ Fields = RuntimeRecord,
3702
+ > {
3703
+ id?: string | null;
3704
+ typeId: RelationTypeId;
3705
+ fromSpaceId: SpaceIdValue;
3706
+ toSpaceId: SpaceIdValue;
3707
+ directed: boolean;
3708
+ fields: Fields;
3709
+ }
3710
+
3711
+ export interface BoardContainerStateRecord<
3712
+ SpaceIdValue extends SpaceId = SpaceId,
3713
+ ContainerIdValue extends BoardContainerId = BoardContainerId,
3714
+ Fields = RuntimeRecord,
3715
+ > {
3716
+ id: ContainerIdValue;
3717
+ name: string;
3718
+ host:
3719
+ | { type: "board" }
3720
+ | {
3721
+ type: "space";
3722
+ spaceId: SpaceIdValue;
3723
+ };
3724
+ allowedCardSetIds?: readonly CardSetId[];
3725
+ zoneId: string;
3726
+ fields: Fields;
3727
+ }
3728
+
3729
+ export interface BoardStateRecordBase<
3730
+ BoardIdValue extends BoardId = BoardId,
3731
+ SpaceIdValue extends SpaceId = SpaceId,
3732
+ ContainerIdValue extends BoardContainerId = BoardContainerId,
3733
+ BoardFields = RuntimeRecord,
3734
+ SpaceFields = RuntimeRecord,
3735
+ RelationFields = RuntimeRecord,
3736
+ ContainerFields = RuntimeRecord,
3737
+ > {
3738
+ id: BoardIdValue;
3739
+ baseId: BoardBaseId;
3740
+ typeId?: BoardTypeId | null;
3741
+ scope: "shared" | "perPlayer";
3742
+ playerId?: PlayerId | null;
3743
+ templateId?: string | null;
3744
+ fields: BoardFields;
3745
+ }
3746
+
3747
+ export interface GenericBoardStateRecord<
3748
+ BoardIdValue extends BoardId = BoardId,
3749
+ SpaceIdValue extends SpaceId = SpaceId,
3750
+ ContainerIdValue extends BoardContainerId = BoardContainerId,
3751
+ BoardFields = RuntimeRecord,
3752
+ SpaceFields = RuntimeRecord,
3753
+ RelationFields = RuntimeRecord,
3754
+ ContainerFields = RuntimeRecord,
3755
+ > extends BoardStateRecordBase<
3756
+ BoardIdValue,
3757
+ SpaceIdValue,
3758
+ ContainerIdValue,
3759
+ BoardFields,
3760
+ SpaceFields,
3761
+ RelationFields,
3762
+ ContainerFields
3763
+ > {
3764
+ layout: "generic";
3765
+ spaces: Record<
3766
+ SpaceIdValue,
3767
+ BoardSpaceStateRecord<SpaceIdValue, SpaceFields>
3768
+ >;
3769
+ relations: Array<BoardRelationStateRecord<SpaceIdValue, RelationFields>>;
3770
+ containers: Record<
3771
+ ContainerIdValue,
3772
+ BoardContainerStateRecord<SpaceIdValue, ContainerIdValue, ContainerFields>
3773
+ >;
3774
+ }
3775
+
3776
+ export interface HexSpaceStateRecord<
3777
+ SpaceIdValue extends SpaceId = SpaceId,
3778
+ Fields = RuntimeRecord,
3779
+ > extends BoardSpaceStateRecord<SpaceIdValue, Fields> {
3780
+ q: number;
3781
+ r: number;
3782
+ }
3783
+
3784
+ export interface SquareSpaceStateRecord<
3785
+ SpaceIdValue extends SpaceId = SpaceId,
3786
+ Fields = RuntimeRecord,
3787
+ > extends BoardSpaceStateRecord<SpaceIdValue, Fields> {
3788
+ row: number;
3789
+ col: number;
3790
+ }
3791
+
3792
+ export interface TiledEdgeStateRecord<
3793
+ SpaceIdValue extends SpaceId = SpaceId,
3794
+ EdgeIdValue extends EdgeId = EdgeId,
3795
+ Fields = RuntimeRecord,
3796
+ > {
3797
+ id: EdgeIdValue;
3798
+ spaceIds: readonly SpaceIdValue[];
3799
+ typeId?: EdgeTypeId | null;
3800
+ label?: string | null;
3801
+ ownerId?: PlayerId | null;
3802
+ fields: Fields;
3803
+ }
3804
+
3805
+ export interface TiledVertexStateRecord<
3806
+ SpaceIdValue extends SpaceId = SpaceId,
3807
+ VertexIdValue extends VertexId = VertexId,
3808
+ Fields = RuntimeRecord,
3809
+ > {
3810
+ id: VertexIdValue;
3811
+ spaceIds: readonly SpaceIdValue[];
3812
+ typeId?: VertexTypeId | null;
3813
+ label?: string | null;
3814
+ ownerId?: PlayerId | null;
3815
+ fields: Fields;
3816
+ }
3817
+
3818
+ export type HexEdgeStateRecord<
3819
+ SpaceIdValue extends SpaceId = SpaceId,
3820
+ EdgeIdValue extends EdgeId = EdgeId,
3821
+ Fields = RuntimeRecord,
3822
+ > = TiledEdgeStateRecord<SpaceIdValue, EdgeIdValue, Fields>;
3823
+
3824
+ export type HexVertexStateRecord<
3825
+ SpaceIdValue extends SpaceId = SpaceId,
3826
+ VertexIdValue extends VertexId = VertexId,
3827
+ Fields = RuntimeRecord,
3828
+ > = TiledVertexStateRecord<SpaceIdValue, VertexIdValue, Fields>;
3829
+
3830
+ export interface HexBoardStateRecord<
3831
+ BoardIdValue extends BoardId = BoardId,
3832
+ SpaceIdValue extends SpaceId = SpaceId,
3833
+ EdgeIdValue extends EdgeId = EdgeId,
3834
+ VertexIdValue extends VertexId = VertexId,
3835
+ BoardFields = RuntimeRecord,
3836
+ SpaceFields = RuntimeRecord,
3837
+ EdgeFields = RuntimeRecord,
3838
+ VertexFields = RuntimeRecord,
3839
+ > extends BoardStateRecordBase<
3840
+ BoardIdValue,
3841
+ SpaceIdValue,
3842
+ never,
3843
+ BoardFields,
3844
+ SpaceFields,
3845
+ RuntimeRecord,
3846
+ RuntimeRecord
3847
+ > {
3848
+ layout: "hex";
3849
+ spaces: Record<SpaceIdValue, HexSpaceStateRecord<SpaceIdValue, SpaceFields>>;
3850
+ relations: Array<BoardRelationStateRecord<SpaceIdValue, RuntimeRecord>>;
3851
+ containers: Record<never, never>;
3852
+ orientation: "pointy-top" | "flat-top";
3853
+ edges: Array<HexEdgeStateRecord<SpaceIdValue, EdgeIdValue, EdgeFields>>;
3854
+ vertices: Array<
3855
+ HexVertexStateRecord<SpaceIdValue, VertexIdValue, VertexFields>
3856
+ >;
3857
+ }
3858
+
3859
+ export interface SquareBoardStateRecord<
3860
+ BoardIdValue extends BoardId = BoardId,
3861
+ SpaceIdValue extends SpaceId = SpaceId,
3862
+ ContainerIdValue extends BoardContainerId = BoardContainerId,
3863
+ EdgeIdValue extends EdgeId = EdgeId,
3864
+ VertexIdValue extends VertexId = VertexId,
3865
+ BoardFields = RuntimeRecord,
3866
+ SpaceFields = RuntimeRecord,
3867
+ RelationFields = RuntimeRecord,
3868
+ ContainerFields = RuntimeRecord,
3869
+ EdgeFields = RuntimeRecord,
3870
+ VertexFields = RuntimeRecord,
3871
+ > extends BoardStateRecordBase<
3872
+ BoardIdValue,
3873
+ SpaceIdValue,
3874
+ ContainerIdValue,
3875
+ BoardFields,
3876
+ SpaceFields,
3877
+ RelationFields,
3878
+ ContainerFields
3879
+ > {
3880
+ layout: "square";
3881
+ spaces: Record<
3882
+ SpaceIdValue,
3883
+ SquareSpaceStateRecord<SpaceIdValue, SpaceFields>
3884
+ >;
3885
+ relations: Array<BoardRelationStateRecord<SpaceIdValue, RelationFields>>;
3886
+ containers: Record<
3887
+ ContainerIdValue,
3888
+ BoardContainerStateRecord<SpaceIdValue, ContainerIdValue, ContainerFields>
3889
+ >;
3890
+ edges: Array<TiledEdgeStateRecord<SpaceIdValue, EdgeIdValue, EdgeFields>>;
3891
+ vertices: Array<
3892
+ TiledVertexStateRecord<SpaceIdValue, VertexIdValue, VertexFields>
3893
+ >;
3894
+ }
3895
+
3896
+ export type TiledBoardStateRecord =
3897
+ | HexBoardStateRecord
3898
+ | SquareBoardStateRecord;
3899
+
3900
+ export type BoardStateById = {
3901
+ ${perBoardStateEntries}
3902
+ };
3903
+
3904
+ export type HexBoardStateById = ${perHexBoardStateEntries.length > 0
3905
+ ? `{\n${perHexBoardStateEntries}\n}`
3906
+ : "Record<string, never>"};
3907
+
3908
+ export type SquareBoardStateById = ${perSquareBoardStateEntries.length > 0
3909
+ ? `{\n${perSquareBoardStateEntries}\n}`
3910
+ : "Record<string, never>"};
3911
+
3912
+ type ManifestRecordValue<T> = T[keyof T];
3913
+ type ManifestArrayElement<T> =
3914
+ T extends readonly (infer Item)[]
3915
+ ? Item
3916
+ : T extends (infer Item)[]
3917
+ ? Item
3918
+ : never;
3919
+
3920
+ export type BoardState<BoardIdValue extends BoardId = BoardId> =
3921
+ BoardIdValue extends keyof BoardStateById ? BoardStateById[BoardIdValue] : never;
3922
+
3923
+ export type BoardFields<BoardIdValue extends BoardId = BoardId> =
3924
+ BoardState<BoardIdValue> extends { fields: infer Fields } ? Fields : never;
3925
+
3926
+ export type BoardSpaceStateByBoardId = {
3927
+ [BoardIdValue in keyof BoardStateById]: ManifestRecordValue<
3928
+ BoardStateById[BoardIdValue]["spaces"]
3929
+ >;
3930
+ };
3931
+
3932
+ export type BoardSpaceState<BoardIdValue extends BoardId = BoardId> =
3933
+ BoardIdValue extends keyof BoardSpaceStateByBoardId
3934
+ ? BoardSpaceStateByBoardId[BoardIdValue]
3935
+ : never;
3936
+
3937
+ export type BoardSpaceFields<BoardIdValue extends BoardId = BoardId> =
3938
+ BoardSpaceState<BoardIdValue> extends { fields: infer Fields }
3939
+ ? Fields
3940
+ : never;
3941
+
3942
+ export type BoardRelationStateByBoardId = {
3943
+ [BoardIdValue in keyof BoardStateById]: ManifestArrayElement<
3944
+ BoardStateById[BoardIdValue]["relations"]
3945
+ >;
3946
+ };
3947
+
3948
+ export type BoardRelationState<BoardIdValue extends BoardId = BoardId> =
3949
+ BoardIdValue extends keyof BoardRelationStateByBoardId
3950
+ ? BoardRelationStateByBoardId[BoardIdValue]
3951
+ : never;
3952
+
3953
+ export type BoardRelationFields<BoardIdValue extends BoardId = BoardId> =
3954
+ BoardRelationState<BoardIdValue> extends { fields: infer Fields }
3955
+ ? Fields
3956
+ : never;
3957
+
3958
+ export type BoardContainerStateByBoardId = {
3959
+ [BoardIdValue in keyof BoardStateById]: ManifestRecordValue<
3960
+ BoardStateById[BoardIdValue]["containers"]
3961
+ >;
3962
+ };
3963
+
3964
+ export type BoardContainerState<BoardIdValue extends BoardId = BoardId> =
3965
+ BoardIdValue extends keyof BoardContainerStateByBoardId
3966
+ ? BoardContainerStateByBoardId[BoardIdValue]
3967
+ : never;
3968
+
3969
+ export type BoardContainerFields<BoardIdValue extends BoardId = BoardId> =
3970
+ BoardContainerState<BoardIdValue> extends { fields: infer Fields }
3971
+ ? Fields
3972
+ : never;
3973
+
3974
+ type HexAuthoredEdgesByBoardId = typeof authoredHexEdgesByBoardIdLookup;
3975
+ type HexAuthoredVerticesByBoardId = typeof authoredHexVerticesByBoardIdLookup;
3976
+
3977
+ export type HexAuthoredEdgeState<
3978
+ BoardIdValue extends keyof HexAuthoredEdgesByBoardId = keyof HexAuthoredEdgesByBoardId,
3979
+ > = BoardIdValue extends keyof HexAuthoredEdgesByBoardId
3980
+ ? ManifestArrayElement<HexAuthoredEdgesByBoardId[BoardIdValue]>
3981
+ : never;
3982
+
3983
+ export type HexAuthoredEdgeRef<
3984
+ BoardIdValue extends keyof HexAuthoredEdgesByBoardId = keyof HexAuthoredEdgesByBoardId,
3985
+ > = HexAuthoredEdgeState<BoardIdValue> extends { ref: infer Ref } ? Ref : never;
3986
+
3987
+ export type HexAuthoredVertexState<
3988
+ BoardIdValue extends keyof HexAuthoredVerticesByBoardId = keyof HexAuthoredVerticesByBoardId,
3989
+ > = BoardIdValue extends keyof HexAuthoredVerticesByBoardId
3990
+ ? ManifestArrayElement<HexAuthoredVerticesByBoardId[BoardIdValue]>
3991
+ : never;
3992
+
3993
+ export type HexAuthoredVertexRef<
3994
+ BoardIdValue extends keyof HexAuthoredVerticesByBoardId = keyof HexAuthoredVerticesByBoardId,
3995
+ > = HexAuthoredVertexState<BoardIdValue> extends { ref: infer Ref }
3996
+ ? Ref
3997
+ : never;
3998
+
3999
+ export type HexEdgeState<
4000
+ BoardIdValue extends keyof HexBoardStateById = keyof HexBoardStateById,
4001
+ > = BoardIdValue extends keyof HexBoardStateById
4002
+ ? ManifestArrayElement<HexBoardStateById[BoardIdValue]["edges"]>
4003
+ : never;
4004
+
4005
+ export type HexEdgeFields<
4006
+ BoardIdValue extends keyof HexBoardStateById = keyof HexBoardStateById,
4007
+ > = HexEdgeState<BoardIdValue> extends { fields: infer Fields }
4008
+ ? Fields
4009
+ : never;
4010
+
4011
+ export type HexVertexState<
4012
+ BoardIdValue extends keyof HexBoardStateById = keyof HexBoardStateById,
4013
+ > = BoardIdValue extends keyof HexBoardStateById
4014
+ ? ManifestArrayElement<HexBoardStateById[BoardIdValue]["vertices"]>
4015
+ : never;
4016
+
4017
+ export type HexVertexFields<
4018
+ BoardIdValue extends keyof HexBoardStateById = keyof HexBoardStateById,
4019
+ > = HexVertexState<BoardIdValue> extends { fields: infer Fields }
4020
+ ? Fields
4021
+ : never;
4022
+
4023
+ export type SquareEdgeState<
4024
+ BoardIdValue extends keyof SquareBoardStateById = keyof SquareBoardStateById,
4025
+ > = BoardIdValue extends keyof SquareBoardStateById
4026
+ ? ManifestArrayElement<SquareBoardStateById[BoardIdValue]["edges"]>
4027
+ : never;
4028
+
4029
+ export type SquareEdgeFields<
4030
+ BoardIdValue extends keyof SquareBoardStateById = keyof SquareBoardStateById,
4031
+ > = SquareEdgeState<BoardIdValue> extends { fields: infer Fields }
4032
+ ? Fields
4033
+ : never;
4034
+
4035
+ export type SquareVertexState<
4036
+ BoardIdValue extends keyof SquareBoardStateById = keyof SquareBoardStateById,
4037
+ > = BoardIdValue extends keyof SquareBoardStateById
4038
+ ? ManifestArrayElement<SquareBoardStateById[BoardIdValue]["vertices"]>
4039
+ : never;
4040
+
4041
+ export type SquareVertexFields<
4042
+ BoardIdValue extends keyof SquareBoardStateById = keyof SquareBoardStateById,
4043
+ > = SquareVertexState<BoardIdValue> extends { fields: infer Fields }
4044
+ ? Fields
4045
+ : never;
4046
+
4047
+ export type TiledBoardId = keyof HexBoardStateById | keyof SquareBoardStateById;
4048
+
4049
+ export type TiledEdgeState<BoardIdValue extends TiledBoardId = TiledBoardId> =
4050
+ BoardIdValue extends keyof HexBoardStateById
4051
+ ? HexEdgeState<BoardIdValue>
4052
+ : BoardIdValue extends keyof SquareBoardStateById
4053
+ ? SquareEdgeState<BoardIdValue>
4054
+ : never;
4055
+
4056
+ export type TiledEdgeFields<BoardIdValue extends TiledBoardId = TiledBoardId> =
4057
+ TiledEdgeState<BoardIdValue> extends { fields: infer Fields }
4058
+ ? Fields
4059
+ : never;
4060
+
4061
+ export type TiledVertexState<
4062
+ BoardIdValue extends TiledBoardId = TiledBoardId,
4063
+ > = BoardIdValue extends keyof HexBoardStateById
4064
+ ? HexVertexState<BoardIdValue>
4065
+ : BoardIdValue extends keyof SquareBoardStateById
4066
+ ? SquareVertexState<BoardIdValue>
4067
+ : never;
4068
+
4069
+ export type TiledVertexFields<
4070
+ BoardIdValue extends TiledBoardId = TiledBoardId,
4071
+ > = TiledVertexState<BoardIdValue> extends { fields: infer Fields }
4072
+ ? Fields
4073
+ : never;
4074
+
4075
+ export type BoardStateRecord = ${analysis.boardIds.length > 0 ? "BoardStateById[BoardId]" : "never"};
4076
+
4077
+ export type TableState = RuntimeTableRecord & {
4078
+ playerOrder: PlayerId[];
4079
+ zones: RuntimeTableRecord["zones"] & {
4080
+ visibility: Record<ZoneId, RuntimeHandVisibilityMode>;
4081
+ cardSetIdsByZoneId?: Record<ZoneId, readonly CardSetId[]>;
4082
+ };
4083
+ decks: Record<SharedZoneId, CardId[]>;
4084
+ hands: Record<PlayerZoneId, PerPlayer<CardId[]>>;
4085
+ handVisibility: Record<PlayerZoneId, RuntimeHandVisibilityMode>;
4086
+ cards: CardStateById;
4087
+ pieces: Record<PieceId, RuntimePieceData>;
4088
+ componentLocations: Record<ComponentId, RuntimeComponentLocation>;
4089
+ ownerOfCard: Record<CardId, PlayerId | null>;
4090
+ visibility: Record<CardId, RuntimeCardVisibility>;
4091
+ resources: PerPlayer<Record<ResourceId, number>>;
4092
+ boards: RuntimeTableRecord["boards"];
4093
+ dice: Record<DieId, RuntimeDieData>;
4094
+ };
4095
+
4096
+ const sharedZoneSchema = z.record(sharedZoneIdSchema, z.array(z.string()));
4097
+ // PerPlayer<Array<string>> wire shape only: { __perPlayer: true, entries: [[playerId, value], ...] }.
4098
+ const playerZoneSchema = z.record(
4099
+ playerZoneIdSchema,
4100
+ perPlayerSchema(z.array(z.string())),
4101
+ );
4102
+ const cardStateSchema = z.object({
4103
+ componentType: z.string().optional(),
4104
+ id: ids.cardId,
4105
+ cardSetId: ids.cardSetId,
4106
+ cardType: ids.cardType,
4107
+ name: z.string().optional(),
4108
+ text: z.string().optional(),
4109
+ properties: unknownRecordSchema,
4110
+ });
4111
+ ${renderCardPropertiesSchemaByCardSetId(analysis.cardSets)}
4112
+ ${renderCardStateSchemaFactory(analysis)}
4113
+ const cardStateByIdSchema = ${renderCardStateSchemaById(analysis)};
4114
+ const pieceStateByIdSchema = ${renderPieceStateSchemaById(analysis)};
4115
+ const dieStateByIdSchema = ${renderDieStateSchemaById(analysis)};
4116
+ const boardStateByIdSchema = ${renderBoardStateSchemaById(analysis)};
4117
+ const hexBoardStateByIdSchema = ${renderHexBoardStateSchemaById(analysis)};
4118
+ const squareBoardStateByIdSchema = ${renderSquareBoardStateSchemaById(analysis)};
4119
+ const boardSpaceTypeIdSchema = ids.spaceTypeId.nullable().optional();
4120
+ const boardSpaceStateSchema = z.object({
4121
+ id: ids.spaceId,
4122
+ name: z.string().nullable().optional(),
4123
+ typeId: boardSpaceTypeIdSchema,
4124
+ fields: unknownRecordSchema,
4125
+ zoneId: z.string().nullable().optional(),
4126
+ });
4127
+ const hexSpaceStateSchema = boardSpaceStateSchema.extend({
4128
+ q: z.number().int(),
4129
+ r: z.number().int(),
4130
+ });
4131
+ const squareSpaceStateSchema = boardSpaceStateSchema.extend({
4132
+ row: z.number().int(),
4133
+ col: z.number().int(),
4134
+ });
4135
+ const boardRelationStateSchema = z.object({
4136
+ id: z.string().nullable().optional(),
4137
+ typeId: ids.relationTypeId,
4138
+ fromSpaceId: ids.spaceId,
4139
+ toSpaceId: ids.spaceId,
4140
+ directed: z.boolean(),
4141
+ fields: unknownRecordSchema,
4142
+ });
4143
+ const boardContainerStateSchema = z.object({
4144
+ id: ids.boardContainerId,
4145
+ name: z.string(),
4146
+ host: z.discriminatedUnion("type", [
4147
+ z.object({ type: z.literal("board") }),
4148
+ z.object({ type: z.literal("space"), spaceId: ids.spaceId }),
4149
+ ]),
4150
+ allowedCardSetIds: z.array(ids.cardSetId).optional(),
4151
+ zoneId: z.string(),
4152
+ fields: unknownRecordSchema,
4153
+ });
4154
+ const runtimeGenericBoardStateSchema = z.object({
4155
+ id: ids.boardId,
4156
+ baseId: ids.boardBaseId,
4157
+ layout: z.literal("generic"),
4158
+ typeId: ids.boardTypeId.nullable().optional(),
4159
+ scope: z.enum(["shared", "perPlayer"]),
4160
+ playerId: ids.playerId.nullable().optional(),
4161
+ templateId: z.string().nullable().optional(),
4162
+ fields: unknownRecordSchema,
4163
+ spaces: z.record(ids.spaceId, boardSpaceStateSchema),
4164
+ relations: z.array(boardRelationStateSchema),
4165
+ containers: z.record(ids.boardContainerId, boardContainerStateSchema),
4166
+ });
4167
+ const hexEdgeStateSchema = z.object({
4168
+ id: ids.edgeId,
4169
+ spaceIds: z.array(ids.spaceId).min(1).max(2),
4170
+ typeId: ids.edgeTypeId.nullable().optional(),
4171
+ label: z.string().nullable().optional(),
4172
+ ownerId: ids.playerId.nullable().optional(),
4173
+ fields: unknownRecordSchema,
4174
+ });
4175
+ const hexVertexStateSchema = z.object({
4176
+ id: ids.vertexId,
4177
+ spaceIds: z.array(ids.spaceId).min(1).max(3),
4178
+ typeId: ids.vertexTypeId.nullable().optional(),
4179
+ label: z.string().nullable().optional(),
4180
+ ownerId: ids.playerId.nullable().optional(),
4181
+ fields: unknownRecordSchema,
4182
+ });
4183
+ const squareVertexStateSchema = z.object({
4184
+ id: ids.vertexId,
4185
+ spaceIds: z.array(ids.spaceId).min(1).max(4),
4186
+ typeId: ids.vertexTypeId.nullable().optional(),
4187
+ label: z.string().nullable().optional(),
4188
+ ownerId: ids.playerId.nullable().optional(),
4189
+ fields: unknownRecordSchema,
4190
+ });
4191
+ const runtimeHexBoardStateSchema = runtimeGenericBoardStateSchema.extend({
4192
+ layout: z.literal("hex"),
4193
+ spaces: z.record(ids.spaceId, hexSpaceStateSchema),
4194
+ relations: z.array(boardRelationStateSchema),
4195
+ containers: z.object({}),
4196
+ orientation: z.enum(["pointy-top", "flat-top"]),
4197
+ edges: z.array(hexEdgeStateSchema),
4198
+ vertices: z.array(hexVertexStateSchema),
4199
+ });
4200
+ const runtimeSquareBoardStateSchema = runtimeGenericBoardStateSchema.extend({
4201
+ layout: z.literal("square"),
4202
+ spaces: z.record(ids.spaceId, squareSpaceStateSchema),
4203
+ relations: z.array(boardRelationStateSchema),
4204
+ containers: z.record(ids.boardContainerId, boardContainerStateSchema),
4205
+ edges: z.array(hexEdgeStateSchema),
4206
+ vertices: z.array(squareVertexStateSchema),
4207
+ });
4208
+ const runtimeBoardStateSchema = z.union([
4209
+ runtimeGenericBoardStateSchema,
4210
+ runtimeHexBoardStateSchema,
4211
+ runtimeSquareBoardStateSchema,
4212
+ ]);
4213
+ const rawTableSchema = z.object({
4214
+ playerOrder: z.array(ids.playerId),
4215
+ zones: z.object({
4216
+ shared: sharedZoneSchema,
4217
+ perPlayer: playerZoneSchema,
4218
+ visibility: z.record(zoneIdSchema, z.enum(["all", "ownerOnly", "public", "hidden"])),
4219
+ cardSetIdsByZoneId: z.record(zoneIdSchema, z.array(ids.cardSetId)).optional(),
4220
+ }),
4221
+ decks: sharedZoneSchema,
4222
+ hands: playerZoneSchema,
4223
+ handVisibility: z.record(
4224
+ playerZoneIdSchema,
4225
+ z.enum(["all", "ownerOnly", "public", "hidden"]),
4226
+ ),
4227
+ cards: cardStateByIdSchema,
4228
+ pieces: pieceStateByIdSchema,
4229
+ componentLocations: z.record(
4230
+ z.string(),
4231
+ z.union([
4232
+ z.object({ type: z.literal("Detached") }),
4233
+ z.object({
4234
+ type: z.literal("InDeck"),
4235
+ deckId: ids.deckId,
4236
+ playedBy: ids.playerId.nullable(),
4237
+ position: z.number().int().nullable().optional(),
4238
+ }),
4239
+ z.object({
4240
+ type: z.literal("InHand"),
4241
+ handId: ids.handId,
4242
+ playerId: ids.playerId,
4243
+ position: z.number().int().nullable().optional(),
4244
+ }),
4245
+ z.object({
4246
+ type: z.literal("InZone"),
4247
+ zoneId: z.string(),
4248
+ playedBy: ids.playerId.nullable().optional(),
4249
+ position: z.number().int().nullable().optional(),
4250
+ }),
4251
+ z.object({
4252
+ type: z.literal("OnSpace"),
4253
+ boardId: ids.boardId,
4254
+ spaceId: ids.spaceId,
4255
+ position: z.number().int().nullable().optional(),
4256
+ }),
4257
+ z.object({
4258
+ type: z.literal("InContainer"),
4259
+ boardId: ids.boardId,
4260
+ containerId: ids.boardContainerId,
4261
+ position: z.number().int().nullable().optional(),
4262
+ }),
4263
+ z.object({
4264
+ type: z.literal("OnEdge"),
4265
+ boardId: ids.boardId,
4266
+ edgeId: ids.edgeId,
4267
+ position: z.number().int().nullable().optional(),
4268
+ }),
4269
+ z.object({
4270
+ type: z.literal("OnVertex"),
4271
+ boardId: ids.boardId,
4272
+ vertexId: ids.vertexId,
4273
+ position: z.number().int().nullable().optional(),
4274
+ }),
4275
+ ${renderStrictSlotLocationSchema(analysis.strictSlotHosts)},
4276
+ ]),
4277
+ ),
4278
+ ownerOfCard: z.record(ids.cardId, ids.playerId.nullable()),
4279
+ visibility: z.record(
4280
+ ids.cardId,
4281
+ z.object({
4282
+ faceUp: z.boolean(),
4283
+ visibleTo: z.array(ids.playerId).nullable().optional(),
4284
+ }),
4285
+ ),
4286
+ resources: perPlayerSchema(z.record(ids.resourceId, z.number().int())),
4287
+ boards: z.object({
4288
+ byId: boardStateByIdSchema,
4289
+ hex: hexBoardStateByIdSchema,
4290
+ square: squareBoardStateByIdSchema,
4291
+ }),
4292
+ dice: dieStateByIdSchema,
4293
+ });
4294
+
4295
+ export const tableSchema = assumeManifestSchema<TableState>(rawTableSchema);
4296
+
4297
+ export const runtimeSchema = createManifestRuntimeSchema({
4298
+ phaseNameSchema: z.string(),
4299
+ playerIdSchema: ids.playerId,
4300
+ setupProfileIdSchema: ids.setupProfileId,
4301
+ });
4302
+
4303
+ // Produces an empty PerPlayer<CardId[]> for every player zone. The entries
4304
+ // array is seeded from the resolved runtime roster so the generated default
4305
+ // never creates a record for a player the session does not have.
4306
+ function buildPerPlayerCardIds(
4307
+ playerIds: readonly PlayerId[],
4308
+ ): CardIdsByPlayerZoneId {
4309
+ return Object.fromEntries(
4310
+ literals.playerZoneIds.map((zoneId) => [
4311
+ zoneId,
4312
+ perPlayer(playerIds, () => [] as CardId[]),
4313
+ ]),
4314
+ ) as CardIdsByPlayerZoneId;
4315
+ }
4316
+
4317
+ function buildPlayerResources(
4318
+ playerIds: readonly PlayerId[],
4319
+ ): PerPlayer<Record<ResourceId, number>> {
4320
+ return perPlayer(playerIds, () =>
4321
+ Object.fromEntries(
4322
+ literals.resourceIds.map((resourceId) => [resourceId, 0]),
4323
+ ) as Record<ResourceId, number>,
4324
+ );
4325
+ }
4326
+
4327
+ export const defaults = {
4328
+ zones: (playerIds?: readonly string[]) => ({
4329
+ shared: cloneManifestDefault(${JSON.stringify(emptySharedZonesTemplate)}),
4330
+ perPlayer: buildPerPlayerCardIds(resolveDefaultPlayerIds(playerIds)),
4331
+ visibility: cloneManifestDefault(${JSON.stringify(zoneVisibilityById)}),
4332
+ cardSetIdsByZoneId: cloneManifestDefault(${JSON.stringify(Object.fromEntries(Array.from(analysis.zoneCardSetIdsById.entries()).sort(([left], [right]) => left.localeCompare(right))))}),
4333
+ }) as TableState["zones"],
4334
+ decks: () => cloneManifestDefault(${JSON.stringify(emptySharedZonesTemplate)}) as TableState["decks"],
4335
+ hands: (playerIds?: readonly string[]) =>
4336
+ buildPerPlayerCardIds(resolveDefaultPlayerIds(playerIds)) as TableState["hands"],
4337
+ handVisibility: () => cloneManifestDefault(${JSON.stringify(Object.fromEntries(playerZoneIds.map((zoneId) => [
4338
+ zoneId,
4339
+ zoneVisibilityById[zoneId] ?? "ownerOnly",
4340
+ ])))}) as TableState["handVisibility"],
4341
+ ownerOfCard: () => cloneManifestDefault(${JSON.stringify(defaultOwnerTemplate)}) as TableState["ownerOfCard"],
4342
+ visibility: () => cloneManifestDefault(${JSON.stringify(defaultVisibilityTemplate)}) as TableState["visibility"],
4343
+ resources: (playerIds?: readonly string[]) =>
4344
+ buildPlayerResources(resolveDefaultPlayerIds(playerIds)),
4345
+ } as const;
4346
+
4347
+ export const staticBoards = ${renderJsonConst(staticBoardsTemplate)};
4348
+
4349
+ const baseInitialTable = ${renderJsonConst(initialTableTemplate)} as unknown as TableState;
4350
+ const baseDeckCardsByZoneId: Record<SharedZoneId, readonly CardId[]> = ${renderJsonConst(Object.fromEntries(sharedZoneIds.map((zoneId) => [
4351
+ zoneId,
4352
+ initialTableTemplate.decks[zoneId] ?? [],
4353
+ ])))};
4354
+
4355
+ export function createInitialTable(options: {
4356
+ playerIds?: readonly string[];
4357
+ shuffleItems?: <Value>(values: readonly Value[]) => Value[];
4358
+ } = {}): TableState {
4359
+ const resolvedPlayerIds = resolveDefaultPlayerIds(options.playerIds);
4360
+ const shuffleItems =
4361
+ options.shuffleItems ?? (<Value>(values: readonly Value[]) => [...values]);
4362
+ const table = cloneManifestDefault(baseInitialTable) as TableState;
4363
+ table.playerOrder = [...resolvedPlayerIds];
4364
+ table.zones = defaults.zones(resolvedPlayerIds);
4365
+ table.decks = defaults.decks();
4366
+ table.hands = defaults.hands(resolvedPlayerIds);
4367
+ table.resources = defaults.resources(resolvedPlayerIds);
4368
+ const componentLocations = cloneManifestDefault(
4369
+ baseInitialTable.componentLocations,
4370
+ ) as TableState["componentLocations"];
4371
+
4372
+ for (const [zoneId, baseDeckCardIds] of Object.entries(
4373
+ baseDeckCardsByZoneId,
4374
+ ) as Array<[SharedZoneId, readonly CardId[]]>) {
4375
+ const shuffled = shuffleItems(baseDeckCardIds);
4376
+ (table.decks as Record<string, CardId[]>)[zoneId] = [...shuffled];
4377
+ (table.zones.shared as Record<string, CardId[]>)[zoneId] = [...shuffled];
4378
+ shuffled.forEach((componentId, position) => {
4379
+ const location = componentLocations[componentId];
4380
+ if (!location || location.type !== "InDeck") {
4381
+ return;
4382
+ }
4383
+ componentLocations[componentId] = {
4384
+ ...location,
4385
+ position,
4386
+ };
4387
+ });
4388
+ }
4389
+
4390
+ table.componentLocations = componentLocations;
4391
+ return tableSchema.parse(table);
4392
+ }
4393
+
4394
+ export const schemas = {
4395
+ table: tableSchema,
4396
+ runtime: runtimeSchema,
4397
+ } as const;
4398
+
4399
+ export function createGameStateSchema<
4400
+ PhaseNameSchema extends z.ZodTypeAny,
4401
+ PublicSchema extends z.ZodTypeAny,
4402
+ PrivateSchema extends z.ZodTypeAny,
4403
+ HiddenSchema extends z.ZodTypeAny,
4404
+ PhasesSchema extends z.ZodTypeAny,
4405
+ >({
4406
+ phaseNameSchema,
4407
+ publicSchema,
4408
+ privateSchema,
4409
+ hiddenSchema,
4410
+ phasesSchema,
4411
+ }: {
4412
+ phaseNameSchema: PhaseNameSchema;
4413
+ publicSchema: PublicSchema;
4414
+ privateSchema: PrivateSchema;
4415
+ hiddenSchema: HiddenSchema;
4416
+ phasesSchema: PhasesSchema;
4417
+ }) {
4418
+ return createManifestGameStateSchema({
4419
+ tableSchema,
4420
+ playerIdSchema: ids.playerId,
4421
+ setupProfileIdSchema: ids.setupProfileId,
4422
+ phaseNameSchema,
4423
+ publicSchema,
4424
+ privateSchema,
4425
+ hiddenSchema,
4426
+ phasesSchema,
4427
+ });
4428
+ }
4429
+
4430
+ export const manifestContract: ReducerManifestContract<
4431
+ RuntimeTableRecord,
4432
+ string,
4433
+ PlayerId,
4434
+ DeckId,
4435
+ HandId,
4436
+ CardId
4437
+ > = {
4438
+ literals,
4439
+ ids,
4440
+ defaults,
4441
+ staticBoards: staticBoards as unknown as StaticBoards<TableState>,
4442
+ setupOptionsById,
4443
+ setupChoiceIdsByOptionId,
4444
+ setupProfilesById,
4445
+ tableSchema,
4446
+ runtimeSchema,
4447
+ createGameStateSchema,
4448
+ };
4449
+
4450
+ ${renderBoardLiteralHelpers(analysis)}
4451
+
4452
+ export type SetupProfilesDefinition = Record<
4453
+ SetupProfileId,
4454
+ SetupProfileDefinition<string, typeof manifestContract>
4455
+ >;
4456
+
4457
+ export function setupProfiles<const Profiles extends SetupProfilesDefinition>(
4458
+ profiles: Profiles,
4459
+ ): Profiles {
4460
+ return profiles;
4461
+ }
4462
+
4463
+ export function shuffle(
4464
+ container: SetupBootstrapContainerRef<typeof manifestContract>,
4465
+ ): SetupBootstrapStep<typeof manifestContract> {
4466
+ return createShuffleStep<typeof manifestContract>(container);
4467
+ }
4468
+
4469
+ export function dealToPlayerZone(options: {
4470
+ from: Extract<
4471
+ SetupBootstrapContainerRef<typeof manifestContract>,
4472
+ { type: "sharedZone" | "sharedBoardContainer" }
4473
+ >;
4474
+ zoneId: Extract<
4475
+ SetupBootstrapPerPlayerContainerTemplateRef<typeof manifestContract>,
4476
+ { type: "playerZone" }
4477
+ >["zoneId"];
4478
+ count: number;
4479
+ playerIds?: readonly PlayerId[];
4480
+ }): SetupBootstrapStep<typeof manifestContract> {
4481
+ return createDealToPlayerZoneStep<typeof manifestContract>(options);
4482
+ }
4483
+
4484
+ export function dealToPlayerBoardContainer(options: {
4485
+ from: Extract<
4486
+ SetupBootstrapContainerRef<typeof manifestContract>,
4487
+ { type: "sharedZone" | "sharedBoardContainer" }
4488
+ >;
4489
+ boardId: Extract<
4490
+ SetupBootstrapPerPlayerContainerTemplateRef<typeof manifestContract>,
4491
+ { type: "playerBoardContainer" }
4492
+ >["boardId"];
4493
+ containerId: Extract<
4494
+ SetupBootstrapPerPlayerContainerTemplateRef<typeof manifestContract>,
4495
+ { type: "playerBoardContainer" }
4496
+ >["containerId"];
4497
+ count: number;
4498
+ playerIds?: readonly PlayerId[];
4499
+ }): SetupBootstrapStep<typeof manifestContract> {
4500
+ return createDealToPlayerBoardContainerStep<typeof manifestContract>(options);
4501
+ }
4502
+
4503
+ export function seedSharedBoardContainer(options: {
4504
+ from: SetupBootstrapContainerRef<typeof manifestContract>;
4505
+ boardId: Extract<
4506
+ SetupBootstrapDestinationRef<typeof manifestContract>,
4507
+ { type: "sharedBoardContainer" }
4508
+ >["boardId"];
4509
+ containerId: Extract<
4510
+ SetupBootstrapDestinationRef<typeof manifestContract>,
4511
+ { type: "sharedBoardContainer" }
4512
+ >["containerId"];
4513
+ count?: number;
4514
+ componentIds?: readonly (
4515
+ | CardIdOfManifest<typeof manifestContract>
4516
+ | PieceIdOfManifest<typeof manifestContract>
4517
+ | DieIdOfManifest<typeof manifestContract>
4518
+ )[];
4519
+ }): SetupBootstrapStep<typeof manifestContract> {
4520
+ return createSeedSharedBoardContainerStep<typeof manifestContract>(options);
4521
+ }
4522
+
4523
+ export function seedSharedBoardSpace(options: {
4524
+ from: SetupBootstrapContainerRef<typeof manifestContract>;
4525
+ boardId: Extract<
4526
+ SetupBootstrapDestinationRef<typeof manifestContract>,
4527
+ { type: "sharedBoardSpace" }
4528
+ >["boardId"];
4529
+ spaceId: Extract<
4530
+ SetupBootstrapDestinationRef<typeof manifestContract>,
4531
+ { type: "sharedBoardSpace" }
4532
+ >["spaceId"];
4533
+ count?: number;
4534
+ componentIds?: readonly (
4535
+ | CardIdOfManifest<typeof manifestContract>
4536
+ | PieceIdOfManifest<typeof manifestContract>
4537
+ | DieIdOfManifest<typeof manifestContract>
4538
+ )[];
4539
+ }): SetupBootstrapStep<typeof manifestContract> {
4540
+ return createSeedSharedBoardSpaceStep<typeof manifestContract>(options);
4541
+ }
4542
+
4543
+ export default manifestContract;
4544
+ `;
4545
+ }
4546
+ export function generateManifestContractSource(manifest) {
4547
+ return renderManifestContractSource(manifest);
4548
+ }
4549
+ const generatedFileBanner = `/**
4550
+ * Generated file.
4551
+ * Do not edit directly.
4552
+ */`;
4553
+ function extractGeneratedLiteralsBlock(source) {
4554
+ const startMarker = "export const literals = {";
4555
+ const endMarker = "\n\n// PlayerId is an opaque brand imported from @dreamboard/app-sdk/reducer.";
4556
+ const start = source.indexOf(startMarker);
4557
+ const end = source.indexOf(endMarker, start);
4558
+ if (start === -1 || end === -1) {
4559
+ throw new Error("Unable to locate generated manifest literals block.");
4560
+ }
4561
+ return source.slice(start, end).trim();
4562
+ }
4563
+ function renderSplitLiteralsBlock(legacySource) {
4564
+ return extractGeneratedLiteralsBlock(legacySource).replace(/ \/\/ literals satisfy `ManifestLiterals<PlayerId, \.\.\.>`\. The cast is safe\n \/\/ because the runtime values are the exact player-id strings the manifest\n \/\/ authored; branding is purely a type-level discipline\.\n playerIds: ([\s\S]*?) as unknown as readonly PlayerId\[],\n phaseNames:/, " playerIds: $1,\n phaseNames:");
4565
+ }
4566
+ function renderManifestLiteralsSource(legacySource) {
4567
+ return `${generatedFileBanner}
4568
+
4569
+ ${renderSplitLiteralsBlock(legacySource)}
4570
+ `;
4571
+ }
4572
+ function renderManifestRuntimeSource(legacySource) {
4573
+ const literalsBlock = extractGeneratedLiteralsBlock(legacySource);
4574
+ const withoutLiterals = legacySource.replace(literalsBlock, "export { literals };");
4575
+ return withoutLiterals
4576
+ .replace(generatedFileBanner, `${generatedFileBanner}\n// @ts-nocheck`)
4577
+ .replace(`} from "@dreamboard/app-sdk/reducer";\n\nconst unknownRecordSchema`, `} from "@dreamboard/app-sdk/reducer";\nimport { literals } from "./manifest-literals";\nimport type { PlayerId as PublicPlayerId, TableState as PublicTableState } from "./manifest-types";\n\nconst unknownRecordSchema`)
4578
+ .replace(`export const manifestContract: ReducerManifestContract<\n RuntimeTableRecord,`, `export const manifestContract: ReducerManifestContract<\n PublicTableState,`)
4579
+ .replace(`export const manifestContract: ReducerManifestContract<\n PublicTableState,\n string,\n PlayerId,`, `export const manifestContract: ReducerManifestContract<\n PublicTableState,\n string,\n PublicPlayerId,`)
4580
+ .replace(`const playerIdSchema = markManifestScopedSchema(\n z\n .string()\n .min(1)\n .transform((value) => asPlayerId(value)),\n);`, `const playerIdSchema = markManifestScopedSchema(\n z\n .string()\n .min(1)\n .transform((value) => asPlayerId(value)),\n) as unknown as z.ZodType<PublicPlayerId>;`);
4581
+ }
4582
+ function renderCardSetTypeSections(analysis) {
4583
+ return analysis.cardSets
4584
+ .map((cardSet) => {
4585
+ const cardIdType = `export type ${toPascalCase(cardSet.id)}CardId = ${cardSet.cards
4586
+ .flatMap(renderCardInstanceIds)
4587
+ .map((cardId) => quote(cardId))
4588
+ .join(" | ") || "never"};`;
4589
+ if (isCardPropertySchemaVariants(cardSet.cardSchema)) {
4590
+ const variantBlocks = Object.keys(cardSet.cardSchema.variants).map((cardType) => `export type ${cardPropertiesTypeName(cardSet, cardType)} = ${renderTypeForObjectSchema(mergeSharedCardProperties(cardSet.cardSchema, cardType))};`);
4591
+ const variantTypeNames = Object.keys(cardSet.cardSchema.variants).map((cardType) => cardPropertiesTypeName(cardSet, cardType));
4592
+ return renderBlocks([
4593
+ ...variantBlocks,
4594
+ `export type ${toPascalCase(cardSet.id)}CardProperties = ${variantTypeNames.join(" | ") || "RuntimeRecord"};`,
4595
+ cardIdType,
4596
+ ]);
4597
+ }
4598
+ return renderBlocks([
4599
+ `export type ${toPascalCase(cardSet.id)}CardProperties = ${renderTypeForObjectSchema(cardSet.cardSchema)};`,
4600
+ cardIdType,
4601
+ ]);
4602
+ })
4603
+ .join("\n\n");
4604
+ }
4605
+ function renderTopologyFieldTypeSections(analysis) {
4606
+ const sections = [];
4607
+ for (const board of analysis.analyzedBoards) {
4608
+ sections.push(`export type ${boardFieldsTypeName(board.board.id)} = ${renderTypeForObjectSchema(board.boardFieldsSchema)};`, `export type ${boardSpaceFieldsTypeName(board.board.id)} = ${renderTypeForObjectSchema(board.spaceFieldsSchema)};`);
4609
+ if (board.layout !== "hex") {
4610
+ sections.push(`export type ${boardRelationFieldsTypeName(board.board.id)} = ${renderTypeForObjectSchema(board.relationFieldsSchema)};`, `export type ${boardContainerFieldsTypeName(board.board.id)} = ${renderTypeForObjectSchema(board.containerFieldsSchema)};`);
4611
+ }
4612
+ if (board.layout !== "generic") {
4613
+ sections.push(`export type ${hexEdgeFieldsTypeName(board.board.id)} = ${renderTypeForObjectSchema(board.edgeFieldsSchema)};`, `export type ${hexVertexFieldsTypeName(board.board.id)} = ${renderTypeForObjectSchema(board.vertexFieldsSchema)};`);
4614
+ }
4615
+ }
4616
+ for (const pieceTypeId of analysis.pieceTypeIds) {
4617
+ sections.push(`export type ${pieceFieldsTypeName(pieceTypeId)} = ${renderTypeForObjectSchema(analysis.pieceTypeSchemasById.get(pieceTypeId))};`);
4618
+ }
4619
+ for (const dieTypeId of analysis.dieTypeIds) {
4620
+ sections.push(`export type ${dieFieldsTypeName(dieTypeId)} = ${renderTypeForObjectSchema(analysis.dieTypeSchemasById.get(dieTypeId))};`);
4621
+ }
4622
+ return sections.join("\n");
4623
+ }
4624
+ function renderManifestTypesSource(manifest) {
4625
+ const analysis = analyzeManifest(manifest);
4626
+ const sharedZoneIds = analysis.sharedZones.map((zone) => zone.id).sort();
4627
+ const playerZoneIds = analysis.playerZones.map((zone) => zone.id).sort();
4628
+ const cardSetCardIdAlias = (cardSetId) => `${toPascalCase(cardSetId)}CardId`;
4629
+ const renderCardIdsForCardSets = (cardSetIds) => cardSetIds.map(cardSetCardIdAlias).join(" | ") || "never";
4630
+ const sharedZoneCardIdEntries = sharedZoneIds
4631
+ .map((zoneId) => {
4632
+ const allowedCardSetIds = analysis.sharedZoneCardSetIds.get(zoneId) ?? [];
4633
+ return ` ${quote(zoneId)}: Array<${renderCardIdsForCardSets(allowedCardSetIds)}>;`;
4634
+ })
4635
+ .join("\n");
4636
+ const playerZoneCardIdEntries = playerZoneIds
4637
+ .map((zoneId) => {
4638
+ const allowedCardSetIds = analysis.playerZoneCardSetIds.get(zoneId) ?? [];
4639
+ return ` ${quote(zoneId)}: PerPlayer<Array<${renderCardIdsForCardSets(allowedCardSetIds)}>>;`;
4640
+ })
4641
+ .join("\n");
4642
+ const perBoardStateEntries = analysis.analyzedBoards
4643
+ .flatMap((board) => board.runtimeBoardIds.map((runtimeBoardId) => {
4644
+ if (board.layout === "hex") {
4645
+ return ` ${quote(runtimeBoardId)}: HexBoardStateRecord<${quote(runtimeBoardId)}, ${renderStringUnion(board.spaces.map((space) => space.id))}, ${renderStringUnion(board.edges.map((edge) => edge.id))}, ${renderStringUnion(board.vertices.map((vertex) => vertex.id))}, ${boardFieldsTypeName(board.board.id)}, ${boardSpaceFieldsTypeName(board.board.id)}, ${hexEdgeFieldsTypeName(board.board.id)}, ${hexVertexFieldsTypeName(board.board.id)}>;`;
4646
+ }
4647
+ if (board.layout === "square") {
4648
+ return ` ${quote(runtimeBoardId)}: SquareBoardStateRecord<${quote(runtimeBoardId)}, ${renderStringUnion(board.spaces.map((space) => space.id))}, ${renderStringUnion(board.containers.map((container) => container.id))}, ${renderStringUnion(board.edges.map((edge) => edge.id))}, ${renderStringUnion(board.vertices.map((vertex) => vertex.id))}, ${boardFieldsTypeName(board.board.id)}, ${boardSpaceFieldsTypeName(board.board.id)}, ${boardRelationFieldsTypeName(board.board.id)}, ${boardContainerFieldsTypeName(board.board.id)}, ${hexEdgeFieldsTypeName(board.board.id)}, ${hexVertexFieldsTypeName(board.board.id)}>;`;
4649
+ }
4650
+ return ` ${quote(runtimeBoardId)}: GenericBoardStateRecord<${quote(runtimeBoardId)}, ${renderStringUnion(board.spaces.map((space) => space.id))}, ${renderStringUnion(board.containers.map((container) => container.id))}, ${boardFieldsTypeName(board.board.id)}, ${boardSpaceFieldsTypeName(board.board.id)}, ${boardRelationFieldsTypeName(board.board.id)}, ${boardContainerFieldsTypeName(board.board.id)}>;`;
4651
+ }))
4652
+ .join("\n");
4653
+ return `${generatedFileBanner}
4654
+
4655
+ import type {
4656
+ PerPlayer,
4657
+ RuntimeCardData,
4658
+ RuntimeCardVisibility,
4659
+ RuntimeComponentLocation,
4660
+ RuntimeDieData,
4661
+ RuntimeHandVisibilityMode,
4662
+ RuntimePieceData,
4663
+ RuntimeRecord,
4664
+ RuntimeTableRecord,
4665
+ } from "@dreamboard/app-sdk/reducer";
4666
+ import { literals } from "./manifest-literals";
4667
+
4668
+ export type PlayerId = (typeof literals.playerIds)[number];
4669
+ export type PhaseName = string;
4670
+ export type BoardLayout = (typeof literals.boardLayouts)[number];
4671
+ export type SetupOptionId = (typeof literals.setupOptionIds)[number];
4672
+ export type SetupProfileId = (typeof literals.setupProfileIds)[number];
4673
+ export type CardSetId = (typeof literals.cardSetIds)[number];
4674
+ export type CardType = (typeof literals.cardTypes)[number];
4675
+ export type CardId = (typeof literals.cardIds)[number];
4676
+ export type DeckId = (typeof literals.deckIds)[number];
4677
+ export type HandId = (typeof literals.handIds)[number];
4678
+ export type SharedZoneId = (typeof literals.sharedZoneIds)[number];
4679
+ export type PlayerZoneId = (typeof literals.playerZoneIds)[number];
4680
+ export type ZoneId = (typeof literals.zoneIds)[number];
4681
+ export type ResourceId = (typeof literals.resourceIds)[number];
4682
+ export type PieceTypeId = (typeof literals.pieceTypeIds)[number];
4683
+ export type PieceId = (typeof literals.pieceIds)[number];
4684
+ export type DieTypeId = (typeof literals.dieTypeIds)[number];
4685
+ export type DieId = (typeof literals.dieIds)[number];
4686
+ export type BoardTypeId = (typeof literals.boardTypeIds)[number];
4687
+ export type BoardBaseId = (typeof literals.boardBaseIds)[number];
4688
+ export type BoardId = (typeof literals.boardIds)[number];
4689
+ export type BoardContainerId = (typeof literals.boardContainerIds)[number];
4690
+ export type RelationTypeId = (typeof literals.relationTypeIds)[number];
4691
+ export type EdgeId = (typeof literals.edgeIds)[number];
4692
+ export type EdgeTypeId = (typeof literals.edgeTypeIds)[number];
4693
+ export type VertexId = (typeof literals.vertexIds)[number];
4694
+ export type VertexTypeId = (typeof literals.vertexTypeIds)[number];
4695
+ export type SpaceId = (typeof literals.spaceIds)[number];
4696
+ export type SpaceTypeId = (typeof literals.spaceTypeIds)[number];
4697
+
4698
+ ${renderCardSetTypeSections(analysis)}
4699
+
4700
+ ${renderTopologyFieldTypeSections(analysis)}
4701
+
4702
+ ${renderBoardFieldMapTypes(analysis)}
4703
+
4704
+ export type CardProperties = ${analysis.cardSets.length > 0
4705
+ ? analysis.cardSets
4706
+ .map((cardSet) => `${toPascalCase(cardSet.id)}CardProperties`)
4707
+ .join(" | ")
4708
+ : "RuntimeRecord"};
4709
+
4710
+ export type CardState = Omit<RuntimeCardData, "id" | "cardSetId" | "cardType" | "properties"> & {
4711
+ id: CardId;
4712
+ cardSetId: CardSetId;
4713
+ cardType: CardType;
4714
+ properties: CardProperties;
4715
+ };
4716
+ export type CardStateById = Record<CardId, CardState>;
4717
+ export type PieceStateById = Record<PieceId, RuntimePieceData>;
4718
+ export type DieStateById = Record<DieId, RuntimeDieData>;
4719
+ export type CardIdsBySharedZoneId = {
4720
+ ${sharedZoneCardIdEntries}
4721
+ };
4722
+ export type CardIdsByPlayerZoneId = {
4723
+ ${playerZoneCardIdEntries}
4724
+ };
4725
+ export type CardIdsByDeckId = CardIdsBySharedZoneId;
4726
+ export type ComponentId = CardId | PieceId | DieId;
4727
+
4728
+ export interface BoardSpaceStateRecord<SpaceIdValue extends SpaceId = SpaceId, Fields = RuntimeRecord> {
4729
+ id: SpaceIdValue;
4730
+ name?: string | null;
4731
+ typeId?: SpaceTypeId | null;
4732
+ fields: Fields;
4733
+ zoneId?: string | null;
4734
+ }
4735
+ export interface BoardRelationStateRecord<SpaceIdValue extends SpaceId = SpaceId, Fields = RuntimeRecord> {
4736
+ id?: string | null;
4737
+ typeId: RelationTypeId;
4738
+ fromSpaceId: SpaceIdValue;
4739
+ toSpaceId: SpaceIdValue;
4740
+ directed: boolean;
4741
+ fields: Fields;
4742
+ }
4743
+ export interface BoardContainerStateRecord<SpaceIdValue extends SpaceId = SpaceId, ContainerIdValue extends BoardContainerId = BoardContainerId, Fields = RuntimeRecord> {
4744
+ id: ContainerIdValue;
4745
+ name: string;
4746
+ host: { type: "board" } | { type: "space"; spaceId: SpaceIdValue };
4747
+ allowedCardSetIds?: readonly CardSetId[];
4748
+ zoneId: string;
4749
+ fields: Fields;
4750
+ }
4751
+ export interface BoardStateRecordBase<BoardIdValue extends BoardId = BoardId, BoardFields = RuntimeRecord> {
4752
+ id: BoardIdValue;
4753
+ baseId: BoardBaseId;
4754
+ typeId?: BoardTypeId | null;
4755
+ scope: "shared" | "perPlayer";
4756
+ playerId?: PlayerId | null;
4757
+ templateId?: string | null;
4758
+ fields: BoardFields;
4759
+ }
4760
+ export interface GenericBoardStateRecord<BoardIdValue extends BoardId = BoardId, SpaceIdValue extends SpaceId = SpaceId, ContainerIdValue extends BoardContainerId = BoardContainerId, BoardFields = RuntimeRecord, SpaceFields = RuntimeRecord, RelationFields = RuntimeRecord, ContainerFields = RuntimeRecord> extends BoardStateRecordBase<BoardIdValue, BoardFields> {
4761
+ layout: "generic";
4762
+ spaces: Record<SpaceIdValue, BoardSpaceStateRecord<SpaceIdValue, SpaceFields>>;
4763
+ relations: Array<BoardRelationStateRecord<SpaceIdValue, RelationFields>>;
4764
+ containers: Record<ContainerIdValue, BoardContainerStateRecord<SpaceIdValue, ContainerIdValue, ContainerFields>>;
4765
+ }
4766
+ export interface HexSpaceStateRecord<SpaceIdValue extends SpaceId = SpaceId, Fields = RuntimeRecord> extends BoardSpaceStateRecord<SpaceIdValue, Fields> {
4767
+ q: number;
4768
+ r: number;
4769
+ }
4770
+ export interface SquareSpaceStateRecord<SpaceIdValue extends SpaceId = SpaceId, Fields = RuntimeRecord> extends BoardSpaceStateRecord<SpaceIdValue, Fields> {
4771
+ row: number;
4772
+ col: number;
4773
+ }
4774
+ export interface TiledEdgeStateRecord<SpaceIdValue extends SpaceId = SpaceId, EdgeIdValue extends EdgeId = EdgeId, Fields = RuntimeRecord> {
4775
+ id: EdgeIdValue;
4776
+ spaceIds: readonly SpaceIdValue[];
4777
+ typeId?: EdgeTypeId | null;
4778
+ label?: string | null;
4779
+ ownerId?: PlayerId | null;
4780
+ fields: Fields;
4781
+ }
4782
+ export interface TiledVertexStateRecord<SpaceIdValue extends SpaceId = SpaceId, VertexIdValue extends VertexId = VertexId, Fields = RuntimeRecord> {
4783
+ id: VertexIdValue;
4784
+ spaceIds: readonly SpaceIdValue[];
4785
+ typeId?: VertexTypeId | null;
4786
+ label?: string | null;
4787
+ ownerId?: PlayerId | null;
4788
+ fields: Fields;
4789
+ }
4790
+ export type HexEdgeStateRecord<SpaceIdValue extends SpaceId = SpaceId, EdgeIdValue extends EdgeId = EdgeId, Fields = RuntimeRecord> = TiledEdgeStateRecord<SpaceIdValue, EdgeIdValue, Fields>;
4791
+ export type HexVertexStateRecord<SpaceIdValue extends SpaceId = SpaceId, VertexIdValue extends VertexId = VertexId, Fields = RuntimeRecord> = TiledVertexStateRecord<SpaceIdValue, VertexIdValue, Fields>;
4792
+ export interface HexBoardStateRecord<BoardIdValue extends BoardId = BoardId, SpaceIdValue extends SpaceId = SpaceId, EdgeIdValue extends EdgeId = EdgeId, VertexIdValue extends VertexId = VertexId, BoardFields = RuntimeRecord, SpaceFields = RuntimeRecord, EdgeFields = RuntimeRecord, VertexFields = RuntimeRecord> extends BoardStateRecordBase<BoardIdValue, BoardFields> {
4793
+ layout: "hex";
4794
+ spaces: Record<SpaceIdValue, HexSpaceStateRecord<SpaceIdValue, SpaceFields>>;
4795
+ relations: Array<BoardRelationStateRecord<SpaceIdValue, RuntimeRecord>>;
4796
+ containers: Record<never, never>;
4797
+ orientation: "pointy-top" | "flat-top";
4798
+ edges: Array<HexEdgeStateRecord<SpaceIdValue, EdgeIdValue, EdgeFields>>;
4799
+ vertices: Array<HexVertexStateRecord<SpaceIdValue, VertexIdValue, VertexFields>>;
4800
+ }
4801
+ export interface SquareBoardStateRecord<BoardIdValue extends BoardId = BoardId, SpaceIdValue extends SpaceId = SpaceId, ContainerIdValue extends BoardContainerId = BoardContainerId, EdgeIdValue extends EdgeId = EdgeId, VertexIdValue extends VertexId = VertexId, BoardFields = RuntimeRecord, SpaceFields = RuntimeRecord, RelationFields = RuntimeRecord, ContainerFields = RuntimeRecord, EdgeFields = RuntimeRecord, VertexFields = RuntimeRecord> extends BoardStateRecordBase<BoardIdValue, BoardFields> {
4802
+ layout: "square";
4803
+ spaces: Record<SpaceIdValue, SquareSpaceStateRecord<SpaceIdValue, SpaceFields>>;
4804
+ relations: Array<BoardRelationStateRecord<SpaceIdValue, RelationFields>>;
4805
+ containers: Record<ContainerIdValue, BoardContainerStateRecord<SpaceIdValue, ContainerIdValue, ContainerFields>>;
4806
+ edges: Array<TiledEdgeStateRecord<SpaceIdValue, EdgeIdValue, EdgeFields>>;
4807
+ vertices: Array<TiledVertexStateRecord<SpaceIdValue, VertexIdValue, VertexFields>>;
4808
+ }
4809
+ export type BoardStateById = {
4810
+ ${perBoardStateEntries}
4811
+ };
4812
+ export type BoardState<BoardIdValue extends BoardId = BoardId> = BoardIdValue extends keyof BoardStateById ? BoardStateById[BoardIdValue] : never;
4813
+ export type BoardFields<BoardIdValue extends BoardId = BoardId> = BoardIdValue extends keyof BoardFieldsByBoardId ? BoardFieldsByBoardId[BoardIdValue] : RuntimeRecord;
4814
+ export type BoardSpaceState<BoardIdValue extends BoardId = BoardId> = BoardState<BoardIdValue> extends { spaces: Record<string, infer Space> } ? Space : never;
4815
+ export type BoardSpaceFields<BoardIdValue extends BoardId = BoardId> = BoardIdValue extends keyof BoardSpaceFieldsByBoardId ? BoardSpaceFieldsByBoardId[BoardIdValue] : RuntimeRecord;
4816
+ export type BoardRelationState<BoardIdValue extends BoardId = BoardId> = BoardState<BoardIdValue> extends { relations: Array<infer Relation> } ? Relation : never;
4817
+ export type BoardRelationFields<BoardIdValue extends BoardId = BoardId> = BoardIdValue extends keyof BoardRelationFieldsByBoardId ? BoardRelationFieldsByBoardId[BoardIdValue] : RuntimeRecord;
4818
+ export type BoardContainerState<BoardIdValue extends BoardId = BoardId> = BoardState<BoardIdValue> extends { containers: Record<string, infer Container> } ? Container : never;
4819
+ export type BoardContainerFields<BoardIdValue extends BoardId = BoardId> = BoardIdValue extends keyof BoardContainerFieldsByBoardId ? BoardContainerFieldsByBoardId[BoardIdValue] : RuntimeRecord;
4820
+ export type HexBoardStateById = {
4821
+ [BoardIdValue in keyof BoardStateById as BoardStateById[BoardIdValue] extends { layout: "hex" } ? BoardIdValue : never]: BoardStateById[BoardIdValue];
4822
+ };
4823
+ export type SquareBoardStateById = {
4824
+ [BoardIdValue in keyof BoardStateById as BoardStateById[BoardIdValue] extends { layout: "square" } ? BoardIdValue : never]: BoardStateById[BoardIdValue];
4825
+ };
4826
+ export type HexEdgeState<BoardIdValue extends BoardId = BoardId> = BoardState<BoardIdValue> extends { layout: "hex"; edges: Array<infer Edge> } ? Edge : never;
4827
+ export type HexEdgeFields<BoardIdValue extends BoardId = BoardId> = BoardIdValue extends keyof HexEdgeFieldsByBoardId ? HexEdgeFieldsByBoardId[BoardIdValue] : RuntimeRecord;
4828
+ export type HexVertexState<BoardIdValue extends BoardId = BoardId> = BoardState<BoardIdValue> extends { layout: "hex"; vertices: Array<infer Vertex> } ? Vertex : never;
4829
+ export type HexVertexFields<BoardIdValue extends BoardId = BoardId> = BoardIdValue extends keyof HexVertexFieldsByBoardId ? HexVertexFieldsByBoardId[BoardIdValue] : RuntimeRecord;
4830
+ export type SquareEdgeState<BoardIdValue extends BoardId = BoardId> = BoardState<BoardIdValue> extends { layout: "square"; edges: Array<infer Edge> } ? Edge : never;
4831
+ export type SquareEdgeFields<BoardIdValue extends BoardId = BoardId> = BoardIdValue extends keyof SquareEdgeFieldsByBoardId ? SquareEdgeFieldsByBoardId[BoardIdValue] : RuntimeRecord;
4832
+ export type SquareVertexState<BoardIdValue extends BoardId = BoardId> = BoardState<BoardIdValue> extends { layout: "square"; vertices: Array<infer Vertex> } ? Vertex : never;
4833
+ export type SquareVertexFields<BoardIdValue extends BoardId = BoardId> = BoardIdValue extends keyof SquareVertexFieldsByBoardId ? SquareVertexFieldsByBoardId[BoardIdValue] : RuntimeRecord;
4834
+ export type TiledBoardId = keyof TiledEdgeFieldsByBoardId | keyof TiledVertexFieldsByBoardId;
4835
+ export type TiledEdgeState<BoardIdValue extends TiledBoardId = TiledBoardId> = BoardIdValue extends BoardId ? HexEdgeState<BoardIdValue> | SquareEdgeState<BoardIdValue> : never;
4836
+ export type TiledEdgeFields<BoardIdValue extends TiledBoardId = TiledBoardId> = BoardIdValue extends keyof TiledEdgeFieldsByBoardId ? TiledEdgeFieldsByBoardId[BoardIdValue] : RuntimeRecord;
4837
+ export type TiledVertexState<BoardIdValue extends TiledBoardId = TiledBoardId> = BoardIdValue extends BoardId ? HexVertexState<BoardIdValue> | SquareVertexState<BoardIdValue> : never;
4838
+ export type TiledVertexFields<BoardIdValue extends TiledBoardId = TiledBoardId> = BoardIdValue extends keyof TiledVertexFieldsByBoardId ? TiledVertexFieldsByBoardId[BoardIdValue] : RuntimeRecord;
4839
+ export type BoardStateRecord = BoardStateById[BoardId];
4840
+
4841
+ export type TableState = Omit<RuntimeTableRecord, "playerOrder" | "zones" | "decks" | "hands" | "handVisibility" | "cards" | "pieces" | "componentLocations" | "ownerOfCard" | "visibility" | "resources" | "boards" | "dice"> & {
4842
+ playerOrder: PlayerId[];
4843
+ zones: RuntimeTableRecord["zones"] & {
4844
+ visibility: Record<ZoneId, RuntimeHandVisibilityMode>;
4845
+ cardSetIdsByZoneId?: Record<ZoneId, readonly CardSetId[]>;
4846
+ };
4847
+ decks: CardIdsBySharedZoneId;
4848
+ hands: CardIdsByPlayerZoneId;
4849
+ handVisibility: Record<PlayerZoneId, RuntimeHandVisibilityMode>;
4850
+ cards: CardStateById;
4851
+ pieces: PieceStateById;
4852
+ componentLocations: Record<ComponentId, RuntimeComponentLocation>;
4853
+ ownerOfCard: Record<CardId, PlayerId | null>;
4854
+ visibility: Record<CardId, RuntimeCardVisibility>;
4855
+ resources: PerPlayer<Record<ResourceId, number>>;
4856
+ boards: {
4857
+ byId: BoardStateById;
4858
+ hex: HexBoardStateById;
4859
+ square: SquareBoardStateById;
4860
+ network: Record<string, RuntimeRecord>;
4861
+ track: Record<string, RuntimeRecord>;
4862
+ };
4863
+ dice: DieStateById;
4864
+ };
4865
+ `;
4866
+ }
4867
+ function extractGeneratedRuntimeValueExports(source) {
4868
+ const exports = new Set();
4869
+ for (const match of source.matchAll(/^export (?:const|function) ([A-Za-z_$][\w$]*)/gm)) {
4870
+ const name = match[1];
4871
+ if (name !== "literals") {
4872
+ exports.add(name);
4873
+ }
4874
+ }
4875
+ return [...exports].sort((left, right) => left.localeCompare(right));
4876
+ }
4877
+ function renderManifestContractBarrelSource(legacySource) {
4878
+ const runtimeExports = extractGeneratedRuntimeValueExports(legacySource);
4879
+ return `${generatedFileBanner}
4880
+
4881
+ export { literals } from "./manifest-literals";
4882
+ export type * from "./manifest-types";
4883
+ export {
4884
+ ${runtimeExports.map((name) => ` ${name},`).join("\n")}
4885
+ } from "./manifest-runtime";
4886
+ export { default } from "./manifest-runtime";
4887
+ `;
4888
+ }
4889
+ export function generateManifestContractSources(manifest) {
4890
+ const legacySource = renderManifestContractSource(manifest);
4891
+ return {
4892
+ "shared/manifest-literals.ts": renderManifestLiteralsSource(legacySource),
4893
+ "shared/manifest-types.ts": renderManifestTypesSource(manifest),
4894
+ "shared/manifest-runtime.ts": renderManifestRuntimeSource(legacySource),
4895
+ "shared/manifest-contract.ts": renderManifestContractBarrelSource(legacySource),
4896
+ };
4897
+ }