@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.
- package/README.md +186 -45
- package/dist/board-target-B0QG6OYx.d.ts +8 -0
- package/dist/chunk-3JXUL4VN.js +139 -0
- package/dist/chunk-3JXUL4VN.js.map +1 -0
- package/dist/{chunk-S2JEDVWH.js → chunk-C77SYIVT.js} +6102 -6505
- package/dist/chunk-C77SYIVT.js.map +1 -0
- package/dist/chunk-DMFVAZI2.js +16 -0
- package/dist/chunk-DMFVAZI2.js.map +1 -0
- package/dist/chunk-OOFMBCFF.js +1338 -0
- package/dist/chunk-OOFMBCFF.js.map +1 -0
- package/dist/chunk-UG2Q4NLG.js +590 -0
- package/dist/chunk-UG2Q4NLG.js.map +1 -0
- package/dist/chunk-XU6WJW6K.js +847 -0
- package/dist/chunk-XU6WJW6K.js.map +1 -0
- package/dist/diagnostics-B8d5aLad.d.ts +65 -0
- package/dist/extract-gVKcB8Og.d.ts +4736 -0
- package/dist/index.d.ts +3782 -1
- package/dist/index.js +1096 -6
- package/dist/index.js.map +1 -1
- package/dist/model-vdpLHrxh.d.ts +432 -0
- package/dist/react.d.ts +60 -0
- package/dist/react.js +208 -0
- package/dist/react.js.map +1 -0
- package/dist/reducer.d.ts +1244 -78
- package/dist/reducer.js +4204 -121
- package/dist/reducer.js.map +1 -1
- package/dist/testing.d.ts +520 -564
- package/dist/testing.js +2451 -915
- package/dist/testing.js.map +1 -1
- package/dist/types-Bj5ZLTUV.d.ts +114 -0
- package/package.json +40 -142
- package/dist/HandView-Bi2lxzQP.d.ts +0 -79
- package/dist/ResourceCounter-BFxJknqp.d.ts +0 -98
- package/dist/ThemeProvider-CElaL2ik.d.ts +0 -99
- package/dist/attributes-DbvyMbXw.d.ts +0 -68
- package/dist/browser-interaction.d.ts +0 -910
- package/dist/browser-interaction.js +0 -119
- package/dist/browser-interaction.js.map +0 -1
- package/dist/chunk-32PFKDV7.js +0 -11705
- package/dist/chunk-32PFKDV7.js.map +0 -1
- package/dist/chunk-3BAD4VC2.js +0 -263
- package/dist/chunk-3BAD4VC2.js.map +0 -1
- package/dist/chunk-3SKDNDPG.js +0 -104
- package/dist/chunk-3SKDNDPG.js.map +0 -1
- package/dist/chunk-5YV2WHJ4.js +0 -25
- package/dist/chunk-5YV2WHJ4.js.map +0 -1
- package/dist/chunk-ANRB4XKA.js +0 -90
- package/dist/chunk-ANRB4XKA.js.map +0 -1
- package/dist/chunk-BI4G3PM2.js +0 -1864
- package/dist/chunk-BI4G3PM2.js.map +0 -1
- package/dist/chunk-EVCLTY4Q.js +0 -935
- package/dist/chunk-EVCLTY4Q.js.map +0 -1
- package/dist/chunk-GDPKTBUR.js +0 -564
- package/dist/chunk-GDPKTBUR.js.map +0 -1
- package/dist/chunk-H6VDGFL5.js +0 -2903
- package/dist/chunk-H6VDGFL5.js.map +0 -1
- package/dist/chunk-LR3ZTWQF.js +0 -5960
- package/dist/chunk-LR3ZTWQF.js.map +0 -1
- package/dist/chunk-PF7L4BMG.js +0 -117
- package/dist/chunk-PF7L4BMG.js.map +0 -1
- package/dist/chunk-PN5O6GG2.js +0 -32
- package/dist/chunk-PN5O6GG2.js.map +0 -1
- package/dist/chunk-PZ5AY32C.js +0 -10
- package/dist/chunk-PZ5AY32C.js.map +0 -1
- package/dist/chunk-S2JEDVWH.js.map +0 -1
- package/dist/chunk-T3ZKNUZ7.js +0 -1
- package/dist/chunk-T3ZKNUZ7.js.map +0 -1
- package/dist/chunk-UGH54WZ2.js +0 -223
- package/dist/chunk-UGH54WZ2.js.map +0 -1
- package/dist/chunk-VCDWP5VL.js +0 -746
- package/dist/chunk-VCDWP5VL.js.map +0 -1
- package/dist/chunk-VDXOF4FW.js +0 -69
- package/dist/chunk-VDXOF4FW.js.map +0 -1
- package/dist/chunk-WHR5UW3F.js +0 -1988
- package/dist/chunk-WHR5UW3F.js.map +0 -1
- package/dist/chunk-Y75CFE77.js +0 -17
- package/dist/chunk-Y75CFE77.js.map +0 -1
- package/dist/chunk-Z7QCREWI.js +0 -3435
- package/dist/chunk-Z7QCREWI.js.map +0 -1
- package/dist/chunk-ZZGN2ATS.js +0 -1941
- package/dist/chunk-ZZGN2ATS.js.map +0 -1
- package/dist/components-gAbIz7hK.d.ts +0 -1284
- package/dist/definitions-BgmV_GhC.d.ts +0 -300
- package/dist/diagnostics-DIi3ina7.d.ts +0 -50
- package/dist/digest.d.ts +0 -8
- package/dist/game-CsZScpcn.d.ts +0 -763
- package/dist/hex-board-view-aKkblDp6.d.ts +0 -1228
- package/dist/index-CcT9Q7N7.d.ts +0 -193
- package/dist/index.d-DjzoK7zn.d.ts +0 -2101
- package/dist/package-set.d.ts +0 -13
- package/dist/package-set.js +0 -12
- package/dist/package-set.js.map +0 -1
- package/dist/player-state-Cqpyeql0.d.ts +0 -371
- package/dist/plugin-runtime-contract.d.ts +0 -17
- package/dist/plugin-runtime-contract.js +0 -92
- package/dist/plugin-runtime-contract.js.map +0 -1
- package/dist/primitive-props-BNHDkgd7.d.ts +0 -16
- package/dist/protocol-dYgafTYY.d.ts +0 -314
- package/dist/reducer/advanced.d.ts +0 -82
- package/dist/reducer/advanced.js +0 -51
- package/dist/reducer/advanced.js.map +0 -1
- package/dist/reducer-contract.d.ts +0 -11
- package/dist/reducer-contract.js +0 -16
- package/dist/reducer-contract.js.map +0 -1
- package/dist/reference-games/index.d.ts +0 -31
- package/dist/reference-games/index.js +0 -48
- package/dist/reference-games/index.js.map +0 -1
- package/dist/runtime/primitives.d.ts +0 -250
- package/dist/runtime/primitives.js +0 -189
- package/dist/runtime/primitives.js.map +0 -1
- package/dist/runtime/runtime-api.d.ts +0 -2
- package/dist/runtime/runtime-api.js +0 -1
- package/dist/runtime/runtime-api.js.map +0 -1
- package/dist/runtime/workspace-contract.d.ts +0 -392
- package/dist/runtime/workspace-contract.js +0 -28
- package/dist/runtime/workspace-contract.js.map +0 -1
- package/dist/runtime-RJ5orDJU.d.ts +0 -1699
- package/dist/runtime-api-Bz5pwNU_.d.ts +0 -296
- package/dist/runtime-json-CQ9QbLZ5.d.ts +0 -5
- package/dist/runtime.d.ts +0 -83
- package/dist/runtime.js +0 -242
- package/dist/runtime.js.map +0 -1
- package/dist/schema.d.ts +0 -379
- package/dist/stale-contract-artifact-error-XLaweZtF.d.ts +0 -18
- package/dist/testing-compiler.d.ts +0 -37
- package/dist/testing-compiler.js +0 -272
- package/dist/testing-compiler.js.map +0 -1
- package/dist/testing-runtime.d.ts +0 -119
- package/dist/testing-runtime.js +0 -212
- package/dist/testing-runtime.js.map +0 -1
- package/dist/types-DJj5MJkl.d.ts +0 -256
- package/dist/types-DR7DoB1x.d.ts +0 -28
- package/dist/types-JWCYHmu7.d.ts +0 -122
- package/dist/types.d.ts +0 -1958
- package/dist/types.js +0 -14
- package/dist/types.js.map +0 -1
- package/dist/ui/components.d.ts +0 -17
- package/dist/ui/components.js +0 -216
- package/dist/ui/components.js.map +0 -1
- package/dist/ui/defaults.d.ts +0 -19
- package/dist/ui/defaults.js +0 -104
- package/dist/ui/defaults.js.map +0 -1
- package/dist/ui/player-state.d.ts +0 -2
- package/dist/ui/player-state.js +0 -1
- package/dist/ui/player-state.js.map +0 -1
- package/dist/ui/plugin-styles.css +0 -2
- package/dist/ui-contract-GTUPkf8z.d.ts +0 -1168
- package/dist/ui.d.ts +0 -317
- package/dist/ui.js +0 -277
- package/dist/ui.js.map +0 -1
- package/dist/views-BpuWcOyN.d.ts +0 -1641
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} from "./chunk-
|
|
49
|
-
import
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
`
|
|
726
|
+
`Invalid topology manifest:
|
|
727
|
+
${validation.errors.join("\n")}`
|
|
102
728
|
);
|
|
103
729
|
}
|
|
104
|
-
|
|
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/
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
defineInputs,
|
|
4234
|
+
defineTopologyManifest,
|
|
4235
|
+
fromCoordinates,
|
|
144
4236
|
gameEvent,
|
|
145
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|