@dreamboard-games/workspace-codegen 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +89 -0
- package/NOTICE +1 -0
- package/dist/hex-geometry.d.ts +2 -0
- package/dist/hex-geometry.js +49 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +22 -0
- package/dist/manifest-contract.d.ts +14 -0
- package/dist/manifest-contract.js +4897 -0
- package/dist/manifest-validation.d.ts +6 -0
- package/dist/manifest-validation.js +506 -0
- package/dist/ownership.d.ts +31 -0
- package/dist/ownership.js +86 -0
- package/dist/preset-card-sets.d.ts +5 -0
- package/dist/preset-card-sets.js +135 -0
- package/dist/seeds.d.ts +6 -0
- package/dist/seeds.js +766 -0
- package/ownership.json +51 -0
- package/package.json +46 -0
- package/src/__fixtures__/sdk-types/invalid-card-properties-extra-key.ts +62 -0
- package/src/__fixtures__/sdk-types/invalid-card-properties-missing-required.ts +60 -0
- package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-enum.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-nested.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-card-properties-wrong-scalar.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-card-visibility.ts +40 -0
- package/src/__fixtures__/sdk-types/invalid-container-card-set-manifest.ts +62 -0
- package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-array-item.ts +43 -0
- package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-player-id.ts +43 -0
- package/src/__fixtures__/sdk-types/invalid-die-fields-wrong-resource-id.ts +43 -0
- package/src/__fixtures__/sdk-types/invalid-die-home-per-player-zone-no-owner.ts +35 -0
- package/src/__fixtures__/sdk-types/invalid-die-seed-type-id.ts +31 -0
- package/src/__fixtures__/sdk-types/invalid-die-visibility.ts +28 -0
- package/src/__fixtures__/sdk-types/invalid-generic-board-edge-id.ts +38 -0
- package/src/__fixtures__/sdk-types/invalid-generic-board-nested-field.ts +45 -0
- package/src/__fixtures__/sdk-types/invalid-hex-edge-field-edge-id.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-hex-vertex-field-vertex-id.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-manifest.ts +143 -0
- package/src/__fixtures__/sdk-types/invalid-piece-fields-extra-key.ts +62 -0
- package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-card-id.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-enum.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-scalar.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-piece-fields-wrong-zone-id.ts +61 -0
- package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-container-no-owner.ts +48 -0
- package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-edge-no-owner.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-space-no-owner.ts +42 -0
- package/src/__fixtures__/sdk-types/invalid-piece-home-per-player-vertex-no-owner.ts +49 -0
- package/src/__fixtures__/sdk-types/invalid-piece-seed-type-id.ts +30 -0
- package/src/__fixtures__/sdk-types/invalid-piece-visibility.ts +28 -0
- package/src/__fixtures__/sdk-types/invalid-slot-host-manifest.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-slot-id-manifest.ts +49 -0
- package/src/__fixtures__/sdk-types/invalid-square-board-edge-id.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-square-board-space-id.ts +47 -0
- package/src/__fixtures__/sdk-types/invalid-square-board-vertex-id.ts +49 -0
- package/src/__fixtures__/sdk-types/invalid-square-container-field-space-id.ts +59 -0
- package/src/__fixtures__/sdk-types/invalid-square-container-host-space-id.ts +49 -0
- package/src/__fixtures__/sdk-types/invalid-square-relation-field-scalar.ts +48 -0
- package/src/__fixtures__/sdk-types/invalid-square-relation-space-id.ts +48 -0
- package/src/__fixtures__/sdk-types/invalid-square-space-fields-enum.ts +44 -0
- package/src/__fixtures__/sdk-types/invalid-square-space-fields-extra-key.ts +45 -0
- package/src/__fixtures__/sdk-types/valid-die-type-omits-sides.ts +29 -0
- package/src/__fixtures__/sdk-types/valid-manifest-omits-board-templates.ts +19 -0
- package/src/__fixtures__/sdk-types/valid-manifest.ts +612 -0
- package/src/__fixtures__/sdk-types/valid-player-scoped-seed-homes.ts +59 -0
- package/src/authoring-benchmark.test.ts +362 -0
- package/src/hex-geometry.ts +69 -0
- package/src/index.ts +64 -0
- package/src/manifest-contract.test.ts +1764 -0
- package/src/manifest-contract.ts +6581 -0
- package/src/manifest-validation.test.ts +393 -0
- package/src/manifest-validation.ts +795 -0
- package/src/ownership.ts +127 -0
- package/src/preset-card-sets.ts +169 -0
- package/src/sdk-types-authoring.test.ts +361 -0
- package/src/seeds.ts +800 -0
|
@@ -0,0 +1,1764 @@
|
|
|
1
|
+
import { mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { expect, test } from "bun:test";
|
|
7
|
+
import type { GameTopologyManifest } from "@dreamboard/sdk-types";
|
|
8
|
+
import {
|
|
9
|
+
generateManifestContractSource,
|
|
10
|
+
generateManifestContractSources,
|
|
11
|
+
} from "./manifest-contract.js";
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const tscBin = require.resolve("typescript/bin/tsc");
|
|
15
|
+
const workspaceCodegenRoot = path.resolve(import.meta.dir, "..");
|
|
16
|
+
const cliWorkspaceRoot = path.resolve(
|
|
17
|
+
import.meta.dir,
|
|
18
|
+
"../../../apps/dreamboard-cli",
|
|
19
|
+
);
|
|
20
|
+
const cliNodeModulesRoot = path.join(cliWorkspaceRoot, "node_modules");
|
|
21
|
+
|
|
22
|
+
const TEST_MANIFEST: GameTopologyManifest = {
|
|
23
|
+
players: {
|
|
24
|
+
minPlayers: 2,
|
|
25
|
+
maxPlayers: 4,
|
|
26
|
+
optimalPlayers: 3,
|
|
27
|
+
},
|
|
28
|
+
cardSets: [
|
|
29
|
+
{
|
|
30
|
+
type: "manual",
|
|
31
|
+
id: "standard-deck",
|
|
32
|
+
name: "Standard Deck",
|
|
33
|
+
cardSchema: {
|
|
34
|
+
properties: {
|
|
35
|
+
value: {
|
|
36
|
+
type: "integer",
|
|
37
|
+
description: "Card value",
|
|
38
|
+
},
|
|
39
|
+
note: {
|
|
40
|
+
type: "string",
|
|
41
|
+
description: "Optional note",
|
|
42
|
+
optional: true,
|
|
43
|
+
nullable: true,
|
|
44
|
+
},
|
|
45
|
+
scoreByPlayer: {
|
|
46
|
+
type: "record",
|
|
47
|
+
description: "Scores by player",
|
|
48
|
+
values: {
|
|
49
|
+
type: "integer",
|
|
50
|
+
description: "Player score",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
cards: [
|
|
56
|
+
{
|
|
57
|
+
type: "CARD_A",
|
|
58
|
+
name: "Card A",
|
|
59
|
+
count: 1,
|
|
60
|
+
properties: {
|
|
61
|
+
value: 1,
|
|
62
|
+
note: null,
|
|
63
|
+
scoreByPlayer: {
|
|
64
|
+
"player-1": 2,
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
zones: [
|
|
72
|
+
{
|
|
73
|
+
id: "draw-deck",
|
|
74
|
+
name: "Draw Deck",
|
|
75
|
+
scope: "shared",
|
|
76
|
+
allowedCardSetIds: ["standard-deck"],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "main-hand",
|
|
80
|
+
name: "Main Hand",
|
|
81
|
+
scope: "perPlayer",
|
|
82
|
+
visibility: "ownerOnly",
|
|
83
|
+
allowedCardSetIds: ["standard-deck"],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
boardTemplates: [],
|
|
87
|
+
boards: [
|
|
88
|
+
{
|
|
89
|
+
id: "track-board",
|
|
90
|
+
name: "Track Board",
|
|
91
|
+
layout: "generic",
|
|
92
|
+
typeId: "track",
|
|
93
|
+
scope: "shared",
|
|
94
|
+
boardFieldsSchema: {
|
|
95
|
+
properties: {
|
|
96
|
+
roundMarker: {
|
|
97
|
+
type: "integer",
|
|
98
|
+
description: "Round marker index",
|
|
99
|
+
},
|
|
100
|
+
notes: {
|
|
101
|
+
type: "record",
|
|
102
|
+
description: "Board notes",
|
|
103
|
+
optional: true,
|
|
104
|
+
values: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description: "Board note",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
spaceFieldsSchema: {
|
|
112
|
+
properties: {
|
|
113
|
+
bonusByPlayer: {
|
|
114
|
+
type: "record",
|
|
115
|
+
description: "Bonus by player",
|
|
116
|
+
values: {
|
|
117
|
+
type: "integer",
|
|
118
|
+
description: "Bonus amount",
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
labelOverride: {
|
|
122
|
+
type: "string",
|
|
123
|
+
description: "Optional override label",
|
|
124
|
+
optional: true,
|
|
125
|
+
nullable: true,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
relationFieldsSchema: {
|
|
130
|
+
properties: {
|
|
131
|
+
cost: {
|
|
132
|
+
type: "integer",
|
|
133
|
+
description: "Movement cost",
|
|
134
|
+
optional: true,
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
containerFieldsSchema: {
|
|
139
|
+
properties: {
|
|
140
|
+
displaySize: {
|
|
141
|
+
type: "integer",
|
|
142
|
+
description: "Display size",
|
|
143
|
+
},
|
|
144
|
+
featuredByPlayer: {
|
|
145
|
+
type: "record",
|
|
146
|
+
description: "Featured markers by player",
|
|
147
|
+
optional: true,
|
|
148
|
+
values: {
|
|
149
|
+
type: "boolean",
|
|
150
|
+
description: "Whether featured",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
fields: {
|
|
156
|
+
roundMarker: 1,
|
|
157
|
+
},
|
|
158
|
+
spaces: [
|
|
159
|
+
{
|
|
160
|
+
id: "space-a",
|
|
161
|
+
typeId: "worker-space",
|
|
162
|
+
fields: {
|
|
163
|
+
bonusByPlayer: {
|
|
164
|
+
"player-1": 2,
|
|
165
|
+
},
|
|
166
|
+
labelOverride: null,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
relations: [
|
|
171
|
+
{
|
|
172
|
+
typeId: "adjacent",
|
|
173
|
+
fromSpaceId: "space-a",
|
|
174
|
+
toSpaceId: "space-a",
|
|
175
|
+
directed: false,
|
|
176
|
+
fields: {
|
|
177
|
+
cost: 1,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
containers: [
|
|
182
|
+
{
|
|
183
|
+
id: "market-row",
|
|
184
|
+
name: "Market Row",
|
|
185
|
+
host: {
|
|
186
|
+
type: "board",
|
|
187
|
+
},
|
|
188
|
+
allowedCardSetIds: ["standard-deck"],
|
|
189
|
+
fields: {
|
|
190
|
+
displaySize: 3,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "hex-map",
|
|
197
|
+
name: "Hex Map",
|
|
198
|
+
layout: "hex",
|
|
199
|
+
typeId: "map",
|
|
200
|
+
scope: "shared",
|
|
201
|
+
orientation: "pointy-top",
|
|
202
|
+
spaceFieldsSchema: {
|
|
203
|
+
properties: {
|
|
204
|
+
richness: {
|
|
205
|
+
type: "integer",
|
|
206
|
+
description: "Space richness",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
edgeFieldsSchema: {
|
|
211
|
+
properties: {
|
|
212
|
+
rate: {
|
|
213
|
+
type: "integer",
|
|
214
|
+
description: "Edge trade rate",
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
vertexFieldsSchema: {
|
|
219
|
+
properties: {
|
|
220
|
+
buildCost: {
|
|
221
|
+
type: "integer",
|
|
222
|
+
description: "Vertex build cost",
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
spaces: [
|
|
227
|
+
{
|
|
228
|
+
id: "hex-a",
|
|
229
|
+
q: 0,
|
|
230
|
+
r: 0,
|
|
231
|
+
typeId: "forest",
|
|
232
|
+
fields: {
|
|
233
|
+
richness: 3,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "hex-b",
|
|
238
|
+
q: 1,
|
|
239
|
+
r: 0,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "hex-c",
|
|
243
|
+
q: 0,
|
|
244
|
+
r: 1,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
edges: [
|
|
248
|
+
{
|
|
249
|
+
ref: {
|
|
250
|
+
spaces: ["hex-a", "hex-b"],
|
|
251
|
+
},
|
|
252
|
+
typeId: "three-to-one",
|
|
253
|
+
fields: {
|
|
254
|
+
rate: 3,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
vertices: [
|
|
259
|
+
{
|
|
260
|
+
ref: {
|
|
261
|
+
spaces: ["hex-a", "hex-b", "hex-c"],
|
|
262
|
+
},
|
|
263
|
+
typeId: "settlement-slot",
|
|
264
|
+
fields: {
|
|
265
|
+
buildCost: 2,
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "square-grid",
|
|
272
|
+
name: "Square Grid",
|
|
273
|
+
layout: "square",
|
|
274
|
+
typeId: "grid",
|
|
275
|
+
scope: "shared",
|
|
276
|
+
relationFieldsSchema: {
|
|
277
|
+
properties: {
|
|
278
|
+
cost: {
|
|
279
|
+
type: "integer",
|
|
280
|
+
description: "Movement cost",
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
containerFieldsSchema: {
|
|
285
|
+
properties: {
|
|
286
|
+
capacity: {
|
|
287
|
+
type: "integer",
|
|
288
|
+
description: "Container capacity",
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
edgeFieldsSchema: {
|
|
293
|
+
properties: {
|
|
294
|
+
durability: {
|
|
295
|
+
type: "integer",
|
|
296
|
+
description: "Edge durability",
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
vertexFieldsSchema: {
|
|
301
|
+
properties: {
|
|
302
|
+
value: {
|
|
303
|
+
type: "integer",
|
|
304
|
+
description: "Vertex value",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
spaces: [
|
|
309
|
+
{
|
|
310
|
+
id: "cell-a1",
|
|
311
|
+
row: 0,
|
|
312
|
+
col: 0,
|
|
313
|
+
typeId: "meadow",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: "cell-a2",
|
|
317
|
+
row: 0,
|
|
318
|
+
col: 1,
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: "cell-b1",
|
|
322
|
+
row: 1,
|
|
323
|
+
col: 0,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: "cell-b2",
|
|
327
|
+
row: 1,
|
|
328
|
+
col: 1,
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
relations: [
|
|
332
|
+
{
|
|
333
|
+
id: "portal",
|
|
334
|
+
typeId: "portal",
|
|
335
|
+
fromSpaceId: "cell-a1",
|
|
336
|
+
toSpaceId: "cell-b2",
|
|
337
|
+
directed: false,
|
|
338
|
+
fields: {
|
|
339
|
+
cost: 2,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
containers: [
|
|
344
|
+
{
|
|
345
|
+
id: "cache",
|
|
346
|
+
name: "Cache",
|
|
347
|
+
host: {
|
|
348
|
+
type: "board",
|
|
349
|
+
},
|
|
350
|
+
fields: {
|
|
351
|
+
capacity: 2,
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
edges: [
|
|
356
|
+
{
|
|
357
|
+
ref: {
|
|
358
|
+
spaces: ["cell-a1", "cell-a2"],
|
|
359
|
+
},
|
|
360
|
+
typeId: "wall-slot",
|
|
361
|
+
fields: {
|
|
362
|
+
durability: 2,
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
vertices: [
|
|
367
|
+
{
|
|
368
|
+
ref: {
|
|
369
|
+
spaces: ["cell-a1", "cell-a2", "cell-b1", "cell-b2"],
|
|
370
|
+
},
|
|
371
|
+
typeId: "crossing",
|
|
372
|
+
fields: {
|
|
373
|
+
value: 1,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
pieceTypes: [
|
|
380
|
+
{
|
|
381
|
+
id: "worker",
|
|
382
|
+
name: "Worker",
|
|
383
|
+
fieldsSchema: {
|
|
384
|
+
properties: {
|
|
385
|
+
exhausted: {
|
|
386
|
+
type: "boolean",
|
|
387
|
+
description: "Whether the worker is exhausted",
|
|
388
|
+
},
|
|
389
|
+
ownerScoreByPlayer: {
|
|
390
|
+
type: "record",
|
|
391
|
+
description: "Score by player",
|
|
392
|
+
values: {
|
|
393
|
+
type: "integer",
|
|
394
|
+
description: "Score",
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: "player-mat",
|
|
402
|
+
name: "Player Mat",
|
|
403
|
+
slots: [{ id: "worker-rest", name: "Worker Rest" }],
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
pieceSeeds: [
|
|
407
|
+
{
|
|
408
|
+
id: "mat-alpha",
|
|
409
|
+
typeId: "player-mat",
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
id: "worker-1",
|
|
413
|
+
typeId: "worker",
|
|
414
|
+
fields: {
|
|
415
|
+
exhausted: false,
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
],
|
|
419
|
+
dieTypes: [
|
|
420
|
+
{
|
|
421
|
+
id: "d6",
|
|
422
|
+
name: "D6",
|
|
423
|
+
sides: 6,
|
|
424
|
+
fieldsSchema: {
|
|
425
|
+
properties: {
|
|
426
|
+
icon: {
|
|
427
|
+
type: "string",
|
|
428
|
+
description: "Icon name",
|
|
429
|
+
},
|
|
430
|
+
weightByFace: {
|
|
431
|
+
type: "record",
|
|
432
|
+
description: "Weight by face",
|
|
433
|
+
values: {
|
|
434
|
+
type: "integer",
|
|
435
|
+
description: "Weight",
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
note: {
|
|
439
|
+
type: "string",
|
|
440
|
+
description: "Optional note",
|
|
441
|
+
optional: true,
|
|
442
|
+
nullable: true,
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
id: "die-holder",
|
|
449
|
+
name: "Die Holder",
|
|
450
|
+
sides: 6,
|
|
451
|
+
slots: [{ id: "staging", name: "Staging" }],
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
dieSeeds: [
|
|
455
|
+
{
|
|
456
|
+
id: "holder-a",
|
|
457
|
+
typeId: "die-holder",
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
id: "d6",
|
|
461
|
+
typeId: "d6",
|
|
462
|
+
fields: {
|
|
463
|
+
icon: "pip",
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
resources: [
|
|
468
|
+
{
|
|
469
|
+
id: "coins",
|
|
470
|
+
name: "Coins",
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
setupOptions: [],
|
|
474
|
+
setupProfiles: [],
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const EMPTY_MANIFEST: GameTopologyManifest = {
|
|
478
|
+
players: {
|
|
479
|
+
minPlayers: 2,
|
|
480
|
+
maxPlayers: 4,
|
|
481
|
+
optimalPlayers: 2,
|
|
482
|
+
},
|
|
483
|
+
cardSets: [],
|
|
484
|
+
zones: [],
|
|
485
|
+
boardTemplates: [],
|
|
486
|
+
boards: [],
|
|
487
|
+
pieceTypes: [],
|
|
488
|
+
pieceSeeds: [],
|
|
489
|
+
dieTypes: [],
|
|
490
|
+
dieSeeds: [],
|
|
491
|
+
resources: [],
|
|
492
|
+
setupOptions: [],
|
|
493
|
+
setupProfiles: [],
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
async function expectGeneratedContractTypechecks(options: {
|
|
497
|
+
tempPrefix: string;
|
|
498
|
+
manifest: GameTopologyManifest;
|
|
499
|
+
usageSource: string;
|
|
500
|
+
}): Promise<void> {
|
|
501
|
+
const tempRoot = await createGeneratedContractTempRoot(options.tempPrefix);
|
|
502
|
+
const source = generateManifestContractSource(options.manifest);
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
const contractPath = path.join(tempRoot, "manifest-contract.ts");
|
|
506
|
+
const usagePath = path.join(tempRoot, "usage.ts");
|
|
507
|
+
await writeFile(contractPath, source, "utf8");
|
|
508
|
+
await writeFile(usagePath, options.usageSource, "utf8");
|
|
509
|
+
|
|
510
|
+
const result = Bun.spawnSync({
|
|
511
|
+
cmd: [
|
|
512
|
+
tscBin,
|
|
513
|
+
"--noEmit",
|
|
514
|
+
"--strict",
|
|
515
|
+
"--target",
|
|
516
|
+
"ES2022",
|
|
517
|
+
"--module",
|
|
518
|
+
"ESNext",
|
|
519
|
+
"--moduleResolution",
|
|
520
|
+
"bundler",
|
|
521
|
+
"--skipLibCheck",
|
|
522
|
+
contractPath,
|
|
523
|
+
usagePath,
|
|
524
|
+
],
|
|
525
|
+
cwd: cliWorkspaceRoot,
|
|
526
|
+
stdout: "pipe",
|
|
527
|
+
stderr: "pipe",
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
if (result.exitCode !== 0) {
|
|
531
|
+
const decoder = new TextDecoder();
|
|
532
|
+
throw new Error(
|
|
533
|
+
`Typecheck failed for generated contract fixture\nstdout:\n${decoder.decode(result.stdout)}\nstderr:\n${decoder.decode(result.stderr)}`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
} finally {
|
|
537
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function withGeneratedContractModule<Result>(options: {
|
|
542
|
+
tempPrefix: string;
|
|
543
|
+
manifest: GameTopologyManifest;
|
|
544
|
+
run: (module: Record<string, any>) => Promise<Result> | Result;
|
|
545
|
+
}): Promise<Result> {
|
|
546
|
+
const tempRoot = await createGeneratedContractTempRoot(options.tempPrefix);
|
|
547
|
+
const source = generateManifestContractSource(options.manifest);
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
const contractPath = path.join(tempRoot, "manifest-contract.ts");
|
|
551
|
+
await writeFile(contractPath, source, "utf8");
|
|
552
|
+
const module = (await import(
|
|
553
|
+
`${pathToFileURL(contractPath).href}?t=${Date.now()}`
|
|
554
|
+
)) as Record<string, any>;
|
|
555
|
+
return await options.run(module);
|
|
556
|
+
} finally {
|
|
557
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function withGeneratedSplitContractModule<Result>(options: {
|
|
562
|
+
tempPrefix: string;
|
|
563
|
+
manifest: GameTopologyManifest;
|
|
564
|
+
run: (module: Record<string, any>) => Promise<Result> | Result;
|
|
565
|
+
}): Promise<Result> {
|
|
566
|
+
const tempRoot = await createGeneratedContractTempRoot(options.tempPrefix);
|
|
567
|
+
const sources = generateManifestContractSources(options.manifest);
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
for (const [relativePath, source] of Object.entries(sources)) {
|
|
571
|
+
await writeFile(path.join(tempRoot, path.basename(relativePath)), source);
|
|
572
|
+
}
|
|
573
|
+
const contractPath = path.join(tempRoot, "manifest-contract.ts");
|
|
574
|
+
const module = (await import(
|
|
575
|
+
`${pathToFileURL(contractPath).href}?t=${Date.now()}`
|
|
576
|
+
)) as Record<string, any>;
|
|
577
|
+
return await options.run(module);
|
|
578
|
+
} finally {
|
|
579
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
async function createGeneratedContractTempRoot(tempPrefix: string) {
|
|
584
|
+
const tempRoot = await mkdtemp(path.join(tmpdir(), tempPrefix));
|
|
585
|
+
await symlink(cliNodeModulesRoot, path.join(tempRoot, "node_modules"), "dir");
|
|
586
|
+
return tempRoot;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
test("generateManifestContractSource emits typed topology fields and helper literals", () => {
|
|
590
|
+
const source = generateManifestContractSource(TEST_MANIFEST);
|
|
591
|
+
|
|
592
|
+
expect(source).toContain("export type TrackBoardBoardFields = {");
|
|
593
|
+
expect(source).toContain(' "roundMarker": number;');
|
|
594
|
+
expect(source).toContain("export type TrackBoardSpaceFields = {");
|
|
595
|
+
expect(source).toContain("export type TrackBoardContainerFields = {");
|
|
596
|
+
expect(source).toContain("export type TrackBoardRelationFields = {");
|
|
597
|
+
expect(source).toContain("export type HexMapSpaceFields = {");
|
|
598
|
+
expect(source).toContain("export type HexMapEdgeFields = {");
|
|
599
|
+
expect(source).toContain("export type HexMapVertexFields = {");
|
|
600
|
+
expect(source).toContain("export type HexAuthoredEdgeRef<");
|
|
601
|
+
expect(source).toContain("export type HexAuthoredEdgeState<");
|
|
602
|
+
expect(source).toContain("export type HexAuthoredVertexRef<");
|
|
603
|
+
expect(source).toContain("export type HexAuthoredVertexState<");
|
|
604
|
+
expect(source).toContain("export type SquareGridRelationFields = {");
|
|
605
|
+
expect(source).toContain("export type SquareGridContainerFields = {");
|
|
606
|
+
expect(source).toContain("export type SquareGridEdgeFields = {");
|
|
607
|
+
expect(source).toContain("export type SquareGridVertexFields = {");
|
|
608
|
+
expect(source).toContain("export type WorkerPieceFields = {");
|
|
609
|
+
expect(source).toContain("export type D6DieFields = {");
|
|
610
|
+
expect(source).toContain(
|
|
611
|
+
'spaceTypeIds: ["forest", "meadow", "worker-space"] as const',
|
|
612
|
+
);
|
|
613
|
+
expect(source).toContain(
|
|
614
|
+
'edgeTypeIds: ["three-to-one", "wall-slot"] as const',
|
|
615
|
+
);
|
|
616
|
+
expect(source).toContain("authoredHexEdges<");
|
|
617
|
+
expect(source).toContain("authoredHexVertices<");
|
|
618
|
+
expect(source).toContain("resolveHexEdgeId<");
|
|
619
|
+
expect(source).toContain("resolveHexVertexId<");
|
|
620
|
+
expect(source).toContain(
|
|
621
|
+
'vertexTypeIds: ["crossing", "settlement-slot"] as const',
|
|
622
|
+
);
|
|
623
|
+
expect(source).toContain('boardTypeIds: ["grid", "map", "track"] as const');
|
|
624
|
+
expect(source).toContain(
|
|
625
|
+
'boardLayouts: ["generic", "hex", "square"] as const',
|
|
626
|
+
);
|
|
627
|
+
expect(source).toContain("const spaceIdsByBoardIdLookup =");
|
|
628
|
+
expect(source).toContain("const spaceTypeIdByBoardIdLookup =");
|
|
629
|
+
expect(source).toContain("const containerIdsByBoardIdLookup =");
|
|
630
|
+
expect(source).toContain("const containerHostByBoardIdLookup =");
|
|
631
|
+
expect(source).toContain(
|
|
632
|
+
'export type BoardSpaceFieldsByBoardId = {\n "track-board": TrackBoardSpaceFields;',
|
|
633
|
+
);
|
|
634
|
+
expect(source).toContain(
|
|
635
|
+
'export type BoardContainerFieldsByBoardId = {\n "track-board": TrackBoardContainerFields;',
|
|
636
|
+
);
|
|
637
|
+
expect(source).toContain(
|
|
638
|
+
"export type BoardState<BoardIdValue extends BoardId = BoardId> =",
|
|
639
|
+
);
|
|
640
|
+
expect(source).toContain("export type BoardSpaceStateByBoardId = {");
|
|
641
|
+
expect(source).toContain(
|
|
642
|
+
"export type BoardSpaceFields<BoardIdValue extends BoardId = BoardId> =",
|
|
643
|
+
);
|
|
644
|
+
expect(source).toContain("export type BoardContainerStateByBoardId = {");
|
|
645
|
+
expect(source).toContain(
|
|
646
|
+
"export interface TiledEdgeStateRecord<\n SpaceIdValue extends SpaceId = SpaceId,\n EdgeIdValue extends EdgeId = EdgeId,",
|
|
647
|
+
);
|
|
648
|
+
expect(source).toContain(" id: EdgeIdValue;");
|
|
649
|
+
expect(source).toContain(
|
|
650
|
+
"export interface TiledVertexStateRecord<\n SpaceIdValue extends SpaceId = SpaceId,\n VertexIdValue extends VertexId = VertexId,",
|
|
651
|
+
);
|
|
652
|
+
expect(source).toContain(" id: VertexIdValue;");
|
|
653
|
+
expect(source).toContain("export type HexEdgeState<");
|
|
654
|
+
expect(source).toContain(
|
|
655
|
+
"export type TiledBoardId = keyof HexBoardStateById | keyof SquareBoardStateById;",
|
|
656
|
+
);
|
|
657
|
+
expect(source).toContain("export type TiledVertexFields<");
|
|
658
|
+
expect(source).toContain('"space-a": "worker-space"');
|
|
659
|
+
expect(source).toContain('"market-row": {\n "type": "board"');
|
|
660
|
+
expect(source).toContain("const boardIdsByTypeIdLookup =");
|
|
661
|
+
expect(source).toContain("const spaceIdsByTypeIdLookup =");
|
|
662
|
+
expect(source).toContain("const relationTypeIdsByBoardIdLookup =");
|
|
663
|
+
expect(source).toContain("const edgeIdsByTypeIdLookup =");
|
|
664
|
+
expect(source).toContain("const vertexIdsByTypeIdLookup =");
|
|
665
|
+
expect(source).toContain("boardIdsForLayout<");
|
|
666
|
+
expect(source).toContain("boardIdsForType<TypeIdValue");
|
|
667
|
+
expect(source).toContain("spaceKinds<BoardIdValue");
|
|
668
|
+
expect(source).toContain("export const records = {");
|
|
669
|
+
expect(source).toContain("export const idGuards = {");
|
|
670
|
+
expect(source).toContain("expectEdgeId(value: string): EdgeId");
|
|
671
|
+
expect(source).toContain("spaceRecord<");
|
|
672
|
+
expect(source).toContain("isSpaceId<");
|
|
673
|
+
expect(source).toContain("expectSpaceId<");
|
|
674
|
+
expect(source).toContain("edgeRecord<");
|
|
675
|
+
expect(source).toContain("vertexRecord<");
|
|
676
|
+
expect(source).toContain("containerHost<");
|
|
677
|
+
expect(source).toContain("function buildPerPlayerCardIds(");
|
|
678
|
+
expect(source).toContain("export type CardIdsBySharedZoneId = {");
|
|
679
|
+
expect(source).toContain("function buildPlayerResources(");
|
|
680
|
+
expect(source).toContain('from "@dreamboard/app-sdk/reducer";');
|
|
681
|
+
expect(source).toContain("export function dealToPlayerZone(options: {");
|
|
682
|
+
expect(source).toContain(
|
|
683
|
+
"export function dealToPlayerBoardContainer(options: {",
|
|
684
|
+
);
|
|
685
|
+
expect(source).toContain(
|
|
686
|
+
"export function seedSharedBoardContainer(options: {",
|
|
687
|
+
);
|
|
688
|
+
expect(source).toContain("export function seedSharedBoardSpace(options: {");
|
|
689
|
+
expect(source).toContain("export function shuffle(");
|
|
690
|
+
expect(source).toContain("createManifestStringLiteralSchema");
|
|
691
|
+
expect(source).toContain(
|
|
692
|
+
"export const runtimeSchema = createManifestRuntimeSchema({",
|
|
693
|
+
);
|
|
694
|
+
expect(source).toContain(
|
|
695
|
+
"componentLocations: z.record(\n z.string(),\n z.union([",
|
|
696
|
+
);
|
|
697
|
+
expect(source).toContain('type: z.literal("InSlot")');
|
|
698
|
+
expect(source).toContain('kind: z.literal("piece")');
|
|
699
|
+
expect(source).toContain('id: z.literal("mat-alpha")');
|
|
700
|
+
expect(source).toContain('slotId: z.literal("worker-rest")');
|
|
701
|
+
expect(source).toContain('kind: z.literal("die")');
|
|
702
|
+
expect(source).toContain('id: z.literal("holder-a")');
|
|
703
|
+
expect(source).toContain('slotId: z.literal("staging")');
|
|
704
|
+
expect(source).toContain("zones: (playerIds?: readonly string[]) => ({");
|
|
705
|
+
expect(source).toContain("export const staticBoards =");
|
|
706
|
+
expect(source).toContain(
|
|
707
|
+
"staticBoards: staticBoards as unknown as StaticBoards<TableState>,",
|
|
708
|
+
);
|
|
709
|
+
expect(source).toContain("cardSetIdsByZoneId: cloneManifestDefault(");
|
|
710
|
+
expect(source).toContain(
|
|
711
|
+
'cardSetIdsBySharedZoneId: {\n "draw-deck": ["standard-deck"] as const,',
|
|
712
|
+
);
|
|
713
|
+
expect(source).toContain("allowedCardSetIds?: readonly CardSetId[];");
|
|
714
|
+
expect(source).not.toContain("export type BoardSpaceId = SpaceId | TileId;");
|
|
715
|
+
expect(source).toContain('layout: z.literal("generic")');
|
|
716
|
+
expect(source).toContain("typeId: ids.relationTypeId");
|
|
717
|
+
expect(source).toContain("export type SquareBoardStateById = {");
|
|
718
|
+
expect(source).toContain("export type CardStateRecord<");
|
|
719
|
+
expect(source).toContain(
|
|
720
|
+
'"CARD_A": CardStateRecord<"CARD_A", "standard-deck", "CARD_A", StandardDeckCardProperties>;',
|
|
721
|
+
);
|
|
722
|
+
expect(source).toContain("name: z.string().optional()");
|
|
723
|
+
expect(source).toContain("text: z.string().optional()");
|
|
724
|
+
expect(source).not.toContain("cardName: z.string().optional()");
|
|
725
|
+
expect(source).not.toContain("description: z.string().optional()");
|
|
726
|
+
expect(source).toContain(
|
|
727
|
+
"const cardPropertiesSchemaByCardSetId: Record<string, z.ZodTypeAny> = {",
|
|
728
|
+
);
|
|
729
|
+
expect(source).toContain(
|
|
730
|
+
"function createCardStateSchema<CardIdValue extends CardId>(",
|
|
731
|
+
);
|
|
732
|
+
expect(source).toContain(
|
|
733
|
+
"literals.cardIds.map((cardId) => [cardId, createCardStateSchema(cardId)])",
|
|
734
|
+
);
|
|
735
|
+
expect(source).not.toContain("const createStringLiteralSchema = <Values");
|
|
736
|
+
expect(source).not.toContain(
|
|
737
|
+
"function createRuntimeSchema<PhaseNameSchema extends z.ZodTypeAny>(",
|
|
738
|
+
);
|
|
739
|
+
expect(source).not.toContain("export type SpaceFieldsBySpaceId =");
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("generateManifestContractSource emits shared home zone literals for card types", () => {
|
|
743
|
+
const source = generateManifestContractSource({
|
|
744
|
+
...EMPTY_MANIFEST,
|
|
745
|
+
cardSets: [
|
|
746
|
+
{
|
|
747
|
+
type: "manual",
|
|
748
|
+
id: "market",
|
|
749
|
+
name: "Market",
|
|
750
|
+
cardSchema: { properties: {} },
|
|
751
|
+
cards: [
|
|
752
|
+
{
|
|
753
|
+
type: "coin",
|
|
754
|
+
name: "Coin",
|
|
755
|
+
count: 1,
|
|
756
|
+
home: { type: "zone", zoneId: "coin-supply" },
|
|
757
|
+
properties: {},
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
type: "curse",
|
|
761
|
+
name: "Curse",
|
|
762
|
+
count: 1,
|
|
763
|
+
home: { type: "zone", zoneId: "discard" },
|
|
764
|
+
properties: {},
|
|
765
|
+
},
|
|
766
|
+
],
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
zones: [
|
|
770
|
+
{
|
|
771
|
+
id: "coin-supply",
|
|
772
|
+
name: "Coin Supply",
|
|
773
|
+
scope: "shared",
|
|
774
|
+
allowedCardSetIds: ["market"],
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
id: "discard",
|
|
778
|
+
name: "Discard",
|
|
779
|
+
scope: "perPlayer",
|
|
780
|
+
allowedCardSetIds: ["market"],
|
|
781
|
+
},
|
|
782
|
+
],
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
expect(source).toContain(
|
|
786
|
+
'sharedZoneIdsByCardSetId: {\n "market": ["coin-supply"] as const,',
|
|
787
|
+
);
|
|
788
|
+
expect(source).toContain(
|
|
789
|
+
'homeSharedZoneIdsByCardType: {\n "coin": ["coin-supply"] as const,\n "curse": [] as const,',
|
|
790
|
+
);
|
|
791
|
+
expect(source).toContain(
|
|
792
|
+
'homeSharedZoneIdByCardType: {\n "coin": "coin-supply",',
|
|
793
|
+
);
|
|
794
|
+
expect(source).not.toContain('"curse": "discard"');
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("generateManifestContractSource defaults omitted die sides to 6 in createInitialTable", async () => {
|
|
798
|
+
await withGeneratedContractModule({
|
|
799
|
+
tempPrefix: ".tmp-die-default-sides-contract-",
|
|
800
|
+
manifest: {
|
|
801
|
+
players: {
|
|
802
|
+
minPlayers: 2,
|
|
803
|
+
maxPlayers: 4,
|
|
804
|
+
optimalPlayers: 3,
|
|
805
|
+
},
|
|
806
|
+
cardSets: [],
|
|
807
|
+
zones: [],
|
|
808
|
+
boardTemplates: [],
|
|
809
|
+
boards: [],
|
|
810
|
+
pieceTypes: [],
|
|
811
|
+
pieceSeeds: [],
|
|
812
|
+
dieTypes: [
|
|
813
|
+
{
|
|
814
|
+
id: "d6",
|
|
815
|
+
name: "D6",
|
|
816
|
+
},
|
|
817
|
+
],
|
|
818
|
+
dieSeeds: [
|
|
819
|
+
{
|
|
820
|
+
id: "d6-a",
|
|
821
|
+
typeId: "d6",
|
|
822
|
+
},
|
|
823
|
+
],
|
|
824
|
+
resources: [],
|
|
825
|
+
setupOptions: [],
|
|
826
|
+
setupProfiles: [],
|
|
827
|
+
} satisfies GameTopologyManifest,
|
|
828
|
+
run: (module) => {
|
|
829
|
+
const table = module.createInitialTable({
|
|
830
|
+
playerIds: ["player-1", "player-2"],
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
expect(table.dice["d6-a"]).toMatchObject({
|
|
834
|
+
id: "d6-a",
|
|
835
|
+
sides: 6,
|
|
836
|
+
});
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
test("generated createInitialTable keeps shared card homes aligned across decks, zones, and locations", async () => {
|
|
842
|
+
await withGeneratedContractModule({
|
|
843
|
+
tempPrefix: ".tmp-shared-card-home-contract-",
|
|
844
|
+
manifest: {
|
|
845
|
+
players: {
|
|
846
|
+
minPlayers: 2,
|
|
847
|
+
maxPlayers: 4,
|
|
848
|
+
optimalPlayers: 3,
|
|
849
|
+
},
|
|
850
|
+
cardSets: [
|
|
851
|
+
{
|
|
852
|
+
type: "manual",
|
|
853
|
+
id: "market",
|
|
854
|
+
name: "Market",
|
|
855
|
+
cardSchema: { properties: {} },
|
|
856
|
+
cards: [
|
|
857
|
+
{
|
|
858
|
+
type: "coin",
|
|
859
|
+
name: "Coin",
|
|
860
|
+
count: 2,
|
|
861
|
+
home: { type: "zone", zoneId: "coin-supply" },
|
|
862
|
+
properties: {},
|
|
863
|
+
},
|
|
864
|
+
{
|
|
865
|
+
type: "gem",
|
|
866
|
+
name: "Gem",
|
|
867
|
+
count: 2,
|
|
868
|
+
home: { type: "zone", zoneId: "gem-supply" },
|
|
869
|
+
properties: {},
|
|
870
|
+
},
|
|
871
|
+
],
|
|
872
|
+
},
|
|
873
|
+
],
|
|
874
|
+
zones: [
|
|
875
|
+
{
|
|
876
|
+
id: "coin-supply",
|
|
877
|
+
name: "Coin Supply",
|
|
878
|
+
scope: "shared",
|
|
879
|
+
allowedCardSetIds: ["market"],
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
id: "gem-supply",
|
|
883
|
+
name: "Gem Supply",
|
|
884
|
+
scope: "shared",
|
|
885
|
+
allowedCardSetIds: ["market"],
|
|
886
|
+
},
|
|
887
|
+
],
|
|
888
|
+
boardTemplates: [],
|
|
889
|
+
boards: [],
|
|
890
|
+
pieceTypes: [],
|
|
891
|
+
pieceSeeds: [],
|
|
892
|
+
dieTypes: [],
|
|
893
|
+
dieSeeds: [],
|
|
894
|
+
resources: [],
|
|
895
|
+
setupOptions: [],
|
|
896
|
+
setupProfiles: [],
|
|
897
|
+
} satisfies GameTopologyManifest,
|
|
898
|
+
run: (module) => {
|
|
899
|
+
const table = module.createInitialTable({
|
|
900
|
+
playerIds: ["player-1", "player-2"],
|
|
901
|
+
shuffleItems: (values: readonly unknown[]) => [...values].reverse(),
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
for (const [zoneId, cardType] of [
|
|
905
|
+
["coin-supply", "coin"],
|
|
906
|
+
["gem-supply", "gem"],
|
|
907
|
+
] as const) {
|
|
908
|
+
expect(table.zones.shared[zoneId]).toEqual(table.decks[zoneId]);
|
|
909
|
+
expect(table.zones.shared[zoneId]).toHaveLength(2);
|
|
910
|
+
table.zones.shared[zoneId].forEach(
|
|
911
|
+
(cardId: string, position: number) => {
|
|
912
|
+
expect(table.cards[cardId].cardType).toBe(cardType);
|
|
913
|
+
expect(table.componentLocations[cardId]).toEqual({
|
|
914
|
+
type: "InDeck",
|
|
915
|
+
deckId: zoneId,
|
|
916
|
+
playedBy: null,
|
|
917
|
+
position,
|
|
918
|
+
});
|
|
919
|
+
},
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test("generateManifestContractSources split runtime module executes generated runtime exports", async () => {
|
|
927
|
+
await withGeneratedSplitContractModule({
|
|
928
|
+
tempPrefix: ".tmp-split-runtime-contract-",
|
|
929
|
+
manifest: TEST_MANIFEST,
|
|
930
|
+
run: (module) => {
|
|
931
|
+
const table = module.createInitialTable({
|
|
932
|
+
playerIds: ["player-1", "player-2"],
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
expect(table.playerOrder).toEqual(["player-1", "player-2"]);
|
|
936
|
+
expect(table.zones.shared["draw-deck"]).toEqual(["CARD_A"]);
|
|
937
|
+
expect(table.zones.perPlayer["main-hand"].entries).toEqual([
|
|
938
|
+
["player-1", []],
|
|
939
|
+
["player-2", []],
|
|
940
|
+
]);
|
|
941
|
+
expect(table.boards.byId["track-board"].fields).toEqual({
|
|
942
|
+
roundMarker: 1,
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
expect(() => module.tableSchema.parse(table)).not.toThrow();
|
|
946
|
+
expect(() =>
|
|
947
|
+
module.runtimeSchema.parse({
|
|
948
|
+
phaseName: "play",
|
|
949
|
+
playerIds: ["player-1", "player-2"],
|
|
950
|
+
setupProfileId: null,
|
|
951
|
+
}),
|
|
952
|
+
).not.toThrow();
|
|
953
|
+
|
|
954
|
+
const gameStateSchema = module.createGameStateSchema({
|
|
955
|
+
phaseNameSchema: module.ids.phaseName,
|
|
956
|
+
publicSchema: module.ids.resourceId.optional(),
|
|
957
|
+
privateSchema: module.ids.cardId.optional(),
|
|
958
|
+
hiddenSchema: module.ids.boardId.optional(),
|
|
959
|
+
phasesSchema: module.ids.phaseName,
|
|
960
|
+
});
|
|
961
|
+
expect(() =>
|
|
962
|
+
gameStateSchema.parse({
|
|
963
|
+
table,
|
|
964
|
+
public: undefined,
|
|
965
|
+
private: {},
|
|
966
|
+
hidden: undefined,
|
|
967
|
+
flow: {
|
|
968
|
+
currentPhase: "play",
|
|
969
|
+
turn: 0,
|
|
970
|
+
round: 0,
|
|
971
|
+
activePlayers: [],
|
|
972
|
+
},
|
|
973
|
+
phase: "play",
|
|
974
|
+
runtime: {},
|
|
975
|
+
}),
|
|
976
|
+
).not.toThrow();
|
|
977
|
+
|
|
978
|
+
expect(module.manifestContract.defaults.zones).toBeFunction();
|
|
979
|
+
expect(module.manifestContract.createGameStateSchema).toBeFunction();
|
|
980
|
+
expect(
|
|
981
|
+
module.manifestContract.staticBoards.byId["track-board"].layout,
|
|
982
|
+
).toBe("generic");
|
|
983
|
+
expect(module.default).toBe(module.manifestContract);
|
|
984
|
+
},
|
|
985
|
+
});
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
test("generateManifestContractSource exposes generated static board topology", async () => {
|
|
989
|
+
await withGeneratedContractModule({
|
|
990
|
+
tempPrefix: ".tmp-static-board-contract-",
|
|
991
|
+
manifest: TEST_MANIFEST,
|
|
992
|
+
run: (module) => {
|
|
993
|
+
const hexBoard = module.staticBoards.hex["hex-map"];
|
|
994
|
+
const squareBoard = module.staticBoards.square["square-grid"];
|
|
995
|
+
|
|
996
|
+
expect(module.manifestContract.staticBoards.hex["hex-map"]).toEqual(
|
|
997
|
+
hexBoard,
|
|
998
|
+
);
|
|
999
|
+
expect(hexBoard.spaces["hex-a"]).toMatchObject({ q: 0, r: 0 });
|
|
1000
|
+
expect(hexBoard.edges).toEqual(module.staticBoards.byId["hex-map"].edges);
|
|
1001
|
+
expect(hexBoard.vertices).toEqual(
|
|
1002
|
+
module.staticBoards.byId["hex-map"].vertices,
|
|
1003
|
+
);
|
|
1004
|
+
expect(
|
|
1005
|
+
hexBoard.vertices.some((vertex: { spaceIds: readonly string[] }) =>
|
|
1006
|
+
["hex-a", "hex-b", "hex-c"].every((spaceId) =>
|
|
1007
|
+
vertex.spaceIds.includes(spaceId),
|
|
1008
|
+
),
|
|
1009
|
+
),
|
|
1010
|
+
).toBe(true);
|
|
1011
|
+
expect(squareBoard.edges).toEqual(
|
|
1012
|
+
module.staticBoards.byId["square-grid"].edges,
|
|
1013
|
+
);
|
|
1014
|
+
expect(squareBoard.vertices).toEqual(
|
|
1015
|
+
module.staticBoards.byId["square-grid"].vertices,
|
|
1016
|
+
);
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
test("generateManifestContractSource typechecks static board topology ids", async () => {
|
|
1022
|
+
await expectGeneratedContractTypechecks({
|
|
1023
|
+
tempPrefix: ".tmp-static-board-types-contract-",
|
|
1024
|
+
manifest: TEST_MANIFEST,
|
|
1025
|
+
usageSource: `import {
|
|
1026
|
+
staticBoards,
|
|
1027
|
+
type HexEdgeState,
|
|
1028
|
+
type HexVertexState,
|
|
1029
|
+
} from "./manifest-contract";
|
|
1030
|
+
|
|
1031
|
+
const hex = staticBoards.hex["hex-map"];
|
|
1032
|
+
const hexEdge: HexEdgeState<"hex-map"> = hex.edges[0];
|
|
1033
|
+
const hexVertex: HexVertexState<"hex-map"> = hex.vertices[0];
|
|
1034
|
+
const hexSpaceId: "hex-a" | "hex-b" | "hex-c" = hex.edges[0].spaceIds[0];
|
|
1035
|
+
const richness: number = hex.spaces["hex-a"].fields.richness;
|
|
1036
|
+
|
|
1037
|
+
const square = staticBoards.square["square-grid"];
|
|
1038
|
+
const squareEdge = square.edges[0];
|
|
1039
|
+
const squareSpaceId: "cell-a1" | "cell-a2" | "cell-b1" | "cell-b2" =
|
|
1040
|
+
squareEdge.spaceIds[0];
|
|
1041
|
+
|
|
1042
|
+
void hexEdge;
|
|
1043
|
+
void hexVertex;
|
|
1044
|
+
void hexSpaceId;
|
|
1045
|
+
void richness;
|
|
1046
|
+
void squareEdge;
|
|
1047
|
+
void squareSpaceId;
|
|
1048
|
+
`,
|
|
1049
|
+
});
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
test("generateManifestContractSource renders optional nullable and record schemas", () => {
|
|
1053
|
+
const source = generateManifestContractSource(TEST_MANIFEST);
|
|
1054
|
+
|
|
1055
|
+
expect(source).toContain(' "note"?: string | null;');
|
|
1056
|
+
expect(source).toContain(' "scoreByPlayer": Record<string, number>;');
|
|
1057
|
+
expect(source).toContain(' "labelOverride"?: string | null;');
|
|
1058
|
+
expect(source).toContain(
|
|
1059
|
+
"export const StandardDeckCardPropertiesSchema = z.object({",
|
|
1060
|
+
);
|
|
1061
|
+
expect(source).toContain('"note": z.string().nullable().optional()');
|
|
1062
|
+
expect(source).toContain("z.record(z.string(), z.number().int())");
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
test("generateManifestContractSource emits variant card property schemas", async () => {
|
|
1066
|
+
const manifest: GameTopologyManifest = {
|
|
1067
|
+
...EMPTY_MANIFEST,
|
|
1068
|
+
cardSets: [
|
|
1069
|
+
{
|
|
1070
|
+
type: "manual",
|
|
1071
|
+
id: "market-cards",
|
|
1072
|
+
name: "Market Cards",
|
|
1073
|
+
cardSchema: {
|
|
1074
|
+
shared: {
|
|
1075
|
+
cost: { type: "integer", optional: true, default: 0 },
|
|
1076
|
+
},
|
|
1077
|
+
variants: {
|
|
1078
|
+
copper: {
|
|
1079
|
+
properties: {
|
|
1080
|
+
coins: { type: "integer" },
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
estate: {
|
|
1084
|
+
properties: {
|
|
1085
|
+
vp: { type: "integer" },
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
},
|
|
1089
|
+
},
|
|
1090
|
+
cards: [
|
|
1091
|
+
{
|
|
1092
|
+
type: "copper",
|
|
1093
|
+
name: "Copper",
|
|
1094
|
+
count: 1,
|
|
1095
|
+
properties: { coins: 1 },
|
|
1096
|
+
},
|
|
1097
|
+
{
|
|
1098
|
+
type: "estate",
|
|
1099
|
+
name: "Estate",
|
|
1100
|
+
count: 1,
|
|
1101
|
+
properties: { vp: 1, cost: 2 },
|
|
1102
|
+
},
|
|
1103
|
+
],
|
|
1104
|
+
},
|
|
1105
|
+
],
|
|
1106
|
+
zones: [
|
|
1107
|
+
{
|
|
1108
|
+
id: "hand",
|
|
1109
|
+
name: "Hand",
|
|
1110
|
+
scope: "perPlayer",
|
|
1111
|
+
visibility: "ownerOnly",
|
|
1112
|
+
allowedCardSetIds: ["market-cards"],
|
|
1113
|
+
},
|
|
1114
|
+
],
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
const source = generateManifestContractSource(manifest);
|
|
1118
|
+
expect(source).toContain("export type MarketCardsCopperCardProperties");
|
|
1119
|
+
expect(source).toContain(' "coins": number;');
|
|
1120
|
+
expect(source).toContain(' "cost": number;');
|
|
1121
|
+
expect(source).toContain('"cost": z.number().int().optional().default(0)');
|
|
1122
|
+
expect(source).toContain("market-cards:copper");
|
|
1123
|
+
|
|
1124
|
+
await withGeneratedContractModule({
|
|
1125
|
+
tempPrefix: ".tmp-variant-card-properties-runtime-",
|
|
1126
|
+
manifest,
|
|
1127
|
+
run: (module) => {
|
|
1128
|
+
const table = module.createInitialTable({
|
|
1129
|
+
playerIds: ["player-1", "player-2"],
|
|
1130
|
+
});
|
|
1131
|
+
expect(table.cards.copper.properties.cost).toBe(0);
|
|
1132
|
+
},
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
await expectGeneratedContractTypechecks({
|
|
1136
|
+
tempPrefix: ".tmp-variant-card-properties-contract-",
|
|
1137
|
+
manifest,
|
|
1138
|
+
usageSource: `import { asPlayerId, createTableQueries } from "@dreamboard/app-sdk/reducer";
|
|
1139
|
+
import { createInitialTable } from "./manifest-contract";
|
|
1140
|
+
|
|
1141
|
+
const table = createInitialTable({ playerIds: ["player-1", "player-2"] });
|
|
1142
|
+
const copperCoins: number = table.cards["copper"].properties.coins;
|
|
1143
|
+
const copperCost: number = table.cards["copper"].properties.cost;
|
|
1144
|
+
const estateVp: number = table.cards["estate"].properties.vp;
|
|
1145
|
+
|
|
1146
|
+
const q = createTableQueries(table);
|
|
1147
|
+
const cardId = q.zone.playerCards(asPlayerId("player-1"), "hand")[0];
|
|
1148
|
+
if (cardId) {
|
|
1149
|
+
const card = q.card.get(cardId);
|
|
1150
|
+
if (card.cardType === "copper") {
|
|
1151
|
+
const coins: number = card.properties.coins;
|
|
1152
|
+
void coins;
|
|
1153
|
+
}
|
|
1154
|
+
if (card.cardType === "estate") {
|
|
1155
|
+
const vp: number = card.properties.vp;
|
|
1156
|
+
void vp;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
void copperCoins;
|
|
1161
|
+
void copperCost;
|
|
1162
|
+
void estateVp;
|
|
1163
|
+
`,
|
|
1164
|
+
});
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
test("generateManifestContractSource preserves enum literal unions inside array items", async () => {
|
|
1168
|
+
const manifest = structuredClone(TEST_MANIFEST);
|
|
1169
|
+
const manualCardSet = manifest.cardSets?.[0];
|
|
1170
|
+
|
|
1171
|
+
if (!manualCardSet || manualCardSet.type !== "manual") {
|
|
1172
|
+
throw new Error("Expected TEST_MANIFEST to include a manual card set.");
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
manualCardSet.cardSchema = {
|
|
1176
|
+
properties: {
|
|
1177
|
+
...manualCardSet.cardSchema?.properties,
|
|
1178
|
+
tags: {
|
|
1179
|
+
type: "array",
|
|
1180
|
+
optional: true,
|
|
1181
|
+
items: {
|
|
1182
|
+
type: "enum",
|
|
1183
|
+
enums: ["intel", "timed", "public", "secret"],
|
|
1184
|
+
},
|
|
1185
|
+
},
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
manualCardSet.cards = manualCardSet.cards.map((card) => ({
|
|
1189
|
+
...card,
|
|
1190
|
+
properties: {
|
|
1191
|
+
...card.properties,
|
|
1192
|
+
tags: ["intel"],
|
|
1193
|
+
},
|
|
1194
|
+
}));
|
|
1195
|
+
|
|
1196
|
+
const source = generateManifestContractSource(manifest);
|
|
1197
|
+
|
|
1198
|
+
expect(source).toContain(
|
|
1199
|
+
' "tags"?: Array<"intel" | "timed" | "public" | "secret">;',
|
|
1200
|
+
);
|
|
1201
|
+
|
|
1202
|
+
await expectGeneratedContractTypechecks({
|
|
1203
|
+
tempPrefix: ".tmp-enum-array-contract-",
|
|
1204
|
+
manifest,
|
|
1205
|
+
usageSource: `import { type StandardDeckCardProperties } from "./manifest-contract";
|
|
1206
|
+
|
|
1207
|
+
declare const metadata: StandardDeckCardProperties;
|
|
1208
|
+
|
|
1209
|
+
const firstTag: "intel" | "timed" | "public" | "secret" | undefined =
|
|
1210
|
+
metadata.tags?.[0];
|
|
1211
|
+
|
|
1212
|
+
void firstTag;
|
|
1213
|
+
`,
|
|
1214
|
+
});
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
test("generateManifestContractSource uses safe empty component state maps", () => {
|
|
1218
|
+
const source = generateManifestContractSource(EMPTY_MANIFEST);
|
|
1219
|
+
|
|
1220
|
+
expect(source).toContain(
|
|
1221
|
+
"export type CardStateById = Record<string, never>;",
|
|
1222
|
+
);
|
|
1223
|
+
expect(source).toContain(
|
|
1224
|
+
"export type PieceStateById = Record<string, never>;",
|
|
1225
|
+
);
|
|
1226
|
+
expect(source).toContain("export type DieStateById = Record<string, never>;");
|
|
1227
|
+
expect(source).not.toContain("function createCardStateSchema<CardIdValue");
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
test("generateManifestContractSource typechecks ergonomic board aliases", async () => {
|
|
1231
|
+
const perPlayerManifest: GameTopologyManifest = {
|
|
1232
|
+
...structuredClone(TEST_MANIFEST),
|
|
1233
|
+
boards: [
|
|
1234
|
+
...structuredClone(TEST_MANIFEST.boards),
|
|
1235
|
+
{
|
|
1236
|
+
id: "player-track",
|
|
1237
|
+
name: "Player Track",
|
|
1238
|
+
layout: "generic",
|
|
1239
|
+
scope: "perPlayer",
|
|
1240
|
+
spaces: [
|
|
1241
|
+
{
|
|
1242
|
+
id: "lane-start",
|
|
1243
|
+
typeId: "worker-space",
|
|
1244
|
+
},
|
|
1245
|
+
],
|
|
1246
|
+
relations: [],
|
|
1247
|
+
containers: [],
|
|
1248
|
+
},
|
|
1249
|
+
],
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
await expectGeneratedContractTypechecks({
|
|
1253
|
+
tempPrefix: ".tmp-board-alias-contract-",
|
|
1254
|
+
manifest: perPlayerManifest,
|
|
1255
|
+
usageSource: `import {
|
|
1256
|
+
boardHelpers,
|
|
1257
|
+
idGuards,
|
|
1258
|
+
literals,
|
|
1259
|
+
records,
|
|
1260
|
+
type BoardFields,
|
|
1261
|
+
type BoardSpaceFields,
|
|
1262
|
+
type BoardSpaceState,
|
|
1263
|
+
type BoardState,
|
|
1264
|
+
type HexAuthoredEdgeRef,
|
|
1265
|
+
type HexAuthoredEdgeState,
|
|
1266
|
+
type HexAuthoredVertexRef,
|
|
1267
|
+
type HexAuthoredVertexState,
|
|
1268
|
+
type HexEdgeFields,
|
|
1269
|
+
type HexEdgeState,
|
|
1270
|
+
type SquareEdgeFields,
|
|
1271
|
+
type SquareEdgeState,
|
|
1272
|
+
type TiledEdgeState,
|
|
1273
|
+
type TiledVertexState,
|
|
1274
|
+
} from "./manifest-contract";
|
|
1275
|
+
|
|
1276
|
+
const sharedBoardId = "track-board" as const;
|
|
1277
|
+
type SharedBoardState = BoardState<typeof sharedBoardId>;
|
|
1278
|
+
type SharedBoardFields = BoardFields<typeof sharedBoardId>;
|
|
1279
|
+
type SharedSpaceState = BoardSpaceState<typeof sharedBoardId>;
|
|
1280
|
+
type SharedSpaceFields = BoardSpaceFields<typeof sharedBoardId>;
|
|
1281
|
+
type SharedHexAuthoredEdgeRef = HexAuthoredEdgeRef<"hex-map">;
|
|
1282
|
+
type SharedHexAuthoredEdgeState = HexAuthoredEdgeState<"hex-map">;
|
|
1283
|
+
type SharedHexAuthoredVertexRef = HexAuthoredVertexRef<"hex-map">;
|
|
1284
|
+
type SharedHexAuthoredVertexState = HexAuthoredVertexState<"hex-map">;
|
|
1285
|
+
type SharedHexEdgeState = HexEdgeState<"hex-map">;
|
|
1286
|
+
type SharedHexEdgeFields = HexEdgeFields<"hex-map">;
|
|
1287
|
+
type SharedSquareEdgeState = SquareEdgeState<"square-grid">;
|
|
1288
|
+
type SharedSquareEdgeFields = SquareEdgeFields<"square-grid">;
|
|
1289
|
+
type SharedTiledEdgeState = TiledEdgeState<"hex-map">;
|
|
1290
|
+
type SharedTiledVertexState = TiledVertexState<"square-grid">;
|
|
1291
|
+
type PerPlayerBoardState = BoardState<"player-track:player-1">;
|
|
1292
|
+
type PerPlayerSpaceState = BoardSpaceState<"player-track:player-1">;
|
|
1293
|
+
type PerPlayerSpaceFields = BoardSpaceFields<"player-track:player-1">;
|
|
1294
|
+
|
|
1295
|
+
const firstPerPlayerBoardId = boardHelpers.boardIdsForBase("player-track")[0];
|
|
1296
|
+
const firstPerPlayerSpaceId =
|
|
1297
|
+
boardHelpers.spaceIds(firstPerPlayerBoardId)[0];
|
|
1298
|
+
const perPlayerSpaceType =
|
|
1299
|
+
boardHelpers.spaceKinds(firstPerPlayerBoardId)[firstPerPlayerSpaceId];
|
|
1300
|
+
const authoredHexEdgeRef: SharedHexAuthoredEdgeRef = {
|
|
1301
|
+
spaces: ["hex-a", "hex-b"],
|
|
1302
|
+
};
|
|
1303
|
+
const authoredHexVertexRef: SharedHexAuthoredVertexRef = {
|
|
1304
|
+
spaces: ["hex-a", "hex-b", "hex-c"],
|
|
1305
|
+
};
|
|
1306
|
+
const firstHexEdgeState = boardHelpers.authoredHexEdges("hex-map")[0];
|
|
1307
|
+
const firstHexVertexState = boardHelpers.authoredHexVertices("hex-map")[0];
|
|
1308
|
+
const firstHexRouteId = boardHelpers.resolveHexEdgeId(
|
|
1309
|
+
"hex-map",
|
|
1310
|
+
authoredHexEdgeRef,
|
|
1311
|
+
);
|
|
1312
|
+
const firstHexSettlementId = boardHelpers.resolveHexVertexId(
|
|
1313
|
+
"hex-map",
|
|
1314
|
+
authoredHexVertexRef,
|
|
1315
|
+
);
|
|
1316
|
+
const edgeOwnerById = records.edgeIds(null);
|
|
1317
|
+
const vertexOwnerById = records.vertexIds((vertexId) => vertexId);
|
|
1318
|
+
const squareSpaceLabelById = boardHelpers.spaceRecord("square-grid", (spaceId) =>
|
|
1319
|
+
spaceId,
|
|
1320
|
+
);
|
|
1321
|
+
const squareContainerStateById = boardHelpers.containerRecord(
|
|
1322
|
+
"square-grid",
|
|
1323
|
+
false,
|
|
1324
|
+
);
|
|
1325
|
+
const squareRelationStateById = boardHelpers.relationTypeRecord(
|
|
1326
|
+
"square-grid",
|
|
1327
|
+
true,
|
|
1328
|
+
);
|
|
1329
|
+
const squareEdgeStateById = boardHelpers.edgeRecord("square-grid", null);
|
|
1330
|
+
const squareVertexStateById = boardHelpers.vertexRecord("square-grid", null);
|
|
1331
|
+
const maybeSpaceId = "cell-a1";
|
|
1332
|
+
if (boardHelpers.isSpaceId("square-grid", maybeSpaceId)) {
|
|
1333
|
+
void maybeSpaceId;
|
|
1334
|
+
}
|
|
1335
|
+
const ensuredSpaceId = boardHelpers.expectSpaceId("square-grid", "cell-a1");
|
|
1336
|
+
const ensuredEdgeId = boardHelpers.expectEdgeId(
|
|
1337
|
+
"square-grid",
|
|
1338
|
+
boardHelpers.edgeIds("square-grid", "wall-slot")[0],
|
|
1339
|
+
);
|
|
1340
|
+
const ensuredVertexId = boardHelpers.expectVertexId(
|
|
1341
|
+
"square-grid",
|
|
1342
|
+
boardHelpers.vertexIds("square-grid", "crossing")[0],
|
|
1343
|
+
);
|
|
1344
|
+
const manifestEdgeId = idGuards.expectEdgeId(firstHexRouteId);
|
|
1345
|
+
if (idGuards.isVertexId(firstHexSettlementId)) {
|
|
1346
|
+
void firstHexSettlementId;
|
|
1347
|
+
}
|
|
1348
|
+
const firstSquareCheckpointId =
|
|
1349
|
+
boardHelpers.vertexIds("square-grid", "crossing")[0];
|
|
1350
|
+
const hexBoardIds = boardHelpers.boardIdsForLayout("hex");
|
|
1351
|
+
const genericBoardBaseIds = boardHelpers.boardBaseIdsForLayout("generic");
|
|
1352
|
+
|
|
1353
|
+
const squareContainerHost =
|
|
1354
|
+
boardHelpers.containerHost("square-grid", "cache");
|
|
1355
|
+
const authoredHexEdgeRate =
|
|
1356
|
+
(null as unknown as SharedHexAuthoredEdgeState).fields.rate;
|
|
1357
|
+
const authoredHexVertexCost =
|
|
1358
|
+
(null as unknown as SharedHexAuthoredVertexState).fields.buildCost;
|
|
1359
|
+
const hexEdgeRate = (null as unknown as SharedHexEdgeState).fields.rate;
|
|
1360
|
+
const squareEdgeDurability =
|
|
1361
|
+
(null as unknown as SharedSquareEdgeState).fields.durability;
|
|
1362
|
+
const tiledEdgeRate = (null as unknown as SharedTiledEdgeState).fields.rate;
|
|
1363
|
+
const tiledVertexValue =
|
|
1364
|
+
(null as unknown as SharedTiledVertexState).fields.value;
|
|
1365
|
+
|
|
1366
|
+
void literals.boardIds;
|
|
1367
|
+
void literals.boardTemplateIds;
|
|
1368
|
+
void perPlayerSpaceType;
|
|
1369
|
+
void firstHexEdgeState.ref;
|
|
1370
|
+
void firstHexVertexState.ref;
|
|
1371
|
+
void firstHexRouteId;
|
|
1372
|
+
void firstHexSettlementId;
|
|
1373
|
+
void edgeOwnerById;
|
|
1374
|
+
void vertexOwnerById;
|
|
1375
|
+
void squareSpaceLabelById;
|
|
1376
|
+
void squareContainerStateById;
|
|
1377
|
+
void squareRelationStateById;
|
|
1378
|
+
void squareEdgeStateById;
|
|
1379
|
+
void squareVertexStateById;
|
|
1380
|
+
void ensuredSpaceId;
|
|
1381
|
+
void ensuredEdgeId;
|
|
1382
|
+
void ensuredVertexId;
|
|
1383
|
+
void manifestEdgeId;
|
|
1384
|
+
void firstSquareCheckpointId;
|
|
1385
|
+
void hexBoardIds;
|
|
1386
|
+
void genericBoardBaseIds;
|
|
1387
|
+
void boardHelpers.boardBaseIdsForTemplate;
|
|
1388
|
+
void boardHelpers.boardTemplateLayout;
|
|
1389
|
+
void squareContainerHost;
|
|
1390
|
+
void authoredHexEdgeRate;
|
|
1391
|
+
void authoredHexVertexCost;
|
|
1392
|
+
void hexEdgeRate;
|
|
1393
|
+
void squareEdgeDurability;
|
|
1394
|
+
void tiledEdgeRate;
|
|
1395
|
+
void tiledVertexValue;
|
|
1396
|
+
void (null as SharedBoardState | SharedBoardFields | SharedSpaceState | SharedSpaceFields | null);
|
|
1397
|
+
void (null as SharedHexAuthoredEdgeState | SharedHexAuthoredVertexState | null);
|
|
1398
|
+
void (null as SharedHexEdgeState | SharedHexEdgeFields | null);
|
|
1399
|
+
void (null as SharedSquareEdgeState | SharedSquareEdgeFields | null);
|
|
1400
|
+
void (null as PerPlayerBoardState | PerPlayerSpaceState | PerPlayerSpaceFields | null);
|
|
1401
|
+
`,
|
|
1402
|
+
});
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
test("generated boardHelpers expose authored hex lookups and resolvers", async () => {
|
|
1406
|
+
await withGeneratedContractModule({
|
|
1407
|
+
tempPrefix: ".tmp-authored-hex-runtime-",
|
|
1408
|
+
manifest: TEST_MANIFEST,
|
|
1409
|
+
async run(module) {
|
|
1410
|
+
const authoredEdges = module.boardHelpers.authoredHexEdges("hex-map");
|
|
1411
|
+
const authoredVertices =
|
|
1412
|
+
module.boardHelpers.authoredHexVertices("hex-map");
|
|
1413
|
+
|
|
1414
|
+
expect(authoredEdges).toEqual([
|
|
1415
|
+
{
|
|
1416
|
+
ref: { spaces: ["hex-a", "hex-b"] },
|
|
1417
|
+
typeId: "three-to-one",
|
|
1418
|
+
fields: { rate: 3 },
|
|
1419
|
+
},
|
|
1420
|
+
]);
|
|
1421
|
+
expect(authoredVertices).toEqual([
|
|
1422
|
+
{
|
|
1423
|
+
ref: { spaces: ["hex-a", "hex-b", "hex-c"] },
|
|
1424
|
+
typeId: "settlement-slot",
|
|
1425
|
+
fields: { buildCost: 2 },
|
|
1426
|
+
},
|
|
1427
|
+
]);
|
|
1428
|
+
expect(
|
|
1429
|
+
module.boardHelpers.resolveHexEdgeId("hex-map", authoredEdges[0]!.ref),
|
|
1430
|
+
).toBe(module.boardHelpers.edgeIds("hex-map", "three-to-one")[0]);
|
|
1431
|
+
expect(
|
|
1432
|
+
module.boardHelpers.resolveHexVertexId(
|
|
1433
|
+
"hex-map",
|
|
1434
|
+
authoredVertices[0]!.ref,
|
|
1435
|
+
),
|
|
1436
|
+
).toBe(module.boardHelpers.vertexIds("hex-map", "settlement-slot")[0]);
|
|
1437
|
+
expect(() =>
|
|
1438
|
+
module.boardHelpers.resolveHexEdgeId(
|
|
1439
|
+
"missing-board" as never,
|
|
1440
|
+
authoredEdges[0]!.ref,
|
|
1441
|
+
),
|
|
1442
|
+
).toThrow("Unknown hex board");
|
|
1443
|
+
expect(() =>
|
|
1444
|
+
module.boardHelpers.resolveHexEdgeId("hex-map", {
|
|
1445
|
+
spaces: ["hex-b", "hex-c"],
|
|
1446
|
+
} as never),
|
|
1447
|
+
).toThrow("Unknown authored hex edge ref");
|
|
1448
|
+
},
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
test("generated boardHelpers support authored hex resolvers for per-player boards", async () => {
|
|
1453
|
+
const perPlayerHexManifest: GameTopologyManifest = {
|
|
1454
|
+
...structuredClone(TEST_MANIFEST),
|
|
1455
|
+
boards: [
|
|
1456
|
+
...structuredClone(TEST_MANIFEST.boards),
|
|
1457
|
+
{
|
|
1458
|
+
id: "player-hex-map",
|
|
1459
|
+
name: "Player Hex Map",
|
|
1460
|
+
layout: "hex",
|
|
1461
|
+
scope: "perPlayer",
|
|
1462
|
+
orientation: "pointy-top",
|
|
1463
|
+
edgeFieldsSchema: {
|
|
1464
|
+
properties: {
|
|
1465
|
+
rate: {
|
|
1466
|
+
type: "integer",
|
|
1467
|
+
description: "Edge trade rate",
|
|
1468
|
+
},
|
|
1469
|
+
},
|
|
1470
|
+
},
|
|
1471
|
+
vertexFieldsSchema: {
|
|
1472
|
+
properties: {
|
|
1473
|
+
buildCost: {
|
|
1474
|
+
type: "integer",
|
|
1475
|
+
description: "Vertex build cost",
|
|
1476
|
+
},
|
|
1477
|
+
},
|
|
1478
|
+
},
|
|
1479
|
+
spaces: [
|
|
1480
|
+
{
|
|
1481
|
+
id: "player-hex-a",
|
|
1482
|
+
q: 0,
|
|
1483
|
+
r: 0,
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
id: "player-hex-b",
|
|
1487
|
+
q: 1,
|
|
1488
|
+
r: 0,
|
|
1489
|
+
},
|
|
1490
|
+
{
|
|
1491
|
+
id: "player-hex-c",
|
|
1492
|
+
q: 0,
|
|
1493
|
+
r: 1,
|
|
1494
|
+
},
|
|
1495
|
+
],
|
|
1496
|
+
edges: [
|
|
1497
|
+
{
|
|
1498
|
+
ref: {
|
|
1499
|
+
spaces: ["player-hex-a", "player-hex-b"],
|
|
1500
|
+
},
|
|
1501
|
+
typeId: "three-to-one",
|
|
1502
|
+
fields: {
|
|
1503
|
+
rate: 2,
|
|
1504
|
+
},
|
|
1505
|
+
},
|
|
1506
|
+
],
|
|
1507
|
+
vertices: [
|
|
1508
|
+
{
|
|
1509
|
+
ref: {
|
|
1510
|
+
spaces: ["player-hex-a", "player-hex-b", "player-hex-c"],
|
|
1511
|
+
},
|
|
1512
|
+
typeId: "settlement-slot",
|
|
1513
|
+
fields: {
|
|
1514
|
+
buildCost: 4,
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
],
|
|
1518
|
+
},
|
|
1519
|
+
],
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
await withGeneratedContractModule({
|
|
1523
|
+
tempPrefix: ".tmp-authored-hex-per-player-runtime-",
|
|
1524
|
+
manifest: perPlayerHexManifest,
|
|
1525
|
+
async run(module) {
|
|
1526
|
+
const playerHexBoardRef = module.boardHelpers.boardRefForPlayer(
|
|
1527
|
+
"player-hex-map",
|
|
1528
|
+
"player-1",
|
|
1529
|
+
);
|
|
1530
|
+
expect(playerHexBoardRef).toEqual({
|
|
1531
|
+
baseId: "player-hex-map",
|
|
1532
|
+
seat: "player-1",
|
|
1533
|
+
});
|
|
1534
|
+
const playerHexBoardId = module.boardHelpers
|
|
1535
|
+
.boardIdsForBase("player-hex-map")
|
|
1536
|
+
.find((id: string) => id.endsWith(":player-1"));
|
|
1537
|
+
if (!playerHexBoardId) {
|
|
1538
|
+
throw new Error("expected runtime board id for player-1");
|
|
1539
|
+
}
|
|
1540
|
+
const authoredEdgeRef =
|
|
1541
|
+
module.boardHelpers.authoredHexEdges(playerHexBoardId)[0]!.ref;
|
|
1542
|
+
const authoredVertexRef =
|
|
1543
|
+
module.boardHelpers.authoredHexVertices(playerHexBoardId)[0]!.ref;
|
|
1544
|
+
|
|
1545
|
+
expect(playerHexBoardId).toBe("player-hex-map:player-1");
|
|
1546
|
+
expect(
|
|
1547
|
+
module.boardHelpers.resolveHexEdgeId(playerHexBoardId, authoredEdgeRef),
|
|
1548
|
+
).toBe(module.boardHelpers.edgeIds(playerHexBoardId, "three-to-one")[0]);
|
|
1549
|
+
expect(
|
|
1550
|
+
module.boardHelpers.resolveHexVertexId(
|
|
1551
|
+
playerHexBoardId,
|
|
1552
|
+
authoredVertexRef,
|
|
1553
|
+
),
|
|
1554
|
+
).toBe(
|
|
1555
|
+
module.boardHelpers.vertexIds(playerHexBoardId, "settlement-slot")[0],
|
|
1556
|
+
);
|
|
1557
|
+
},
|
|
1558
|
+
});
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
test("generated records and idGuards preserve exact manifest ids", async () => {
|
|
1562
|
+
await withGeneratedContractModule({
|
|
1563
|
+
tempPrefix: ".tmp-generated-id-helpers-",
|
|
1564
|
+
manifest: TEST_MANIFEST,
|
|
1565
|
+
async run(module) {
|
|
1566
|
+
const expectedEdgeRecord = Object.fromEntries(
|
|
1567
|
+
module.literals.edgeIds.map((edgeId: string) => [edgeId, null]),
|
|
1568
|
+
);
|
|
1569
|
+
const expectedVertexRecord = Object.fromEntries(
|
|
1570
|
+
module.literals.vertexIds.map((vertexId: string) => [
|
|
1571
|
+
vertexId,
|
|
1572
|
+
vertexId,
|
|
1573
|
+
]),
|
|
1574
|
+
);
|
|
1575
|
+
const expectedSquareEdgeRecord = Object.fromEntries(
|
|
1576
|
+
module.literals.edgeIds
|
|
1577
|
+
.filter((edgeId: string) =>
|
|
1578
|
+
module.boardHelpers.isEdgeId("square-grid", edgeId),
|
|
1579
|
+
)
|
|
1580
|
+
.map((edgeId: string) => [edgeId, false]),
|
|
1581
|
+
);
|
|
1582
|
+
const expectedSquareVertexRecord = Object.fromEntries(
|
|
1583
|
+
module.literals.vertexIds
|
|
1584
|
+
.filter((vertexId: string) =>
|
|
1585
|
+
module.boardHelpers.isVertexId("square-grid", vertexId),
|
|
1586
|
+
)
|
|
1587
|
+
.map((vertexId: string) => [vertexId, null]),
|
|
1588
|
+
);
|
|
1589
|
+
const firstHexEdgeId = module.boardHelpers.resolveHexEdgeId("hex-map", {
|
|
1590
|
+
spaces: ["hex-a", "hex-b"],
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
expect(module.records.edgeIds(null)).toEqual(expectedEdgeRecord);
|
|
1594
|
+
expect(module.records.vertexIds((vertexId: string) => vertexId)).toEqual(
|
|
1595
|
+
expectedVertexRecord,
|
|
1596
|
+
);
|
|
1597
|
+
expect(module.cardTypes.cardA).toBe("CARD_A");
|
|
1598
|
+
expect(module.zones.drawDeck).toBe("draw-deck");
|
|
1599
|
+
expect(module.zones.mainHand).toBe("main-hand");
|
|
1600
|
+
expect(module.boardHelpers.spaceRecord("square-grid", 0)).toEqual({
|
|
1601
|
+
"cell-a1": 0,
|
|
1602
|
+
"cell-a2": 0,
|
|
1603
|
+
"cell-b1": 0,
|
|
1604
|
+
"cell-b2": 0,
|
|
1605
|
+
});
|
|
1606
|
+
expect(module.boardHelpers.edgeRecord("square-grid", false)).toEqual(
|
|
1607
|
+
expectedSquareEdgeRecord,
|
|
1608
|
+
);
|
|
1609
|
+
expect(module.boardHelpers.vertexRecord("square-grid", null)).toEqual(
|
|
1610
|
+
expectedSquareVertexRecord,
|
|
1611
|
+
);
|
|
1612
|
+
expect(module.idGuards.isEdgeId(firstHexEdgeId)).toBe(true);
|
|
1613
|
+
expect(module.idGuards.isEdgeId("missing-edge")).toBe(false);
|
|
1614
|
+
expect(module.idGuards.expectEdgeId(firstHexEdgeId)).toBe(firstHexEdgeId);
|
|
1615
|
+
expect(() => module.idGuards.expectEdgeId("missing-edge")).toThrow(
|
|
1616
|
+
"Unknown edge id 'missing-edge'.",
|
|
1617
|
+
);
|
|
1618
|
+
expect(module.boardHelpers.isSpaceId("square-grid", "cell-a1")).toBe(
|
|
1619
|
+
true,
|
|
1620
|
+
);
|
|
1621
|
+
expect(module.boardHelpers.isSpaceId("square-grid", "missing")).toBe(
|
|
1622
|
+
false,
|
|
1623
|
+
);
|
|
1624
|
+
expect(module.boardHelpers.expectSpaceId("square-grid", "cell-a1")).toBe(
|
|
1625
|
+
"cell-a1",
|
|
1626
|
+
);
|
|
1627
|
+
expect(() =>
|
|
1628
|
+
module.boardHelpers.expectSpaceId("square-grid", "missing"),
|
|
1629
|
+
).toThrow("Unknown space id 'missing' on board 'square-grid'.");
|
|
1630
|
+
},
|
|
1631
|
+
});
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
test("generateManifestContractSource typechecks preset standard deck contracts", async () => {
|
|
1635
|
+
await expectGeneratedContractTypechecks({
|
|
1636
|
+
tempPrefix: ".tmp-preset-contract-",
|
|
1637
|
+
manifest: {
|
|
1638
|
+
players: {
|
|
1639
|
+
minPlayers: 2,
|
|
1640
|
+
maxPlayers: 4,
|
|
1641
|
+
optimalPlayers: 4,
|
|
1642
|
+
},
|
|
1643
|
+
cardSets: [
|
|
1644
|
+
{
|
|
1645
|
+
type: "preset",
|
|
1646
|
+
id: "playing-cards",
|
|
1647
|
+
presetId: "standard_52_deck",
|
|
1648
|
+
name: "Standard 52-Card Deck",
|
|
1649
|
+
},
|
|
1650
|
+
],
|
|
1651
|
+
zones: [],
|
|
1652
|
+
boardTemplates: [],
|
|
1653
|
+
boards: [],
|
|
1654
|
+
pieceTypes: [],
|
|
1655
|
+
pieceSeeds: [],
|
|
1656
|
+
dieTypes: [],
|
|
1657
|
+
dieSeeds: [],
|
|
1658
|
+
resources: [],
|
|
1659
|
+
setupOptions: [],
|
|
1660
|
+
setupProfiles: [],
|
|
1661
|
+
} satisfies GameTopologyManifest,
|
|
1662
|
+
usageSource: `export {};`,
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
test("generateManifestContractSource typechecks typed zone card helpers against card state names", async () => {
|
|
1667
|
+
await expectGeneratedContractTypechecks({
|
|
1668
|
+
tempPrefix: ".tmp-zone-card-helper-contract-",
|
|
1669
|
+
manifest: TEST_MANIFEST,
|
|
1670
|
+
usageSource: `import { asPlayerId, createTableQueries } from "@dreamboard/app-sdk/reducer";
|
|
1671
|
+
import { createInitialTable } from "./manifest-contract";
|
|
1672
|
+
|
|
1673
|
+
const table = createInitialTable({
|
|
1674
|
+
playerIds: ["player-1", "player-2"],
|
|
1675
|
+
});
|
|
1676
|
+
const q = createTableQueries(table);
|
|
1677
|
+
|
|
1678
|
+
const sharedCardId = q.zone.sharedCards("draw-deck")[0];
|
|
1679
|
+
const sharedCardName: string | undefined = sharedCardId
|
|
1680
|
+
? table.cards[sharedCardId].name
|
|
1681
|
+
: undefined;
|
|
1682
|
+
const sharedCardText: string | undefined = sharedCardId
|
|
1683
|
+
? table.cards[sharedCardId].text
|
|
1684
|
+
: undefined;
|
|
1685
|
+
|
|
1686
|
+
const handCardId = q.zone.playerCards(asPlayerId("player-1"), "main-hand")[0];
|
|
1687
|
+
const handCardName: string | undefined = handCardId
|
|
1688
|
+
? table.cards[handCardId].name
|
|
1689
|
+
: undefined;
|
|
1690
|
+
const handCardText: string | undefined = handCardId
|
|
1691
|
+
? table.cards[handCardId].text
|
|
1692
|
+
: undefined;
|
|
1693
|
+
|
|
1694
|
+
void sharedCardName;
|
|
1695
|
+
void sharedCardText;
|
|
1696
|
+
void handCardName;
|
|
1697
|
+
void handCardText;
|
|
1698
|
+
`,
|
|
1699
|
+
});
|
|
1700
|
+
}, 20_000);
|
|
1701
|
+
|
|
1702
|
+
test("generateManifestContractSource typechecks manifest-bound setup bootstrap helpers", async () => {
|
|
1703
|
+
await expectGeneratedContractTypechecks({
|
|
1704
|
+
tempPrefix: ".tmp-setup-bootstrap-contract-",
|
|
1705
|
+
manifest: TEST_MANIFEST,
|
|
1706
|
+
usageSource: `import {
|
|
1707
|
+
dealToPlayerBoardContainer,
|
|
1708
|
+
dealToPlayerZone,
|
|
1709
|
+
seedSharedBoardContainer,
|
|
1710
|
+
seedSharedBoardSpace,
|
|
1711
|
+
setupProfiles,
|
|
1712
|
+
shuffle,
|
|
1713
|
+
} from "./manifest-contract";
|
|
1714
|
+
|
|
1715
|
+
const profiles = setupProfiles({
|
|
1716
|
+
draft: {
|
|
1717
|
+
bootstrap: [
|
|
1718
|
+
shuffle({
|
|
1719
|
+
type: "sharedZone",
|
|
1720
|
+
zoneId: "draw-deck",
|
|
1721
|
+
}),
|
|
1722
|
+
seedSharedBoardContainer({
|
|
1723
|
+
from: {
|
|
1724
|
+
type: "sharedZone",
|
|
1725
|
+
zoneId: "draw-deck",
|
|
1726
|
+
},
|
|
1727
|
+
boardId: "track-board",
|
|
1728
|
+
containerId: "market-row",
|
|
1729
|
+
count: 1,
|
|
1730
|
+
}),
|
|
1731
|
+
dealToPlayerZone({
|
|
1732
|
+
from: {
|
|
1733
|
+
type: "sharedZone",
|
|
1734
|
+
zoneId: "draw-deck",
|
|
1735
|
+
},
|
|
1736
|
+
zoneId: "main-hand",
|
|
1737
|
+
count: 1,
|
|
1738
|
+
}),
|
|
1739
|
+
dealToPlayerBoardContainer({
|
|
1740
|
+
from: {
|
|
1741
|
+
type: "sharedZone",
|
|
1742
|
+
zoneId: "draw-deck",
|
|
1743
|
+
},
|
|
1744
|
+
boardId: "track-board",
|
|
1745
|
+
containerId: "market-row",
|
|
1746
|
+
count: 1,
|
|
1747
|
+
}),
|
|
1748
|
+
seedSharedBoardSpace({
|
|
1749
|
+
from: {
|
|
1750
|
+
type: "sharedZone",
|
|
1751
|
+
zoneId: "draw-deck",
|
|
1752
|
+
},
|
|
1753
|
+
boardId: "track-board",
|
|
1754
|
+
spaceId: "space-a",
|
|
1755
|
+
count: 1,
|
|
1756
|
+
}),
|
|
1757
|
+
],
|
|
1758
|
+
},
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
void profiles;
|
|
1762
|
+
`,
|
|
1763
|
+
});
|
|
1764
|
+
});
|