@dreamboard-games/sdk 0.5.0-alpha.2 → 0.5.0-alpha.4

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 (151) hide show
  1. package/README.md +186 -45
  2. package/dist/board-target-B0QG6OYx.d.ts +8 -0
  3. package/dist/chunk-3JXUL4VN.js +139 -0
  4. package/dist/chunk-3JXUL4VN.js.map +1 -0
  5. package/dist/{chunk-S2JEDVWH.js → chunk-C77SYIVT.js} +6102 -6505
  6. package/dist/chunk-C77SYIVT.js.map +1 -0
  7. package/dist/chunk-DMFVAZI2.js +16 -0
  8. package/dist/chunk-DMFVAZI2.js.map +1 -0
  9. package/dist/chunk-OOFMBCFF.js +1338 -0
  10. package/dist/chunk-OOFMBCFF.js.map +1 -0
  11. package/dist/chunk-UG2Q4NLG.js +590 -0
  12. package/dist/chunk-UG2Q4NLG.js.map +1 -0
  13. package/dist/chunk-XU6WJW6K.js +847 -0
  14. package/dist/chunk-XU6WJW6K.js.map +1 -0
  15. package/dist/diagnostics-B8d5aLad.d.ts +65 -0
  16. package/dist/extract-gVKcB8Og.d.ts +4736 -0
  17. package/dist/index.d.ts +3782 -1
  18. package/dist/index.js +1096 -6
  19. package/dist/index.js.map +1 -1
  20. package/dist/model-vdpLHrxh.d.ts +432 -0
  21. package/dist/react.d.ts +60 -0
  22. package/dist/react.js +208 -0
  23. package/dist/react.js.map +1 -0
  24. package/dist/reducer.d.ts +1244 -78
  25. package/dist/reducer.js +4204 -121
  26. package/dist/reducer.js.map +1 -1
  27. package/dist/testing.d.ts +520 -564
  28. package/dist/testing.js +2451 -915
  29. package/dist/testing.js.map +1 -1
  30. package/dist/types-Bj5ZLTUV.d.ts +114 -0
  31. package/package.json +40 -142
  32. package/dist/HandView-Bi2lxzQP.d.ts +0 -79
  33. package/dist/ResourceCounter-BFxJknqp.d.ts +0 -98
  34. package/dist/ThemeProvider-CElaL2ik.d.ts +0 -99
  35. package/dist/attributes-DbvyMbXw.d.ts +0 -68
  36. package/dist/browser-interaction.d.ts +0 -910
  37. package/dist/browser-interaction.js +0 -119
  38. package/dist/browser-interaction.js.map +0 -1
  39. package/dist/chunk-32PFKDV7.js +0 -11705
  40. package/dist/chunk-32PFKDV7.js.map +0 -1
  41. package/dist/chunk-3BAD4VC2.js +0 -263
  42. package/dist/chunk-3BAD4VC2.js.map +0 -1
  43. package/dist/chunk-3SKDNDPG.js +0 -104
  44. package/dist/chunk-3SKDNDPG.js.map +0 -1
  45. package/dist/chunk-5YV2WHJ4.js +0 -25
  46. package/dist/chunk-5YV2WHJ4.js.map +0 -1
  47. package/dist/chunk-ANRB4XKA.js +0 -90
  48. package/dist/chunk-ANRB4XKA.js.map +0 -1
  49. package/dist/chunk-BI4G3PM2.js +0 -1864
  50. package/dist/chunk-BI4G3PM2.js.map +0 -1
  51. package/dist/chunk-EVCLTY4Q.js +0 -935
  52. package/dist/chunk-EVCLTY4Q.js.map +0 -1
  53. package/dist/chunk-GDPKTBUR.js +0 -564
  54. package/dist/chunk-GDPKTBUR.js.map +0 -1
  55. package/dist/chunk-H6VDGFL5.js +0 -2903
  56. package/dist/chunk-H6VDGFL5.js.map +0 -1
  57. package/dist/chunk-LR3ZTWQF.js +0 -5960
  58. package/dist/chunk-LR3ZTWQF.js.map +0 -1
  59. package/dist/chunk-PF7L4BMG.js +0 -117
  60. package/dist/chunk-PF7L4BMG.js.map +0 -1
  61. package/dist/chunk-PN5O6GG2.js +0 -32
  62. package/dist/chunk-PN5O6GG2.js.map +0 -1
  63. package/dist/chunk-PZ5AY32C.js +0 -10
  64. package/dist/chunk-PZ5AY32C.js.map +0 -1
  65. package/dist/chunk-S2JEDVWH.js.map +0 -1
  66. package/dist/chunk-T3ZKNUZ7.js +0 -1
  67. package/dist/chunk-T3ZKNUZ7.js.map +0 -1
  68. package/dist/chunk-UGH54WZ2.js +0 -223
  69. package/dist/chunk-UGH54WZ2.js.map +0 -1
  70. package/dist/chunk-VCDWP5VL.js +0 -746
  71. package/dist/chunk-VCDWP5VL.js.map +0 -1
  72. package/dist/chunk-VDXOF4FW.js +0 -69
  73. package/dist/chunk-VDXOF4FW.js.map +0 -1
  74. package/dist/chunk-WHR5UW3F.js +0 -1988
  75. package/dist/chunk-WHR5UW3F.js.map +0 -1
  76. package/dist/chunk-Y75CFE77.js +0 -17
  77. package/dist/chunk-Y75CFE77.js.map +0 -1
  78. package/dist/chunk-Z7QCREWI.js +0 -3435
  79. package/dist/chunk-Z7QCREWI.js.map +0 -1
  80. package/dist/chunk-ZZGN2ATS.js +0 -1941
  81. package/dist/chunk-ZZGN2ATS.js.map +0 -1
  82. package/dist/components-gAbIz7hK.d.ts +0 -1284
  83. package/dist/definitions-BgmV_GhC.d.ts +0 -300
  84. package/dist/diagnostics-DIi3ina7.d.ts +0 -50
  85. package/dist/digest.d.ts +0 -8
  86. package/dist/game-CsZScpcn.d.ts +0 -763
  87. package/dist/hex-board-view-aKkblDp6.d.ts +0 -1228
  88. package/dist/index-CcT9Q7N7.d.ts +0 -193
  89. package/dist/index.d-DjzoK7zn.d.ts +0 -2101
  90. package/dist/package-set.d.ts +0 -13
  91. package/dist/package-set.js +0 -12
  92. package/dist/package-set.js.map +0 -1
  93. package/dist/player-state-Cqpyeql0.d.ts +0 -371
  94. package/dist/plugin-runtime-contract.d.ts +0 -17
  95. package/dist/plugin-runtime-contract.js +0 -92
  96. package/dist/plugin-runtime-contract.js.map +0 -1
  97. package/dist/primitive-props-BNHDkgd7.d.ts +0 -16
  98. package/dist/protocol-dYgafTYY.d.ts +0 -314
  99. package/dist/reducer/advanced.d.ts +0 -82
  100. package/dist/reducer/advanced.js +0 -51
  101. package/dist/reducer/advanced.js.map +0 -1
  102. package/dist/reducer-contract.d.ts +0 -11
  103. package/dist/reducer-contract.js +0 -16
  104. package/dist/reducer-contract.js.map +0 -1
  105. package/dist/reference-games/index.d.ts +0 -31
  106. package/dist/reference-games/index.js +0 -48
  107. package/dist/reference-games/index.js.map +0 -1
  108. package/dist/runtime/primitives.d.ts +0 -250
  109. package/dist/runtime/primitives.js +0 -189
  110. package/dist/runtime/primitives.js.map +0 -1
  111. package/dist/runtime/runtime-api.d.ts +0 -2
  112. package/dist/runtime/runtime-api.js +0 -1
  113. package/dist/runtime/runtime-api.js.map +0 -1
  114. package/dist/runtime/workspace-contract.d.ts +0 -392
  115. package/dist/runtime/workspace-contract.js +0 -28
  116. package/dist/runtime/workspace-contract.js.map +0 -1
  117. package/dist/runtime-RJ5orDJU.d.ts +0 -1699
  118. package/dist/runtime-api-Bz5pwNU_.d.ts +0 -296
  119. package/dist/runtime-json-CQ9QbLZ5.d.ts +0 -5
  120. package/dist/runtime.d.ts +0 -83
  121. package/dist/runtime.js +0 -242
  122. package/dist/runtime.js.map +0 -1
  123. package/dist/schema.d.ts +0 -379
  124. package/dist/stale-contract-artifact-error-XLaweZtF.d.ts +0 -18
  125. package/dist/testing-compiler.d.ts +0 -37
  126. package/dist/testing-compiler.js +0 -272
  127. package/dist/testing-compiler.js.map +0 -1
  128. package/dist/testing-runtime.d.ts +0 -119
  129. package/dist/testing-runtime.js +0 -212
  130. package/dist/testing-runtime.js.map +0 -1
  131. package/dist/types-DJj5MJkl.d.ts +0 -256
  132. package/dist/types-DR7DoB1x.d.ts +0 -28
  133. package/dist/types-JWCYHmu7.d.ts +0 -122
  134. package/dist/types.d.ts +0 -1958
  135. package/dist/types.js +0 -14
  136. package/dist/types.js.map +0 -1
  137. package/dist/ui/components.d.ts +0 -17
  138. package/dist/ui/components.js +0 -216
  139. package/dist/ui/components.js.map +0 -1
  140. package/dist/ui/defaults.d.ts +0 -19
  141. package/dist/ui/defaults.js +0 -104
  142. package/dist/ui/defaults.js.map +0 -1
  143. package/dist/ui/player-state.d.ts +0 -2
  144. package/dist/ui/player-state.js +0 -1
  145. package/dist/ui/player-state.js.map +0 -1
  146. package/dist/ui/plugin-styles.css +0 -2
  147. package/dist/ui-contract-GTUPkf8z.d.ts +0 -1168
  148. package/dist/ui.d.ts +0 -317
  149. package/dist/ui.js +0 -277
  150. package/dist/ui.js.map +0 -1
  151. package/dist/views-BpuWcOyN.d.ts +0 -1641
package/dist/reducer.js CHANGED
@@ -1,172 +1,4255 @@
1
- import {
2
- StaleContractArtifactError,
3
- contractFingerprint,
4
- createDerivedResolver,
5
- createReducerBundle,
6
- createReducerOps,
7
- createStateQueries,
8
- createTableQueries,
9
- defineDerived,
10
- isStaleContractArtifactError,
11
- noopDiagnosticsSink
12
- } from "./chunk-S2JEDVWH.js";
13
- import {
14
- compileManifest,
15
- createGame,
16
- defineInputs,
17
- many,
18
- normalizeCommandParams,
19
- sparseCounts,
20
- sparseMap
21
- } from "./chunk-ZZGN2ATS.js";
22
1
  import {
23
2
  FrameworkErrorCodes,
3
+ REDUCER_CONTRACT_VERSION,
24
4
  asPlayerId,
25
5
  assertCardAllowedInContainer,
6
+ assertReducerBundleContract,
7
+ assumeManifestSchema,
26
8
  boardRef,
27
9
  boardRefKey,
28
10
  boardRefSchema,
29
- isPerPlayer,
11
+ createManifestGameStateSchema,
12
+ createManifestRuntimeSchema,
13
+ createManifestStringLiteralSchema,
14
+ createReducerBundle,
15
+ createStateQueries,
16
+ createTableQueries,
17
+ isManifestScopedSchema,
30
18
  isPerPlayerBoardRef,
31
19
  isPlayerId,
32
20
  isSharedBoardRef,
21
+ markManifestScopedSchema,
22
+ noopDiagnosticsSink,
33
23
  parseBoardRefKey,
34
- perPlayer,
35
24
  perPlayerBoardRef,
36
- perPlayerEntries,
37
- perPlayerGet,
38
- perPlayerHas,
39
- perPlayerKeys,
40
- perPlayerMap,
41
- perPlayerRequire,
42
- perPlayerSchema,
43
- perPlayerSet,
44
- perPlayerSize,
45
- perPlayerValues,
46
- setActivePlayers,
47
- sharedBoardRef
48
- } from "./chunk-H6VDGFL5.js";
49
- import "./chunk-5YV2WHJ4.js";
50
- import "./chunk-PF7L4BMG.js";
51
- import "./chunk-Z7QCREWI.js";
52
- import "./chunk-UGH54WZ2.js";
53
- import "./chunk-PZ5AY32C.js";
54
-
55
- // src/reducer/authoring/effect.ts
56
- import { z } from "zod";
57
- function createContinuationCallable(definition) {
58
- const continuation = ((data) => {
59
- return {
60
- id: continuation.id,
61
- data
62
- };
25
+ sharedBoardRef,
26
+ validatedReducerDefinition
27
+ } from "./chunk-C77SYIVT.js";
28
+ import {
29
+ createHexBoardGeometry,
30
+ fromCoordinates,
31
+ hexagon,
32
+ rectangle,
33
+ resolveHexSpaces,
34
+ ring,
35
+ runtime_schema_exports,
36
+ spiral
37
+ } from "./chunk-XU6WJW6K.js";
38
+ import {
39
+ isPlayerBoardSpaceTarget,
40
+ samePlayerBoardSpaceTarget
41
+ } from "./chunk-3JXUL4VN.js";
42
+
43
+ // src/reducer/manifest/compiler.ts
44
+ import { z as z2 } from "zod";
45
+
46
+ // src/reducer/manifest/generated-helpers.ts
47
+ function buildTypedRecord(ids, initial) {
48
+ const createValue = typeof initial === "function" ? initial : () => initial;
49
+ return Object.fromEntries(ids.map((id) => [id, createValue(id)]));
50
+ }
51
+
52
+ // src/reducer/manifest/manifest-validation.ts
53
+ function isHexBoardSpec(board) {
54
+ return board.layout === "hex";
55
+ }
56
+ function collectDuplicateIdIssues(options) {
57
+ const issues = [];
58
+ const pathsById = /* @__PURE__ */ new Map();
59
+ for (const entry of options.entries) {
60
+ const id = entry.id?.trim();
61
+ if (!id) {
62
+ continue;
63
+ }
64
+ const paths = pathsById.get(id) ?? [];
65
+ paths.push(entry.path);
66
+ pathsById.set(id, paths);
67
+ }
68
+ for (const [id, paths] of pathsById.entries()) {
69
+ if (paths.length > 1) {
70
+ issues.push(`${paths.join(", ")}: Duplicate ${options.label} '${id}'.`);
71
+ }
72
+ }
73
+ return issues;
74
+ }
75
+ var PROTOTYPE_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
76
+ "__proto__",
77
+ "prototype",
78
+ "constructor"
79
+ ]);
80
+ function validateRecordKey(value, path) {
81
+ if (!value || !PROTOTYPE_SENSITIVE_KEYS.has(value)) {
82
+ return [];
83
+ }
84
+ return [
85
+ `${path}: '${value}' is reserved and cannot be used as a generated record key.`
86
+ ];
87
+ }
88
+ function collectKeyIssues(entries) {
89
+ return entries.flatMap(({ value, path }) => validateRecordKey(value, path));
90
+ }
91
+ function renderCardInstanceIds(card) {
92
+ return card.count > 1 ? Array.from(
93
+ { length: card.count },
94
+ (_, index) => `${card.id}-${index + 1}`
95
+ ) : [card.id];
96
+ }
97
+ function collectPropertySchemaKeyIssues(schema, path) {
98
+ if (!schema) {
99
+ return [];
100
+ }
101
+ const issues = [];
102
+ if (schema.type === "object") {
103
+ for (const [key, property] of Object.entries(schema.properties ?? {})) {
104
+ issues.push(
105
+ ...validateRecordKey(key, `${path}.properties.${key}`),
106
+ ...collectPropertySchemaKeyIssues(
107
+ property,
108
+ `${path}.properties.${key}`
109
+ )
110
+ );
111
+ }
112
+ }
113
+ if (schema.type === "array") {
114
+ issues.push(
115
+ ...collectPropertySchemaKeyIssues(schema.items, `${path}.items`)
116
+ );
117
+ }
118
+ if (schema.type === "record") {
119
+ issues.push(
120
+ ...collectPropertySchemaKeyIssues(schema.values, `${path}.values`)
121
+ );
122
+ }
123
+ return issues;
124
+ }
125
+ function collectObjectSchemaKeyIssues(schema, path) {
126
+ const issues = [];
127
+ for (const [key, property] of Object.entries(schema?.properties ?? {})) {
128
+ issues.push(
129
+ ...validateRecordKey(key, `${path}.properties.${key}`),
130
+ ...collectPropertySchemaKeyIssues(property, `${path}.properties.${key}`)
131
+ );
132
+ }
133
+ return issues;
134
+ }
135
+ function collectCardSchemaKeyIssues(cardSet, path) {
136
+ const schema = cardSet.cardSchema;
137
+ if ("variants" in schema) {
138
+ return [
139
+ ...Object.entries(schema.shared ?? {}).flatMap(([key, property]) => [
140
+ ...validateRecordKey(key, `${path}.cardSchema.shared.${key}`),
141
+ ...collectPropertySchemaKeyIssues(
142
+ property,
143
+ `${path}.cardSchema.shared.${key}`
144
+ )
145
+ ]),
146
+ ...Object.entries(schema.variants).flatMap(
147
+ ([variantKey, variantSchema]) => [
148
+ ...validateRecordKey(
149
+ variantKey,
150
+ `${path}.cardSchema.variants.${variantKey}`
151
+ ),
152
+ ...collectObjectSchemaKeyIssues(
153
+ variantSchema,
154
+ `${path}.cardSchema.variants.${variantKey}`
155
+ )
156
+ ]
157
+ )
158
+ ];
159
+ }
160
+ return collectObjectSchemaKeyIssues(schema, `${path}.cardSchema`);
161
+ }
162
+ function expandSeedIds(seeds) {
163
+ return seeds.flatMap((seed) => {
164
+ const count = seed.count ?? 1;
165
+ const baseId = seed.id ?? seed.typeId;
166
+ return count > 1 ? Array.from({ length: count }, (_, index) => `${baseId}-${index + 1}`) : [baseId];
63
167
  });
64
- continuation.id = definition.id;
65
- continuation.source = "effect";
66
- continuation.dataSchema = definition.data;
67
- continuation.responseSchema = definition.response;
68
- continuation.reduce = definition.reduce;
69
- if (definition.effectKind !== void 0) {
70
- continuation.effectKind = definition.effectKind;
71
- }
72
- return continuation;
73
- }
74
- function defineEffect() {
75
- function effect(definition) {
76
- if (definition.type === "rollDie" || definition.type === "shuffleSharedZone" || definition.type === "shufflePlayerZone") {
77
- const effectKind = definition.type;
78
- if (!definition.reduce) {
79
- return {
80
- type: definition.type,
81
- id: definition.id,
82
- contextSchema: definition.context
83
- };
168
+ }
169
+ function validateTypeSlotDuplicates(options) {
170
+ const issues = [];
171
+ for (const [index, pieceType] of options.pieceTypes.entries()) {
172
+ issues.push(
173
+ ...collectDuplicateIdIssues({
174
+ entries: (pieceType.slots ?? []).map((slot, slotIndex) => ({
175
+ id: slot.id,
176
+ path: `manifest.pieceTypes[${index}].slots[${slotIndex}].id`
177
+ })),
178
+ label: "piece slot id"
179
+ })
180
+ );
181
+ }
182
+ for (const [index, dieType] of options.dieTypes.entries()) {
183
+ issues.push(
184
+ ...collectDuplicateIdIssues({
185
+ entries: (dieType.slots ?? []).map((slot, slotIndex) => ({
186
+ id: slot.id,
187
+ path: `manifest.dieTypes[${index}].slots[${slotIndex}].id`
188
+ })),
189
+ label: "die slot id"
190
+ })
191
+ );
192
+ }
193
+ return issues;
194
+ }
195
+ function validateSlotHostsAndHomes(manifest) {
196
+ const issues = [];
197
+ const pieceTypesById = new Map(
198
+ (manifest.pieceTypes ?? []).map(
199
+ (pieceType) => [pieceType.id, pieceType]
200
+ )
201
+ );
202
+ const dieTypesById = new Map(
203
+ (manifest.dieTypes ?? []).map((dieType) => [dieType.id, dieType])
204
+ );
205
+ const slotIdsByHostKey = /* @__PURE__ */ new Map();
206
+ for (const [index, seed] of (manifest.pieceSeeds ?? []).entries()) {
207
+ const pieceType = pieceTypesById.get(seed.typeId);
208
+ const slotIds = (pieceType?.slots ?? []).map((slot) => slot.id);
209
+ if (slotIds.length === 0) {
210
+ continue;
211
+ }
212
+ if (typeof seed.id !== "string" || seed.id.length === 0) {
213
+ issues.push(
214
+ `manifest.pieceSeeds[${index}].id: Piece seed for slot-bearing type '${seed.typeId}' must declare an explicit id.`
215
+ );
216
+ continue;
217
+ }
218
+ if ((seed.count ?? 1) !== 1) {
219
+ issues.push(
220
+ `manifest.pieceSeeds[${index}].count: Piece seed '${seed.id}' for slot-bearing type '${seed.typeId}' must omit count or set it to 1.`
221
+ );
222
+ continue;
223
+ }
224
+ slotIdsByHostKey.set(`piece:${seed.id}`, new Set(slotIds));
225
+ }
226
+ for (const [index, seed] of (manifest.dieSeeds ?? []).entries()) {
227
+ const dieType = dieTypesById.get(seed.typeId);
228
+ const slotIds = (dieType?.slots ?? []).map((slot) => slot.id);
229
+ if (slotIds.length === 0) {
230
+ continue;
231
+ }
232
+ if (typeof seed.id !== "string" || seed.id.length === 0) {
233
+ issues.push(
234
+ `manifest.dieSeeds[${index}].id: Die seed for slot-bearing type '${seed.typeId}' must declare an explicit id.`
235
+ );
236
+ continue;
237
+ }
238
+ if ((seed.count ?? 1) !== 1) {
239
+ issues.push(
240
+ `manifest.dieSeeds[${index}].count: Die seed '${seed.id}' for slot-bearing type '${seed.typeId}' must omit count or set it to 1.`
241
+ );
242
+ continue;
243
+ }
244
+ slotIdsByHostKey.set(`die:${seed.id}`, new Set(slotIds));
245
+ }
246
+ const validateHome = (home, path) => {
247
+ if (home?.type !== "slot") {
248
+ return;
249
+ }
250
+ const hostKey = `${home.host.kind}:${home.host.id}`;
251
+ const slotIds = slotIdsByHostKey.get(hostKey);
252
+ if (!slotIds) {
253
+ issues.push(
254
+ `${path}.host: Unknown strict slot host '${home.host.kind}:${home.host.id}'. Hosts must be singleton piece/die seeds whose type declares slots.`
255
+ );
256
+ return;
257
+ }
258
+ if (!slotIds.has(home.slotId)) {
259
+ issues.push(
260
+ `${path}.slotId: Unknown slot '${home.slotId}' for host '${home.host.kind}:${home.host.id}'.`
261
+ );
262
+ }
263
+ };
264
+ for (const [cardSetIndex, cardSet] of manifest.cardSets.entries()) {
265
+ validateHome(
266
+ cardSet.defaultHome,
267
+ `manifest.cardSets[${cardSetIndex}].defaultHome`
268
+ );
269
+ for (const [cardIndex, card] of cardSet.cards.entries()) {
270
+ validateHome(
271
+ card.home,
272
+ `manifest.cardSets[${cardSetIndex}].cards[${cardIndex}].home`
273
+ );
274
+ }
275
+ }
276
+ for (const [index, seed] of (manifest.pieceSeeds ?? []).entries()) {
277
+ validateHome(seed.home, `manifest.pieceSeeds[${index}].home`);
278
+ }
279
+ for (const [index, seed] of (manifest.dieSeeds ?? []).entries()) {
280
+ validateHome(seed.home, `manifest.dieSeeds[${index}].home`);
281
+ }
282
+ return issues;
283
+ }
284
+ function validatePlayerScopedSeedHomes(manifest) {
285
+ const issues = [];
286
+ const boardScopeById = new Map(
287
+ (manifest.boards ?? []).map((board) => [board.id, board.scope])
288
+ );
289
+ const zoneScopeById = new Map(
290
+ (manifest.zones ?? []).map((zone) => [zone.id, zone.scope])
291
+ );
292
+ const validateSeedHome = (seed, path, label) => {
293
+ const authoredId = seed.id ?? seed.typeId;
294
+ if (seed.ownerId) {
295
+ return;
296
+ }
297
+ if (homeTargetsBoard(seed.home) && boardScopeById.get(seed.home.boardId) === "perPlayer") {
298
+ issues.push(
299
+ `${path}.boardId: ${label} '${authoredId}' requires ownerId because board '${seed.home.boardId}' has scope 'perPlayer'. Add ownerId to resolve the player-scoped destination.`
300
+ );
301
+ return;
302
+ }
303
+ if (seed.home?.type === "zone" && zoneScopeById.get(seed.home.zoneId) === "perPlayer") {
304
+ issues.push(
305
+ `${path}.zoneId: ${label} '${authoredId}' requires ownerId because zone '${seed.home.zoneId}' has scope 'perPlayer'. Add ownerId to resolve the player-scoped destination.`
306
+ );
307
+ }
308
+ };
309
+ for (const [index, seed] of (manifest.pieceSeeds ?? []).entries()) {
310
+ validateSeedHome(seed, `manifest.pieceSeeds[${index}].home`, "Piece seed");
311
+ }
312
+ for (const [index, seed] of (manifest.dieSeeds ?? []).entries()) {
313
+ validateSeedHome(seed, `manifest.dieSeeds[${index}].home`, "Die seed");
314
+ }
315
+ return issues;
316
+ }
317
+ function validateCardHomes(manifest) {
318
+ const issues = [];
319
+ const boardScopeById = new Map(
320
+ (manifest.boards ?? []).map((board) => [board.id, board.scope])
321
+ );
322
+ const zoneScopeById = new Map(
323
+ (manifest.zones ?? []).map((zone) => [zone.id, zone.scope])
324
+ );
325
+ for (const [cardSetIndex, cardSet] of manifest.cardSets.entries()) {
326
+ if (!cardSet.defaultHome) {
327
+ issues.push(
328
+ `manifest.cardSets[${cardSetIndex}].defaultHome: Card sets must declare defaultHome.`
329
+ );
330
+ continue;
331
+ }
332
+ const validateCardHome = (home, path, label) => {
333
+ if (home?.type === "zone" && zoneScopeById.get(home.zoneId) === "perPlayer") {
334
+ issues.push(
335
+ `${path}.zoneId: ${label} cannot target per-player zone '${home.zoneId}' because card inventory has no ownerId. Place it during reducer setup instead.`
336
+ );
337
+ }
338
+ if (homeTargetsBoard(home) && boardScopeById.get(home.boardId) === "perPlayer") {
339
+ issues.push(
340
+ `${path}.boardId: ${label} cannot target per-player board '${home.boardId}' because card inventory has no ownerId. Place it during reducer setup instead.`
341
+ );
84
342
  }
85
- const contextSchema = definition.context ?? z.object({});
86
- const continuation = createContinuationCallable({
87
- id: definition.id,
88
- data: contextSchema,
89
- response: z.unknown(),
90
- effectKind,
91
- reduce: definition.reduce
343
+ };
344
+ validateCardHome(
345
+ cardSet.defaultHome,
346
+ `manifest.cardSets[${cardSetIndex}].defaultHome`,
347
+ `Card set '${cardSet.id}' defaultHome`
348
+ );
349
+ for (const [cardIndex, card] of cardSet.cards.entries()) {
350
+ const path = `manifest.cardSets[${cardSetIndex}].cards[${cardIndex}].home`;
351
+ validateCardHome(card.home, path, `Card '${card.id}'`);
352
+ }
353
+ }
354
+ return issues;
355
+ }
356
+ var CARD_IMAGE_PATH = /^assets\/(?:[\w-][\w.-]*\/)*[\w-][\w.-]*\.(?:avif|gif|jpe?g|png|svg|webp)$/i;
357
+ function validateCardImages(manifest) {
358
+ return manifest.cardSets.flatMap(
359
+ (cardSet, cardSetIndex) => cardSet.cards.flatMap(
360
+ (card, cardIndex) => ["frontImage", "backImage"].flatMap((key) => {
361
+ const image = card[key];
362
+ return image === void 0 || CARD_IMAGE_PATH.test(image) ? [] : [
363
+ `manifest.cardSets[${cardSetIndex}].cards[${cardIndex}].${key}: '${image}' must be an image path under assets/, such as assets/cards/front.webp.`
364
+ ];
365
+ })
366
+ )
367
+ );
368
+ }
369
+ function homeTargetsBoard(home) {
370
+ return home?.type === "space" || home?.type === "container" || home?.type === "edge" || home?.type === "vertex";
371
+ }
372
+ function validateBoardDuplicates(boards) {
373
+ const issues = [];
374
+ for (const [index, board] of boards.entries()) {
375
+ if (isHexBoardSpec(board)) {
376
+ issues.push(
377
+ ...collectDuplicateIdIssues({
378
+ entries: Object.values(board.spaces ?? {}).map(
379
+ (space, spaceIndex) => ({
380
+ id: space.id,
381
+ path: `manifest.boards[${index}].spaces[${spaceIndex}].id`
382
+ })
383
+ ),
384
+ label: "space id"
385
+ })
386
+ );
387
+ continue;
388
+ }
389
+ issues.push(
390
+ ...collectDuplicateIdIssues({
391
+ entries: (board.spaces ?? []).map((space, spaceIndex) => ({
392
+ id: space.id,
393
+ path: `manifest.boards[${index}].spaces[${spaceIndex}].id`
394
+ })),
395
+ label: "space id"
396
+ })
397
+ );
398
+ issues.push(
399
+ ...collectDuplicateIdIssues({
400
+ entries: (board.containers ?? []).map((container, containerIndex) => ({
401
+ id: container.id,
402
+ path: `manifest.boards[${index}].containers[${containerIndex}].id`
403
+ })),
404
+ label: "container id"
405
+ })
406
+ );
407
+ issues.push(
408
+ ...collectDuplicateIdIssues({
409
+ entries: (board.relations ?? []).map((relation, relationIndex) => ({
410
+ id: relation.id,
411
+ path: `manifest.boards[${index}].relations[${relationIndex}].id`
412
+ })),
413
+ label: "relation id"
414
+ })
415
+ );
416
+ }
417
+ return issues;
418
+ }
419
+ function validateHexBoardVertexRefs(manifest) {
420
+ const issues = [];
421
+ for (const board of manifest.boards ?? []) {
422
+ if (board.layout !== "hex") continue;
423
+ try {
424
+ const geometry = createHexBoardGeometry({
425
+ ...board,
426
+ spaces: resolveHexSpaces(board)
92
427
  });
93
- return {
94
- type: definition.type,
95
- id: definition.id,
96
- contextSchema,
97
- __continuation: continuation
98
- };
428
+ const vertices = (board.vertices ?? []).map(
429
+ (vertex) => "spaces" in vertex.ref ? geometry.vertex(...vertex.ref.spaces) : geometry.vertexAt(vertex.ref.space, vertex.ref.corner)
430
+ );
431
+ const edges = (board.edges ?? []).map(
432
+ (edge) => "spaces" in edge.ref ? geometry.edge(...edge.ref.spaces) : geometry.edgeAt(edge.ref.space, edge.ref.side)
433
+ );
434
+ if (new Set(vertices).size !== vertices.length)
435
+ throw new Error("Duplicate hex vertex refs.");
436
+ if (new Set(edges).size !== edges.length)
437
+ throw new Error("Duplicate hex edge refs.");
438
+ } catch (error) {
439
+ issues.push(
440
+ `Hex board '${board.id}': ${error instanceof Error ? error.message : String(error)}`
441
+ );
442
+ }
443
+ }
444
+ return issues;
445
+ }
446
+ function collectAmbiguousBoardTypeWarnings(manifest) {
447
+ const warnings = [];
448
+ const boardsBySpaceType = /* @__PURE__ */ new Map();
449
+ const boardsByEdgeType = /* @__PURE__ */ new Map();
450
+ const boardsByVertexType = /* @__PURE__ */ new Map();
451
+ const addBoardUsage = (target, typeId, boardId) => {
452
+ if (!typeId) {
453
+ return;
454
+ }
455
+ const boardIds = target.get(typeId) ?? /* @__PURE__ */ new Set();
456
+ boardIds.add(boardId);
457
+ target.set(typeId, boardIds);
458
+ };
459
+ for (const board of manifest.boards ?? []) {
460
+ for (const space of [
461
+ ...board.layout === "hex" ? Object.values(board.spaces ?? {}) : board.spaces ?? []
462
+ ]) {
463
+ addBoardUsage(boardsBySpaceType, space.typeId, board.id);
464
+ }
465
+ if (board.layout === "hex" || board.layout === "square") {
466
+ for (const edge of [...board.edges ?? []]) {
467
+ addBoardUsage(boardsByEdgeType, edge.typeId, board.id);
468
+ }
469
+ for (const vertex of [...board.vertices ?? []]) {
470
+ addBoardUsage(boardsByVertexType, vertex.typeId, board.id);
471
+ }
472
+ }
473
+ }
474
+ const pushWarnings = (kind, boardsByType, helperName) => {
475
+ for (const [typeId, boardIds] of boardsByType.entries()) {
476
+ if (boardIds.size < 2) {
477
+ continue;
478
+ }
479
+ warnings.push(
480
+ `Ambiguous ${kind}.typeId '${typeId}' is authored on multiple boards (${Array.from(boardIds).sort().join(", ")}). Prefer ${helperName} for board-scoped lookups.`
481
+ );
482
+ }
483
+ };
484
+ pushWarnings(
485
+ "space",
486
+ boardsBySpaceType,
487
+ "boardHelpers.spaceIdsByBoardId / boardHelpers.spaceTypeIdByBoardId"
488
+ );
489
+ pushWarnings(
490
+ "edge",
491
+ boardsByEdgeType,
492
+ "boardHelpers.edgeIdsByBoardIdAndTypeId"
493
+ );
494
+ pushWarnings(
495
+ "vertex",
496
+ boardsByVertexType,
497
+ "boardHelpers.vertexIdsByBoardIdAndTypeId"
498
+ );
499
+ return warnings;
500
+ }
501
+ function collectBoardRecordKeyIssues(manifest) {
502
+ const issues = [];
503
+ const maxPlayers = manifest.players.maxPlayers;
504
+ const playerIds = Array.from(
505
+ { length: maxPlayers },
506
+ (_, index) => `player-${index + 1}`
507
+ );
508
+ for (const [boardIndex, board] of (manifest.boards ?? []).entries()) {
509
+ const boardPath = `manifest.boards[${boardIndex}]`;
510
+ const runtimeBoardIds = board.scope === "perPlayer" ? playerIds.map((playerId) => `${board.id}:${playerId}`) : [board.id];
511
+ issues.push(
512
+ ...collectKeyIssues([
513
+ { value: board.id, path: `${boardPath}.id` },
514
+ { value: board.typeId, path: `${boardPath}.typeId` },
515
+ ...runtimeBoardIds.map((runtimeBoardId) => ({
516
+ value: runtimeBoardId,
517
+ path: `${boardPath}.runtimeBoardId`
518
+ }))
519
+ ]),
520
+ ...collectObjectSchemaKeyIssues(
521
+ board.boardFieldsSchema,
522
+ `${boardPath}.boardFieldsSchema`
523
+ ),
524
+ ...collectObjectSchemaKeyIssues(
525
+ board.spaceFieldsSchema,
526
+ `${boardPath}.spaceFieldsSchema`
527
+ )
528
+ );
529
+ if (board.layout !== "hex") {
530
+ issues.push(
531
+ ...collectObjectSchemaKeyIssues(
532
+ board.relationFieldsSchema,
533
+ `${boardPath}.relationFieldsSchema`
534
+ ),
535
+ ...collectObjectSchemaKeyIssues(
536
+ board.containerFieldsSchema,
537
+ `${boardPath}.containerFieldsSchema`
538
+ ),
539
+ ...collectKeyIssues([
540
+ ...(board.containers ?? []).flatMap((container, containerIndex) => [
541
+ {
542
+ value: container.id,
543
+ path: `${boardPath}.containers[${containerIndex}].id`
544
+ },
545
+ {
546
+ value: container.host.type === "space" ? container.host.spaceId : void 0,
547
+ path: `${boardPath}.containers[${containerIndex}].host.spaceId`
548
+ }
549
+ ]),
550
+ ...(board.relations ?? []).flatMap((relation, relationIndex) => [
551
+ {
552
+ value: relation.id,
553
+ path: `${boardPath}.relations[${relationIndex}].id`
554
+ },
555
+ {
556
+ value: relation.typeId,
557
+ path: `${boardPath}.relations[${relationIndex}].typeId`
558
+ },
559
+ {
560
+ value: relation.fromSpaceId,
561
+ path: `${boardPath}.relations[${relationIndex}].fromSpaceId`
562
+ },
563
+ {
564
+ value: relation.toSpaceId,
565
+ path: `${boardPath}.relations[${relationIndex}].toSpaceId`
566
+ }
567
+ ])
568
+ ])
569
+ );
570
+ }
571
+ if (board.layout !== "generic") {
572
+ issues.push(
573
+ ...collectObjectSchemaKeyIssues(
574
+ board.edgeFieldsSchema,
575
+ `${boardPath}.edgeFieldsSchema`
576
+ ),
577
+ ...collectObjectSchemaKeyIssues(
578
+ board.vertexFieldsSchema,
579
+ `${boardPath}.vertexFieldsSchema`
580
+ ),
581
+ ...collectKeyIssues([
582
+ ...(board.edges ?? []).map((edge, edgeIndex) => ({
583
+ value: edge.typeId,
584
+ path: `${boardPath}.edges[${edgeIndex}].typeId`
585
+ })),
586
+ ...(board.vertices ?? []).map((vertex, vertexIndex) => ({
587
+ value: vertex.typeId,
588
+ path: `${boardPath}.vertices[${vertexIndex}].typeId`
589
+ }))
590
+ ])
591
+ );
592
+ }
593
+ issues.push(
594
+ ...collectKeyIssues(
595
+ (board.layout === "hex" ? Object.values(board.spaces ?? {}) : board.spaces ?? []).flatMap((space, spaceIndex) => [
596
+ {
597
+ value: space.id,
598
+ path: `${boardPath}.spaces[${spaceIndex}].id`
599
+ },
600
+ {
601
+ value: space.typeId,
602
+ path: `${boardPath}.spaces[${spaceIndex}].typeId`
603
+ }
604
+ ])
605
+ )
606
+ );
607
+ }
608
+ return issues;
609
+ }
610
+ function collectManifestRecordKeyIssues(manifest) {
611
+ const cardSets = manifest.cardSets;
612
+ const cards = cardSets.flatMap(
613
+ (cardSet, cardSetIndex) => cardSet.cards.flatMap(
614
+ (card, cardIndex) => renderCardInstanceIds(card).map((cardId) => ({
615
+ card,
616
+ cardId,
617
+ cardIndex,
618
+ cardSetIndex
619
+ }))
620
+ )
621
+ );
622
+ return [
623
+ ...collectKeyIssues([
624
+ ...cardSets.map((cardSet, index) => ({
625
+ value: cardSet.id,
626
+ path: `manifest.cardSets[${index}].id`
627
+ })),
628
+ ...cards.flatMap(({ card, cardId, cardIndex, cardSetIndex }) => [
629
+ {
630
+ value: card.id,
631
+ path: `manifest.cardSets[${cardSetIndex}].cards[${cardIndex}].id`
632
+ },
633
+ {
634
+ value: card.cardType,
635
+ path: `manifest.cardSets[${cardSetIndex}].cards[${cardIndex}].cardType`
636
+ },
637
+ {
638
+ value: cardId,
639
+ path: `manifest.cardSets[${cardSetIndex}].cards[${cardIndex}].runtimeId`
640
+ }
641
+ ]),
642
+ ...(manifest.zones ?? []).map((zone, index) => ({
643
+ value: zone.id,
644
+ path: `manifest.zones[${index}].id`
645
+ })),
646
+ ...(manifest.resources ?? []).map((resource, index) => ({
647
+ value: resource.id,
648
+ path: `manifest.resources[${index}].id`
649
+ })),
650
+ ...(manifest.pieceTypes ?? []).flatMap((pieceType, typeIndex) => [
651
+ {
652
+ value: pieceType.id,
653
+ path: `manifest.pieceTypes[${typeIndex}].id`
654
+ },
655
+ ...(pieceType.slots ?? []).map((slot, slotIndex) => ({
656
+ value: slot.id,
657
+ path: `manifest.pieceTypes[${typeIndex}].slots[${slotIndex}].id`
658
+ }))
659
+ ]),
660
+ ...(manifest.dieTypes ?? []).flatMap((dieType, typeIndex) => [
661
+ {
662
+ value: dieType.id,
663
+ path: `manifest.dieTypes[${typeIndex}].id`
664
+ },
665
+ ...(dieType.slots ?? []).map((slot, slotIndex) => ({
666
+ value: slot.id,
667
+ path: `manifest.dieTypes[${typeIndex}].slots[${slotIndex}].id`
668
+ }))
669
+ ]),
670
+ ...expandSeedIds(manifest.pieceSeeds ?? []).map((pieceId, index) => ({
671
+ value: pieceId,
672
+ path: `manifest.pieceSeeds[*][${index}]`
673
+ })),
674
+ ...expandSeedIds(manifest.dieSeeds ?? []).map((dieId, index) => ({
675
+ value: dieId,
676
+ path: `manifest.dieSeeds[*][${index}]`
677
+ }))
678
+ ]),
679
+ ...cardSets.flatMap(
680
+ (cardSet, cardSetIndex) => collectCardSchemaKeyIssues(cardSet, `manifest.cardSets[${cardSetIndex}]`)
681
+ ),
682
+ ...(manifest.pieceTypes ?? []).flatMap(
683
+ (pieceType, typeIndex) => collectObjectSchemaKeyIssues(
684
+ pieceType.fieldsSchema,
685
+ `manifest.pieceTypes[${typeIndex}].fieldsSchema`
686
+ )
687
+ ),
688
+ ...(manifest.dieTypes ?? []).flatMap(
689
+ (dieType, typeIndex) => collectObjectSchemaKeyIssues(
690
+ dieType.fieldsSchema,
691
+ `manifest.dieTypes[${typeIndex}].fieldsSchema`
692
+ )
693
+ ),
694
+ ...collectBoardRecordKeyIssues(manifest)
695
+ ];
696
+ }
697
+ function validateCounts(manifest) {
698
+ const errors = [];
699
+ const check = (count, path) => {
700
+ if (!Number.isSafeInteger(count) || count < 1) {
701
+ errors.push(`${path}: Expected a positive safe integer.`);
99
702
  }
703
+ };
704
+ manifest.cardSets.forEach((set, setIndex) => {
705
+ set.cards.forEach(
706
+ (card, index) => check(card.count, `manifest.cardSets[${setIndex}].cards[${index}].count`)
707
+ );
708
+ });
709
+ for (const family of ["pieceSeeds", "dieSeeds"]) {
710
+ manifest[family]?.forEach((seed, index) => {
711
+ if (seed.count !== void 0)
712
+ check(seed.count, `manifest.${family}[${index}].count`);
713
+ });
714
+ }
715
+ check(manifest.players.minPlayers, "manifest.players.minPlayers");
716
+ check(manifest.players.maxPlayers, "manifest.players.maxPlayers");
717
+ if (manifest.players.minPlayers > manifest.players.maxPlayers) {
718
+ errors.push("manifest.players: minPlayers must not exceed maxPlayers.");
719
+ }
720
+ return errors;
721
+ }
722
+ function assertValidManifest(manifest) {
723
+ const validation = validateManifestAuthoring(manifest);
724
+ if (validation.errors.length) {
100
725
  throw new Error(
101
- `defineEffect: unknown effect definition. Expected type 'rollDie' | 'shuffleSharedZone' | 'shufflePlayerZone'. Addressed prompts are authored as prompt-kind interactions via defineInteraction({ kind: "prompt" }).`
726
+ `Invalid topology manifest:
727
+ ${validation.errors.join("\n")}`
102
728
  );
103
729
  }
104
- return effect;
730
+ }
731
+ function validateManifestAuthoring(manifest) {
732
+ const errors = validateCounts(manifest);
733
+ if (errors.length) return { errors, warnings: [] };
734
+ errors.push(...collectManifestRecordKeyIssues(manifest));
735
+ errors.push(
736
+ ...collectDuplicateIdIssues({
737
+ entries: manifest.cardSets.map((cardSet, index) => ({
738
+ id: cardSet.id,
739
+ path: `manifest.cardSets[${index}].id`
740
+ })),
741
+ label: "card set id"
742
+ })
743
+ );
744
+ errors.push(
745
+ ...collectDuplicateIdIssues({
746
+ entries: manifest.cardSets.flatMap(
747
+ (cardSet, cardSetIndex) => cardSet.cards.flatMap(
748
+ (card, cardIndex) => renderCardInstanceIds(card).map((cardId) => ({
749
+ id: cardId,
750
+ path: `manifest.cardSets[${cardSetIndex}].cards[${cardIndex}].id`
751
+ }))
752
+ )
753
+ ),
754
+ label: "card runtime id"
755
+ })
756
+ );
757
+ errors.push(
758
+ ...collectDuplicateIdIssues({
759
+ entries: (manifest.zones ?? []).map((zone, index) => ({
760
+ id: zone.id,
761
+ path: `manifest.zones[${index}].id`
762
+ })),
763
+ label: "zone id"
764
+ })
765
+ );
766
+ errors.push(
767
+ ...collectDuplicateIdIssues({
768
+ entries: (manifest.boards ?? []).map((board, index) => ({
769
+ id: board.id,
770
+ path: `manifest.boards[${index}].id`
771
+ })),
772
+ label: "board id"
773
+ })
774
+ );
775
+ errors.push(...validateBoardDuplicates(manifest.boards ?? []));
776
+ errors.push(
777
+ ...validateTypeSlotDuplicates({
778
+ pieceTypes: manifest.pieceTypes ?? [],
779
+ dieTypes: manifest.dieTypes ?? []
780
+ })
781
+ );
782
+ errors.push(
783
+ ...collectDuplicateIdIssues({
784
+ entries: (manifest.pieceTypes ?? []).map((pieceType, index) => ({
785
+ id: pieceType.id,
786
+ path: `manifest.pieceTypes[${index}].id`
787
+ })),
788
+ label: "piece type id"
789
+ })
790
+ );
791
+ errors.push(
792
+ ...collectDuplicateIdIssues({
793
+ entries: expandSeedIds(manifest.pieceSeeds ?? []).map(
794
+ (pieceId, index) => ({
795
+ id: pieceId,
796
+ path: `manifest.pieceSeeds[*][${index}]`
797
+ })
798
+ ),
799
+ label: "piece runtime id"
800
+ })
801
+ );
802
+ errors.push(
803
+ ...collectDuplicateIdIssues({
804
+ entries: (manifest.dieTypes ?? []).map((dieType, index) => ({
805
+ id: dieType.id,
806
+ path: `manifest.dieTypes[${index}].id`
807
+ })),
808
+ label: "die type id"
809
+ })
810
+ );
811
+ errors.push(
812
+ ...collectDuplicateIdIssues({
813
+ entries: expandSeedIds(manifest.dieSeeds ?? []).map((dieId, index) => ({
814
+ id: dieId,
815
+ path: `manifest.dieSeeds[*][${index}]`
816
+ })),
817
+ label: "die runtime id"
818
+ })
819
+ );
820
+ errors.push(
821
+ ...collectDuplicateIdIssues({
822
+ entries: (manifest.resources ?? []).map((resource, index) => ({
823
+ id: resource.id,
824
+ path: `manifest.resources[${index}].id`
825
+ })),
826
+ label: "resource id"
827
+ })
828
+ );
829
+ errors.push(...validateSlotHostsAndHomes(manifest));
830
+ errors.push(...validatePlayerScopedSeedHomes(manifest));
831
+ errors.push(...validateCardHomes(manifest));
832
+ for (const [cardSetIndex, cardSet] of manifest.cardSets.entries()) {
833
+ for (const [cardIndex, card] of cardSet.cards.entries()) {
834
+ const path = `manifest.cardSets[${cardSetIndex}].cards[${cardIndex}]`;
835
+ if (typeof card.id !== "string" || card.id.length === 0) {
836
+ errors.push(`${path}.id: Card definition id is required.`);
837
+ }
838
+ if (typeof card.cardType !== "string" || card.cardType.length === 0) {
839
+ errors.push(`${path}.cardType: Card category is required.`);
840
+ } else if ("variants" in cardSet.cardSchema && !Object.hasOwn(cardSet.cardSchema.variants, card.cardType)) {
841
+ errors.push(
842
+ `${path}.cardType: Unknown card category '${card.cardType}' for card set '${cardSet.id}'.`
843
+ );
844
+ }
845
+ }
846
+ }
847
+ errors.push(...validateCardImages(manifest));
848
+ errors.push(...validateHexBoardVertexRefs(manifest));
849
+ return {
850
+ errors,
851
+ warnings: collectAmbiguousBoardTypeWarnings(manifest)
852
+ };
105
853
  }
106
854
 
107
- // src/reducer/game-event.ts
108
- var gameEvent = {
109
- systemAction(event) {
855
+ // src/reducer/manifest/materialize.ts
856
+ function isCardPropertySchemaVariants(schema) {
857
+ return Boolean(schema && "variants" in schema);
858
+ }
859
+ function mergeSharedCardProperties(schema, cardType) {
860
+ const variant = schema.variants[cardType];
861
+ return {
862
+ properties: {
863
+ ...schema.shared ?? {},
864
+ ...variant.properties
865
+ }
866
+ };
867
+ }
868
+ function hasPropertySchemaDefault(property) {
869
+ return Boolean(property && "default" in property);
870
+ }
871
+ function dedupeSorted(values) {
872
+ return Array.from(new Set(values)).sort();
873
+ }
874
+ function sortedObject(entries) {
875
+ return Object.fromEntries(
876
+ Array.from(entries).sort(([left], [right]) => left.localeCompare(right))
877
+ );
878
+ }
879
+ function createRecord() {
880
+ return /* @__PURE__ */ Object.create(null);
881
+ }
882
+ function renderCardInstanceIds2(card) {
883
+ if (card.count > 1) {
884
+ return Array.from(
885
+ { length: card.count },
886
+ (_, index) => `${card.id}-${index + 1}`
887
+ );
888
+ }
889
+ return [card.id];
890
+ }
891
+ function expandSeedIds2(seeds) {
892
+ const expanded = [];
893
+ for (const seed of seeds) {
894
+ const count = seed.count ?? 1;
895
+ const baseId = seed.id ?? seed.typeId;
896
+ if (count <= 1) {
897
+ expanded.push(baseId);
898
+ continue;
899
+ }
900
+ for (let index = 1; index <= count; index += 1) {
901
+ expanded.push(`${baseId}-${index}`);
902
+ }
903
+ }
904
+ return expanded;
905
+ }
906
+ function isHexBoardSpec2(board) {
907
+ return board.layout === "hex";
908
+ }
909
+ function isSquareBoardSpec(board) {
910
+ return board.layout === "square";
911
+ }
912
+ var SQUARE_SIDES = ["north", "east", "south", "west"];
913
+ var SQUARE_CORNERS = ["nw", "ne", "se", "sw"];
914
+ function squareEdgeIdFromGeometryKey(key) {
915
+ return `square-edge:${key}`;
916
+ }
917
+ function squareVertexIdFromGeometryKey(key) {
918
+ return `square-vertex:${key}`;
919
+ }
920
+ function squareCornerGeometryKey(space, corner) {
921
+ switch (corner) {
922
+ case "nw":
923
+ return `${space.col},${space.row}`;
924
+ case "ne":
925
+ return `${space.col + 1},${space.row}`;
926
+ case "se":
927
+ return `${space.col + 1},${space.row + 1}`;
928
+ case "sw":
929
+ return `${space.col},${space.row + 1}`;
930
+ }
931
+ }
932
+ function squareEdgeGeometryKey(space, side) {
933
+ const endpoints = side === "north" ? [`${space.col},${space.row}`, `${space.col + 1},${space.row}`] : side === "east" ? [`${space.col + 1},${space.row}`, `${space.col + 1},${space.row + 1}`] : side === "south" ? [
934
+ `${space.col},${space.row + 1}`,
935
+ `${space.col + 1},${space.row + 1}`
936
+ ] : [`${space.col},${space.row}`, `${space.col},${space.row + 1}`];
937
+ return endpoints.sort((left, right) => left.localeCompare(right)).join("::");
938
+ }
939
+ function geometryKeyFromSquareEdgeRef(ref, spacesById) {
940
+ const resolvedSpaces = [...ref.spaces].sort((a, b) => a.localeCompare(b)).map((spaceId) => {
941
+ const space = spacesById.get(spaceId);
942
+ if (!space) {
943
+ throw new Error(
944
+ `Square edge ref references unknown space '${spaceId}'.`
945
+ );
946
+ }
947
+ return space;
948
+ });
949
+ const keyCounts = /* @__PURE__ */ new Map();
950
+ for (const space of resolvedSpaces) {
951
+ for (const side of SQUARE_SIDES) {
952
+ const key = squareEdgeGeometryKey(space, side);
953
+ keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
954
+ }
955
+ }
956
+ const candidates = [...keyCounts.entries()].filter(([, count]) => count === resolvedSpaces.length).map(([key]) => key).sort((left, right) => left.localeCompare(right));
957
+ if (candidates.length !== 1) {
958
+ throw new Error(
959
+ `Square edge ref spaces '${ref.spaces.join(", ")}' do not resolve to exactly one shared edge.`
960
+ );
961
+ }
962
+ const [only] = candidates;
963
+ if (only === void 0) {
964
+ throw new Error(
965
+ "unreachable: candidates.length === 1 but first is undefined"
966
+ );
967
+ }
968
+ return only;
969
+ }
970
+ function geometryKeyFromSquareVertexRef(ref, spacesById) {
971
+ const resolvedSpaces = [...ref.spaces].sort((a, b) => a.localeCompare(b)).map((spaceId) => {
972
+ const space = spacesById.get(spaceId);
973
+ if (!space) {
974
+ throw new Error(
975
+ `Square vertex ref references unknown space '${spaceId}'.`
976
+ );
977
+ }
978
+ return space;
979
+ });
980
+ const keyCounts = /* @__PURE__ */ new Map();
981
+ for (const space of resolvedSpaces) {
982
+ for (const corner of SQUARE_CORNERS) {
983
+ const key = squareCornerGeometryKey(space, corner);
984
+ keyCounts.set(key, (keyCounts.get(key) ?? 0) + 1);
985
+ }
986
+ }
987
+ const candidates = [...keyCounts.entries()].filter(([, count]) => count === resolvedSpaces.length).map(([key]) => key).sort((left, right) => left.localeCompare(right));
988
+ if (candidates.length !== 1) {
989
+ throw new Error(
990
+ `Square vertex ref spaces '${ref.spaces.join(", ")}' do not resolve to exactly one shared vertex.`
991
+ );
992
+ }
993
+ const [only] = candidates;
994
+ if (only === void 0) {
995
+ throw new Error(
996
+ "unreachable: candidates.length === 1 but first is undefined"
997
+ );
998
+ }
999
+ return only;
1000
+ }
1001
+ function resolveAuthoredHexEdges(board, geometry) {
1002
+ return (board.edges ?? []).map((edge) => ({
1003
+ ...edge,
1004
+ id: "spaces" in edge.ref ? geometry.edge(...edge.ref.spaces) : geometry.edgeAt(edge.ref.space, edge.ref.side)
1005
+ }));
1006
+ }
1007
+ function resolveAuthoredHexVertices(board, geometry) {
1008
+ return (board.vertices ?? []).map((vertex) => ({
1009
+ ...vertex,
1010
+ id: "spaces" in vertex.ref ? geometry.vertex(...vertex.ref.spaces) : geometry.vertexAt(vertex.ref.space, vertex.ref.corner)
1011
+ }));
1012
+ }
1013
+ function resolveSquareSpaces(board) {
1014
+ return [...board.spaces ?? []].sort(
1015
+ (left, right) => left.id.localeCompare(right.id)
1016
+ );
1017
+ }
1018
+ function deriveSquareEdges(spaces) {
1019
+ const edgeMap = /* @__PURE__ */ new Map();
1020
+ for (const space of spaces) {
1021
+ for (const side of SQUARE_SIDES) {
1022
+ const geometryKey = squareEdgeGeometryKey(space, side);
1023
+ const existing = edgeMap.get(geometryKey);
1024
+ const nextSpaceIds = dedupeSorted([
1025
+ ...existing?.spaceIds ?? [],
1026
+ space.id
1027
+ ]);
1028
+ edgeMap.set(geometryKey, {
1029
+ id: squareEdgeIdFromGeometryKey(geometryKey),
1030
+ geometryKey,
1031
+ spaceIds: nextSpaceIds,
1032
+ typeId: existing?.typeId ?? null,
1033
+ label: existing?.label ?? null,
1034
+ fields: existing?.fields ?? null
1035
+ });
1036
+ }
1037
+ }
1038
+ return [...edgeMap.values()].sort(
1039
+ (left, right) => left.id.localeCompare(right.id)
1040
+ );
1041
+ }
1042
+ function deriveSquareVertices(spaces) {
1043
+ const vertexMap = /* @__PURE__ */ new Map();
1044
+ for (const space of spaces) {
1045
+ for (const corner of SQUARE_CORNERS) {
1046
+ const geometryKey = squareCornerGeometryKey(space, corner);
1047
+ const existing = vertexMap.get(geometryKey);
1048
+ const nextSpaceIds = dedupeSorted([
1049
+ ...existing?.spaceIds ?? [],
1050
+ space.id
1051
+ ]);
1052
+ vertexMap.set(geometryKey, {
1053
+ id: squareVertexIdFromGeometryKey(geometryKey),
1054
+ geometryKey,
1055
+ spaceIds: nextSpaceIds,
1056
+ typeId: existing?.typeId ?? null,
1057
+ label: existing?.label ?? null,
1058
+ fields: existing?.fields ?? null
1059
+ });
1060
+ }
1061
+ }
1062
+ return [...vertexMap.values()].sort(
1063
+ (left, right) => left.id.localeCompare(right.id)
1064
+ );
1065
+ }
1066
+ function indexSquareEdgeMetadata(specs, spacesById, ownerLabel) {
1067
+ const metadataByKey = /* @__PURE__ */ new Map();
1068
+ for (const spec of specs) {
1069
+ const geometryKey = geometryKeyFromSquareEdgeRef(spec.ref, spacesById);
1070
+ if (metadataByKey.has(geometryKey)) {
1071
+ throw new Error(`${ownerLabel} contains duplicate square edge refs.`);
1072
+ }
1073
+ metadataByKey.set(geometryKey, {
1074
+ geometryKey,
1075
+ typeId: spec.typeId ?? null,
1076
+ label: spec.label ?? null,
1077
+ fields: spec.fields ?? null
1078
+ });
1079
+ }
1080
+ return metadataByKey;
1081
+ }
1082
+ function indexSquareVertexMetadata(specs, spacesById, ownerLabel) {
1083
+ const metadataByKey = /* @__PURE__ */ new Map();
1084
+ for (const spec of specs) {
1085
+ const geometryKey = geometryKeyFromSquareVertexRef(spec.ref, spacesById);
1086
+ if (metadataByKey.has(geometryKey)) {
1087
+ throw new Error(`${ownerLabel} contains duplicate square vertex refs.`);
1088
+ }
1089
+ metadataByKey.set(geometryKey, {
1090
+ geometryKey,
1091
+ typeId: spec.typeId ?? null,
1092
+ label: spec.label ?? null,
1093
+ fields: spec.fields ?? null
1094
+ });
1095
+ }
1096
+ return metadataByKey;
1097
+ }
1098
+ function resolveSquareEdges(board, spaces) {
1099
+ const spacesById = new Map(spaces.map((space) => [space.id, space]));
1100
+ const derived = deriveSquareEdges(spaces);
1101
+ const edgesByGeometryKey = new Map(
1102
+ derived.map((edge) => [edge.geometryKey, edge])
1103
+ );
1104
+ const metadataByKey = indexSquareEdgeMetadata(
1105
+ board.edges ?? [],
1106
+ spacesById,
1107
+ `Square board '${board.id}'`
1108
+ );
1109
+ for (const [geometryKey, metadata] of metadataByKey.entries()) {
1110
+ const edge = edgesByGeometryKey.get(geometryKey);
1111
+ if (!edge) {
1112
+ throw new Error(
1113
+ `Square edge ref on board '${board.id}' does not resolve to a derived edge.`
1114
+ );
1115
+ }
1116
+ edgesByGeometryKey.set(geometryKey, {
1117
+ ...edge,
1118
+ typeId: metadata.typeId ?? null,
1119
+ label: metadata.label ?? null,
1120
+ fields: metadata.fields ?? null
1121
+ });
1122
+ }
1123
+ return [...edgesByGeometryKey.values()].sort(
1124
+ (left, right) => left.id.localeCompare(right.id)
1125
+ );
1126
+ }
1127
+ function resolveSquareVertices(board, spaces) {
1128
+ const spacesById = new Map(spaces.map((space) => [space.id, space]));
1129
+ const derived = deriveSquareVertices(spaces);
1130
+ const verticesByGeometryKey = new Map(
1131
+ derived.map((vertex) => [vertex.geometryKey, vertex])
1132
+ );
1133
+ const metadataByKey = indexSquareVertexMetadata(
1134
+ board.vertices ?? [],
1135
+ spacesById,
1136
+ `Square board '${board.id}'`
1137
+ );
1138
+ for (const [geometryKey, metadata] of metadataByKey.entries()) {
1139
+ const vertex = verticesByGeometryKey.get(geometryKey);
1140
+ if (!vertex) {
1141
+ throw new Error(
1142
+ `Square vertex ref on board '${board.id}' does not resolve to a derived vertex.`
1143
+ );
1144
+ }
1145
+ verticesByGeometryKey.set(geometryKey, {
1146
+ ...vertex,
1147
+ typeId: metadata.typeId ?? null,
1148
+ label: metadata.label ?? null,
1149
+ fields: metadata.fields ?? null
1150
+ });
1151
+ }
1152
+ return [...verticesByGeometryKey.values()].sort(
1153
+ (left, right) => left.id.localeCompare(right.id)
1154
+ );
1155
+ }
1156
+ function analyzeBoards(manifest, playerIds) {
1157
+ return (manifest.boards ?? []).map((board) => {
1158
+ const runtimeBoardIds = board.scope === "perPlayer" ? playerIds.map((playerId) => `${board.id}:${playerId}`) : [board.id];
1159
+ if (isHexBoardSpec2(board)) {
1160
+ const spaces = resolveHexSpaces(board);
1161
+ const geometry = createHexBoardGeometry({ ...board, spaces });
1162
+ const authoredEdges = resolveAuthoredHexEdges(board, geometry);
1163
+ const authoredVertices = resolveAuthoredHexVertices(board, geometry);
1164
+ return {
1165
+ layout: "hex",
1166
+ board,
1167
+ boardTypeId: board.typeId,
1168
+ runtimeBoardIds,
1169
+ boardFieldsSchema: board.boardFieldsSchema,
1170
+ spaceFieldsSchema: board.spaceFieldsSchema,
1171
+ edgeFieldsSchema: board.edgeFieldsSchema,
1172
+ vertexFieldsSchema: board.vertexFieldsSchema,
1173
+ spaces,
1174
+ authoredEdges,
1175
+ authoredVertices,
1176
+ edges: geometry.edges.map((edge) => ({
1177
+ ...edge,
1178
+ ...authoredEdges.find((authored) => authored.id === edge.id)
1179
+ })),
1180
+ vertices: geometry.vertices.map((vertex) => ({
1181
+ ...vertex,
1182
+ ...authoredVertices.find((authored) => authored.id === vertex.id)
1183
+ }))
1184
+ };
1185
+ }
1186
+ if (isSquareBoardSpec(board)) {
1187
+ const squareBoard = board;
1188
+ const spaces = resolveSquareSpaces(squareBoard);
1189
+ return {
1190
+ layout: "square",
1191
+ board: squareBoard,
1192
+ boardTypeId: squareBoard.typeId,
1193
+ runtimeBoardIds,
1194
+ boardFieldsSchema: squareBoard.boardFieldsSchema,
1195
+ spaceFieldsSchema: squareBoard.spaceFieldsSchema,
1196
+ relationFieldsSchema: squareBoard.relationFieldsSchema,
1197
+ containerFieldsSchema: squareBoard.containerFieldsSchema,
1198
+ edgeFieldsSchema: squareBoard.edgeFieldsSchema,
1199
+ vertexFieldsSchema: squareBoard.vertexFieldsSchema,
1200
+ spaces,
1201
+ relations: [...squareBoard.relations ?? []],
1202
+ containers: [...squareBoard.containers ?? []].sort(
1203
+ (left, right) => left.id.localeCompare(right.id)
1204
+ ),
1205
+ edges: resolveSquareEdges(squareBoard, spaces),
1206
+ vertices: resolveSquareVertices(squareBoard, spaces)
1207
+ };
1208
+ }
1209
+ const genericBoard = board;
110
1210
  return {
111
- kind: "systemAction",
112
- ...event
1211
+ layout: "generic",
1212
+ board: genericBoard,
1213
+ boardTypeId: genericBoard.typeId,
1214
+ runtimeBoardIds,
1215
+ boardFieldsSchema: genericBoard.boardFieldsSchema,
1216
+ spaceFieldsSchema: genericBoard.spaceFieldsSchema,
1217
+ relationFieldsSchema: genericBoard.relationFieldsSchema,
1218
+ containerFieldsSchema: genericBoard.containerFieldsSchema,
1219
+ spaces: [...genericBoard.spaces ?? []].sort(
1220
+ (left, right) => left.id.localeCompare(right.id)
1221
+ ),
1222
+ relations: [...genericBoard.relations ?? []],
1223
+ containers: [...genericBoard.containers ?? []].sort(
1224
+ (left, right) => left.id.localeCompare(right.id)
1225
+ )
113
1226
  };
1227
+ });
1228
+ }
1229
+ function isSingletonExplicitSeed(seed) {
1230
+ return typeof seed.id === "string" && seed.id.length > 0 && (seed.count ?? 1) === 1;
1231
+ }
1232
+ function analyzeManifest(manifest, runtimePlayerIds) {
1233
+ assertValidManifest(manifest);
1234
+ const playerIds = runtimePlayerIds ? [...runtimePlayerIds] : Array.from(
1235
+ { length: manifest.players.maxPlayers },
1236
+ (_, index) => `player-${index + 1}`
1237
+ );
1238
+ const sharedZones = (manifest.zones ?? []).filter(
1239
+ (zone) => zone.scope === "shared"
1240
+ );
1241
+ const playerZones = (manifest.zones ?? []).filter(
1242
+ (zone) => zone.scope === "perPlayer"
1243
+ );
1244
+ const zoneIds = dedupeSorted((manifest.zones ?? []).map((zone) => zone.id));
1245
+ const cardSets = manifest.cardSets;
1246
+ const cardSetIds = dedupeSorted(cardSets.map((cardSet) => cardSet.id));
1247
+ const cardTypes = dedupeSorted(
1248
+ cardSets.flatMap((cardSet) => cardSet.cards.map((card) => card.cardType))
1249
+ );
1250
+ const cardIds = dedupeSorted(
1251
+ cardSets.flatMap((cardSet) => cardSet.cards.flatMap(renderCardInstanceIds2))
1252
+ );
1253
+ const cardSetIdByCardId = /* @__PURE__ */ new Map();
1254
+ const cardTypeByCardId = /* @__PURE__ */ new Map();
1255
+ for (const cardSet of cardSets) {
1256
+ for (const card of cardSet.cards) {
1257
+ for (const cardId of renderCardInstanceIds2(card)) {
1258
+ cardSetIdByCardId.set(cardId, cardSet.id);
1259
+ cardTypeByCardId.set(cardId, card.cardType);
1260
+ }
1261
+ }
114
1262
  }
115
- };
116
-
117
- // src/reducer/compose.ts
118
- function pipe(state, ...ops) {
119
- let result = state;
120
- for (const op of ops) {
121
- result = op(result);
1263
+ const sharedZoneCardSetIds = /* @__PURE__ */ new Map();
1264
+ for (const zone of sharedZones) {
1265
+ sharedZoneCardSetIds.set(
1266
+ zone.id,
1267
+ dedupeSorted(zone.allowedCardSetIds ?? [])
1268
+ );
1269
+ }
1270
+ const playerZoneCardSetIds = /* @__PURE__ */ new Map();
1271
+ for (const zone of playerZones) {
1272
+ playerZoneCardSetIds.set(
1273
+ zone.id,
1274
+ dedupeSorted(zone.allowedCardSetIds ?? [])
1275
+ );
1276
+ }
1277
+ const zoneCardSetIdsById = /* @__PURE__ */ new Map();
1278
+ for (const [zoneId, cardSetIds2] of sharedZoneCardSetIds.entries()) {
1279
+ zoneCardSetIdsById.set(zoneId, cardSetIds2);
1280
+ }
1281
+ for (const [zoneId, cardSetIds2] of playerZoneCardSetIds.entries()) {
1282
+ zoneCardSetIdsById.set(zoneId, cardSetIds2);
1283
+ }
1284
+ const sharedZoneIdsByCardSetId = new Map(
1285
+ cardSetIds.map((cardSetId) => [cardSetId, []])
1286
+ );
1287
+ for (const [zoneId, allowedCardSetIds] of sharedZoneCardSetIds.entries()) {
1288
+ for (const cardSetId of allowedCardSetIds) {
1289
+ const zoneIds2 = sharedZoneIdsByCardSetId.get(cardSetId) ?? [];
1290
+ zoneIds2.push(zoneId);
1291
+ sharedZoneIdsByCardSetId.set(cardSetId, zoneIds2);
1292
+ }
1293
+ }
1294
+ for (const [cardSetId, zoneIds2] of sharedZoneIdsByCardSetId.entries()) {
1295
+ sharedZoneIdsByCardSetId.set(cardSetId, dedupeSorted(zoneIds2));
1296
+ }
1297
+ const sharedZoneIdSet = new Set(sharedZones.map((zone) => zone.id));
1298
+ const homeSharedZoneIdsByCardType = new Map(
1299
+ cardTypes.map((cardType) => [cardType, []])
1300
+ );
1301
+ for (const cardSet of cardSets) {
1302
+ for (const card of cardSet.cards) {
1303
+ if (card.home?.type !== "zone" || !sharedZoneIdSet.has(card.home.zoneId)) {
1304
+ continue;
1305
+ }
1306
+ const cardType = card.cardType;
1307
+ const zoneIds2 = homeSharedZoneIdsByCardType.get(cardType) ?? [];
1308
+ zoneIds2.push(card.home.zoneId);
1309
+ homeSharedZoneIdsByCardType.set(cardType, zoneIds2);
1310
+ }
1311
+ }
1312
+ const homeSharedZoneIdByCardType = /* @__PURE__ */ new Map();
1313
+ for (const [cardType, zoneIds2] of homeSharedZoneIdsByCardType.entries()) {
1314
+ const uniqueZoneIds = dedupeSorted(zoneIds2);
1315
+ homeSharedZoneIdsByCardType.set(cardType, uniqueZoneIds);
1316
+ const [homeZoneId] = uniqueZoneIds;
1317
+ if (uniqueZoneIds.length === 1 && homeZoneId !== void 0) {
1318
+ homeSharedZoneIdByCardType.set(cardType, homeZoneId);
1319
+ }
1320
+ }
1321
+ const zoneVisibilityById = new Map(
1322
+ (manifest.zones ?? []).map((zone) => [
1323
+ zone.id,
1324
+ zone.visibility ?? "public"
1325
+ ])
1326
+ );
1327
+ const resourceIds = dedupeSorted(
1328
+ (manifest.resources ?? []).map((resource) => resource.id)
1329
+ );
1330
+ const resourcePresentationById = sortedObject(
1331
+ (manifest.resources ?? []).map((resource) => [
1332
+ resource.id,
1333
+ {
1334
+ label: resource.name,
1335
+ ...resource.icon ? { icon: resource.icon } : {}
1336
+ }
1337
+ ])
1338
+ );
1339
+ const pieceTypeIds = dedupeSorted(
1340
+ (manifest.pieceTypes ?? []).map((pieceType) => pieceType.id)
1341
+ );
1342
+ const pieceTypeSchemasById = new Map(
1343
+ (manifest.pieceTypes ?? []).map((pieceType) => [
1344
+ pieceType.id,
1345
+ pieceType.fieldsSchema
1346
+ ])
1347
+ );
1348
+ const pieceTypeSlotIdsById = new Map(
1349
+ (manifest.pieceTypes ?? []).map((pieceType) => [
1350
+ pieceType.id,
1351
+ dedupeSorted((pieceType.slots ?? []).map((slot) => slot.id))
1352
+ ])
1353
+ );
1354
+ const pieceTypeIdByPieceId = /* @__PURE__ */ new Map();
1355
+ for (const seed of manifest.pieceSeeds ?? []) {
1356
+ for (const pieceId of expandSeedIds2([seed])) {
1357
+ pieceTypeIdByPieceId.set(pieceId, seed.typeId);
1358
+ }
122
1359
  }
123
- return result;
1360
+ const pieceIds = dedupeSorted(pieceTypeIdByPieceId.keys());
1361
+ const dieTypeIds = dedupeSorted(
1362
+ (manifest.dieTypes ?? []).map((dieType) => dieType.id)
1363
+ );
1364
+ const dieTypeSchemasById = new Map(
1365
+ (manifest.dieTypes ?? []).map((dieType) => [
1366
+ dieType.id,
1367
+ dieType.fieldsSchema
1368
+ ])
1369
+ );
1370
+ const dieTypeSlotIdsById = new Map(
1371
+ (manifest.dieTypes ?? []).map((dieType) => [
1372
+ dieType.id,
1373
+ dedupeSorted((dieType.slots ?? []).map((slot) => slot.id))
1374
+ ])
1375
+ );
1376
+ const dieTypeIdByDieId = /* @__PURE__ */ new Map();
1377
+ for (const seed of manifest.dieSeeds ?? []) {
1378
+ for (const dieId of expandSeedIds2([seed])) {
1379
+ dieTypeIdByDieId.set(dieId, seed.typeId);
1380
+ }
1381
+ }
1382
+ const dieIds = dedupeSorted(dieTypeIdByDieId.keys());
1383
+ const strictSlotHosts = [
1384
+ ...(manifest.pieceSeeds ?? []).flatMap((seed) => {
1385
+ if (!isSingletonExplicitSeed(seed)) {
1386
+ return [];
1387
+ }
1388
+ const slotIds = pieceTypeSlotIdsById.get(seed.typeId) ?? [];
1389
+ return slotIds.length > 0 ? [
1390
+ {
1391
+ kind: "piece",
1392
+ id: seed.id,
1393
+ slotIds
1394
+ }
1395
+ ] : [];
1396
+ }),
1397
+ ...(manifest.dieSeeds ?? []).flatMap((seed) => {
1398
+ if (!isSingletonExplicitSeed(seed)) {
1399
+ return [];
1400
+ }
1401
+ const slotIds = dieTypeSlotIdsById.get(seed.typeId) ?? [];
1402
+ return slotIds.length > 0 ? [
1403
+ {
1404
+ kind: "die",
1405
+ id: seed.id,
1406
+ slotIds
1407
+ }
1408
+ ] : [];
1409
+ })
1410
+ ].sort(
1411
+ (left, right) => left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id)
1412
+ );
1413
+ const analyzedBoards = analyzeBoards(manifest, playerIds);
1414
+ const boardBaseIds = dedupeSorted(
1415
+ analyzedBoards.map(({ board }) => board.id)
1416
+ );
1417
+ const boardIds = dedupeSorted(
1418
+ analyzedBoards.flatMap((entry) => entry.runtimeBoardIds)
1419
+ );
1420
+ const boardTypeIds = dedupeSorted(
1421
+ analyzedBoards.map((board) => board.boardTypeId).filter((typeId) => typeof typeId === "string")
1422
+ );
1423
+ const boardLayoutById = /* @__PURE__ */ new Map();
1424
+ const boardIdsByLayout = /* @__PURE__ */ new Map();
1425
+ const boardBaseIdsByLayout = /* @__PURE__ */ new Map();
1426
+ const boardIdsByBaseId = /* @__PURE__ */ new Map();
1427
+ const boardIdsByTypeId = /* @__PURE__ */ new Map();
1428
+ const spaceIdsByBoardId = /* @__PURE__ */ new Map();
1429
+ const spaceTypeIdByBoardId = /* @__PURE__ */ new Map();
1430
+ const spaceIdsByTypeId = /* @__PURE__ */ new Map();
1431
+ const containerIdsByBoardId = /* @__PURE__ */ new Map();
1432
+ const containerHostByBoardId = /* @__PURE__ */ new Map();
1433
+ const relationTypeIdsByBoardId = /* @__PURE__ */ new Map();
1434
+ const edgeIdsByTypeId = /* @__PURE__ */ new Map();
1435
+ const edgeIdsByBoardIdAndTypeId = /* @__PURE__ */ new Map();
1436
+ const vertexIdsByTypeId = /* @__PURE__ */ new Map();
1437
+ const vertexIdsByBoardIdAndTypeId = /* @__PURE__ */ new Map();
1438
+ for (const analyzedBoard of analyzedBoards) {
1439
+ boardIdsByBaseId.set(analyzedBoard.board.id, analyzedBoard.runtimeBoardIds);
1440
+ boardIdsByLayout.set(
1441
+ analyzedBoard.board.layout,
1442
+ dedupeSorted([
1443
+ ...boardIdsByLayout.get(analyzedBoard.board.layout) ?? [],
1444
+ ...analyzedBoard.runtimeBoardIds
1445
+ ])
1446
+ );
1447
+ boardBaseIdsByLayout.set(
1448
+ analyzedBoard.board.layout,
1449
+ dedupeSorted([
1450
+ ...boardBaseIdsByLayout.get(analyzedBoard.board.layout) ?? [],
1451
+ analyzedBoard.board.id
1452
+ ])
1453
+ );
1454
+ const runtimeSpaceIds = analyzedBoard.spaces.map((space) => space.id);
1455
+ const runtimeSpaceTypeIds = sortedObject(
1456
+ analyzedBoard.spaces.map(
1457
+ (space) => [space.id, space.typeId ?? null]
1458
+ )
1459
+ );
1460
+ const runtimeContainerIds = analyzedBoard.layout === "hex" ? [] : analyzedBoard.containers.map((container) => container.id);
1461
+ const runtimeContainerHosts = analyzedBoard.layout === "hex" ? {} : sortedObject(
1462
+ analyzedBoard.containers.map((container) => [
1463
+ container.id,
1464
+ container.host.type === "space" ? { type: "space", spaceId: container.host.spaceId } : { type: "board" }
1465
+ ])
1466
+ );
1467
+ const runtimeRelationTypeIds = analyzedBoard.layout === "hex" ? ["adjacent"] : analyzedBoard.layout === "square" ? dedupeSorted([
1468
+ "adjacent",
1469
+ ...analyzedBoard.relations.map((relation) => relation.typeId)
1470
+ ]) : dedupeSorted(
1471
+ analyzedBoard.relations.map((relation) => relation.typeId)
1472
+ );
1473
+ for (const runtimeBoardId of analyzedBoard.runtimeBoardIds) {
1474
+ boardLayoutById.set(runtimeBoardId, analyzedBoard.board.layout);
1475
+ spaceIdsByBoardId.set(runtimeBoardId, runtimeSpaceIds);
1476
+ spaceTypeIdByBoardId.set(runtimeBoardId, runtimeSpaceTypeIds);
1477
+ containerIdsByBoardId.set(runtimeBoardId, runtimeContainerIds);
1478
+ containerHostByBoardId.set(runtimeBoardId, runtimeContainerHosts);
1479
+ relationTypeIdsByBoardId.set(runtimeBoardId, runtimeRelationTypeIds);
1480
+ }
1481
+ if (analyzedBoard.boardTypeId) {
1482
+ boardIdsByTypeId.set(
1483
+ analyzedBoard.boardTypeId,
1484
+ dedupeSorted([
1485
+ ...boardIdsByTypeId.get(analyzedBoard.boardTypeId) ?? [],
1486
+ ...analyzedBoard.runtimeBoardIds
1487
+ ])
1488
+ );
1489
+ }
1490
+ for (const space of analyzedBoard.spaces) {
1491
+ if (!space.typeId) {
1492
+ continue;
1493
+ }
1494
+ spaceIdsByTypeId.set(
1495
+ space.typeId,
1496
+ dedupeSorted([...spaceIdsByTypeId.get(space.typeId) ?? [], space.id])
1497
+ );
1498
+ }
1499
+ if (analyzedBoard.layout !== "generic") {
1500
+ const edgeIdsForBoardByType = {};
1501
+ for (const edge of analyzedBoard.edges) {
1502
+ if (!edge.typeId) {
1503
+ continue;
1504
+ }
1505
+ edgeIdsByTypeId.set(
1506
+ edge.typeId,
1507
+ dedupeSorted([...edgeIdsByTypeId.get(edge.typeId) ?? [], edge.id])
1508
+ );
1509
+ edgeIdsForBoardByType[edge.typeId] = dedupeSorted([
1510
+ ...edgeIdsForBoardByType[edge.typeId] ?? [],
1511
+ edge.id
1512
+ ]);
1513
+ }
1514
+ const vertexIdsForBoardByType = {};
1515
+ for (const vertex of analyzedBoard.vertices) {
1516
+ if (!vertex.typeId) {
1517
+ continue;
1518
+ }
1519
+ vertexIdsByTypeId.set(
1520
+ vertex.typeId,
1521
+ dedupeSorted([
1522
+ ...vertexIdsByTypeId.get(vertex.typeId) ?? [],
1523
+ vertex.id
1524
+ ])
1525
+ );
1526
+ vertexIdsForBoardByType[vertex.typeId] = dedupeSorted([
1527
+ ...vertexIdsForBoardByType[vertex.typeId] ?? [],
1528
+ vertex.id
1529
+ ]);
1530
+ }
1531
+ for (const runtimeBoardId of analyzedBoard.runtimeBoardIds) {
1532
+ edgeIdsByBoardIdAndTypeId.set(runtimeBoardId, edgeIdsForBoardByType);
1533
+ vertexIdsByBoardIdAndTypeId.set(
1534
+ runtimeBoardId,
1535
+ vertexIdsForBoardByType
1536
+ );
1537
+ }
1538
+ }
1539
+ }
1540
+ const relationTypeIds = dedupeSorted(
1541
+ Array.from(relationTypeIdsByBoardId.values()).flat()
1542
+ );
1543
+ const edgeIds = dedupeSorted(
1544
+ analyzedBoards.flatMap(
1545
+ (board) => board.layout === "generic" ? [] : board.edges.map((edge) => edge.id)
1546
+ )
1547
+ );
1548
+ const edgeTypeIds = dedupeSorted(edgeIdsByTypeId.keys());
1549
+ const vertexIds = dedupeSorted(
1550
+ analyzedBoards.flatMap(
1551
+ (board) => board.layout === "generic" ? [] : board.vertices.map((vertex) => vertex.id)
1552
+ )
1553
+ );
1554
+ const vertexTypeIds = dedupeSorted(vertexIdsByTypeId.keys());
1555
+ const boardContainerIds = dedupeSorted(
1556
+ analyzedBoards.flatMap(
1557
+ (board) => board.layout === "hex" ? [] : board.containers.map((container) => container.id)
1558
+ )
1559
+ );
1560
+ const spaceIds = dedupeSorted(
1561
+ analyzedBoards.flatMap((board) => board.spaces.map((space) => space.id))
1562
+ );
1563
+ const spaceTypeIds = dedupeSorted(
1564
+ analyzedBoards.flatMap(
1565
+ (board) => board.spaces.map((space) => space.typeId).filter((typeId) => typeof typeId === "string")
1566
+ )
1567
+ );
1568
+ return {
1569
+ manifest,
1570
+ playerIds,
1571
+ sharedZones,
1572
+ playerZones,
1573
+ zoneIds,
1574
+ cardSets,
1575
+ cardSetIds,
1576
+ cardTypes,
1577
+ cardIds,
1578
+ cardSetIdByCardId,
1579
+ cardTypeByCardId,
1580
+ sharedZoneCardSetIds,
1581
+ sharedZoneIdsByCardSetId,
1582
+ homeSharedZoneIdsByCardType,
1583
+ homeSharedZoneIdByCardType,
1584
+ playerZoneCardSetIds,
1585
+ zoneCardSetIdsById,
1586
+ zoneVisibilityById,
1587
+ resourceIds,
1588
+ resourcePresentationById,
1589
+ pieceTypeIds,
1590
+ pieceIds,
1591
+ pieceTypeIdByPieceId,
1592
+ dieTypeIds,
1593
+ dieIds,
1594
+ dieTypeIdByDieId,
1595
+ strictSlotHosts,
1596
+ boardBaseIds,
1597
+ boardIds,
1598
+ boardContainerIds,
1599
+ boardTypeIds,
1600
+ boardLayoutById,
1601
+ boardIdsByLayout,
1602
+ boardBaseIdsByLayout,
1603
+ boardIdsByBaseId,
1604
+ boardIdsByTypeId,
1605
+ spaceIdsByBoardId,
1606
+ spaceTypeIdByBoardId,
1607
+ spaceIdsByTypeId,
1608
+ containerIdsByBoardId,
1609
+ containerHostByBoardId,
1610
+ relationTypeIds,
1611
+ relationTypeIdsByBoardId,
1612
+ edgeIds,
1613
+ edgeTypeIds,
1614
+ edgeIdsByTypeId,
1615
+ edgeIdsByBoardIdAndTypeId,
1616
+ vertexIds,
1617
+ vertexTypeIds,
1618
+ vertexIdsByTypeId,
1619
+ vertexIdsByBoardIdAndTypeId,
1620
+ spaceIds,
1621
+ spaceTypeIds,
1622
+ analyzedBoards,
1623
+ pieceTypeSchemasById,
1624
+ dieTypeSchemasById
1625
+ };
1626
+ }
1627
+ function cloneJson(value) {
1628
+ return JSON.parse(JSON.stringify(value));
1629
+ }
1630
+ function boardSpaceRefKey(spaceIds) {
1631
+ return [...spaceIds].sort((left, right) => left.localeCompare(right)).join("$$");
1632
+ }
1633
+ function materializePropertySchemaDefault(property, analysis, runtimeBoardId) {
1634
+ if (hasPropertySchemaDefault(property)) {
1635
+ return cloneJson(property.default);
1636
+ }
1637
+ if (property.optional) {
1638
+ return void 0;
1639
+ }
1640
+ if (property.nullable) {
1641
+ return null;
1642
+ }
1643
+ switch (property.type) {
1644
+ case "string":
1645
+ return "";
1646
+ case "integer":
1647
+ case "number":
1648
+ return 0;
1649
+ case "boolean":
1650
+ return false;
1651
+ case "enum":
1652
+ return property.enums?.[0] ?? "";
1653
+ case "array":
1654
+ return [];
1655
+ case "object":
1656
+ return materializeObjectSchemaDefaults(
1657
+ property.properties ? { properties: property.properties } : void 0,
1658
+ analysis,
1659
+ runtimeBoardId
1660
+ );
1661
+ case "record":
1662
+ return {};
1663
+ case "zoneId":
1664
+ return analysis.zoneIds[0] ?? "";
1665
+ case "cardId":
1666
+ return analysis.cardIds[0] ?? "";
1667
+ case "playerId":
1668
+ return analysis.playerIds[0] ?? "";
1669
+ case "boardId":
1670
+ return runtimeBoardId ?? analysis.boardIds[0] ?? "";
1671
+ case "edgeId":
1672
+ return analysis.edgeIds[0] ?? "";
1673
+ case "vertexId":
1674
+ return analysis.vertexIds[0] ?? "";
1675
+ case "spaceId":
1676
+ return (runtimeBoardId ? analysis.spaceIdsByBoardId.get(runtimeBoardId)?.[0] : void 0) ?? analysis.spaceIds[0] ?? "";
1677
+ case "pieceId":
1678
+ return analysis.pieceIds[0] ?? "";
1679
+ case "dieId":
1680
+ return analysis.dieIds[0] ?? "";
1681
+ case "resourceId":
1682
+ return analysis.resourceIds[0] ?? "";
1683
+ }
1684
+ }
1685
+ function materializeObjectSchemaDefaults(schema, analysis, runtimeBoardId) {
1686
+ if (!schema?.properties) {
1687
+ return {};
1688
+ }
1689
+ return Object.fromEntries(
1690
+ Object.entries(schema.properties).flatMap(([key, property]) => {
1691
+ const value = materializePropertySchemaDefault(
1692
+ property,
1693
+ analysis,
1694
+ runtimeBoardId
1695
+ );
1696
+ return value === void 0 ? [] : [[key, value]];
1697
+ })
1698
+ );
1699
+ }
1700
+ function materializeCardPropertiesDefaults(schema, cardType, analysis) {
1701
+ const objectSchema = isCardPropertySchemaVariants(schema) ? mergeSharedCardProperties(schema, cardType) : schema;
1702
+ return materializeObjectSchemaDefaults(objectSchema, analysis);
1703
+ }
1704
+ function materializeManifestTable(options) {
1705
+ const analysis = analyzeManifest(options.manifest, options.playerIds);
1706
+ const manifest = analysis.manifest;
1707
+ const playerIds = [...options.playerIds];
1708
+ const cards = createRecord();
1709
+ const pieces = createRecord();
1710
+ const dice = createRecord();
1711
+ const componentLocations = createRecord();
1712
+ const locationOrder = /* @__PURE__ */ new Map();
1713
+ const boardAnalysisByBaseId = new Map(
1714
+ analysis.analyzedBoards.map((board) => [board.board.id, board])
1715
+ );
1716
+ const edgeIdByBoardBaseIdAndSpaces = /* @__PURE__ */ new Map();
1717
+ const vertexIdByBoardBaseIdAndSpaces = /* @__PURE__ */ new Map();
1718
+ for (const analyzedBoard of analysis.analyzedBoards) {
1719
+ if (analyzedBoard.layout === "generic") {
1720
+ continue;
1721
+ }
1722
+ edgeIdByBoardBaseIdAndSpaces.set(
1723
+ analyzedBoard.board.id,
1724
+ new Map(
1725
+ analyzedBoard.edges.map((edge) => [
1726
+ boardSpaceRefKey(edge.spaceIds),
1727
+ edge.id
1728
+ ])
1729
+ )
1730
+ );
1731
+ vertexIdByBoardBaseIdAndSpaces.set(
1732
+ analyzedBoard.board.id,
1733
+ new Map(
1734
+ analyzedBoard.vertices.map((vertex) => [
1735
+ boardSpaceRefKey(vertex.spaceIds),
1736
+ vertex.id
1737
+ ])
1738
+ )
1739
+ );
1740
+ }
1741
+ const nextLocationPosition = (location) => {
1742
+ const key = JSON.stringify(location);
1743
+ const position = locationOrder.get(key) ?? 0;
1744
+ locationOrder.set(key, position + 1);
1745
+ return position;
1746
+ };
1747
+ const zoneScopeById = new Map([
1748
+ ...analysis.sharedZones.map((zone) => [zone.id, "shared"]),
1749
+ ...analysis.playerZones.map((zone) => [zone.id, "perPlayer"])
1750
+ ]);
1751
+ const resolveRuntimeBoardId = (boardBaseId, ownerId, context) => {
1752
+ const analyzedBoard = boardAnalysisByBaseId.get(boardBaseId);
1753
+ if (!analyzedBoard) {
1754
+ throw new Error(
1755
+ `${context ?? "Component home"}.boardId: Unknown board '${boardBaseId}'.`
1756
+ );
1757
+ }
1758
+ if (analyzedBoard?.board.scope === "perPlayer") {
1759
+ if (!ownerId) {
1760
+ throw new Error(
1761
+ `${context ?? `Home on board '${boardBaseId}'`} requires ownerId because board '${boardBaseId}' has scope 'perPlayer'. Add ownerId to resolve the player-scoped destination.`
1762
+ );
1763
+ }
1764
+ return `${boardBaseId}:${ownerId}`;
1765
+ }
1766
+ return boardBaseId;
1767
+ };
1768
+ const resolveBoardEdgeId = (boardBaseId, spaceIds) => {
1769
+ const edgeId = edgeIdByBoardBaseIdAndSpaces.get(boardBaseId)?.get(boardSpaceRefKey(spaceIds));
1770
+ if (!edgeId) {
1771
+ throw new Error(
1772
+ `Unknown edge on board '${boardBaseId}' for spaces [${spaceIds.join(", ")}].`
1773
+ );
1774
+ }
1775
+ return edgeId;
1776
+ };
1777
+ const resolveBoardVertexId = (boardBaseId, spaceIds) => {
1778
+ const vertexId = vertexIdByBoardBaseIdAndSpaces.get(boardBaseId)?.get(boardSpaceRefKey(spaceIds));
1779
+ if (!vertexId) {
1780
+ throw new Error(
1781
+ `Unknown vertex on board '${boardBaseId}' for spaces [${spaceIds.join(", ")}].`
1782
+ );
1783
+ }
1784
+ return vertexId;
1785
+ };
1786
+ const materializeComponentLocation = (home, context) => {
1787
+ if (home.type === "detached") {
1788
+ return { type: "Detached" };
1789
+ }
1790
+ if (home.type === "slot") {
1791
+ return {
1792
+ type: "InSlot",
1793
+ host: home.host,
1794
+ slotId: home.slotId,
1795
+ position: nextLocationPosition({
1796
+ type: "InSlot",
1797
+ host: home.host,
1798
+ slotId: home.slotId
1799
+ })
1800
+ };
1801
+ }
1802
+ if (home.type === "zone") {
1803
+ const zoneScope = zoneScopeById.get(home.zoneId);
1804
+ if (!zoneScope) {
1805
+ throw new Error(
1806
+ `${context.path}.zoneId: ${context.label} targets unknown zone '${home.zoneId}'.`
1807
+ );
1808
+ }
1809
+ if (zoneScope === "perPlayer") {
1810
+ throw new Error(
1811
+ `${context.path}.zoneId: ${context.label} cannot target per-player zone '${home.zoneId}' because card inventory has no ownerId. Place it during reducer setup instead.`
1812
+ );
1813
+ }
1814
+ return {
1815
+ type: "InDeck",
1816
+ deckId: home.zoneId,
1817
+ playedBy: null,
1818
+ position: nextLocationPosition({
1819
+ type: "InDeck",
1820
+ deckId: home.zoneId,
1821
+ playedBy: null
1822
+ })
1823
+ };
1824
+ }
1825
+ if (home.type === "space") {
1826
+ const boardId = resolveRuntimeBoardId(
1827
+ home.boardId,
1828
+ null,
1829
+ `${context.path}.boardId: ${context.label}`
1830
+ );
1831
+ if (!analysis.spaceIdsByBoardId.get(home.boardId)?.includes(home.spaceId)) {
1832
+ throw new Error(
1833
+ `${context.path}.spaceId: ${context.label} targets unknown space '${home.spaceId}' on board '${home.boardId}'.`
1834
+ );
1835
+ }
1836
+ return {
1837
+ type: "OnSpace",
1838
+ boardId,
1839
+ spaceId: home.spaceId,
1840
+ position: nextLocationPosition({
1841
+ type: "OnSpace",
1842
+ boardId,
1843
+ spaceId: home.spaceId
1844
+ })
1845
+ };
1846
+ }
1847
+ if (home.type === "container") {
1848
+ const boardId = resolveRuntimeBoardId(
1849
+ home.boardId,
1850
+ null,
1851
+ `${context.path}.boardId: ${context.label}`
1852
+ );
1853
+ if (!analysis.containerIdsByBoardId.get(home.boardId)?.includes(home.containerId)) {
1854
+ throw new Error(
1855
+ `${context.path}.containerId: ${context.label} targets unknown container '${home.containerId}' on board '${home.boardId}'.`
1856
+ );
1857
+ }
1858
+ return {
1859
+ type: "InContainer",
1860
+ boardId,
1861
+ containerId: home.containerId,
1862
+ position: nextLocationPosition({
1863
+ type: "InContainer",
1864
+ boardId,
1865
+ containerId: home.containerId
1866
+ })
1867
+ };
1868
+ }
1869
+ if (home.type === "edge") {
1870
+ const boardId = resolveRuntimeBoardId(
1871
+ home.boardId,
1872
+ null,
1873
+ `${context.path}.boardId: ${context.label}`
1874
+ );
1875
+ const edgeId = resolveBoardEdgeId(home.boardId, home.ref.spaces);
1876
+ return {
1877
+ type: "OnEdge",
1878
+ boardId,
1879
+ edgeId,
1880
+ position: nextLocationPosition({
1881
+ type: "OnEdge",
1882
+ boardId,
1883
+ edgeId
1884
+ })
1885
+ };
1886
+ }
1887
+ if (home.type === "vertex") {
1888
+ const boardId = resolveRuntimeBoardId(
1889
+ home.boardId,
1890
+ null,
1891
+ `${context.path}.boardId: ${context.label}`
1892
+ );
1893
+ const vertexId = resolveBoardVertexId(home.boardId, home.ref.spaces);
1894
+ return {
1895
+ type: "OnVertex",
1896
+ boardId,
1897
+ vertexId,
1898
+ position: nextLocationPosition({
1899
+ type: "OnVertex",
1900
+ boardId,
1901
+ vertexId
1902
+ })
1903
+ };
1904
+ }
1905
+ const exhaustive = home;
1906
+ throw new Error(
1907
+ `Unsupported component home: ${JSON.stringify(exhaustive)}`
1908
+ );
1909
+ };
1910
+ for (const [cardSetIndex, cardSet] of manifest.cardSets.entries()) {
1911
+ for (const [cardIndex, card] of cardSet.cards.entries()) {
1912
+ const cardInstanceIds = renderCardInstanceIds2(card);
1913
+ for (const [instanceIndex, cardId] of cardInstanceIds.entries()) {
1914
+ cards[cardId] = {
1915
+ id: cardId,
1916
+ cardSetId: cardSet.id,
1917
+ cardType: card.cardType,
1918
+ name: card.name,
1919
+ text: card.text,
1920
+ frontImage: card.frontImage,
1921
+ backImage: card.backImage,
1922
+ properties: {
1923
+ ...materializeCardPropertiesDefaults(
1924
+ cardSet.cardSchema,
1925
+ card.cardType,
1926
+ analysis
1927
+ ),
1928
+ ...card.properties ?? {}
1929
+ }
1930
+ };
1931
+ const resolvedHome = card.home ?? cardSet.defaultHome;
1932
+ const path = card.home === void 0 ? `manifest.cardSets[${cardSetIndex}].defaultHome` : `manifest.cardSets[${cardSetIndex}].cards[${cardIndex}].home`;
1933
+ componentLocations[cardId] = materializeComponentLocation(
1934
+ resolvedHome,
1935
+ {
1936
+ path,
1937
+ label: `Card '${card.id}' instance ${instanceIndex + 1}`
1938
+ }
1939
+ );
1940
+ }
1941
+ }
1942
+ }
1943
+ if (Object.keys(cards).length !== analysis.cardIds.length) {
1944
+ throw new Error(
1945
+ "Materialized card record cardinality drifted from analysis."
1946
+ );
1947
+ }
1948
+ if (Object.keys(componentLocations).length !== analysis.cardIds.length) {
1949
+ throw new Error(
1950
+ "Materialized card locations drifted from analysis before seed placement."
1951
+ );
1952
+ }
1953
+ const pieceTypesById = new Map(
1954
+ (manifest.pieceTypes ?? []).map((pieceType) => [pieceType.id, pieceType])
1955
+ );
1956
+ const dieTypesById = new Map(
1957
+ (manifest.dieTypes ?? []).map((dieType) => [dieType.id, dieType])
1958
+ );
1959
+ const assignSeedLocation = (componentId, ownerId, home, context) => {
1960
+ if (!home) {
1961
+ componentLocations[componentId] = { type: "Detached" };
1962
+ return;
1963
+ }
1964
+ if (home.type === "slot") {
1965
+ componentLocations[componentId] = {
1966
+ type: "InSlot",
1967
+ host: home.host,
1968
+ slotId: home.slotId,
1969
+ position: nextLocationPosition({
1970
+ type: "InSlot",
1971
+ host: home.host,
1972
+ slotId: home.slotId
1973
+ })
1974
+ };
1975
+ return;
1976
+ }
1977
+ if (home.type === "zone") {
1978
+ if (zoneScopeById.get(home.zoneId) === "perPlayer") {
1979
+ if (!ownerId) {
1980
+ throw new Error(
1981
+ `${context.path}.zoneId: ${context.label} requires ownerId because zone '${home.zoneId}' has scope 'perPlayer'. Add ownerId to resolve the player-scoped destination.`
1982
+ );
1983
+ }
1984
+ componentLocations[componentId] = {
1985
+ type: "InHand",
1986
+ handId: home.zoneId,
1987
+ playerId: ownerId,
1988
+ position: nextLocationPosition({
1989
+ type: "InHand",
1990
+ handId: home.zoneId,
1991
+ playerId: ownerId
1992
+ })
1993
+ };
1994
+ return;
1995
+ }
1996
+ componentLocations[componentId] = {
1997
+ type: "InZone",
1998
+ zoneId: home.zoneId,
1999
+ playedBy: null,
2000
+ position: nextLocationPosition({
2001
+ type: "InZone",
2002
+ zoneId: home.zoneId
2003
+ })
2004
+ };
2005
+ return;
2006
+ }
2007
+ if (home.type === "space") {
2008
+ const boardId = resolveRuntimeBoardId(
2009
+ home.boardId,
2010
+ ownerId,
2011
+ `${context.path}.boardId: ${context.label}`
2012
+ );
2013
+ componentLocations[componentId] = {
2014
+ type: "OnSpace",
2015
+ boardId,
2016
+ spaceId: home.spaceId,
2017
+ position: nextLocationPosition({
2018
+ type: "OnSpace",
2019
+ boardId,
2020
+ spaceId: home.spaceId
2021
+ })
2022
+ };
2023
+ return;
2024
+ }
2025
+ if (home.type === "container") {
2026
+ const boardId = resolveRuntimeBoardId(
2027
+ home.boardId,
2028
+ ownerId,
2029
+ `${context.path}.boardId: ${context.label}`
2030
+ );
2031
+ componentLocations[componentId] = {
2032
+ type: "InContainer",
2033
+ boardId,
2034
+ containerId: home.containerId,
2035
+ position: nextLocationPosition({
2036
+ type: "InContainer",
2037
+ boardId,
2038
+ containerId: home.containerId
2039
+ })
2040
+ };
2041
+ return;
2042
+ }
2043
+ if (home.type === "edge") {
2044
+ const boardId = resolveRuntimeBoardId(
2045
+ home.boardId,
2046
+ ownerId,
2047
+ `${context.path}.boardId: ${context.label}`
2048
+ );
2049
+ const edgeId = resolveBoardEdgeId(home.boardId, home.ref.spaces);
2050
+ componentLocations[componentId] = {
2051
+ type: "OnEdge",
2052
+ boardId,
2053
+ edgeId,
2054
+ position: nextLocationPosition({
2055
+ type: "OnEdge",
2056
+ boardId,
2057
+ edgeId
2058
+ })
2059
+ };
2060
+ return;
2061
+ }
2062
+ if (home.type === "vertex") {
2063
+ const boardId = resolveRuntimeBoardId(
2064
+ home.boardId,
2065
+ ownerId,
2066
+ `${context.path}.boardId: ${context.label}`
2067
+ );
2068
+ const vertexId = resolveBoardVertexId(home.boardId, home.ref.spaces);
2069
+ componentLocations[componentId] = {
2070
+ type: "OnVertex",
2071
+ boardId,
2072
+ vertexId,
2073
+ position: nextLocationPosition({
2074
+ type: "OnVertex",
2075
+ boardId,
2076
+ vertexId
2077
+ })
2078
+ };
2079
+ return;
2080
+ }
2081
+ componentLocations[componentId] = { type: "Detached" };
2082
+ };
2083
+ for (const [seedIndex, seed] of (manifest.pieceSeeds ?? []).entries()) {
2084
+ const pieceType = pieceTypesById.get(seed.typeId);
2085
+ for (const componentId of expandSeedIds2([seed])) {
2086
+ pieces[componentId] = {
2087
+ componentType: "piece",
2088
+ id: componentId,
2089
+ pieceTypeId: seed.typeId,
2090
+ pieceName: pieceType?.name ?? seed.typeId,
2091
+ ownerId: seed.ownerId ?? null,
2092
+ properties: {
2093
+ ...materializeObjectSchemaDefaults(
2094
+ analysis.pieceTypeSchemasById.get(seed.typeId),
2095
+ analysis
2096
+ ),
2097
+ ...seed.fields ?? {}
2098
+ }
2099
+ };
2100
+ assignSeedLocation(componentId, seed.ownerId, seed.home, {
2101
+ path: `manifest.pieceSeeds[${seedIndex}].home`,
2102
+ label: `Piece seed '${seed.id ?? seed.typeId}'`
2103
+ });
2104
+ }
2105
+ }
2106
+ for (const [seedIndex, seed] of (manifest.dieSeeds ?? []).entries()) {
2107
+ const dieType = dieTypesById.get(seed.typeId);
2108
+ for (const componentId of expandSeedIds2([seed])) {
2109
+ dice[componentId] = {
2110
+ componentType: "die",
2111
+ id: componentId,
2112
+ dieTypeId: seed.typeId,
2113
+ dieName: dieType?.name ?? seed.typeId,
2114
+ ownerId: seed.ownerId ?? null,
2115
+ sides: dieType?.sides ?? 6,
2116
+ value: null,
2117
+ properties: {
2118
+ ...materializeObjectSchemaDefaults(
2119
+ analysis.dieTypeSchemasById.get(seed.typeId),
2120
+ analysis
2121
+ ),
2122
+ ...seed.fields ?? {}
2123
+ }
2124
+ };
2125
+ assignSeedLocation(componentId, seed.ownerId, seed.home, {
2126
+ path: `manifest.dieSeeds[${seedIndex}].home`,
2127
+ label: `Die seed '${seed.id ?? seed.typeId}'`
2128
+ });
2129
+ }
2130
+ }
2131
+ const boardStatesById = createRecord();
2132
+ const hexBoardStatesById = createRecord();
2133
+ const squareBoardStatesById = createRecord();
2134
+ for (const analyzedBoard of analysis.analyzedBoards) {
2135
+ const sharedBoardState = {
2136
+ baseId: analyzedBoard.board.id,
2137
+ layout: analyzedBoard.layout,
2138
+ typeId: analyzedBoard.boardTypeId ?? null,
2139
+ scope: analyzedBoard.board.scope,
2140
+ fields: {
2141
+ ...materializeObjectSchemaDefaults(
2142
+ analyzedBoard.boardFieldsSchema,
2143
+ analysis
2144
+ ),
2145
+ ...analyzedBoard.board.fields ?? {}
2146
+ }
2147
+ };
2148
+ const buildSpaces = () => Object.fromEntries(
2149
+ analyzedBoard.spaces.map((space) => {
2150
+ const spaceId = space.id;
2151
+ const baseSpaceState = {
2152
+ id: spaceId,
2153
+ name: "name" in space ? space.name ?? null : null,
2154
+ typeId: space?.typeId ?? null,
2155
+ fields: {
2156
+ ...materializeObjectSchemaDefaults(
2157
+ analyzedBoard.spaceFieldsSchema,
2158
+ analysis
2159
+ ),
2160
+ ...space.fields ?? {}
2161
+ },
2162
+ zoneId: "zoneId" in space ? space.zoneId ?? null : null
2163
+ };
2164
+ if (analyzedBoard.layout === "hex") {
2165
+ const hexSpace = space;
2166
+ return [
2167
+ spaceId,
2168
+ {
2169
+ ...baseSpaceState,
2170
+ q: hexSpace.q,
2171
+ r: hexSpace.r
2172
+ }
2173
+ ];
2174
+ }
2175
+ if (analyzedBoard.layout === "square") {
2176
+ const squareSpace = space;
2177
+ return [
2178
+ spaceId,
2179
+ {
2180
+ ...baseSpaceState,
2181
+ row: squareSpace.row,
2182
+ col: squareSpace.col
2183
+ }
2184
+ ];
2185
+ }
2186
+ return [spaceId, baseSpaceState];
2187
+ })
2188
+ );
2189
+ const relations = analyzedBoard.layout === "hex" ? [] : analyzedBoard.relations.map((relation) => ({
2190
+ id: relation.id ?? null,
2191
+ typeId: relation.typeId,
2192
+ fromSpaceId: relation.fromSpaceId,
2193
+ toSpaceId: relation.toSpaceId,
2194
+ directed: relation.directed ?? false,
2195
+ fields: {
2196
+ ...materializeObjectSchemaDefaults(
2197
+ analyzedBoard.relationFieldsSchema,
2198
+ analysis
2199
+ ),
2200
+ ...relation.fields ?? {}
2201
+ }
2202
+ }));
2203
+ const buildContainers = (runtimeBoardId) => analyzedBoard.layout === "hex" ? {} : Object.fromEntries(
2204
+ analyzedBoard.containers.map((container) => {
2205
+ const containerId = container.id;
2206
+ return [
2207
+ containerId,
2208
+ {
2209
+ id: containerId,
2210
+ name: container.name ?? containerId,
2211
+ host: container.host.type === "space" ? { type: "space", spaceId: container.host.spaceId } : { type: "board" },
2212
+ allowedCardSetIds: container.allowedCardSetIds,
2213
+ zoneId: `board:${runtimeBoardId}:container:${containerId}`,
2214
+ fields: {
2215
+ ...materializeObjectSchemaDefaults(
2216
+ analyzedBoard.containerFieldsSchema,
2217
+ analysis,
2218
+ runtimeBoardId
2219
+ ),
2220
+ ...container.fields ?? {}
2221
+ }
2222
+ }
2223
+ ];
2224
+ })
2225
+ );
2226
+ const edges = analyzedBoard.layout === "generic" ? [] : analyzedBoard.edges.map((edge) => ({
2227
+ id: edge.id,
2228
+ spaceIds: [...edge.spaceIds],
2229
+ typeId: edge.typeId ?? null,
2230
+ label: edge.label ?? null,
2231
+ ownerId: null,
2232
+ fields: {
2233
+ ...materializeObjectSchemaDefaults(
2234
+ analyzedBoard.edgeFieldsSchema,
2235
+ analysis
2236
+ ),
2237
+ ...edge.fields ?? {}
2238
+ }
2239
+ }));
2240
+ const vertices = analyzedBoard.layout === "generic" ? [] : analyzedBoard.vertices.map((vertex) => ({
2241
+ id: vertex.id,
2242
+ spaceIds: [...vertex.spaceIds],
2243
+ typeId: vertex.typeId ?? null,
2244
+ label: vertex.label ?? null,
2245
+ ownerId: null,
2246
+ fields: {
2247
+ ...materializeObjectSchemaDefaults(
2248
+ analyzedBoard.vertexFieldsSchema,
2249
+ analysis
2250
+ ),
2251
+ ...vertex.fields ?? {}
2252
+ }
2253
+ }));
2254
+ for (const runtimeBoardId of analyzedBoard.runtimeBoardIds) {
2255
+ const playerId = analyzedBoard.board.scope === "perPlayer" ? runtimeBoardId.slice(analyzedBoard.board.id.length + 1) : null;
2256
+ const boardState = {
2257
+ id: runtimeBoardId,
2258
+ ...sharedBoardState,
2259
+ playerId,
2260
+ spaces: cloneJson(buildSpaces()),
2261
+ relations: cloneJson(relations),
2262
+ containers: cloneJson(buildContainers(runtimeBoardId)),
2263
+ ...analyzedBoard.layout === "hex" ? {
2264
+ orientation: analyzedBoard.board.orientation ?? "pointy",
2265
+ edges: cloneJson(edges),
2266
+ vertices: cloneJson(vertices)
2267
+ } : analyzedBoard.layout === "square" ? {
2268
+ edges: cloneJson(edges),
2269
+ vertices: cloneJson(vertices)
2270
+ } : {}
2271
+ };
2272
+ boardStatesById[runtimeBoardId] = boardState;
2273
+ if (analyzedBoard.layout === "hex") {
2274
+ hexBoardStatesById[runtimeBoardId] = boardState;
2275
+ }
2276
+ if (analyzedBoard.layout === "square") {
2277
+ squareBoardStatesById[runtimeBoardId] = boardState;
2278
+ }
2279
+ }
2280
+ }
2281
+ const sharedZones = Object.fromEntries(
2282
+ analysis.sharedZones.map((zone) => [zone.id, []])
2283
+ );
2284
+ const perPlayerZones = Object.fromEntries(
2285
+ analysis.playerZones.map((zone) => [
2286
+ zone.id,
2287
+ Object.fromEntries(
2288
+ playerIds.map((playerId) => [playerId, []])
2289
+ )
2290
+ ])
2291
+ );
2292
+ const zoneVisibility = Object.fromEntries(
2293
+ (manifest.zones ?? []).map((zone) => [
2294
+ zone.id,
2295
+ zone.visibility ?? "public"
2296
+ ])
2297
+ );
2298
+ const zoneCardSetIdsByZoneId = Object.fromEntries(
2299
+ Array.from(analysis.zoneCardSetIdsById.entries()).sort(
2300
+ ([left], [right]) => left.localeCompare(right)
2301
+ )
2302
+ );
2303
+ const handVisibility = Object.fromEntries(
2304
+ analysis.playerZones.map((zone) => [
2305
+ zone.id,
2306
+ zone.visibility ?? "ownerOnly"
2307
+ ])
2308
+ );
2309
+ const componentSortPosition = (position) => typeof position === "number" ? position : Number.MAX_SAFE_INTEGER;
2310
+ const pushSharedComponent = (zoneId, componentId) => {
2311
+ const zone = sharedZones[zoneId];
2312
+ if (!zone) {
2313
+ return;
2314
+ }
2315
+ zone.push(componentId);
2316
+ zone.sort(
2317
+ (left, right) => componentSortPosition(componentLocations[left]?.position) - componentSortPosition(componentLocations[right]?.position)
2318
+ );
2319
+ };
2320
+ const pushPlayerComponent = (zoneId, playerId, componentId) => {
2321
+ const zone = perPlayerZones[zoneId]?.[playerId];
2322
+ if (!zone) {
2323
+ return;
2324
+ }
2325
+ zone.push(componentId);
2326
+ zone.sort(
2327
+ (left, right) => componentSortPosition(componentLocations[left]?.position) - componentSortPosition(componentLocations[right]?.position)
2328
+ );
2329
+ };
2330
+ for (const [componentId, location] of Object.entries(componentLocations)) {
2331
+ switch (location.type) {
2332
+ case "InDeck":
2333
+ pushSharedComponent(location.deckId, componentId);
2334
+ break;
2335
+ case "InHand":
2336
+ pushPlayerComponent(
2337
+ location.handId,
2338
+ location.playerId,
2339
+ componentId
2340
+ );
2341
+ break;
2342
+ case "InZone":
2343
+ pushSharedComponent(location.zoneId, componentId);
2344
+ break;
2345
+ default:
2346
+ break;
2347
+ }
2348
+ }
2349
+ const ownerOfCard = Object.fromEntries(
2350
+ Object.keys(cards).map((cardId) => [cardId, null])
2351
+ );
2352
+ const visibility = Object.fromEntries(
2353
+ Object.keys(cards).map((cardId) => [cardId, { faceUp: true }])
2354
+ );
2355
+ const resourcesByPlayer = Object.fromEntries(
2356
+ playerIds.map((playerId) => [
2357
+ playerId,
2358
+ Object.fromEntries(
2359
+ analysis.resourceIds.map((resourceId) => [resourceId, 0])
2360
+ )
2361
+ ])
2362
+ );
2363
+ return cloneJson({
2364
+ playerOrder: playerIds,
2365
+ zones: {
2366
+ shared: sharedZones,
2367
+ perPlayer: perPlayerZones,
2368
+ visibility: zoneVisibility,
2369
+ cardSetIdsByZoneId: zoneCardSetIdsByZoneId
2370
+ },
2371
+ decks: cloneJson(sharedZones),
2372
+ hands: cloneJson(perPlayerZones),
2373
+ handVisibility,
2374
+ cards,
2375
+ pieces,
2376
+ componentLocations,
2377
+ ownerOfCard,
2378
+ visibility,
2379
+ resources: resourcesByPlayer,
2380
+ boards: {
2381
+ byId: boardStatesById,
2382
+ hex: hexBoardStatesById,
2383
+ square: squareBoardStatesById,
2384
+ network: {},
2385
+ track: {}
2386
+ },
2387
+ dice
2388
+ });
2389
+ }
2390
+
2391
+ // src/reducer/manifest/schema.ts
2392
+ import { z } from "zod";
2393
+ function createTableSchema(analysis, ids) {
2394
+ const unknownRecordSchema = z.record(z.string(), z.unknown());
2395
+ const objectSchema = (schema) => schema && Object.keys(schema.properties).length ? z.object(
2396
+ Object.fromEntries(
2397
+ Object.entries(schema.properties).map(([key, value]) => [
2398
+ key,
2399
+ propertySchema(value)
2400
+ ])
2401
+ )
2402
+ ) : unknownRecordSchema;
2403
+ function propertySchema(schema) {
2404
+ if (!schema) return z.unknown();
2405
+ let result;
2406
+ switch (schema.type) {
2407
+ case "string":
2408
+ result = z.string();
2409
+ break;
2410
+ case "integer":
2411
+ result = z.number().int();
2412
+ break;
2413
+ case "number":
2414
+ result = z.number();
2415
+ break;
2416
+ case "boolean":
2417
+ result = z.boolean();
2418
+ break;
2419
+ case "enum":
2420
+ result = schema.enums?.length ? z.enum(schema.enums) : z.string();
2421
+ break;
2422
+ case "array":
2423
+ result = z.array(propertySchema(schema.items));
2424
+ break;
2425
+ case "record":
2426
+ result = z.record(z.string(), propertySchema(schema.values));
2427
+ break;
2428
+ case "object":
2429
+ result = objectSchema({ properties: schema.properties ?? {} });
2430
+ break;
2431
+ default:
2432
+ result = ids[schema.type];
2433
+ }
2434
+ if (schema.nullable) result = result.nullable();
2435
+ if (schema.optional) result = result.optional();
2436
+ if (Object.prototype.hasOwnProperty.call(schema, "default"))
2437
+ result = result.default(schema.default);
2438
+ return result;
2439
+ }
2440
+ const shape = (items, getKey, getSchema) => z.object(
2441
+ Object.fromEntries(items.map((item) => [getKey(item), getSchema(item)]))
2442
+ );
2443
+ const cardStateByIdSchema = shape(
2444
+ analysis.cardIds,
2445
+ (id) => id,
2446
+ (id) => {
2447
+ const setId = analysis.cardSetIdByCardId.get(id);
2448
+ const type = analysis.cardTypeByCardId.get(id);
2449
+ const schema = analysis.cardSets.find(
2450
+ (set) => set.id === setId
2451
+ ).cardSchema;
2452
+ const properties = schema && "variants" in schema ? objectSchema({
2453
+ properties: {
2454
+ ...schema.shared,
2455
+ ...schema.variants[type].properties
2456
+ }
2457
+ }) : objectSchema(schema);
2458
+ return z.object({
2459
+ componentType: z.string().optional(),
2460
+ id: z.literal(id),
2461
+ cardSetId: z.literal(setId),
2462
+ cardType: z.literal(type),
2463
+ name: z.string().optional(),
2464
+ text: z.string().optional(),
2465
+ frontImage: z.string().optional(),
2466
+ backImage: z.string().optional(),
2467
+ properties
2468
+ });
2469
+ }
2470
+ );
2471
+ const pieceStateByIdSchema = shape(
2472
+ analysis.pieceIds,
2473
+ (id) => id,
2474
+ (id) => {
2475
+ const type = analysis.pieceTypeIdByPieceId.get(id);
2476
+ return z.object({
2477
+ componentType: z.string().optional(),
2478
+ id: z.literal(id),
2479
+ pieceTypeId: z.literal(type),
2480
+ pieceName: z.string().nullish(),
2481
+ ownerId: ids.playerId.nullish(),
2482
+ properties: objectSchema(analysis.pieceTypeSchemasById.get(type))
2483
+ });
2484
+ }
2485
+ );
2486
+ const dieStateByIdSchema = shape(
2487
+ analysis.dieIds,
2488
+ (id) => id,
2489
+ (id) => {
2490
+ const type = analysis.dieTypeIdByDieId.get(id);
2491
+ return z.object({
2492
+ componentType: z.string().optional(),
2493
+ id: z.literal(id),
2494
+ dieTypeId: z.literal(type),
2495
+ dieName: z.string().nullish(),
2496
+ ownerId: ids.playerId.nullish(),
2497
+ sides: z.literal(
2498
+ analysis.manifest.dieTypes?.find((die) => die.id === type)?.sides ?? 6
2499
+ ),
2500
+ value: z.number().int().nullish(),
2501
+ properties: objectSchema(analysis.dieTypeSchemasById.get(type))
2502
+ });
2503
+ }
2504
+ );
2505
+ const sharedZoneSchema = shape(
2506
+ analysis.sharedZones,
2507
+ (zone) => zone.id,
2508
+ () => z.array(ids.cardId)
2509
+ );
2510
+ const playerZoneSchema = shape(
2511
+ analysis.playerZones,
2512
+ (zone) => zone.id,
2513
+ () => z.record(ids.playerId, z.array(ids.cardId))
2514
+ );
2515
+ const zoneIdSchema = ids.zoneId;
2516
+ const playerZoneIdSchema = ids.playerZoneId;
2517
+ const slotVariants = analysis.strictSlotHosts.flatMap(
2518
+ (host) => host.slotIds.map(
2519
+ (slotId) => z.object({
2520
+ type: z.literal("InSlot"),
2521
+ host: z.object({ kind: z.literal(host.kind), id: z.literal(host.id) }),
2522
+ slotId: z.literal(slotId),
2523
+ position: z.number().int().nullish()
2524
+ })
2525
+ )
2526
+ );
2527
+ const slotLocationSchema = slotVariants.length ? z.union(
2528
+ slotVariants
2529
+ ) : z.never();
2530
+ const boardSpaceTypeIdSchema = ids.spaceTypeId.nullable().optional();
2531
+ const boardSpaceStateSchema = z.object({
2532
+ id: ids.spaceId,
2533
+ name: z.string().nullable().optional(),
2534
+ typeId: boardSpaceTypeIdSchema,
2535
+ fields: unknownRecordSchema,
2536
+ zoneId: z.string().nullable().optional()
2537
+ });
2538
+ const hexSpaceStateSchema = boardSpaceStateSchema.extend({
2539
+ q: z.number().int(),
2540
+ r: z.number().int()
2541
+ });
2542
+ const squareSpaceStateSchema = boardSpaceStateSchema.extend({
2543
+ row: z.number().int(),
2544
+ col: z.number().int()
2545
+ });
2546
+ const boardRelationStateSchema = z.object({
2547
+ id: z.string().nullable().optional(),
2548
+ typeId: ids.relationTypeId,
2549
+ fromSpaceId: ids.spaceId,
2550
+ toSpaceId: ids.spaceId,
2551
+ directed: z.boolean(),
2552
+ fields: unknownRecordSchema
2553
+ });
2554
+ const boardContainerStateSchema = z.object({
2555
+ id: ids.boardContainerId,
2556
+ name: z.string(),
2557
+ host: z.discriminatedUnion("type", [
2558
+ z.object({ type: z.literal("board") }),
2559
+ z.object({ type: z.literal("space"), spaceId: ids.spaceId })
2560
+ ]),
2561
+ allowedCardSetIds: z.array(ids.cardSetId).optional(),
2562
+ zoneId: z.string(),
2563
+ fields: unknownRecordSchema
2564
+ });
2565
+ const runtimeGenericBoardStateSchema = z.object({
2566
+ id: ids.boardId,
2567
+ baseId: ids.boardBaseId,
2568
+ layout: z.literal("generic"),
2569
+ typeId: ids.boardTypeId.nullable().optional(),
2570
+ scope: z.enum(["shared", "perPlayer"]),
2571
+ playerId: ids.playerId.nullable().optional(),
2572
+ fields: unknownRecordSchema,
2573
+ // T220: per-board state.spaces is loose-keyed by string. See the
2574
+ // codegen-template comment in renderGenericBoardStateSchema for
2575
+ // the rationale; the wire shape is unchanged (additionalProperties
2576
+ // JSON), and the inner id field narrows at parse time.
2577
+ spaces: z.record(z.string(), boardSpaceStateSchema),
2578
+ relations: z.array(boardRelationStateSchema),
2579
+ containers: z.record(z.string(), boardContainerStateSchema)
2580
+ });
2581
+ const hexEdgeStateSchema = z.object({
2582
+ id: ids.edgeId,
2583
+ spaceIds: z.array(ids.spaceId).min(1).max(2),
2584
+ typeId: ids.edgeTypeId.nullable().optional(),
2585
+ label: z.string().nullable().optional(),
2586
+ ownerId: ids.playerId.nullable().optional(),
2587
+ fields: unknownRecordSchema
2588
+ });
2589
+ const hexVertexStateSchema = z.object({
2590
+ id: ids.vertexId,
2591
+ spaceIds: z.array(ids.spaceId).min(1).max(3),
2592
+ typeId: ids.vertexTypeId.nullable().optional(),
2593
+ label: z.string().nullable().optional(),
2594
+ ownerId: ids.playerId.nullable().optional(),
2595
+ fields: unknownRecordSchema
2596
+ });
2597
+ const squareVertexStateSchema = z.object({
2598
+ id: ids.vertexId,
2599
+ spaceIds: z.array(ids.spaceId).min(1).max(4),
2600
+ typeId: ids.vertexTypeId.nullable().optional(),
2601
+ label: z.string().nullable().optional(),
2602
+ ownerId: ids.playerId.nullable().optional(),
2603
+ fields: unknownRecordSchema
2604
+ });
2605
+ const runtimeHexBoardStateSchema = runtimeGenericBoardStateSchema.extend({
2606
+ layout: z.literal("hex"),
2607
+ // T220: loose-keyed by string — see comment above.
2608
+ spaces: z.record(z.string(), hexSpaceStateSchema),
2609
+ relations: z.array(boardRelationStateSchema),
2610
+ containers: z.object({}),
2611
+ orientation: z.enum(["pointy", "flat"]),
2612
+ edges: z.array(hexEdgeStateSchema),
2613
+ vertices: z.array(hexVertexStateSchema)
2614
+ });
2615
+ const runtimeSquareBoardStateSchema = runtimeGenericBoardStateSchema.extend({
2616
+ layout: z.literal("square"),
2617
+ // T220: loose-keyed by string — see comment above.
2618
+ spaces: z.record(z.string(), squareSpaceStateSchema),
2619
+ relations: z.array(boardRelationStateSchema),
2620
+ containers: z.record(z.string(), boardContainerStateSchema),
2621
+ edges: z.array(hexEdgeStateSchema),
2622
+ vertices: z.array(squareVertexStateSchema)
2623
+ });
2624
+ const boards = analysis.analyzedBoards.flatMap(
2625
+ (board) => board.runtimeBoardIds.map((id) => {
2626
+ const base = {
2627
+ id: board.board.scope === "perPlayer" ? z.templateLiteral([board.board.id, ":", z.string().min(1)]) : z.literal(id),
2628
+ baseId: z.literal(board.board.id),
2629
+ scope: z.literal(board.board.scope),
2630
+ fields: objectSchema(board.boardFieldsSchema)
2631
+ };
2632
+ const spaces = boardSpaceStateSchema.extend({
2633
+ fields: objectSchema(board.spaceFieldsSchema)
2634
+ });
2635
+ const relations = z.array(
2636
+ boardRelationStateSchema.extend({
2637
+ fields: objectSchema(
2638
+ "relationFieldsSchema" in board ? board.relationFieldsSchema : void 0
2639
+ )
2640
+ })
2641
+ );
2642
+ const containers = "containers" in board ? shape(
2643
+ board.containers,
2644
+ (item) => item.id,
2645
+ (item) => boardContainerStateSchema.extend({
2646
+ id: z.literal(item.id),
2647
+ fields: objectSchema(board.containerFieldsSchema)
2648
+ })
2649
+ ) : z.object({});
2650
+ if (board.layout === "generic")
2651
+ return {
2652
+ id,
2653
+ scope: board.board.scope,
2654
+ layout: board.layout,
2655
+ schema: runtimeGenericBoardStateSchema.extend({
2656
+ ...base,
2657
+ spaces: z.record(z.string(), spaces),
2658
+ relations,
2659
+ containers
2660
+ })
2661
+ };
2662
+ const edges = z.array(
2663
+ hexEdgeStateSchema.extend({
2664
+ fields: objectSchema(board.edgeFieldsSchema)
2665
+ })
2666
+ );
2667
+ const vertices = z.array(
2668
+ (board.layout === "hex" ? hexVertexStateSchema : squareVertexStateSchema).extend({ fields: objectSchema(board.vertexFieldsSchema) })
2669
+ );
2670
+ return {
2671
+ id,
2672
+ scope: board.board.scope,
2673
+ layout: board.layout,
2674
+ schema: board.layout === "hex" ? runtimeHexBoardStateSchema.extend({
2675
+ ...base,
2676
+ spaces: z.record(
2677
+ z.string(),
2678
+ spaces.extend({ q: z.number().int(), r: z.number().int() })
2679
+ ),
2680
+ edges,
2681
+ vertices
2682
+ }) : runtimeSquareBoardStateSchema.extend({
2683
+ ...base,
2684
+ spaces: z.record(
2685
+ z.string(),
2686
+ spaces.extend({
2687
+ row: z.number().int(),
2688
+ col: z.number().int()
2689
+ })
2690
+ ),
2691
+ relations,
2692
+ containers,
2693
+ edges,
2694
+ vertices
2695
+ })
2696
+ };
2697
+ })
2698
+ );
2699
+ const boardCollection = (entries) => {
2700
+ const shared = entries.filter((board) => board.scope === "shared");
2701
+ const scoped = entries.filter((board) => board.scope === "perPlayer").map((board) => board.schema);
2702
+ return shape(
2703
+ shared,
2704
+ (board) => board.id,
2705
+ (board) => board.schema
2706
+ ).catchall(scoped.length ? z.union(scoped) : z.never());
2707
+ };
2708
+ const boardStateByIdSchema = boardCollection(boards);
2709
+ const hexBoardStateByIdSchema = boardCollection(
2710
+ boards.filter((board) => board.layout === "hex")
2711
+ );
2712
+ const squareBoardStateByIdSchema = boardCollection(
2713
+ boards.filter((board) => board.layout === "square")
2714
+ );
2715
+ return z.object({
2716
+ playerOrder: z.array(ids.playerId),
2717
+ zones: z.object({
2718
+ shared: sharedZoneSchema,
2719
+ perPlayer: playerZoneSchema,
2720
+ visibility: z.record(
2721
+ zoneIdSchema,
2722
+ z.enum(["all", "ownerOnly", "public", "hidden"])
2723
+ ),
2724
+ cardSetIdsByZoneId: z.record(zoneIdSchema, z.array(ids.cardSetId)).optional()
2725
+ }),
2726
+ decks: sharedZoneSchema,
2727
+ hands: playerZoneSchema,
2728
+ handVisibility: z.record(
2729
+ playerZoneIdSchema,
2730
+ z.enum(["all", "ownerOnly", "public", "hidden"])
2731
+ ),
2732
+ cards: cardStateByIdSchema,
2733
+ pieces: pieceStateByIdSchema,
2734
+ componentLocations: z.record(
2735
+ z.string(),
2736
+ z.union([
2737
+ z.object({ type: z.literal("Detached") }),
2738
+ z.object({
2739
+ type: z.literal("InDeck"),
2740
+ deckId: ids.deckId,
2741
+ playedBy: ids.playerId.nullable(),
2742
+ position: z.number().int().nullable().optional()
2743
+ }),
2744
+ z.object({
2745
+ type: z.literal("InHand"),
2746
+ handId: ids.handId,
2747
+ playerId: ids.playerId,
2748
+ position: z.number().int().nullable().optional()
2749
+ }),
2750
+ z.object({
2751
+ type: z.literal("InZone"),
2752
+ zoneId: z.string(),
2753
+ playedBy: ids.playerId.nullable().optional(),
2754
+ position: z.number().int().nullable().optional()
2755
+ }),
2756
+ z.object({
2757
+ type: z.literal("OnSpace"),
2758
+ boardId: ids.boardId,
2759
+ spaceId: ids.spaceId,
2760
+ position: z.number().int().nullable().optional()
2761
+ }),
2762
+ z.object({
2763
+ type: z.literal("InContainer"),
2764
+ boardId: ids.boardId,
2765
+ containerId: ids.boardContainerId,
2766
+ position: z.number().int().nullable().optional()
2767
+ }),
2768
+ z.object({
2769
+ type: z.literal("OnEdge"),
2770
+ boardId: ids.boardId,
2771
+ edgeId: ids.edgeId,
2772
+ position: z.number().int().nullable().optional()
2773
+ }),
2774
+ z.object({
2775
+ type: z.literal("OnVertex"),
2776
+ boardId: ids.boardId,
2777
+ vertexId: ids.vertexId,
2778
+ position: z.number().int().nullable().optional()
2779
+ }),
2780
+ slotLocationSchema
2781
+ ])
2782
+ ),
2783
+ ownerOfCard: z.record(ids.cardId, ids.playerId.nullable()),
2784
+ visibility: z.record(
2785
+ ids.cardId,
2786
+ z.object({
2787
+ faceUp: z.boolean(),
2788
+ visibleTo: z.array(ids.playerId).nullable().optional()
2789
+ })
2790
+ ),
2791
+ resources: z.record(
2792
+ ids.playerId,
2793
+ z.record(ids.resourceId, z.number().int())
2794
+ ),
2795
+ boards: z.object({
2796
+ byId: boardStateByIdSchema,
2797
+ hex: hexBoardStateByIdSchema,
2798
+ square: squareBoardStateByIdSchema,
2799
+ network: z.record(z.string(), unknownRecordSchema).default({}),
2800
+ track: z.record(z.string(), unknownRecordSchema).default({})
2801
+ }),
2802
+ dice: dieStateByIdSchema
2803
+ }).superRefine((table, context) => {
2804
+ const players = new Set(table.playerOrder);
2805
+ if (players.size !== table.playerOrder.length) {
2806
+ context.addIssue({
2807
+ code: "custom",
2808
+ path: ["playerOrder"],
2809
+ message: "Duplicate player id"
2810
+ });
2811
+ }
2812
+ const checkPlayers = (record, path) => {
2813
+ for (const playerId of table.playerOrder) {
2814
+ if (!Object.hasOwn(record, playerId))
2815
+ context.addIssue({
2816
+ code: "custom",
2817
+ path: [...path, playerId],
2818
+ message: "Missing active player"
2819
+ });
2820
+ }
2821
+ for (const playerId of Object.keys(record)) {
2822
+ if (!players.has(playerId))
2823
+ context.addIssue({
2824
+ code: "custom",
2825
+ path: [...path, playerId],
2826
+ message: "Unknown active player"
2827
+ });
2828
+ }
2829
+ };
2830
+ checkPlayers(table.resources, ["resources"]);
2831
+ for (const [id, players2] of Object.entries(
2832
+ table.hands
2833
+ ))
2834
+ checkPlayers(players2, ["hands", id]);
2835
+ for (const [id, players2] of Object.entries(
2836
+ table.zones.perPlayer
2837
+ ))
2838
+ checkPlayers(players2, ["zones", "perPlayer", id]);
2839
+ });
2840
+ }
2841
+
2842
+ // src/reducer/manifest/compiler.ts
2843
+ function compileManifest(manifest) {
2844
+ const source = structuredClone(manifest);
2845
+ const analysis = analyzeManifest(source);
2846
+ const initial = materializeManifestTable({
2847
+ manifest: source,
2848
+ playerIds: analysis.playerIds,
2849
+ shuffleItems: (values) => [...values]
2850
+ });
2851
+ const sharedZoneIds = analysis.sharedZones.map((zone) => zone.id).sort();
2852
+ const playerZoneIds = analysis.playerZones.map((zone) => zone.id).sort();
2853
+ const literals = {
2854
+ ...Object.fromEntries(
2855
+ Object.entries(analysis).filter(([key]) => key.endsWith("Ids") || key === "cardTypes").map(([key, value]) => [key, value])
2856
+ ),
2857
+ playerIds: analysis.playerIds.map(asPlayerId),
2858
+ phaseNames: [],
2859
+ boardLayouts: ["generic", "hex", "square"],
2860
+ deckIds: sharedZoneIds,
2861
+ handIds: playerZoneIds,
2862
+ sharedZoneIds,
2863
+ playerZoneIds,
2864
+ resourcePresentationById: analysis.resourcePresentationById,
2865
+ ownerResourceIds: (source.resources ?? []).filter((resource) => resource.visibility === "owner").map((resource) => resource.id).sort(),
2866
+ handVisibilityById: Object.fromEntries(
2867
+ playerZoneIds.map((id) => [
2868
+ id,
2869
+ analysis.zoneVisibilityById.get(id) ?? "ownerOnly"
2870
+ ])
2871
+ ),
2872
+ zoneVisibilityById: Object.fromEntries(analysis.zoneVisibilityById),
2873
+ cardSetIdByCardId: Object.fromEntries(analysis.cardSetIdByCardId),
2874
+ cardTypeByCardId: Object.fromEntries(analysis.cardTypeByCardId),
2875
+ cardSetIdsBySharedZoneId: Object.fromEntries(analysis.sharedZoneCardSetIds),
2876
+ cardSetIdsByPlayerZoneId: Object.fromEntries(analysis.playerZoneCardSetIds)
2877
+ };
2878
+ const families = [
2879
+ "phaseName",
2880
+ "boardLayout",
2881
+ "cardSetId",
2882
+ "cardType",
2883
+ "cardId",
2884
+ "deckId",
2885
+ "handId",
2886
+ "sharedZoneId",
2887
+ "playerZoneId",
2888
+ "zoneId",
2889
+ "resourceId",
2890
+ "pieceTypeId",
2891
+ "pieceId",
2892
+ "dieTypeId",
2893
+ "dieId",
2894
+ "boardTypeId",
2895
+ "boardBaseId",
2896
+ "boardId",
2897
+ "boardContainerId",
2898
+ "relationTypeId",
2899
+ "edgeId",
2900
+ "edgeTypeId",
2901
+ "vertexId",
2902
+ "vertexTypeId",
2903
+ "spaceId",
2904
+ "spaceTypeId"
2905
+ ];
2906
+ const ids = {
2907
+ ...Object.fromEntries(
2908
+ families.map((family) => {
2909
+ const values = literals[`${family}s`];
2910
+ return [
2911
+ family,
2912
+ family === "phaseName" ? markManifestScopedSchema(z2.string(), family) : values.length ? createManifestStringLiteralSchema(values, family) : markManifestScopedSchema(z2.never(), family)
2913
+ ];
2914
+ })
2915
+ ),
2916
+ playerId: markManifestScopedSchema(
2917
+ z2.string().min(1).transform(asPlayerId),
2918
+ "playerId"
2919
+ )
2920
+ };
2921
+ const boardIdSchemas = analysis.analyzedBoards.map(
2922
+ (board) => board.board.scope === "perPlayer" ? z2.templateLiteral([board.board.id, ":", z2.string().min(1)]) : z2.literal(board.board.id)
2923
+ );
2924
+ ids.boardId = markManifestScopedSchema(
2925
+ boardIdSchemas.length ? z2.union(boardIdSchemas) : z2.never(),
2926
+ "boardId"
2927
+ );
2928
+ const tableSchema = assumeManifestSchema(
2929
+ createTableSchema(analysis, ids)
2930
+ );
2931
+ const resolvePlayers = (players = analysis.playerIds) => players.map(asPlayerId);
2932
+ const emptyDecks = () => Object.fromEntries(sharedZoneIds.map((id) => [id, []]));
2933
+ const hands = (players) => Object.fromEntries(
2934
+ playerZoneIds.map((id) => [
2935
+ id,
2936
+ Object.fromEntries(resolvePlayers(players).map((id2) => [id2, []]))
2937
+ ])
2938
+ );
2939
+ const defaults = {
2940
+ zones: (players) => ({
2941
+ shared: emptyDecks(),
2942
+ perPlayer: hands(players),
2943
+ visibility: structuredClone(literals.zoneVisibilityById),
2944
+ cardSetIdsByZoneId: Object.fromEntries(analysis.zoneCardSetIdsById)
2945
+ }),
2946
+ decks: emptyDecks,
2947
+ hands,
2948
+ handVisibility: () => structuredClone(literals.handVisibilityById),
2949
+ ownerOfCard: () => Object.fromEntries(analysis.cardIds.map((id) => [id, null])),
2950
+ visibility: () => Object.fromEntries(analysis.cardIds.map((id) => [id, { faceUp: true }])),
2951
+ resources: (players) => Object.fromEntries(
2952
+ resolvePlayers(players).map((id) => [
2953
+ id,
2954
+ Object.fromEntries(analysis.resourceIds.map((id2) => [id2, 0]))
2955
+ ])
2956
+ )
2957
+ };
2958
+ const runtimeSchema = createManifestRuntimeSchema({
2959
+ phaseNameSchema: z2.string(),
2960
+ playerIdSchema: ids.playerId
2961
+ });
2962
+ const createInitialTable = (options = {}) => tableSchema.parse(
2963
+ materializeManifestTable({
2964
+ manifest: source,
2965
+ playerIds: options.playerIds ?? analysis.playerIds,
2966
+ shuffleItems: options.shuffleItems ?? ((values) => [...values])
2967
+ })
2968
+ );
2969
+ return {
2970
+ literals,
2971
+ ids,
2972
+ defaults,
2973
+ records: Object.fromEntries(
2974
+ families.map((family) => [
2975
+ `${family}s`,
2976
+ (initial2) => buildTypedRecord(
2977
+ literals[`${family}s`],
2978
+ initial2
2979
+ )
2980
+ ])
2981
+ ),
2982
+ tableSchema,
2983
+ runtimeSchema,
2984
+ schemas: { table: tableSchema, runtime: runtimeSchema },
2985
+ staticBoards: initial.boards,
2986
+ createInitialTable,
2987
+ normalSetup: {
2988
+ minPlayers: source.players.minPlayers,
2989
+ maxPlayers: source.players.maxPlayers,
2990
+ createInitialTable
2991
+ },
2992
+ createGameStateSchema: (config) => createManifestGameStateSchema({
2993
+ ...config,
2994
+ tableSchema,
2995
+ playerIdSchema: ids.playerId
2996
+ })
2997
+ };
2998
+ }
2999
+
3000
+ // src/reducer/schema-helpers.ts
3001
+ import { z as z3 } from "zod";
3002
+ var sparseSchemas = /* @__PURE__ */ new WeakMap();
3003
+ function sparseMap(keySchema, valueSchema) {
3004
+ const schema = z3.partialRecord(keySchema, valueSchema);
3005
+ sparseSchemas.set(schema, {
3006
+ keySchema,
3007
+ valueSchema
3008
+ });
3009
+ return schema;
3010
+ }
3011
+ function sparseCounts(keySchema) {
3012
+ return sparseMap(keySchema, z3.number().int().min(0));
3013
+ }
3014
+ function getZodDef(schema) {
3015
+ const anySchema = schema;
3016
+ const fromInternal = anySchema?._zod?.def;
3017
+ if (fromInternal && typeof fromInternal === "object") {
3018
+ return fromInternal;
3019
+ }
3020
+ const fromLegacy = anySchema?.def;
3021
+ if (fromLegacy && typeof fromLegacy === "object") {
3022
+ return fromLegacy;
3023
+ }
3024
+ return {};
3025
+ }
3026
+ function unwrapWrappers(schema) {
3027
+ let current = schema;
3028
+ while (true) {
3029
+ const def = getZodDef(current);
3030
+ const innerType = def.innerType;
3031
+ if ((def.type === "nullable" || def.type === "optional" || def.type === "default" || def.type === "readonly" || def.type === "catch") && innerType) {
3032
+ current = innerType;
3033
+ continue;
3034
+ }
3035
+ return current;
3036
+ }
3037
+ }
3038
+ function isPlainZodString(schema) {
3039
+ return getZodDef(schema).type === "string";
3040
+ }
3041
+ function isZodArray(schema) {
3042
+ return getZodDef(schema).type === "array";
3043
+ }
3044
+ function isEnumKeyedRecordSchema(schema) {
3045
+ const def = getZodDef(schema);
3046
+ const keyType = def.keyType;
3047
+ return def.type === "record" && typeof keyType === "object" && keyType !== null && getZodDef(keyType).type === "enum";
3048
+ }
3049
+ function isSparseMapSchema(schema) {
3050
+ const inner = unwrapWrappers(schema);
3051
+ return typeof inner === "object" && inner !== null && sparseSchemas.has(inner);
3052
+ }
3053
+ function getSparseSchemaMetadata(schema) {
3054
+ const inner = unwrapWrappers(schema);
3055
+ if (typeof inner !== "object" || inner === null) {
3056
+ return void 0;
3057
+ }
3058
+ return sparseSchemas.get(inner);
3059
+ }
3060
+ function isPlainObject(value) {
3061
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3062
+ }
3063
+ function getObjectShape(schema) {
3064
+ const def = getZodDef(schema);
3065
+ if (def.type !== "object") {
3066
+ return null;
3067
+ }
3068
+ const shape = def.shape;
3069
+ if (typeof shape === "function") {
3070
+ const resolved = shape();
3071
+ return resolved && typeof resolved === "object" ? resolved : null;
3072
+ }
3073
+ return shape && typeof shape === "object" ? shape : null;
3074
+ }
3075
+ function normalizeSchemaInput(schema, input) {
3076
+ const inner = unwrapWrappers(schema);
3077
+ const sparse = getSparseSchemaMetadata(inner);
3078
+ if (sparse && isPlainObject(input)) {
3079
+ const filtered = Object.fromEntries(
3080
+ Object.entries(input).filter(
3081
+ ([key, value]) => typeof value !== "undefined" && sparse.keySchema.safeParse(key).success
3082
+ ).map(([key, value]) => [
3083
+ key,
3084
+ normalizeSchemaInput(sparse.valueSchema, value)
3085
+ ])
3086
+ );
3087
+ return filtered;
3088
+ }
3089
+ const shape = getObjectShape(inner);
3090
+ if (shape && isPlainObject(input)) {
3091
+ return Object.fromEntries(
3092
+ Object.entries(input).map(([key, value]) => {
3093
+ const fieldSchema = shape[key];
3094
+ return [
3095
+ key,
3096
+ typeof fieldSchema === "undefined" ? value : normalizeSchemaInput(fieldSchema, value)
3097
+ ];
3098
+ })
3099
+ );
3100
+ }
3101
+ const def = getZodDef(inner);
3102
+ if (def.type === "array" && Array.isArray(input) && def.element) {
3103
+ return input.map((value) => normalizeSchemaInput(def.element, value));
3104
+ }
3105
+ return input;
3106
+ }
3107
+ function normalizeCommandParams(schema, input) {
3108
+ return schema.parse(normalizeSchemaInput(schema, input));
3109
+ }
3110
+
3111
+ // src/reducer/authoring/validation.ts
3112
+ var MANIFEST_SCOPED_ID_NAMES = [
3113
+ "playerId",
3114
+ "cardId",
3115
+ "deckId",
3116
+ "handId",
3117
+ "zoneId",
3118
+ "sharedZoneId",
3119
+ "playerZoneId",
3120
+ "boardId",
3121
+ "boardContainerId",
3122
+ "boardTypeId",
3123
+ "boardBaseId",
3124
+ "edgeId",
3125
+ "edgeTypeId",
3126
+ "vertexId",
3127
+ "vertexTypeId",
3128
+ "spaceId",
3129
+ "spaceTypeId",
3130
+ "relationTypeId",
3131
+ "resourceId",
3132
+ "pieceId",
3133
+ "pieceTypeId",
3134
+ "dieId",
3135
+ "dieTypeId",
3136
+ "cardSetId",
3137
+ "cardType"
3138
+ ];
3139
+ function matchManifestScopedIdName(key) {
3140
+ for (const name of MANIFEST_SCOPED_ID_NAMES) {
3141
+ const capitalized = `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
3142
+ if (key === name || key.endsWith(capitalized)) {
3143
+ return { name, kind: "singular" };
3144
+ }
3145
+ const plural = `${name}s`;
3146
+ const capitalizedPlural = `${capitalized}s`;
3147
+ if (key === plural || key.endsWith(capitalizedPlural)) {
3148
+ return { name, kind: "plural" };
3149
+ }
3150
+ }
3151
+ return null;
3152
+ }
3153
+ function validateInteractionParamsSchema(schema, context, path) {
3154
+ const inner = unwrapWrappers(schema);
3155
+ if (isEnumKeyedRecordSchema(inner) && !isSparseMapSchema(inner)) {
3156
+ throw new Error(
3157
+ `${context}: ${path} uses an enum-keyed z.record(...), which parses as a total map at runtime. Use sparseMap(...) or sparseCounts(...) from @dreamboard-games/sdk/reducer instead so interaction params stay sparse and match runtime validation.`
3158
+ );
3159
+ }
3160
+ const shape = getObjectShape(inner);
3161
+ if (shape) {
3162
+ for (const [key, fieldSchema] of Object.entries(shape)) {
3163
+ validateInteractionParamsSchema(fieldSchema, context, `${path}.${key}`);
3164
+ }
3165
+ return;
3166
+ }
3167
+ const def = getZodDef(inner);
3168
+ if (def.type === "array" && def.element) {
3169
+ validateInteractionParamsSchema(def.element, context, `${path}[]`);
3170
+ return;
3171
+ }
3172
+ if (def.type === "record" && def.valueType) {
3173
+ validateInteractionParamsSchema(def.valueType, context, `${path}.*`);
3174
+ }
3175
+ }
3176
+ function validateInteractionInputsSchema(inputs, context) {
3177
+ if (!inputs) return;
3178
+ for (const [key, collector] of Object.entries(inputs)) {
3179
+ validateInteractionParamsSchema(collector.schema, context, `inputs.${key}`);
3180
+ }
3181
+ }
3182
+ function validateManyCommitPolicy(input) {
3183
+ if (input.commit?.mode !== "autoWhenReady") return;
3184
+ if (Object.values(input.inputs ?? {}).some(
3185
+ (collector) => collector.selection?.mode === "many"
3186
+ )) {
3187
+ throw new Error(
3188
+ `${input.context}: interactions with many(...) inputs must use commit: { mode: "manual" }.`
3189
+ );
3190
+ }
3191
+ }
3192
+ function validateInteractionLikeDefinition(input, context) {
3193
+ if (input.steps) {
3194
+ if (input.inputs !== void 0)
3195
+ throw new Error("An interaction declares inputs or steps, never both.");
3196
+ if (input.steps.entries.length === 0)
3197
+ throw new Error("An interaction requires at least one step.");
3198
+ const keys = input.steps.entries.map((entry) => entry.key);
3199
+ if (new Set(keys).size !== keys.length)
3200
+ throw new Error("Interaction step keys must be unique.");
3201
+ }
3202
+ validateInteractionInputsSchema(input.inputs, context);
3203
+ validateManyCommitPolicy({
3204
+ inputs: input.inputs,
3205
+ commit: input.commit,
3206
+ context
3207
+ });
3208
+ if (input.paramsSchema) {
3209
+ validateInteractionParamsSchema(
3210
+ input.paramsSchema,
3211
+ context,
3212
+ "paramsSchema"
3213
+ );
3214
+ }
3215
+ }
3216
+ function validateStateSchemaIdBranding(schema, scope) {
3217
+ const def = getZodDef(schema);
3218
+ if (def.type !== "object") {
3219
+ return;
3220
+ }
3221
+ const shape = def.shape;
3222
+ if (!shape) {
3223
+ return;
3224
+ }
3225
+ for (const [key, fieldSchema] of Object.entries(shape)) {
3226
+ const match = matchManifestScopedIdName(key);
3227
+ if (!match) {
3228
+ continue;
3229
+ }
3230
+ const inner = unwrapWrappers(fieldSchema);
3231
+ if (match.kind === "singular") {
3232
+ if (isPlainZodString(inner) && !isManifestScopedSchema(inner)) {
3233
+ throw new Error(
3234
+ `defineGameContract: state.${scope}.${key} uses a raw z.string() but its name identifies it as a manifest-scoped '${match.name}'. Use gameContract.schemas.${match.name} (or manifest.ids.${match.name}) so the branded literal union flows through state types.`
3235
+ );
3236
+ }
3237
+ continue;
3238
+ }
3239
+ if (match.kind === "plural" && isZodArray(inner)) {
3240
+ const element = unwrapWrappers(getZodDef(inner).element);
3241
+ if (isPlainZodString(element) && !isManifestScopedSchema(element)) {
3242
+ throw new Error(
3243
+ `defineGameContract: state.${scope}.${key} uses z.array(z.string()) but its name identifies it as manifest-scoped '${match.name}' values. Use z.array(gameContract.schemas.${match.name}) (or manifest.ids.${match.name}) so the branded literal union flows through state types.`
3244
+ );
3245
+ }
3246
+ }
3247
+ }
3248
+ }
3249
+ function validateDefineGamePhaseNames(definition) {
3250
+ const contractPhaseNames = definition.contract.phaseNames;
3251
+ if (!Array.isArray(contractPhaseNames)) {
3252
+ return;
3253
+ }
3254
+ const declared = new Set(contractPhaseNames);
3255
+ const actual = new Set(Object.keys(definition.phases));
3256
+ const missing = [...declared].filter((name) => !actual.has(name));
3257
+ const extra = [...actual].filter((name) => !declared.has(name));
3258
+ if (missing.length > 0 || extra.length > 0) {
3259
+ const parts = [];
3260
+ if (missing.length > 0) {
3261
+ parts.push(`missing: [${missing.join(", ")}]`);
3262
+ }
3263
+ if (extra.length > 0) {
3264
+ parts.push(`extra: [${extra.join(", ")}]`);
3265
+ }
3266
+ throw new Error(
3267
+ `defineGame: phases record does not match contract.phaseNames (${parts.join(
3268
+ "; "
3269
+ )}). Update contract.phaseNames or the phases record so they agree.`
3270
+ );
3271
+ }
3272
+ if (definition.initialPhase !== void 0 && !declared.has(definition.initialPhase)) {
3273
+ throw new Error(
3274
+ `defineGame: initialPhase '${definition.initialPhase}' is not declared in contract.phaseNames.`
3275
+ );
3276
+ }
3277
+ }
3278
+ function validateDefineGamePhases(definition) {
3279
+ for (const [phaseName, phase] of Object.entries(definition.phases)) {
3280
+ const phaseRecord = typeof phase === "object" && phase !== null ? phase : null;
3281
+ for (const interaction of Object.values(phaseRecord?.interactions ?? {})) {
3282
+ validateInteractionLikeDefinition(
3283
+ interaction,
3284
+ "defineInteraction"
3285
+ );
3286
+ }
3287
+ if (!phaseRecord || phaseRecord.kind !== "simultaneousPlayer") {
3288
+ continue;
3289
+ }
3290
+ if (!phaseRecord.submit) {
3291
+ throw new Error(
3292
+ `defineGame: phases.${phaseName} is kind 'simultaneousPlayer' but does not declare submit.`
3293
+ );
3294
+ }
3295
+ if (typeof phaseRecord.resolve !== "function") {
3296
+ throw new Error(
3297
+ `defineGame: phases.${phaseName} is kind 'simultaneousPlayer' but does not declare resolve.`
3298
+ );
3299
+ }
3300
+ if (phaseRecord.interactions?.submit) {
3301
+ throw new Error(
3302
+ `defineGame: phases.${phaseName}.interactions.submit conflicts with the reserved simultaneous submit interaction id.`
3303
+ );
3304
+ }
3305
+ validateInteractionLikeDefinition(
3306
+ phaseRecord.submit,
3307
+ `defineGame: phases.${phaseName}.submit`
3308
+ );
3309
+ }
3310
+ }
3311
+
3312
+ // src/reducer/authoring/steps.ts
3313
+ var InteractionSteps = class _InteractionSteps {
3314
+ constructor(entries = []) {
3315
+ this.entries = entries;
3316
+ }
3317
+ entries;
3318
+ input(key, collector) {
3319
+ if (this.entries.some((entry) => entry.key === key)) {
3320
+ throw new Error(`Duplicate interaction step '${key}'.`);
3321
+ }
3322
+ const factory = typeof collector === "function" ? collector : () => collector;
3323
+ const resolve = (args) => {
3324
+ const resolved = factory(args);
3325
+ if (resolved.kind === "rng")
3326
+ throw new Error("Interaction steps cannot contain RNG collectors.");
3327
+ validateInteractionInputsSchema({ [key]: resolved }, "defineInteraction");
3328
+ return resolved;
3329
+ };
3330
+ if (typeof collector !== "function" && collector.kind === "rng") {
3331
+ throw new Error("Interaction steps cannot contain RNG collectors.");
3332
+ }
3333
+ if (typeof collector !== "function")
3334
+ validateInteractionInputsSchema(
3335
+ { [key]: collector },
3336
+ "defineInteraction"
3337
+ );
3338
+ return new _InteractionSteps([...this.entries, { key, resolve }]);
3339
+ }
3340
+ };
3341
+
3342
+ // src/reducer/inputs/formInput.ts
3343
+ import { z as z4 } from "zod";
3344
+ function isResourceMapChoiceSource(choices) {
3345
+ return choices === "resourceMap" || typeof choices === "object" && choices !== null && !Array.isArray(choices) && "source" in choices && choices.source === "resourceMap";
3346
+ }
3347
+ function resolveDomainNumber(value, context, fallback) {
3348
+ if (typeof value === "function") return value(context);
3349
+ return value ?? fallback;
3350
+ }
3351
+ function resolveDomainChoices(choices, context) {
3352
+ if (isResourceMapChoiceSource(choices)) {
3353
+ return resolveResourceMapChoices(choices, context);
3354
+ }
3355
+ const resolved = typeof choices === "function" ? choices(context) : choices;
3356
+ return resolved.map((choice) => ({
3357
+ value: choice.value,
3358
+ label: choice.label,
3359
+ icon: choice.icon,
3360
+ badge: choice.badge,
3361
+ description: choice.description,
3362
+ disabled: choice.disabled,
3363
+ disabledReason: choice.disabledReason
3364
+ }));
3365
+ }
3366
+ function choiceValuesInclude(choices, value) {
3367
+ return choices.some((choice) => Object.is(choice.value, value));
3368
+ }
3369
+ function assertChoiceDefaultInChoices(inputName, choices, defaultValue) {
3370
+ if (choiceValuesInclude(choices, defaultValue)) return;
3371
+ const printable = defaultValue === null ? "null" : `'${defaultValue}'`;
3372
+ throw new Error(
3373
+ `${inputName} defaultValue ${printable} must be one of its choices. If null is a valid value, add an explicit { value: null, label: ... } choice.`
3374
+ );
3375
+ }
3376
+ function choiceSchema(choices) {
3377
+ if (choices.length === 0) return z4.never();
3378
+ const values = choices.map((choice) => choice.value);
3379
+ const stringValues = values.filter(
3380
+ (value) => value !== null
3381
+ );
3382
+ const hasNull = values.some((value) => value === null);
3383
+ if (stringValues.length === 0) {
3384
+ return z4.null();
3385
+ }
3386
+ const stringSchema = z4.enum(
3387
+ stringValues
3388
+ );
3389
+ if (hasNull) {
3390
+ return z4.union([stringSchema, z4.null()]);
3391
+ }
3392
+ return stringSchema;
3393
+ }
3394
+ function resolveResourceMapChoices(choices, context) {
3395
+ const firstPlayerId = context.state.table.playerOrder[0];
3396
+ const firstResourceMap = firstPlayerId === void 0 ? void 0 : context.state.table.resources[firstPlayerId];
3397
+ if (typeof firstResourceMap !== "object" || firstResourceMap === null || Array.isArray(firstResourceMap)) {
3398
+ return [];
3399
+ }
3400
+ const decorate = choices === "resourceMap" ? void 0 : choices.decorate;
3401
+ return Object.keys(firstResourceMap).sort().map((resourceId) => {
3402
+ const label = resourceId;
3403
+ const presentation = decorate?.({ ...context, resourceId, label });
3404
+ return {
3405
+ value: resourceId,
3406
+ label,
3407
+ badge: presentation?.badge,
3408
+ description: presentation?.description,
3409
+ disabled: presentation?.disabled,
3410
+ disabledReason: presentation?.disabledReason
3411
+ };
3412
+ });
3413
+ }
3414
+ function createFormInput(schema, options) {
3415
+ return {
3416
+ kind: "form",
3417
+ schema,
3418
+ ...options && "defaultValue" in options ? { defaultValue: options.defaultValue } : {}
3419
+ };
3420
+ }
3421
+ function baseFormInput(schema, options) {
3422
+ return createFormInput(schema, options);
3423
+ }
3424
+ function resourceMapInput(options) {
3425
+ return {
3426
+ kind: "form",
3427
+ schema: z4.record(z4.string(), z4.number().int().nonnegative()),
3428
+ ..."defaultValue" in options ? { defaultValue: options.defaultValue } : {},
3429
+ domain: (state, playerId, q) => {
3430
+ const context = {
3431
+ state,
3432
+ playerId,
3433
+ q
3434
+ };
3435
+ return {
3436
+ type: "resourceMap",
3437
+ resources: options.resources.map((resource) => ({
3438
+ resourceId: resource.resourceId,
3439
+ label: resource.label,
3440
+ icon: resource.icon,
3441
+ min: resolveDomainNumber(resource.min, context, 0),
3442
+ max: resolveDomainNumber(resource.max, context, 0)
3443
+ }))
3444
+ };
3445
+ }
3446
+ };
3447
+ }
3448
+ function numberInput(options) {
3449
+ return {
3450
+ kind: "form",
3451
+ schema: z4.number(),
3452
+ ..."defaultValue" in options ? { defaultValue: options.defaultValue } : {},
3453
+ domain: (state, playerId, q) => {
3454
+ const context = {
3455
+ state,
3456
+ playerId,
3457
+ q
3458
+ };
3459
+ return {
3460
+ type: "boundedNumber",
3461
+ min: resolveDomainNumber(options.min, context, 0),
3462
+ max: resolveDomainNumber(options.max, context, 0),
3463
+ step: options.step === void 0 ? void 0 : resolveDomainNumber(options.step, context, 1)
3464
+ };
3465
+ }
3466
+ };
3467
+ }
3468
+ function choiceInput(options) {
3469
+ const staticChoices = Array.isArray(options.choices) ? options.choices : null;
3470
+ const hasStaticDefault = typeof options.defaultValue !== "function";
3471
+ const staticDefault = options.defaultValue;
3472
+ if (staticChoices && hasStaticDefault) {
3473
+ assertChoiceDefaultInChoices(
3474
+ "formInput.choice",
3475
+ staticChoices,
3476
+ staticDefault
3477
+ );
3478
+ }
3479
+ const schema = staticChoices ? choiceSchema(staticChoices) : z4.string().nullable();
3480
+ const dynamicDefault = typeof options.defaultValue === "function" ? options.defaultValue : void 0;
3481
+ return {
3482
+ kind: "form",
3483
+ schema,
3484
+ ...hasStaticDefault ? { defaultValue: staticDefault } : {},
3485
+ domain: (state, playerId, q) => {
3486
+ const context = {
3487
+ state,
3488
+ playerId,
3489
+ q
3490
+ };
3491
+ const choices = resolveDomainChoices(options.choices, context);
3492
+ if (hasStaticDefault) {
3493
+ assertChoiceDefaultInChoices(
3494
+ "formInput.choice",
3495
+ choices,
3496
+ staticDefault
3497
+ );
3498
+ }
3499
+ return {
3500
+ type: "choice",
3501
+ choices
3502
+ };
3503
+ },
3504
+ ...dynamicDefault ? {
3505
+ resolveDefaultValue: (state, playerId, q, domain) => {
3506
+ const choices = domain.type === "choice" ? domain.choices.map((choice) => ({
3507
+ value: choice.value,
3508
+ label: choice.label,
3509
+ icon: choice.icon,
3510
+ badge: choice.badge,
3511
+ description: choice.description,
3512
+ disabled: choice.disabled,
3513
+ disabledReason: choice.disabledReason
3514
+ })) : [];
3515
+ const resolved = dynamicDefault({
3516
+ state,
3517
+ playerId,
3518
+ q,
3519
+ choices
3520
+ });
3521
+ if (resolved === void 0) return void 0;
3522
+ assertChoiceDefaultInChoices("formInput.choice", choices, resolved);
3523
+ return resolved;
3524
+ }
3525
+ } : {}
3526
+ };
3527
+ }
3528
+ function choiceListInput(options) {
3529
+ if (Array.isArray(options.choices) && options.choices.length === 0) {
3530
+ throw new Error("formInput.choiceList requires at least one choice.");
3531
+ }
3532
+ const staticDefaultValue = Array.isArray(options.defaultValue) ? options.defaultValue : void 0;
3533
+ const dynamicDefaultValue = options.defaultValue === "all" || typeof options.defaultValue === "function" ? options.defaultValue : void 0;
3534
+ return {
3535
+ kind: "form",
3536
+ schema: z4.array(z4.string()),
3537
+ ...staticDefaultValue !== void 0 ? { defaultValue: staticDefaultValue } : {},
3538
+ domain: (state, playerId, q) => {
3539
+ const context = {
3540
+ state,
3541
+ playerId,
3542
+ q
3543
+ };
3544
+ const choices = resolveDomainChoices(options.choices, context).map(
3545
+ (choice) => ({
3546
+ ...choice,
3547
+ value: choice.value
3548
+ })
3549
+ );
3550
+ return {
3551
+ type: "choiceList",
3552
+ choices,
3553
+ min: resolveDomainNumber(options.min, context, 0),
3554
+ max: resolveDomainNumber(options.max, context, choices.length)
3555
+ };
3556
+ },
3557
+ ...dynamicDefaultValue ? {
3558
+ resolveDefaultValue: (state, playerId, q, domain) => {
3559
+ const choices = domain.type === "choiceList" ? domain.choices.map((choice) => ({
3560
+ value: choice.value,
3561
+ label: choice.label,
3562
+ icon: choice.icon,
3563
+ badge: choice.badge,
3564
+ description: choice.description,
3565
+ disabled: choice.disabled,
3566
+ disabledReason: choice.disabledReason
3567
+ })) : [];
3568
+ if (dynamicDefaultValue === "all") {
3569
+ return choices.map((choice) => choice.value);
3570
+ }
3571
+ return dynamicDefaultValue({
3572
+ state,
3573
+ playerId,
3574
+ q,
3575
+ choices
3576
+ });
3577
+ }
3578
+ } : {}
3579
+ };
3580
+ }
3581
+ function formInputForState() {
3582
+ return Object.assign(
3583
+ ((schema, options) => baseFormInput(schema, options)),
3584
+ {
3585
+ resourceMap: (options) => resourceMapInput(options),
3586
+ resourceChoices: (options) => formInput.resourceChoices(options),
3587
+ number: (options) => numberInput(options),
3588
+ choice: (options) => choiceInput(options),
3589
+ choiceList: (options) => choiceListInput(options)
3590
+ }
3591
+ );
3592
+ }
3593
+ var formInput = Object.assign(baseFormInput, {
3594
+ resourceMap: resourceMapInput,
3595
+ resourceChoices: (options) => ({
3596
+ source: "resourceMap",
3597
+ ...options
3598
+ }),
3599
+ number: numberInput,
3600
+ choice: choiceInput,
3601
+ choiceList: choiceListInput,
3602
+ forState: formInputForState
3603
+ });
3604
+
3605
+ // src/reducer/inputs/boardInput.ts
3606
+ import { z as z5 } from "zod";
3607
+ function makeBoardCollector(kind) {
3608
+ return function collector(options) {
3609
+ const target = options.target;
3610
+ return {
3611
+ kind,
3612
+ schema: z5.string(),
3613
+ eligibleTargets: ((state, playerId, q) => target.eligible({
3614
+ state,
3615
+ playerId,
3616
+ q
3617
+ })),
3618
+ validateTarget: ((state, playerId, q, targetId) => target.validate(
3619
+ {
3620
+ state,
3621
+ playerId,
3622
+ q
3623
+ },
3624
+ targetId
3625
+ )),
3626
+ domain: (state, playerId, q) => ({
3627
+ type: "boardTarget",
3628
+ projection: "resolved",
3629
+ targetKind: target.targetKind,
3630
+ boardId: target.boardId,
3631
+ valueKind: "board-id",
3632
+ eligibleTargets: target.eligible({
3633
+ state,
3634
+ playerId,
3635
+ q
3636
+ })
3637
+ }),
3638
+ meta: {
3639
+ targetKind: target.targetKind,
3640
+ boardId: target.boardId,
3641
+ valueKind: "board-id"
3642
+ }
3643
+ };
3644
+ };
3645
+ }
3646
+ var vertexInput = makeBoardCollector("board-vertex");
3647
+ var edgeInput = makeBoardCollector("board-edge");
3648
+ var tileInput = makeBoardCollector("board-tile");
3649
+ var spaceInput = makeBoardCollector("board-space");
3650
+ function playerSpaceInput(options) {
3651
+ const target = options.target;
3652
+ const playerIdSchema = markManifestScopedSchema(
3653
+ z5.string(),
3654
+ "playerId"
3655
+ );
3656
+ return {
3657
+ kind: "board-space",
3658
+ schema: z5.strictObject({
3659
+ boardId: z5.literal(target.boardId),
3660
+ playerId: playerIdSchema,
3661
+ spaceId: z5.string()
3662
+ }),
3663
+ eligibleTargets: ((state, playerId, q) => target.eligible({
3664
+ state,
3665
+ playerId,
3666
+ q
3667
+ })),
3668
+ validateTarget: ((state, playerId, q, targetValue) => target.validate(
3669
+ {
3670
+ state,
3671
+ playerId,
3672
+ q
3673
+ },
3674
+ targetValue
3675
+ )),
3676
+ domain: (state, playerId, q) => ({
3677
+ type: "boardTarget",
3678
+ projection: "resolved",
3679
+ targetKind: "space",
3680
+ boardId: target.boardId,
3681
+ valueKind: "player-board-space",
3682
+ eligibleTargets: target.eligible({
3683
+ state,
3684
+ playerId,
3685
+ q
3686
+ })
3687
+ }),
3688
+ meta: {
3689
+ targetKind: target.targetKind,
3690
+ boardId: target.boardId,
3691
+ valueKind: "player-board-space"
3692
+ }
3693
+ };
3694
+ }
3695
+ var boardInput = {
3696
+ vertex: vertexInput,
3697
+ edge: edgeInput,
3698
+ tile: tileInput,
3699
+ space: spaceInput,
3700
+ playerSpace: playerSpaceInput
3701
+ };
3702
+
3703
+ // src/reducer/inputs/targetRule.ts
3704
+ var DEFAULT_MISSING_CANDIDATE_ISSUE = {
3705
+ errorCode: "TARGET_NOT_ELIGIBLE",
3706
+ message: "Target is not eligible."
3707
+ };
3708
+ function createTargetRule(candidates, predicates, options = {}) {
3709
+ const missingCandidateIssue = options.missingCandidateIssue ?? DEFAULT_MISSING_CANDIDATE_ISSUE;
3710
+ const equals = options.equals ?? Object.is;
3711
+ const validate = (ctx, target) => {
3712
+ if (!candidates(ctx).some((candidate) => equals(candidate, target))) {
3713
+ return missingCandidateIssue;
3714
+ }
3715
+ for (const predicate of predicates) {
3716
+ if (!predicate.test({ ...ctx, targetId: target, target })) {
3717
+ return {
3718
+ errorCode: predicate.errorCode,
3719
+ message: predicate.message
3720
+ };
3721
+ }
3722
+ }
3723
+ return null;
3724
+ };
3725
+ const rule = {
3726
+ eligible: (ctx) => candidates(ctx).filter((targetId) => validate(ctx, targetId) == null),
3727
+ validate,
3728
+ isEligible: (ctx, target) => validate(ctx, target) == null,
3729
+ bind: (ctx) => ({
3730
+ eligible: () => rule.eligible(ctx),
3731
+ validate: (id) => rule.validate(ctx, id),
3732
+ isEligible: (id) => rule.isEligible(ctx, id)
3733
+ })
3734
+ };
3735
+ return rule;
3736
+ }
3737
+ function createTargetRuleBuilder(buildRule, predicates = []) {
3738
+ return {
3739
+ where: (predicate) => createTargetRuleBuilder(buildRule, [...predicates, predicate]),
3740
+ build: () => buildRule(predicates)
3741
+ };
3742
+ }
3743
+
3744
+ // src/reducer/inputs/boardTarget.ts
3745
+ function candidateIdsForKind(q, boardId, targetKind) {
3746
+ const board = q.board(boardId).state;
3747
+ if (targetKind === "edge")
3748
+ return idsFromCollection(board.layout === "generic" ? [] : board.edges);
3749
+ if (targetKind === "vertex")
3750
+ return idsFromCollection(
3751
+ board.layout === "generic" ? [] : board.vertices
3752
+ );
3753
+ return idsFromCollection(board.spaces);
3754
+ }
3755
+ function idsFromCollection(collection) {
3756
+ if (!collection) return [];
3757
+ const values = Array.isArray(collection) ? collection : Object.values(collection);
3758
+ return values.flatMap((value) => {
3759
+ if (typeof value === "string") return [value];
3760
+ if (typeof value === "object" && value !== null && "id" in value && typeof value.id === "string") {
3761
+ return [value.id];
3762
+ }
3763
+ return [];
3764
+ });
3765
+ }
3766
+ function createBoardTargetBuilder(targetKind, boardId) {
3767
+ return createTargetRuleBuilder(
3768
+ (predicates) => ({
3769
+ ...createTargetRule(
3770
+ ({ q }) => candidateIdsForKind(q, boardId, targetKind),
3771
+ predicates,
3772
+ {
3773
+ missingCandidateIssue: {
3774
+ errorCode: "BOARD_TARGET_NOT_ELIGIBLE",
3775
+ message: "Board target is not eligible."
3776
+ }
3777
+ }
3778
+ ),
3779
+ boardId,
3780
+ targetKind,
3781
+ valueKind: "board-id"
3782
+ })
3783
+ );
3784
+ }
3785
+ function createPlayerSpaceTargetBuilder(boardId) {
3786
+ return createTargetRuleBuilder((predicates) => ({
3787
+ ...createTargetRule(
3788
+ ({ state, q }) => {
3789
+ const spacesForPlayer = (playerId) => candidateIdsForKind(
3790
+ q,
3791
+ `${boardId}:${playerId}`,
3792
+ "space"
3793
+ );
3794
+ return state.table.playerOrder.flatMap(
3795
+ (playerId) => spacesForPlayer(playerId).map((spaceId) => ({
3796
+ boardId,
3797
+ playerId,
3798
+ spaceId
3799
+ }))
3800
+ );
3801
+ },
3802
+ predicates,
3803
+ {
3804
+ missingCandidateIssue: {
3805
+ errorCode: "BOARD_TARGET_NOT_ELIGIBLE",
3806
+ message: "Board target is not eligible."
3807
+ },
3808
+ equals: (left, right) => isPlayerBoardSpaceTarget(left) && isPlayerBoardSpaceTarget(right) && samePlayerBoardSpaceTarget(left, right)
3809
+ }
3810
+ ),
3811
+ boardId,
3812
+ targetKind: "space",
3813
+ valueKind: "player-board-space"
3814
+ }));
3815
+ }
3816
+ function makeBoardTargetFactory(targetKind) {
3817
+ return function target(boardId) {
3818
+ return createBoardTargetBuilder(targetKind, boardId);
3819
+ };
3820
+ }
3821
+ var boardTarget = {
3822
+ edge: makeBoardTargetFactory("edge"),
3823
+ vertex: makeBoardTargetFactory("vertex"),
3824
+ space: makeBoardTargetFactory("space"),
3825
+ tile: makeBoardTargetFactory("tile"),
3826
+ playerSpace(boardId) {
3827
+ return createPlayerSpaceTargetBuilder(boardId);
3828
+ }
3829
+ };
3830
+
3831
+ // src/reducer/inputs/cardTarget.ts
3832
+ function cardIdsForZone(state, playerId, q, zoneId) {
3833
+ const table = state.table;
3834
+ if (zoneId in (table.hands ?? {}) || zoneId in (table.zones?.perPlayer ?? {})) {
3835
+ return q.zone.playerCards(
3836
+ playerId,
3837
+ zoneId
3838
+ );
3839
+ }
3840
+ if (zoneId in (table.decks ?? {}) || zoneId in (table.zones?.shared ?? {})) {
3841
+ return q.zone.sharedCards(zoneId);
3842
+ }
3843
+ return [];
3844
+ }
3845
+ function createCardTargetBuilder(zoneIds) {
3846
+ return createTargetRuleBuilder(
3847
+ (predicates) => ({
3848
+ ...createTargetRule(
3849
+ ({ state, playerId, q }) => zoneIds.flatMap(
3850
+ (zoneId) => cardIdsForZone(state, playerId, q, zoneId)
3851
+ ),
3852
+ predicates,
3853
+ {
3854
+ missingCandidateIssue: {
3855
+ errorCode: "CARD_TARGET_NOT_ELIGIBLE",
3856
+ message: "Card target is not eligible."
3857
+ }
3858
+ }
3859
+ ),
3860
+ zoneIds,
3861
+ zoneId: zoneIds[0],
3862
+ targetKind: "card"
3863
+ })
3864
+ );
3865
+ }
3866
+ var cardTarget = {
3867
+ zones(zoneIds) {
3868
+ return createCardTargetBuilder(zoneIds);
3869
+ }
3870
+ };
3871
+
3872
+ // src/reducer/inputs/cardInput.ts
3873
+ import { z as z6 } from "zod";
3874
+ function cardInput(options) {
3875
+ const target = options.target;
3876
+ return {
3877
+ kind: "card",
3878
+ schema: z6.string(),
3879
+ eligibleTargets: ((state, playerId, q) => target.eligible({
3880
+ state,
3881
+ playerId,
3882
+ q
3883
+ })),
3884
+ validateTarget: ((state, playerId, q, targetId) => target.validate(
3885
+ {
3886
+ state,
3887
+ playerId,
3888
+ q
3889
+ },
3890
+ targetId
3891
+ )),
3892
+ domain: (state, playerId, q) => ({
3893
+ type: "cardTarget",
3894
+ projection: "resolved",
3895
+ targetKind: target.targetKind,
3896
+ zoneIds: target.zoneIds,
3897
+ eligibleTargets: target.eligible({
3898
+ state,
3899
+ playerId,
3900
+ q
3901
+ }).map(String)
3902
+ }),
3903
+ meta: {
3904
+ zoneId: target.zoneId,
3905
+ zoneIds: target.zoneIds,
3906
+ targetKind: target.targetKind
3907
+ }
3908
+ };
3909
+ }
3910
+
3911
+ // src/reducer/inputs/rngInput.ts
3912
+ import { z as z7 } from "zod";
3913
+ function d6(count = 1) {
3914
+ return {
3915
+ kind: "rng",
3916
+ schema: z7.object({
3917
+ values: z7.array(z7.number().int().min(1).max(6)).length(count)
3918
+ }),
3919
+ meta: { rng: "d6", count }
3920
+ };
3921
+ }
3922
+ function coin() {
3923
+ return {
3924
+ kind: "rng",
3925
+ schema: z7.object({
3926
+ value: z7.enum(["heads", "tails"])
3927
+ }),
3928
+ meta: { rng: "coin" }
3929
+ };
3930
+ }
3931
+ var rngInput = { d6, coin };
3932
+
3933
+ // src/reducer/inputs/many.ts
3934
+ import { z as z8 } from "zod";
3935
+ function normalizeManyOptions(options) {
3936
+ if ("count" in options) {
3937
+ assertNonNegativeInteger(options.count, "many(...).count");
3938
+ return {
3939
+ mode: "many",
3940
+ min: options.count,
3941
+ max: options.count,
3942
+ distinct: options.distinct
3943
+ };
3944
+ }
3945
+ const min = options.min ?? 0;
3946
+ assertNonNegativeInteger(min, "many(...).min");
3947
+ if (options.max !== void 0) {
3948
+ assertNonNegativeInteger(options.max, "many(...).max");
3949
+ if (options.max < min) {
3950
+ throw new Error("many(...).max must be greater than or equal to min.");
3951
+ }
3952
+ }
3953
+ return {
3954
+ mode: "many",
3955
+ min,
3956
+ max: options.max,
3957
+ distinct: options.distinct
3958
+ };
3959
+ }
3960
+ function assertNonNegativeInteger(value, label) {
3961
+ if (!Number.isInteger(value) || value < 0) {
3962
+ throw new Error(`${label} must be a non-negative integer.`);
3963
+ }
3964
+ }
3965
+ function many(collector, options) {
3966
+ if (collector.kind === "rng") {
3967
+ throw new Error("many(...) cannot wrap rngInput collectors.");
3968
+ }
3969
+ const selection = normalizeManyOptions(options);
3970
+ const rest = { ...collector };
3971
+ delete rest.schema;
3972
+ delete rest.selection;
3973
+ delete rest.defaultValue;
3974
+ return {
3975
+ ...rest,
3976
+ schema: z8.array(
3977
+ collector.schema
3978
+ ),
3979
+ selection
3980
+ };
3981
+ }
3982
+
3983
+ // src/reducer/authoring/interaction.ts
3984
+ function defineInteraction() {
3985
+ return (definition) => {
3986
+ validateInteractionLikeDefinition(
3987
+ definition,
3988
+ "defineInteraction"
3989
+ );
3990
+ return definition;
3991
+ };
3992
+ }
3993
+ function defineInteractionRule() {
3994
+ return (definition) => definition;
3995
+ }
3996
+
3997
+ // src/reducer/authoring/phase.ts
3998
+ function definePhase() {
3999
+ return (definition) => {
4000
+ return definition;
4001
+ };
4002
+ }
4003
+
4004
+ // src/reducer/authoring/views.ts
4005
+ function defineView() {
4006
+ return (view) => view;
4007
+ }
4008
+
4009
+ // src/reducer/authoring/contract-authoring.ts
4010
+ var PHANTOM_TYPES_MESSAGE = "`.types` is a compile-time carrier: use it only in `typeof` positions.";
4011
+ function phantomTypes() {
4012
+ return new Proxy(Object.freeze({}), {
4013
+ get() {
4014
+ throw new TypeError(PHANTOM_TYPES_MESSAGE);
4015
+ }
4016
+ });
4017
+ }
4018
+ function toPredicateList(where) {
4019
+ if (!where) return [];
4020
+ return Array.isArray(where) ? where : [where];
4021
+ }
4022
+ function applyWhere(builder, where) {
4023
+ return toPredicateList(where).reduce(
4024
+ (current, predicate) => current.where(predicate),
4025
+ builder
4026
+ );
4027
+ }
4028
+ function createFusedCardInput() {
4029
+ return ((options) => {
4030
+ const target = applyWhere(
4031
+ cardTarget.zones(options.from),
4032
+ options.where
4033
+ ).build();
4034
+ return cardInput({
4035
+ target
4036
+ });
4037
+ });
4038
+ }
4039
+ function createFusedBoardInputs() {
4040
+ const fuse = (kind) => (options) => {
4041
+ const target = applyWhere(
4042
+ boardTarget[kind](options.boardId),
4043
+ options.where
4044
+ ).build();
4045
+ return boardInput[kind]({
4046
+ target
4047
+ });
4048
+ };
4049
+ const playerSpace = (options) => boardInput.playerSpace({
4050
+ target: applyWhere(
4051
+ boardTarget.playerSpace(options.boardId),
4052
+ options.where
4053
+ ).build()
4054
+ });
4055
+ return {
4056
+ vertex: fuse("vertex"),
4057
+ edge: fuse("edge"),
4058
+ space: fuse("space"),
4059
+ tile: fuse("tile"),
4060
+ playerSpace
4061
+ };
4062
+ }
4063
+ function createBoundInputBuilders() {
4064
+ return {
4065
+ board: createFusedBoardInputs(),
4066
+ card: createFusedCardInput(),
4067
+ form: formInput.forState(),
4068
+ rng: rngInput
4069
+ };
4070
+ }
4071
+ function createPhaseAuthoring(_contract, schema) {
4072
+ return {
4073
+ steps: () => new InteractionSteps(),
4074
+ interaction: defineInteraction(),
4075
+ rule: (rule) => defineInteractionRule()(
4076
+ rule
4077
+ ),
4078
+ define: (definition) => definePhase()({
4079
+ ...definition,
4080
+ state: schema
4081
+ }),
4082
+ inputs: createBoundInputBuilders(),
4083
+ types: phantomTypes()
4084
+ };
4085
+ }
4086
+ function createContractAuthoring(contract) {
4087
+ const phaseCache = /* @__PURE__ */ new Map();
4088
+ const assemble = (definition) => {
4089
+ const game = { ...definition, contract };
4090
+ validateDefineGamePhaseNames(game);
4091
+ validateDefineGamePhases(game);
4092
+ return { ...game, [validatedReducerDefinition]: true };
4093
+ };
4094
+ return {
4095
+ contract,
4096
+ view: defineView(),
4097
+ types: phantomTypes(),
4098
+ assemble,
4099
+ phase: (name) => {
4100
+ const cached = phaseCache.get(name);
4101
+ if (cached) return cached;
4102
+ const schema = contract.phases[name];
4103
+ const bound = createPhaseAuthoring(contract, schema);
4104
+ phaseCache.set(name, bound);
4105
+ return bound;
4106
+ }
4107
+ };
4108
+ }
4109
+
4110
+ // src/reducer/authoring/contract.ts
4111
+ import { z as z9 } from "zod";
4112
+ function validateErrorMap(errors) {
4113
+ if (!errors) return;
4114
+ const codePattern = /^[A-Z][A-Z0-9_]*$/;
4115
+ for (const [code, message] of Object.entries(errors)) {
4116
+ if (!codePattern.test(code)) {
4117
+ throw new Error(
4118
+ `defineGameContract: error code '${code}' must use UPPER_SNAKE_CASE.`
4119
+ );
4120
+ }
4121
+ if (typeof message !== "string" || message.length === 0) {
4122
+ throw new Error(
4123
+ `defineGameContract: error code '${code}' must declare a non-empty default message.`
4124
+ );
4125
+ }
4126
+ }
4127
+ }
4128
+ function validateOptionsSchema(schema) {
4129
+ z9.toJSONSchema(schema, {
4130
+ override: ({ zodSchema }) => {
4131
+ const definition = zodSchema._zod.def;
4132
+ if (definition.type === "pipe" || "coerce" in definition && definition.coerce || definition.checks?.some((check) => check._zod.def.check === "overwrite")) {
4133
+ throw new Error(
4134
+ "Game options schemas must not transform or coerce persisted values."
4135
+ );
4136
+ }
4137
+ }
4138
+ });
4139
+ }
4140
+ function defineGameContract(definition) {
4141
+ validateStateSchemaIdBranding(definition.state.public, "public");
4142
+ validateStateSchemaIdBranding(definition.state.private, "private");
4143
+ validateStateSchemaIdBranding(definition.state.hidden, "hidden");
4144
+ validateErrorMap(definition.errors);
4145
+ const optionsSchema = definition.options ?? z9.strictObject({});
4146
+ validateOptionsSchema(optionsSchema);
4147
+ const phaseNames = Object.keys(definition.phases);
4148
+ if (phaseNames.length === 0) {
4149
+ throw new Error(
4150
+ "defineGameContract: phases must be a non-empty record of phase schemas."
4151
+ );
4152
+ }
4153
+ for (const name of phaseNames) {
4154
+ if (typeof name !== "string" || name.length === 0) {
4155
+ throw new Error(
4156
+ "defineGameContract: phase names must be non-empty strings."
4157
+ );
4158
+ }
4159
+ }
4160
+ const phaseNameSchema = createManifestStringLiteralSchema(phaseNames);
4161
+ const narrowedManifest = {
4162
+ ...definition.manifest,
4163
+ literals: {
4164
+ ...definition.manifest.literals,
4165
+ phaseNames
4166
+ },
4167
+ ids: {
4168
+ ...definition.manifest.ids,
4169
+ phaseName: phaseNameSchema
4170
+ }
4171
+ };
4172
+ const contract = {
4173
+ manifest: narrowedManifest,
4174
+ state: definition.state,
4175
+ phases: definition.phases,
4176
+ errors: definition.errors,
4177
+ options: optionsSchema,
4178
+ phaseNames,
4179
+ schemas: narrowedManifest.ids
4180
+ };
4181
+ return contract;
4182
+ }
4183
+
4184
+ // src/reducer/authoring/game.ts
4185
+ function createGame(model) {
4186
+ const manifest = "literals" in model.manifest ? model.manifest : compileManifest(model.manifest);
4187
+ return createContractAuthoring(defineGameContract({ ...model, manifest }));
4188
+ }
4189
+
4190
+ // src/reducer/game-event.ts
4191
+ var gameEvent = {
4192
+ systemAction(event) {
4193
+ return {
4194
+ kind: "systemAction",
4195
+ ...event
4196
+ };
4197
+ }
4198
+ };
4199
+
4200
+ // src/reducer/memoize.ts
4201
+ function memoize(fn) {
4202
+ const cache = /* @__PURE__ */ new WeakMap();
4203
+ return (a) => {
4204
+ if (cache.has(a)) return cache.get(a);
4205
+ const value = fn(a);
4206
+ cache.set(a, value);
4207
+ return value;
4208
+ };
4209
+ }
4210
+
4211
+ // src/reducer/manifest/authoring.ts
4212
+ function defineTopologyManifest(manifest) {
4213
+ const validated = structuredClone(manifest);
4214
+ assertValidManifest(
4215
+ validated
4216
+ );
4217
+ return validated;
124
4218
  }
125
4219
  export {
126
4220
  FrameworkErrorCodes,
127
- StaleContractArtifactError,
4221
+ REDUCER_CONTRACT_VERSION,
4222
+ runtime_schema_exports as ReducerWireZod,
128
4223
  asPlayerId,
129
4224
  assertCardAllowedInContainer,
4225
+ assertReducerBundleContract,
130
4226
  boardRef,
131
4227
  boardRefKey,
132
4228
  boardRefSchema,
133
4229
  compileManifest,
134
- contractFingerprint,
135
- createDerivedResolver,
136
4230
  createGame,
137
4231
  createReducerBundle,
138
- createReducerOps,
139
4232
  createStateQueries,
140
4233
  createTableQueries,
141
- defineDerived,
142
- defineEffect,
143
- defineInputs,
4234
+ defineTopologyManifest,
4235
+ fromCoordinates,
144
4236
  gameEvent,
145
- isPerPlayer,
4237
+ hexagon,
146
4238
  isPerPlayerBoardRef,
147
4239
  isPlayerId,
148
4240
  isSharedBoardRef,
149
- isStaleContractArtifactError,
150
4241
  many,
4242
+ materializeManifestTable,
4243
+ memoize,
151
4244
  noopDiagnosticsSink,
152
4245
  normalizeCommandParams,
153
4246
  parseBoardRefKey,
154
- perPlayer,
155
4247
  perPlayerBoardRef,
156
- perPlayerEntries,
157
- perPlayerGet,
158
- perPlayerHas,
159
- perPlayerKeys,
160
- perPlayerMap,
161
- perPlayerRequire,
162
- perPlayerSchema,
163
- perPlayerSet,
164
- perPlayerSize,
165
- perPlayerValues,
166
- pipe,
167
- setActivePlayers,
4248
+ rectangle,
4249
+ ring,
168
4250
  sharedBoardRef,
169
4251
  sparseCounts,
170
- sparseMap
4252
+ sparseMap,
4253
+ spiral
171
4254
  };
172
4255
  //# sourceMappingURL=reducer.js.map