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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +186 -45
  2. package/dist/board-target-B0QG6OYx.d.ts +8 -0
  3. package/dist/chunk-3JXUL4VN.js +139 -0
  4. package/dist/chunk-3JXUL4VN.js.map +1 -0
  5. package/dist/{chunk-S2JEDVWH.js → chunk-C77SYIVT.js} +6102 -6505
  6. package/dist/chunk-C77SYIVT.js.map +1 -0
  7. package/dist/chunk-DMFVAZI2.js +16 -0
  8. package/dist/chunk-DMFVAZI2.js.map +1 -0
  9. package/dist/chunk-OOFMBCFF.js +1338 -0
  10. package/dist/chunk-OOFMBCFF.js.map +1 -0
  11. package/dist/chunk-UG2Q4NLG.js +590 -0
  12. package/dist/chunk-UG2Q4NLG.js.map +1 -0
  13. package/dist/chunk-XU6WJW6K.js +847 -0
  14. package/dist/chunk-XU6WJW6K.js.map +1 -0
  15. package/dist/diagnostics-B8d5aLad.d.ts +65 -0
  16. package/dist/extract-gVKcB8Og.d.ts +4736 -0
  17. package/dist/index.d.ts +3782 -1
  18. package/dist/index.js +1096 -6
  19. package/dist/index.js.map +1 -1
  20. package/dist/model-vdpLHrxh.d.ts +432 -0
  21. package/dist/react.d.ts +60 -0
  22. package/dist/react.js +208 -0
  23. package/dist/react.js.map +1 -0
  24. package/dist/reducer.d.ts +1244 -78
  25. package/dist/reducer.js +4204 -121
  26. package/dist/reducer.js.map +1 -1
  27. package/dist/testing.d.ts +520 -564
  28. package/dist/testing.js +2451 -915
  29. package/dist/testing.js.map +1 -1
  30. package/dist/types-Bj5ZLTUV.d.ts +114 -0
  31. package/package.json +40 -142
  32. package/dist/HandView-Bi2lxzQP.d.ts +0 -79
  33. package/dist/ResourceCounter-BFxJknqp.d.ts +0 -98
  34. package/dist/ThemeProvider-CElaL2ik.d.ts +0 -99
  35. package/dist/attributes-DbvyMbXw.d.ts +0 -68
  36. package/dist/browser-interaction.d.ts +0 -910
  37. package/dist/browser-interaction.js +0 -119
  38. package/dist/browser-interaction.js.map +0 -1
  39. package/dist/chunk-32PFKDV7.js +0 -11705
  40. package/dist/chunk-32PFKDV7.js.map +0 -1
  41. package/dist/chunk-3BAD4VC2.js +0 -263
  42. package/dist/chunk-3BAD4VC2.js.map +0 -1
  43. package/dist/chunk-3SKDNDPG.js +0 -104
  44. package/dist/chunk-3SKDNDPG.js.map +0 -1
  45. package/dist/chunk-5YV2WHJ4.js +0 -25
  46. package/dist/chunk-5YV2WHJ4.js.map +0 -1
  47. package/dist/chunk-ANRB4XKA.js +0 -90
  48. package/dist/chunk-ANRB4XKA.js.map +0 -1
  49. package/dist/chunk-BI4G3PM2.js +0 -1864
  50. package/dist/chunk-BI4G3PM2.js.map +0 -1
  51. package/dist/chunk-EVCLTY4Q.js +0 -935
  52. package/dist/chunk-EVCLTY4Q.js.map +0 -1
  53. package/dist/chunk-GDPKTBUR.js +0 -564
  54. package/dist/chunk-GDPKTBUR.js.map +0 -1
  55. package/dist/chunk-H6VDGFL5.js +0 -2903
  56. package/dist/chunk-H6VDGFL5.js.map +0 -1
  57. package/dist/chunk-LR3ZTWQF.js +0 -5960
  58. package/dist/chunk-LR3ZTWQF.js.map +0 -1
  59. package/dist/chunk-PF7L4BMG.js +0 -117
  60. package/dist/chunk-PF7L4BMG.js.map +0 -1
  61. package/dist/chunk-PN5O6GG2.js +0 -32
  62. package/dist/chunk-PN5O6GG2.js.map +0 -1
  63. package/dist/chunk-PZ5AY32C.js +0 -10
  64. package/dist/chunk-PZ5AY32C.js.map +0 -1
  65. package/dist/chunk-S2JEDVWH.js.map +0 -1
  66. package/dist/chunk-T3ZKNUZ7.js +0 -1
  67. package/dist/chunk-T3ZKNUZ7.js.map +0 -1
  68. package/dist/chunk-UGH54WZ2.js +0 -223
  69. package/dist/chunk-UGH54WZ2.js.map +0 -1
  70. package/dist/chunk-VCDWP5VL.js +0 -746
  71. package/dist/chunk-VCDWP5VL.js.map +0 -1
  72. package/dist/chunk-VDXOF4FW.js +0 -69
  73. package/dist/chunk-VDXOF4FW.js.map +0 -1
  74. package/dist/chunk-WHR5UW3F.js +0 -1988
  75. package/dist/chunk-WHR5UW3F.js.map +0 -1
  76. package/dist/chunk-Y75CFE77.js +0 -17
  77. package/dist/chunk-Y75CFE77.js.map +0 -1
  78. package/dist/chunk-Z7QCREWI.js +0 -3435
  79. package/dist/chunk-Z7QCREWI.js.map +0 -1
  80. package/dist/chunk-ZZGN2ATS.js +0 -1941
  81. package/dist/chunk-ZZGN2ATS.js.map +0 -1
  82. package/dist/components-gAbIz7hK.d.ts +0 -1284
  83. package/dist/definitions-BgmV_GhC.d.ts +0 -300
  84. package/dist/diagnostics-DIi3ina7.d.ts +0 -50
  85. package/dist/digest.d.ts +0 -8
  86. package/dist/game-CsZScpcn.d.ts +0 -763
  87. package/dist/hex-board-view-aKkblDp6.d.ts +0 -1228
  88. package/dist/index-CcT9Q7N7.d.ts +0 -193
  89. package/dist/index.d-DjzoK7zn.d.ts +0 -2101
  90. package/dist/package-set.d.ts +0 -13
  91. package/dist/package-set.js +0 -12
  92. package/dist/package-set.js.map +0 -1
  93. package/dist/player-state-Cqpyeql0.d.ts +0 -371
  94. package/dist/plugin-runtime-contract.d.ts +0 -17
  95. package/dist/plugin-runtime-contract.js +0 -92
  96. package/dist/plugin-runtime-contract.js.map +0 -1
  97. package/dist/primitive-props-BNHDkgd7.d.ts +0 -16
  98. package/dist/protocol-dYgafTYY.d.ts +0 -314
  99. package/dist/reducer/advanced.d.ts +0 -82
  100. package/dist/reducer/advanced.js +0 -51
  101. package/dist/reducer/advanced.js.map +0 -1
  102. package/dist/reducer-contract.d.ts +0 -11
  103. package/dist/reducer-contract.js +0 -16
  104. package/dist/reducer-contract.js.map +0 -1
  105. package/dist/reference-games/index.d.ts +0 -31
  106. package/dist/reference-games/index.js +0 -48
  107. package/dist/reference-games/index.js.map +0 -1
  108. package/dist/runtime/primitives.d.ts +0 -250
  109. package/dist/runtime/primitives.js +0 -189
  110. package/dist/runtime/primitives.js.map +0 -1
  111. package/dist/runtime/runtime-api.d.ts +0 -2
  112. package/dist/runtime/runtime-api.js +0 -1
  113. package/dist/runtime/runtime-api.js.map +0 -1
  114. package/dist/runtime/workspace-contract.d.ts +0 -392
  115. package/dist/runtime/workspace-contract.js +0 -28
  116. package/dist/runtime/workspace-contract.js.map +0 -1
  117. package/dist/runtime-RJ5orDJU.d.ts +0 -1699
  118. package/dist/runtime-api-Bz5pwNU_.d.ts +0 -296
  119. package/dist/runtime-json-CQ9QbLZ5.d.ts +0 -5
  120. package/dist/runtime.d.ts +0 -83
  121. package/dist/runtime.js +0 -242
  122. package/dist/runtime.js.map +0 -1
  123. package/dist/schema.d.ts +0 -379
  124. package/dist/stale-contract-artifact-error-XLaweZtF.d.ts +0 -18
  125. package/dist/testing-compiler.d.ts +0 -37
  126. package/dist/testing-compiler.js +0 -272
  127. package/dist/testing-compiler.js.map +0 -1
  128. package/dist/testing-runtime.d.ts +0 -119
  129. package/dist/testing-runtime.js +0 -212
  130. package/dist/testing-runtime.js.map +0 -1
  131. package/dist/types-DJj5MJkl.d.ts +0 -256
  132. package/dist/types-DR7DoB1x.d.ts +0 -28
  133. package/dist/types-JWCYHmu7.d.ts +0 -122
  134. package/dist/types.d.ts +0 -1958
  135. package/dist/types.js +0 -14
  136. package/dist/types.js.map +0 -1
  137. package/dist/ui/components.d.ts +0 -17
  138. package/dist/ui/components.js +0 -216
  139. package/dist/ui/components.js.map +0 -1
  140. package/dist/ui/defaults.d.ts +0 -19
  141. package/dist/ui/defaults.js +0 -104
  142. package/dist/ui/defaults.js.map +0 -1
  143. package/dist/ui/player-state.d.ts +0 -2
  144. package/dist/ui/player-state.js +0 -1
  145. package/dist/ui/player-state.js.map +0 -1
  146. package/dist/ui/plugin-styles.css +0 -2
  147. package/dist/ui-contract-GTUPkf8z.d.ts +0 -1168
  148. package/dist/ui.d.ts +0 -317
  149. package/dist/ui.js +0 -277
  150. package/dist/ui.js.map +0 -1
  151. package/dist/views-BpuWcOyN.d.ts +0 -1641
@@ -1,1864 +0,0 @@
1
- import {
2
- createReducerTestingBundle
3
- } from "./chunk-S2JEDVWH.js";
4
- import {
5
- createClientParamSchemasByPhase
6
- } from "./chunk-PN5O6GG2.js";
7
- import {
8
- digestPluginRuntimeJson,
9
- encodeCanonicalPluginRuntimeJson
10
- } from "./chunk-VCDWP5VL.js";
11
- import {
12
- manifestSchemaFamily
13
- } from "./chunk-H6VDGFL5.js";
14
-
15
- // src/testing/scenario-schema.ts
16
- import { z } from "zod";
17
- var ScenarioSchemaValueError = class extends Error {
18
- path;
19
- constructor(path, message) {
20
- super(message);
21
- this.name = "ScenarioSchemaValueError";
22
- this.path = path;
23
- }
24
- };
25
- function appendScenarioPath(path, key) {
26
- if (typeof key === "number") {
27
- return `${path}[${key}]`;
28
- }
29
- if (typeof key === "symbol") {
30
- return `${path}[${String(key)}]`;
31
- }
32
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
33
- }
34
- function isPlainObject(value) {
35
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
36
- return false;
37
- }
38
- const prototype = Object.getPrototypeOf(value);
39
- return prototype === Object.prototype || prototype === null;
40
- }
41
- function resolvePlayerSeatReference(options) {
42
- if (!isPlainObject(options.value)) {
43
- throw new ScenarioSchemaValueError(
44
- options.path,
45
- "expected a seat reference shaped as { seat: number }"
46
- );
47
- }
48
- const keys = Object.keys(options.value);
49
- if (keys.length !== 1 || keys[0] !== "seat") {
50
- throw new ScenarioSchemaValueError(
51
- options.path,
52
- "expected a seat reference with only the 'seat' field"
53
- );
54
- }
55
- const seat = options.value.seat;
56
- const seatPath = appendScenarioPath(options.path, "seat");
57
- if (typeof seat !== "number" || !Number.isSafeInteger(seat)) {
58
- throw new ScenarioSchemaValueError(
59
- seatPath,
60
- "expected a finite safe integer"
61
- );
62
- }
63
- if (seat < 0 || seat >= options.playerIds.length) {
64
- throw new ScenarioSchemaValueError(
65
- seatPath,
66
- `expected a seat from 0 through ${Math.max(0, options.playerIds.length - 1)}`
67
- );
68
- }
69
- const playerId = options.playerIds[seat];
70
- if (playerId === void 0) {
71
- throw new ScenarioSchemaValueError(
72
- seatPath,
73
- `could not resolve seat ${seat}`
74
- );
75
- }
76
- return playerId;
77
- }
78
- function projectPlayerSeatReference(options) {
79
- if (typeof options.value !== "string") {
80
- throw new ScenarioSchemaValueError(
81
- options.path,
82
- "expected a runtime player id string"
83
- );
84
- }
85
- const seat = options.playerIds.indexOf(options.value);
86
- if (seat < 0) {
87
- throw new ScenarioSchemaValueError(
88
- options.path,
89
- `runtime player id '${options.value}' is not assigned to a scenario seat`
90
- );
91
- }
92
- return { seat };
93
- }
94
- function tupleDefinition(schema) {
95
- return schema._zod.def;
96
- }
97
- function intersectionDefinition(schema) {
98
- return schema._zod.def;
99
- }
100
- function asClassicSchema(schema) {
101
- return schema;
102
- }
103
- function resolveScenarioSeatReferences(options) {
104
- return transformScenarioSchemaValue({ ...options, direction: "resolve" });
105
- }
106
- function projectScenarioSeatReferences(options) {
107
- return transformScenarioSchemaValue({ ...options, direction: "project" });
108
- }
109
- function transformScenarioSchemaValue(options) {
110
- const { schema, value, playerIds, path } = options;
111
- if (manifestSchemaFamily(schema) === "playerId") {
112
- return options.direction === "resolve" ? resolvePlayerSeatReference({ value, playerIds, path }) : projectPlayerSeatReference({ value, playerIds, path });
113
- }
114
- if (schema instanceof z.ZodOptional || schema instanceof z.ZodExactOptional) {
115
- return value === void 0 ? value : transformScenarioSchemaValue({
116
- schema: asClassicSchema(schema.unwrap()),
117
- value,
118
- playerIds,
119
- path,
120
- direction: options.direction
121
- });
122
- }
123
- if (schema instanceof z.ZodNullable) {
124
- return value === null ? value : transformScenarioSchemaValue({
125
- schema: asClassicSchema(schema.unwrap()),
126
- value,
127
- playerIds,
128
- path,
129
- direction: options.direction
130
- });
131
- }
132
- if (schema instanceof z.ZodDefault || schema instanceof z.ZodPrefault || schema instanceof z.ZodCatch || schema instanceof z.ZodReadonly || schema instanceof z.ZodNonOptional) {
133
- return transformScenarioSchemaValue({
134
- schema: asClassicSchema(schema.unwrap()),
135
- value,
136
- playerIds,
137
- path,
138
- direction: options.direction
139
- });
140
- }
141
- if (schema instanceof z.ZodArray) {
142
- if (!Array.isArray(value)) return value;
143
- return value.map(
144
- (item, index) => transformScenarioSchemaValue({
145
- schema: asClassicSchema(schema.element),
146
- value: item,
147
- playerIds,
148
- path: appendScenarioPath(path, index),
149
- direction: options.direction
150
- })
151
- );
152
- }
153
- if (schema instanceof z.ZodTuple) {
154
- if (!Array.isArray(value)) return value;
155
- const definition = tupleDefinition(schema);
156
- const items = definition.items ?? [];
157
- return value.map((item, index) => {
158
- const itemSchema = items[index] ?? definition.rest;
159
- return itemSchema ? transformScenarioSchemaValue({
160
- schema: asClassicSchema(itemSchema),
161
- value: item,
162
- playerIds,
163
- path: appendScenarioPath(path, index),
164
- direction: options.direction
165
- }) : item;
166
- });
167
- }
168
- if (schema instanceof z.ZodObject) {
169
- if (!isPlainObject(value)) return value;
170
- const output = { ...value };
171
- for (const [key, childSchema] of Object.entries(schema.shape)) {
172
- if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
173
- output[key] = transformScenarioSchemaValue({
174
- schema: asClassicSchema(childSchema),
175
- value: value[key],
176
- playerIds,
177
- path: appendScenarioPath(path, key),
178
- direction: options.direction
179
- });
180
- }
181
- return output;
182
- }
183
- if (schema instanceof z.ZodRecord) {
184
- if (!isPlainObject(value)) return value;
185
- if (manifestSchemaFamily(schema.keyType) === "playerId") {
186
- throw new ScenarioSchemaValueError(
187
- path,
188
- "player-valued record keys cannot be represented by scenario seat references"
189
- );
190
- }
191
- return Object.fromEntries(
192
- Object.entries(value).map(([key, item]) => [
193
- key,
194
- transformScenarioSchemaValue({
195
- schema: asClassicSchema(schema.valueType),
196
- value: item,
197
- playerIds,
198
- path: appendScenarioPath(path, key),
199
- direction: options.direction
200
- })
201
- ])
202
- );
203
- }
204
- if (schema instanceof z.ZodUnion) {
205
- let firstSeatError;
206
- for (const option of schema.options) {
207
- try {
208
- if (options.direction === "project" && !asClassicSchema(option).safeParse(value).success) {
209
- continue;
210
- }
211
- const resolved = transformScenarioSchemaValue({
212
- schema: asClassicSchema(option),
213
- value,
214
- playerIds,
215
- path,
216
- direction: options.direction
217
- });
218
- if (options.direction === "project" || asClassicSchema(option).safeParse(resolved).success) {
219
- return resolved;
220
- }
221
- } catch (error) {
222
- if (firstSeatError === void 0 && error instanceof ScenarioSchemaValueError) {
223
- firstSeatError = error;
224
- }
225
- }
226
- }
227
- if (firstSeatError) throw firstSeatError;
228
- return value;
229
- }
230
- if (schema instanceof z.ZodIntersection) {
231
- const definition = intersectionDefinition(schema);
232
- const leftResolved = transformScenarioSchemaValue({
233
- schema: asClassicSchema(definition.left),
234
- value,
235
- playerIds,
236
- path,
237
- direction: options.direction
238
- });
239
- return transformScenarioSchemaValue({
240
- schema: asClassicSchema(definition.right),
241
- value: leftResolved,
242
- playerIds,
243
- path,
244
- direction: options.direction
245
- });
246
- }
247
- if (schema instanceof z.ZodPipe) {
248
- return transformScenarioSchemaValue({
249
- schema: asClassicSchema(schema.in),
250
- value,
251
- playerIds,
252
- path,
253
- direction: options.direction
254
- });
255
- }
256
- if (schema instanceof z.ZodLazy) {
257
- return transformScenarioSchemaValue({
258
- schema: asClassicSchema(schema.unwrap()),
259
- value,
260
- playerIds,
261
- path,
262
- direction: options.direction
263
- });
264
- }
265
- return value;
266
- }
267
-
268
- // src/testing/scenario-definition-validation.ts
269
- var ScenarioDefinitionValidationError = class extends Error {
270
- code;
271
- path;
272
- constructor(options) {
273
- super(
274
- `Invalid scenario definition at '${options.path}': ${options.reason}.`
275
- );
276
- this.name = "ScenarioDefinitionValidationError";
277
- this.code = options.code;
278
- this.path = options.path;
279
- }
280
- };
281
- var checkpointIdPattern = /^[a-z0-9][a-z0-9-]*$/u;
282
- function validateCheckpoints(definition) {
283
- if (!Object.prototype.hasOwnProperty.call(definition, "checkpoints")) return;
284
- const checkpoints = requireObject(
285
- definition.checkpoints,
286
- "scenario.checkpoints"
287
- );
288
- assertJsonSerializable(checkpoints, "scenario.checkpoints");
289
- const occupied = /* @__PURE__ */ new Map();
290
- for (const [id, candidate] of Object.entries(checkpoints)) {
291
- const path = appendScenarioPath("scenario.checkpoints", id);
292
- if (!checkpointIdPattern.test(id) || id === "setup") {
293
- fail({
294
- code: "INVALID_VALUE",
295
- path,
296
- reason: "checkpoint ids must be lowercase kebab-case and cannot be 'setup'"
297
- });
298
- }
299
- const checkpoint = requireObject(candidate, path);
300
- assertExactKeys(checkpoint, ["segment", "completed"], path);
301
- assertOwn(checkpoint, "segment", path);
302
- assertOwn(checkpoint, "completed", path);
303
- if (!["setup", "given", "when"].includes(String(checkpoint.segment))) {
304
- fail({
305
- code: "INVALID_VALUE",
306
- path: appendScenarioPath(path, "segment"),
307
- reason: "expected setup, given, or when"
308
- });
309
- }
310
- const completed = assertSafeInteger(
311
- checkpoint.completed,
312
- appendScenarioPath(path, "completed")
313
- );
314
- const segment = checkpoint.segment;
315
- const maximum = segment === "setup" ? 0 : definition[segment].length;
316
- if (completed < 0 || completed > maximum || segment === "setup" && completed !== 0) {
317
- fail({
318
- code: "OUT_OF_RANGE",
319
- path: appendScenarioPath(path, "completed"),
320
- reason: `expected 0 through ${maximum} for ${segment}`
321
- });
322
- }
323
- const coordinate = `${segment}:${completed}`;
324
- const previous = occupied.get(coordinate);
325
- if (previous !== void 0) {
326
- fail({
327
- code: "INVALID_VALUE",
328
- path,
329
- reason: `duplicates checkpoint '${previous}' at ${coordinate}`
330
- });
331
- }
332
- occupied.set(coordinate, id);
333
- }
334
- }
335
- function fail(options) {
336
- throw new ScenarioDefinitionValidationError(options);
337
- }
338
- function isPlainObject2(value) {
339
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
340
- return false;
341
- }
342
- const prototype = Object.getPrototypeOf(value);
343
- return prototype === Object.prototype || prototype === null;
344
- }
345
- function requireObject(value, path) {
346
- if (!isPlainObject2(value)) {
347
- fail({ code: "INVALID_TYPE", path, reason: "expected a plain object" });
348
- }
349
- return value;
350
- }
351
- function assertExactKeys(value, allowed, path) {
352
- const allowedSet = new Set(allowed);
353
- for (const key of Object.keys(value)) {
354
- if (!allowedSet.has(key)) {
355
- fail({
356
- code: "UNKNOWN_FIELD",
357
- path: appendScenarioPath(path, key),
358
- reason: "field is not part of the scenario contract"
359
- });
360
- }
361
- }
362
- }
363
- function assertOwn(value, key, path) {
364
- if (!Object.prototype.hasOwnProperty.call(value, key)) {
365
- fail({
366
- code: "INVALID_TYPE",
367
- path: appendScenarioPath(path, key),
368
- reason: "required field is missing"
369
- });
370
- }
371
- }
372
- function assertSafeInteger(value, path) {
373
- if (typeof value !== "number" || !Number.isSafeInteger(value)) {
374
- fail({
375
- code: "UNSAFE_INTEGER",
376
- path,
377
- reason: "expected a finite safe integer"
378
- });
379
- }
380
- return value;
381
- }
382
- function assertJsonSerializable(value, path, ancestors = /* @__PURE__ */ new Set()) {
383
- if (value === null || typeof value === "string" || typeof value === "boolean") {
384
- return;
385
- }
386
- if (typeof value === "number") {
387
- if (!Number.isFinite(value)) {
388
- fail({
389
- code: "NON_SERIALIZABLE",
390
- path,
391
- reason: "non-finite numbers are not JSON-serializable"
392
- });
393
- }
394
- return;
395
- }
396
- if (typeof value !== "object") {
397
- fail({
398
- code: "NON_SERIALIZABLE",
399
- path,
400
- reason: `${typeof value} values are not JSON-serializable`
401
- });
402
- }
403
- if (ancestors.has(value)) {
404
- fail({
405
- code: "NON_SERIALIZABLE",
406
- path,
407
- reason: "cyclic values are not JSON-serializable"
408
- });
409
- }
410
- if (!Array.isArray(value) && !isPlainObject2(value)) {
411
- fail({
412
- code: "NON_SERIALIZABLE",
413
- path,
414
- reason: "class instances are not JSON-serializable scenario data"
415
- });
416
- }
417
- ancestors.add(value);
418
- if (Array.isArray(value)) {
419
- for (let index = 0; index < value.length; index += 1) {
420
- if (!Object.prototype.hasOwnProperty.call(value, index)) {
421
- fail({
422
- code: "NON_SERIALIZABLE",
423
- path: appendScenarioPath(path, index),
424
- reason: "sparse array entries are not allowed"
425
- });
426
- }
427
- assertJsonSerializable(
428
- value[index],
429
- appendScenarioPath(path, index),
430
- ancestors
431
- );
432
- }
433
- const extraKeys = Reflect.ownKeys(value).filter((key) => {
434
- if (key === "length") return false;
435
- return typeof key !== "string" || !/^(0|[1-9][0-9]*)$/.test(key);
436
- });
437
- if (extraKeys.length > 0) {
438
- const key = extraKeys[0];
439
- fail({
440
- code: "NON_SERIALIZABLE",
441
- path: appendScenarioPath(path, key),
442
- reason: "array metadata is not JSON-serializable"
443
- });
444
- }
445
- ancestors.delete(value);
446
- return;
447
- }
448
- const descriptors = Object.getOwnPropertyDescriptors(value);
449
- for (const key of Reflect.ownKeys(value)) {
450
- if (typeof key === "symbol") {
451
- fail({
452
- code: "NON_SERIALIZABLE",
453
- path: appendScenarioPath(path, key),
454
- reason: "symbol keys are not JSON-serializable"
455
- });
456
- }
457
- const descriptor = descriptors[key];
458
- if (descriptor === void 0 || !descriptor.enumerable || !("value" in descriptor)) {
459
- fail({
460
- code: "NON_SERIALIZABLE",
461
- path: appendScenarioPath(path, key),
462
- reason: "only enumerable data properties are allowed"
463
- });
464
- }
465
- assertJsonSerializable(
466
- descriptor.value,
467
- appendScenarioPath(path, key),
468
- ancestors
469
- );
470
- }
471
- ancestors.delete(value);
472
- }
473
- function playerIdsForDefinition(game, players) {
474
- const declared = game.contract.manifest.literals.playerIds;
475
- return Array.from(
476
- { length: players },
477
- (_, index) => declared[index] ?? `player-${index + 1}`
478
- );
479
- }
480
- function commandSchemas(game, interactionId) {
481
- const schemasByPhase = createClientParamSchemasByPhase(
482
- game
483
- );
484
- return Object.values(schemasByPhase).flatMap((schemas) => {
485
- const schema = schemas[interactionId];
486
- return schema ? [schema] : [];
487
- });
488
- }
489
- function issuePath(path, issue) {
490
- return issue.path.reduce(appendScenarioPath, path);
491
- }
492
- function validateCommand(options) {
493
- const command = requireObject(options.command, options.path);
494
- assertJsonSerializable(command, options.path);
495
- assertExactKeys(command, ["actor", "interactionId", "params"], options.path);
496
- for (const field of ["actor", "interactionId", "params"]) {
497
- assertOwn(command, field, options.path);
498
- }
499
- const actorPath = appendScenarioPath(options.path, "actor");
500
- const actor = requireObject(command.actor, actorPath);
501
- assertExactKeys(actor, ["seat"], actorPath);
502
- assertOwn(actor, "seat", actorPath);
503
- const actorSeat = assertSafeInteger(
504
- actor.seat,
505
- appendScenarioPath(actorPath, "seat")
506
- );
507
- if (actorSeat < 0 || actorSeat >= options.playerIds.length) {
508
- fail({
509
- code: "OUT_OF_RANGE",
510
- path: appendScenarioPath(actorPath, "seat"),
511
- reason: `expected a seat from 0 through ${Math.max(0, options.playerIds.length - 1)}`
512
- });
513
- }
514
- const interactionPath = appendScenarioPath(options.path, "interactionId");
515
- if (typeof command.interactionId !== "string" || command.interactionId === "") {
516
- fail({
517
- code: "INVALID_TYPE",
518
- path: interactionPath,
519
- reason: "expected a non-empty string"
520
- });
521
- }
522
- const paramsPath = appendScenarioPath(options.path, "params");
523
- const schemas = commandSchemas(options.game, command.interactionId);
524
- if (schemas.length === 0) {
525
- fail({
526
- code: "UNKNOWN_INTERACTION",
527
- path: interactionPath,
528
- reason: `interaction '${command.interactionId}' is not declared by the game`
529
- });
530
- }
531
- let firstSeatError;
532
- let firstIssue;
533
- for (const schema of schemas) {
534
- try {
535
- const resolved = resolveScenarioSeatReferences({
536
- schema,
537
- value: command.params,
538
- playerIds: options.playerIds,
539
- path: paramsPath
540
- });
541
- const parsed = schema.safeParse(resolved);
542
- if (parsed.success) return;
543
- firstIssue ??= parsed.error.issues[0];
544
- } catch (error) {
545
- if (error instanceof ScenarioSchemaValueError) {
546
- firstSeatError ??= error;
547
- continue;
548
- }
549
- throw error;
550
- }
551
- }
552
- if (firstSeatError) {
553
- fail({
554
- code: firstSeatError.message.includes("seat from") ? "OUT_OF_RANGE" : "INVALID_COMMAND_PARAMS",
555
- path: firstSeatError.path,
556
- reason: firstSeatError.message
557
- });
558
- }
559
- fail({
560
- code: "INVALID_COMMAND_PARAMS",
561
- path: firstIssue ? issuePath(paramsPath, firstIssue) : paramsPath,
562
- reason: firstIssue?.message ?? "parameters do not match the interaction contract"
563
- });
564
- }
565
- function validateScenarioDefinition(game, value) {
566
- const definition = requireObject(value, "scenario");
567
- assertExactKeys(
568
- definition,
569
- ["id", "description", "setup", "given", "when", "checkpoints", "then"],
570
- "scenario"
571
- );
572
- for (const field of ["id", "setup", "given", "when", "then"]) {
573
- assertOwn(definition, field, "scenario");
574
- }
575
- if (typeof definition.id !== "string" || definition.id === "") {
576
- fail({
577
- code: "INVALID_TYPE",
578
- path: "scenario.id",
579
- reason: "expected a non-empty string"
580
- });
581
- }
582
- if (Object.prototype.hasOwnProperty.call(definition, "description") && typeof definition.description !== "string") {
583
- fail({
584
- code: "INVALID_TYPE",
585
- path: "scenario.description",
586
- reason: "expected a string when provided"
587
- });
588
- }
589
- const setup = requireObject(definition.setup, "scenario.setup");
590
- assertExactKeys(
591
- setup,
592
- ["players", "seed", "setupProfileId"],
593
- "scenario.setup"
594
- );
595
- assertOwn(setup, "players", "scenario.setup");
596
- assertOwn(setup, "seed", "scenario.setup");
597
- const players = assertSafeInteger(setup.players, "scenario.setup.players");
598
- const normalSetup = game.contract.manifest.normalSetup;
599
- if (normalSetup === void 0 || !Number.isSafeInteger(normalSetup.minPlayers) || !Number.isSafeInteger(normalSetup.maxPlayers) || normalSetup.minPlayers <= 0 || normalSetup.maxPlayers < normalSetup.minPlayers) {
600
- fail({
601
- code: "NORMAL_SETUP_UNAVAILABLE",
602
- path: "scenario.setup.players",
603
- reason: "the game manifest does not provide valid normal setup bounds"
604
- });
605
- }
606
- if (players < normalSetup.minPlayers || players > normalSetup.maxPlayers) {
607
- fail({
608
- code: "OUT_OF_RANGE",
609
- path: "scenario.setup.players",
610
- reason: `expected ${normalSetup.minPlayers} through ${normalSetup.maxPlayers} players`
611
- });
612
- }
613
- assertSafeInteger(setup.seed, "scenario.setup.seed");
614
- if (Object.prototype.hasOwnProperty.call(setup, "setupProfileId")) {
615
- if (setup.setupProfileId !== null && typeof setup.setupProfileId !== "string") {
616
- fail({
617
- code: "INVALID_TYPE",
618
- path: "scenario.setup.setupProfileId",
619
- reason: "expected a declared setup profile id or null"
620
- });
621
- }
622
- if (typeof setup.setupProfileId === "string" && !Object.prototype.hasOwnProperty.call(
623
- game.contract.manifest.setupProfilesById,
624
- setup.setupProfileId
625
- )) {
626
- fail({
627
- code: "UNKNOWN_SETUP_PROFILE",
628
- path: "scenario.setup.setupProfileId",
629
- reason: `setup profile '${setup.setupProfileId}' is not declared by the manifest`
630
- });
631
- }
632
- }
633
- assertJsonSerializable(definition.id, "scenario.id");
634
- if (Object.prototype.hasOwnProperty.call(definition, "description")) {
635
- assertJsonSerializable(definition.description, "scenario.description");
636
- }
637
- assertJsonSerializable(setup, "scenario.setup");
638
- if (!Array.isArray(definition.given)) {
639
- fail({
640
- code: "INVALID_TYPE",
641
- path: "scenario.given",
642
- reason: "expected an array"
643
- });
644
- }
645
- if (!Array.isArray(definition.when)) {
646
- fail({
647
- code: "INVALID_TYPE",
648
- path: "scenario.when",
649
- reason: "expected an array"
650
- });
651
- }
652
- if (typeof definition.then !== "function") {
653
- fail({
654
- code: "INVALID_TYPE",
655
- path: "scenario.then",
656
- reason: "expected an assertion function"
657
- });
658
- }
659
- validateCheckpoints(definition);
660
- const playerIds = playerIdsForDefinition(game, players);
661
- definition.given.forEach(
662
- (command, index) => validateCommand({
663
- game,
664
- command,
665
- path: `scenario.given[${index}]`,
666
- playerIds
667
- })
668
- );
669
- definition.when.forEach(
670
- (command, index) => validateCommand({
671
- game,
672
- command,
673
- path: `scenario.when[${index}]`,
674
- playerIds
675
- })
676
- );
677
- }
678
-
679
- // src/testing/definitions.ts
680
- function resolveScenarioCheckpoint(definition, selector) {
681
- if (typeof selector !== "string") {
682
- return structuredClone(selector);
683
- }
684
- const checkpoint = definition.checkpoints?.[selector];
685
- if (checkpoint === void 0) {
686
- const available = Object.keys(definition.checkpoints ?? {}).sort();
687
- throw new ScenarioDefinitionValidationError({
688
- code: "INVALID_VALUE",
689
- path: "scenario.checkpoints",
690
- reason: `checkpoint '${selector}' is not declared by scenario '${definition.id}'` + (available.length === 0 ? "; no named checkpoints are available" : `; available checkpoints: ${available.join(", ")}`)
691
- });
692
- }
693
- return structuredClone(checkpoint);
694
- }
695
- var ScenarioReplayError = class extends Error {
696
- scenarioId;
697
- segment;
698
- index;
699
- interactionId;
700
- errorCode;
701
- reducerMessage;
702
- trace;
703
- constructor(options) {
704
- super(
705
- `Scenario '${options.scenarioId}' rejected ${options.segment}[${options.index}] interaction '${options.interactionId}' with ${options.errorCode}` + (options.reducerMessage ? `: ${options.reducerMessage}` : ".")
706
- );
707
- this.name = "ScenarioReplayError";
708
- this.scenarioId = options.scenarioId;
709
- this.segment = options.segment;
710
- this.index = options.index;
711
- this.interactionId = options.interactionId;
712
- this.errorCode = options.errorCode;
713
- this.reducerMessage = options.reducerMessage;
714
- this.trace = options.trace ?? [];
715
- }
716
- };
717
- function createScenarioAuthoring(game) {
718
- return {
719
- defineScenario(definition) {
720
- validateScenarioDefinition(game, definition);
721
- return definition;
722
- }
723
- };
724
- }
725
- function toScenarioReplayDefinition(definition) {
726
- const replay = {
727
- id: definition.id,
728
- ...definition.description === void 0 ? {} : { description: definition.description },
729
- setup: definition.setup,
730
- given: definition.given,
731
- when: definition.when
732
- };
733
- return structuredClone(replay);
734
- }
735
-
736
- // src/testing/scenario-assertion-error.ts
737
- var SCENARIO_ASSERTION_ERROR_CODE = "SCENARIO_ASSERTION_FAILED";
738
- var ScenarioAssertionError = class extends Error {
739
- code = SCENARIO_ASSERTION_ERROR_CODE;
740
- constructor(message) {
741
- super(message);
742
- this.name = "ScenarioAssertionError";
743
- }
744
- };
745
- function scenarioAssertionFailure(message) {
746
- throw new ScenarioAssertionError(message);
747
- }
748
-
749
- // src/testing/create-expect-api.ts
750
- function isRecord(value) {
751
- return typeof value === "object" && value !== null && !Array.isArray(value);
752
- }
753
- function isDeepStrictEqual(actual, expected) {
754
- if (Object.is(actual, expected)) return true;
755
- if (typeof actual !== typeof expected) return false;
756
- if (actual === null || expected === null) return false;
757
- if (typeof actual !== "object" || typeof expected !== "object") return false;
758
- if (actual instanceof Date || expected instanceof Date) {
759
- return actual instanceof Date && expected instanceof Date && Object.is(actual.getTime(), expected.getTime());
760
- }
761
- if (actual instanceof RegExp || expected instanceof RegExp) {
762
- return actual instanceof RegExp && expected instanceof RegExp && actual.source === expected.source && actual.flags === expected.flags;
763
- }
764
- if (Array.isArray(actual) || Array.isArray(expected)) {
765
- return Array.isArray(actual) && Array.isArray(expected) && actual.length === expected.length && actual.every((value, index) => isDeepStrictEqual(value, expected[index]));
766
- }
767
- const actualPrototype = Object.getPrototypeOf(actual);
768
- const expectedPrototype = Object.getPrototypeOf(expected);
769
- if (actualPrototype !== expectedPrototype) return false;
770
- const actualRecord = actual;
771
- const expectedRecord = expected;
772
- const actualKeys = Object.keys(actualRecord);
773
- const expectedKeys = Object.keys(expectedRecord);
774
- return actualKeys.length === expectedKeys.length && actualKeys.every(
775
- (key) => Object.prototype.hasOwnProperty.call(expectedRecord, key) && isDeepStrictEqual(actualRecord[key], expectedRecord[key])
776
- );
777
- }
778
- function matchPartial(actual, expected, path = "value") {
779
- if (!isRecord(expected)) {
780
- return isDeepStrictEqual(actual, expected) ? null : `${path} does not match`;
781
- }
782
- if (!isRecord(actual)) {
783
- return `${path} is not an object`;
784
- }
785
- for (const [key, expectedValue] of Object.entries(expected)) {
786
- const actualValue = actual[key];
787
- if (!(key in actual)) {
788
- return `${path}.${key} is missing`;
789
- }
790
- const mismatch = matchPartial(actualValue, expectedValue, `${path}.${key}`);
791
- if (mismatch) {
792
- return mismatch;
793
- }
794
- }
795
- return null;
796
- }
797
- function asDescriptorList(actual) {
798
- if (!Array.isArray(actual)) {
799
- scenarioAssertionFailure("Expected interaction descriptor array.");
800
- }
801
- return actual;
802
- }
803
- function findInteraction(descriptors, interactionId) {
804
- return descriptors.find(
805
- (descriptor) => descriptor.interactionId === interactionId
806
- ) ?? null;
807
- }
808
- function assertDescriptorMatches(descriptor, opts) {
809
- if (!opts) {
810
- return;
811
- }
812
- const mismatch = matchPartial(descriptor, opts, "interaction");
813
- if (mismatch) {
814
- scenarioAssertionFailure(mismatch);
815
- }
816
- }
817
- function createSubmissionErrorMatcher(actual, expected, options) {
818
- if (isRecord(actual) && (actual.kind === "accepted" || actual.kind === "rejected")) {
819
- if (actual.kind === "accepted") {
820
- scenarioAssertionFailure(
821
- "Expected probe command to reject, but it was accepted."
822
- );
823
- }
824
- const errorCode = typeof actual.errorCode === "string" ? actual.errorCode : void 0;
825
- const message = typeof actual.message === "string" ? actual.message : "";
826
- if (expected.errorCode !== void 0 && errorCode !== expected.errorCode) {
827
- scenarioAssertionFailure(
828
- `Expected rejection errorCode '${expected.errorCode}', received '${errorCode ?? "undefined"}'.`
829
- );
830
- }
831
- if (typeof expected.message === "string" && message !== expected.message) {
832
- scenarioAssertionFailure(
833
- `Expected rejection message '${expected.message}', received '${message}'.`
834
- );
835
- }
836
- if (expected.message instanceof RegExp && !expected.message.test(message)) {
837
- scenarioAssertionFailure(
838
- `Expected rejection message '${message}' to match ${String(expected.message)}.`
839
- );
840
- }
841
- return Promise.resolve();
842
- }
843
- const resolvePromise = () => {
844
- if (typeof actual === "function") {
845
- return Promise.resolve().then(() => actual());
846
- }
847
- return Promise.resolve(actual);
848
- };
849
- return resolvePromise().then(
850
- () => scenarioAssertionFailure("Expected promise to reject."),
851
- (error) => {
852
- if (!(error instanceof Error)) {
853
- scenarioAssertionFailure("Expected rejection to be an Error.");
854
- }
855
- if (expected.errorCode !== void 0 && error.errorCode !== expected.errorCode) {
856
- scenarioAssertionFailure(
857
- `Expected rejection errorCode '${expected.errorCode}', received '${error.errorCode ?? "undefined"}'.${formatLastDiagnosticRejection(options)}`
858
- );
859
- }
860
- if (typeof expected.message === "string" && error.message !== expected.message) {
861
- scenarioAssertionFailure(
862
- `Expected rejection message '${expected.message}', received '${error.message}'.${formatLastDiagnosticRejection(
863
- options
864
- )}`
865
- );
866
- }
867
- if (expected.message instanceof RegExp && !expected.message.test(error.message)) {
868
- scenarioAssertionFailure(
869
- `Expected rejection message '${error.message}' to match ${String(
870
- expected.message
871
- )}.${formatLastDiagnosticRejection(options)}`
872
- );
873
- }
874
- }
875
- );
876
- }
877
- function formatLastDiagnosticRejection(options) {
878
- const rejection = options.lastDiagnosticRejection?.();
879
- if (!rejection) return "";
880
- const rule = rejection.ruleId ? ` ruleId=${rejection.ruleId}` : "";
881
- const message = rejection.message ? ` message=${rejection.message}` : "";
882
- return `
883
- last rejection: errorCode=${rejection.errorCode}${rule}${message}`;
884
- }
885
- function assertLength(actual, expected) {
886
- if (actual === null || actual === void 0 || typeof actual.length !== "number") {
887
- scenarioAssertionFailure(
888
- "toHaveLength expects a value with a numeric length."
889
- );
890
- }
891
- const length = actual.length;
892
- if (length !== expected) {
893
- scenarioAssertionFailure(
894
- `Expected length ${expected}, received ${length}.`
895
- );
896
- }
897
- }
898
- function formatInteractionExplanation(explanation) {
899
- if (!explanation) {
900
- return "";
901
- }
902
- const lines = [
903
- `availability: ${explanation.availability}`,
904
- ...explanation.rules.map((rule) => {
905
- const code = rule.errorCode ? ` (${rule.errorCode})` : "";
906
- const message = rule.message ? `: ${rule.message}` : "";
907
- return `rule ${rule.ruleId} ${rule.outcome}${code}${message}`;
908
- }),
909
- ...explanation.inputs.map(
910
- (input) => `input ${input.key} eligibleCount=${input.eligibleCount}`
911
- )
912
- ];
913
- return `
914
- ${lines.join("\n")}`;
915
- }
916
- function assertThrown(actual, predicate) {
917
- if (typeof actual !== "function") {
918
- scenarioAssertionFailure("toThrow expects a function.");
919
- }
920
- try {
921
- actual();
922
- } catch (error) {
923
- if (!(error instanceof Error)) {
924
- scenarioAssertionFailure("Thrown value is not an Error.");
925
- }
926
- if (predicate === void 0) {
927
- return;
928
- }
929
- if (typeof predicate === "string" && error.message !== predicate) {
930
- scenarioAssertionFailure(
931
- `Expected thrown message '${predicate}', received '${error.message}'.`
932
- );
933
- }
934
- if (predicate instanceof RegExp && !predicate.test(error.message)) {
935
- scenarioAssertionFailure(
936
- `Expected thrown message '${error.message}' to match ${String(predicate)}.`
937
- );
938
- }
939
- if (typeof predicate === "function" && !predicate(error)) {
940
- scenarioAssertionFailure("Thrown error did not satisfy predicate.");
941
- }
942
- return;
943
- }
944
- scenarioAssertionFailure("Expected function to throw.");
945
- }
946
- function createExpectApi(options = {}) {
947
- const buildMatchers = (actual) => ({
948
- toBe: (expected) => {
949
- if (actual !== expected) {
950
- scenarioAssertionFailure(
951
- `Expected '${String(actual)}' to be '${String(expected)}'.`
952
- );
953
- }
954
- },
955
- toEqual: (expected) => {
956
- if (!isDeepStrictEqual(actual, expected)) {
957
- scenarioAssertionFailure("Expected values to be deeply equal.");
958
- }
959
- },
960
- toMatchObject: (expected) => {
961
- const mismatch = matchPartial(actual, expected);
962
- if (mismatch) {
963
- scenarioAssertionFailure(mismatch);
964
- }
965
- },
966
- toBeDefined: () => {
967
- if (actual === void 0) {
968
- scenarioAssertionFailure("Expected value to be defined.");
969
- }
970
- },
971
- toBeUndefined: () => {
972
- if (actual !== void 0) {
973
- scenarioAssertionFailure(
974
- `Expected value to be undefined, but received '${String(actual)}'.`
975
- );
976
- }
977
- },
978
- toBeNull: () => {
979
- if (actual !== null) {
980
- scenarioAssertionFailure(
981
- `Expected value to be null, but received '${String(actual)}'.`
982
- );
983
- }
984
- },
985
- toContain: (expected) => {
986
- if (Array.isArray(actual)) {
987
- if (!actual.includes(expected)) {
988
- scenarioAssertionFailure("Expected array to contain value.");
989
- }
990
- return;
991
- }
992
- if (typeof actual === "string") {
993
- if (!actual.includes(String(expected))) {
994
- scenarioAssertionFailure("Expected string to contain value.");
995
- }
996
- return;
997
- }
998
- scenarioAssertionFailure(
999
- "toContain expects an array or string actual value."
1000
- );
1001
- },
1002
- toContainEqual: (expected) => {
1003
- if (!Array.isArray(actual)) {
1004
- scenarioAssertionFailure(
1005
- "toContainEqual expects an array actual value."
1006
- );
1007
- }
1008
- if (!actual.some((value) => isDeepStrictEqual(value, expected))) {
1009
- scenarioAssertionFailure("Expected array to contain an equal value.");
1010
- }
1011
- },
1012
- toHaveLength: (expected) => {
1013
- assertLength(actual, expected);
1014
- },
1015
- toBeGreaterThan: (expected) => {
1016
- if (typeof actual !== "number") {
1017
- scenarioAssertionFailure(
1018
- "toBeGreaterThan expects a number actual value."
1019
- );
1020
- }
1021
- if (actual <= expected) {
1022
- scenarioAssertionFailure(`Expected ${actual} to be > ${expected}.`);
1023
- }
1024
- },
1025
- toBeGreaterThanOrEqual: (expected) => {
1026
- if (typeof actual !== "number") {
1027
- scenarioAssertionFailure(
1028
- "toBeGreaterThanOrEqual expects a number actual value."
1029
- );
1030
- }
1031
- if (actual < expected) {
1032
- scenarioAssertionFailure(`Expected ${actual} to be >= ${expected}.`);
1033
- }
1034
- },
1035
- toThrow: (predicate) => {
1036
- assertThrown(actual, predicate);
1037
- },
1038
- toMatchSnapshot: (filename) => {
1039
- if (!options.matchSnapshot) {
1040
- scenarioAssertionFailure(
1041
- "Snapshot matching is not configured for this expect API."
1042
- );
1043
- }
1044
- options.matchSnapshot(filename, actual);
1045
- },
1046
- toRejectWith: (expected) => createSubmissionErrorMatcher(actual, expected, options),
1047
- toHaveInteraction: (interactionId, opts) => {
1048
- const descriptors = asDescriptorList(actual);
1049
- const descriptor = findInteraction(descriptors, interactionId);
1050
- if (!descriptor) {
1051
- scenarioAssertionFailure(
1052
- `Expected interaction '${interactionId}' to exist.`
1053
- );
1054
- }
1055
- assertDescriptorMatches(descriptor, opts);
1056
- },
1057
- toBeGatedBy: (reason, opts) => {
1058
- const descriptor = Array.isArray(actual) ? (() => {
1059
- if (!opts?.interactionId) {
1060
- scenarioAssertionFailure(
1061
- "toBeGatedBy on a descriptor array requires opts.interactionId."
1062
- );
1063
- }
1064
- return findInteraction(
1065
- asDescriptorList(actual),
1066
- opts.interactionId
1067
- );
1068
- })() : actual;
1069
- if (!descriptor) {
1070
- scenarioAssertionFailure("Expected interaction descriptor to exist.");
1071
- }
1072
- if (descriptor.availability?.status === "available") {
1073
- scenarioAssertionFailure("Expected interaction to be unavailable.");
1074
- }
1075
- const actualReason = descriptor.availability?.status === "notYourTurn" || descriptor.availability?.status === "insufficientResources" || descriptor.availability?.status === "blocked" ? descriptor.availability.reason : void 0;
1076
- if (actualReason !== reason) {
1077
- scenarioAssertionFailure(
1078
- `Expected unavailable reason '${reason}', received '${actualReason ?? "undefined"}'.`
1079
- );
1080
- }
1081
- },
1082
- toBeAvailable: (explanation) => {
1083
- const descriptor = Array.isArray(actual) ? (() => {
1084
- if (!explanation?.interactionId) {
1085
- scenarioAssertionFailure(
1086
- "toBeAvailable on a descriptor array requires an explanation."
1087
- );
1088
- }
1089
- return findInteraction(
1090
- asDescriptorList(actual),
1091
- explanation.interactionId
1092
- );
1093
- })() : actual;
1094
- if (!descriptor) {
1095
- scenarioAssertionFailure(
1096
- `Expected interaction descriptor to exist.${formatInteractionExplanation(
1097
- explanation
1098
- )}`
1099
- );
1100
- }
1101
- if (descriptor.availability?.status !== "available") {
1102
- scenarioAssertionFailure(
1103
- `Expected interaction '${descriptor.interactionId ?? "unknown"}' to be available.${formatInteractionExplanation(
1104
- explanation
1105
- )}`
1106
- );
1107
- }
1108
- },
1109
- toBeActiveFor: (playerId, opts) => {
1110
- const descriptor = Array.isArray(actual) ? (() => {
1111
- if (!opts?.interactionId) {
1112
- scenarioAssertionFailure(
1113
- "toBeActiveFor on a descriptor array requires opts.interactionId."
1114
- );
1115
- }
1116
- return findInteraction(
1117
- asDescriptorList(actual),
1118
- opts.interactionId
1119
- );
1120
- })() : actual;
1121
- if (!descriptor) {
1122
- scenarioAssertionFailure("Expected interaction descriptor to exist.");
1123
- }
1124
- if (descriptor.context?.to !== playerId) {
1125
- scenarioAssertionFailure(
1126
- `Expected interaction to target '${playerId}', received '${descriptor.context?.to ?? "undefined"}'.`
1127
- );
1128
- }
1129
- if (descriptor.availability?.status !== "available") {
1130
- scenarioAssertionFailure("Expected interaction to be available.");
1131
- }
1132
- },
1133
- not: {
1134
- toHaveInteraction: (interactionId) => {
1135
- const descriptors = asDescriptorList(actual);
1136
- if (findInteraction(descriptors, interactionId)) {
1137
- scenarioAssertionFailure(
1138
- `Expected interaction '${interactionId}' to be absent.`
1139
- );
1140
- }
1141
- }
1142
- }
1143
- });
1144
- return (actual) => buildMatchers(actual);
1145
- }
1146
-
1147
- // src/testing/scenario-player-refs.ts
1148
- var ScenarioCommandParamsError = class extends Error {
1149
- path;
1150
- constructor(path, message) {
1151
- super(`${path}: ${message}`);
1152
- this.name = "ScenarioCommandParamsError";
1153
- this.path = path;
1154
- }
1155
- };
1156
- function isScenarioSeatRef(value) {
1157
- if (!isPlainRecord(value)) return false;
1158
- const keys = Object.keys(value);
1159
- return keys.length === 1 && keys[0] === "seat" && Number.isSafeInteger(value.seat) && value.seat >= 0;
1160
- }
1161
- function resolveScenarioSeatRef(options) {
1162
- if (!isScenarioSeatRef(options.ref)) {
1163
- throw new ScenarioCommandParamsError(
1164
- options.path,
1165
- "expected a scenario seat reference"
1166
- );
1167
- }
1168
- const playerId = options.playerIds[options.ref.seat];
1169
- if (playerId === void 0) {
1170
- throw new ScenarioCommandParamsError(
1171
- `${options.path}.seat`,
1172
- `seat ${options.ref.seat} is out of range for ${options.playerIds.length} player(s)`
1173
- );
1174
- }
1175
- return playerId;
1176
- }
1177
- function resolveScenarioCommandParams(options) {
1178
- const candidateSchemas = clientParamSchemaCandidates(options);
1179
- let firstSeatError;
1180
- let firstIssue;
1181
- for (const schema of candidateSchemas) {
1182
- try {
1183
- const resolved = resolveScenarioSeatReferences({
1184
- schema,
1185
- value: options.params,
1186
- playerIds: options.playerIds,
1187
- path: `${options.path}.params`
1188
- });
1189
- const parsed = schema.safeParse(resolved);
1190
- if (parsed.success) {
1191
- return parsed.data;
1192
- }
1193
- firstIssue ??= parsed.error.issues[0];
1194
- } catch (error) {
1195
- if (error instanceof ScenarioSchemaValueError) {
1196
- firstSeatError ??= error;
1197
- continue;
1198
- }
1199
- throw error;
1200
- }
1201
- }
1202
- if (firstSeatError) {
1203
- throw new ScenarioCommandParamsError(
1204
- firstSeatError.path,
1205
- firstSeatError.message
1206
- );
1207
- }
1208
- const suffix = firstIssue?.path.map(
1209
- (part) => typeof part === "number" ? `[${part}]` : `.${String(part)}`
1210
- ).join("") ?? "";
1211
- throw new ScenarioCommandParamsError(
1212
- `${options.path}.params${suffix}`,
1213
- firstIssue?.message ?? "invalid interaction parameters"
1214
- );
1215
- }
1216
- function projectScenarioCommandParams(options) {
1217
- const candidateSchemas = clientParamSchemaCandidates(options);
1218
- let firstSeatError;
1219
- for (const schema of candidateSchemas) {
1220
- if (!schema.safeParse(options.params).success) continue;
1221
- try {
1222
- const projected = projectScenarioSeatReferences({
1223
- schema,
1224
- value: options.params,
1225
- playerIds: options.playerIds,
1226
- path: `${options.path}.params`
1227
- });
1228
- if (isPlainRecord(projected)) return projected;
1229
- } catch (error) {
1230
- if (error instanceof ScenarioSchemaValueError) {
1231
- firstSeatError ??= error;
1232
- continue;
1233
- }
1234
- throw error;
1235
- }
1236
- }
1237
- if (firstSeatError) {
1238
- throw new ScenarioCommandParamsError(
1239
- firstSeatError.path,
1240
- firstSeatError.message
1241
- );
1242
- }
1243
- throw new ScenarioCommandParamsError(
1244
- `${options.path}.params`,
1245
- "runtime interaction parameters do not match a client parameter schema"
1246
- );
1247
- }
1248
- function clientParamSchemaCandidates(options) {
1249
- const schemas = createClientParamSchemasByPhase(options.game);
1250
- const currentPhaseSchema = schemas[options.phase]?.[options.interactionId];
1251
- const candidateSchemas = [
1252
- ...currentPhaseSchema ? [currentPhaseSchema] : [],
1253
- ...Object.entries(schemas).flatMap(([phase, schemasForPhase]) => {
1254
- if (phase === options.phase) return [];
1255
- const schema = schemasForPhase[options.interactionId];
1256
- return schema ? [schema] : [];
1257
- })
1258
- ];
1259
- if (candidateSchemas.length === 0) {
1260
- throw new ScenarioCommandParamsError(
1261
- `${options.path}.interactionId`,
1262
- `interaction '${options.interactionId}' has no client parameter schema`
1263
- );
1264
- }
1265
- return candidateSchemas;
1266
- }
1267
- function isPlainRecord(value) {
1268
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
1269
- return false;
1270
- }
1271
- const prototype = Object.getPrototypeOf(value);
1272
- return prototype === Object.prototype || prototype === null;
1273
- }
1274
-
1275
- // src/testing/scenario-replay.ts
1276
- async function replayScenario(options) {
1277
- validateReplayDefinition(options.game, options.scenario);
1278
- const scenario = cloneReplayDefinition(options.scenario);
1279
- const checkpoint = normalizeCheckpoint(scenario, options.at);
1280
- const playerIds = resolvePlayerIds(options.game, scenario.setup.players);
1281
- const events = [];
1282
- const bundle = createScenarioBundle(options.game, events);
1283
- const normalSetup = options.game.contract.manifest.normalSetup;
1284
- if (!normalSetup) {
1285
- throw new ScenarioDefinitionValidationError({
1286
- code: "NORMAL_SETUP_UNAVAILABLE",
1287
- path: "scenario.setup.players",
1288
- reason: "the game manifest does not expose normal setup"
1289
- });
1290
- }
1291
- const table = normalSetup.createInitialTable({ playerIds });
1292
- const reducerState = await bundle.initialize({
1293
- table,
1294
- playerIds: [...playerIds],
1295
- rngSeed: scenario.setup.seed,
1296
- setup: typeof scenario.setup.setupProfileId === "string" ? {
1297
- profileId: scenario.setup.setupProfileId,
1298
- optionValues: {}
1299
- } : null
1300
- });
1301
- const runtime = new ScenarioReplayImplementation({
1302
- game: options.game,
1303
- scenario,
1304
- playerIds,
1305
- reducerState,
1306
- checkpoint: { segment: "setup", completed: 0 },
1307
- trace: [],
1308
- events
1309
- });
1310
- await runtime.replayTo(checkpoint);
1311
- return runtime;
1312
- }
1313
- async function materializeScenarioRuntimeCheckpoint(options) {
1314
- const replay = requireScenarioReplayImplementation(
1315
- await replayScenario(options)
1316
- );
1317
- return {
1318
- checkpoint: structuredClone(replay.checkpoint),
1319
- checkpointDigest: replay.checkpointDigest,
1320
- playerIds: [...replay.playerIds],
1321
- state: structuredClone(replay.reducerState)
1322
- };
1323
- }
1324
- function cloneReplayDefinition(scenario) {
1325
- return structuredClone({
1326
- id: scenario.id,
1327
- ...scenario.description === void 0 ? {} : { description: scenario.description },
1328
- setup: scenario.setup,
1329
- given: scenario.given,
1330
- when: scenario.when
1331
- });
1332
- }
1333
- async function assertScenario(options) {
1334
- const replay = requireScenarioReplayImplementation(options.replay);
1335
- if (!replay.complete) {
1336
- throw new Error(
1337
- `Scenario '${replay.scenarioId}' assertions require a full replay; checkpoint is ${replay.checkpoint.segment}:${replay.checkpoint.completed}.`
1338
- );
1339
- }
1340
- const context = {
1341
- expect: createExpectApi({
1342
- lastDiagnosticRejection: () => lastRejectedEvent(replay.diagnostics)
1343
- }),
1344
- state: replay.state,
1345
- view: replay.view,
1346
- interactions: replay.interactions,
1347
- explain: replay.explain,
1348
- get diagnostics() {
1349
- return replay.diagnostics;
1350
- },
1351
- probe: (command) => replay.probe(command)
1352
- };
1353
- await options.assertion(context);
1354
- }
1355
- async function probeScenarioCommand(options) {
1356
- return requireScenarioReplayImplementation(options.replay).probe(
1357
- options.command
1358
- );
1359
- }
1360
- var ScenarioReplayImplementation = class _ScenarioReplayImplementation {
1361
- scenarioId;
1362
- scenario;
1363
- game;
1364
- playerIds;
1365
- bundle;
1366
- events;
1367
- checkpoint;
1368
- reducerState;
1369
- trace;
1370
- constructor(state) {
1371
- this.scenarioId = state.scenario.id;
1372
- this.scenario = state.scenario;
1373
- this.game = state.game;
1374
- this.playerIds = [...state.playerIds];
1375
- this.reducerState = structuredClone(state.reducerState);
1376
- this.checkpoint = structuredClone(state.checkpoint);
1377
- this.trace = structuredClone(
1378
- state.trace
1379
- );
1380
- this.events = structuredClone(state.events);
1381
- this.bundle = createScenarioBundle(state.game, this.events);
1382
- this.state = this.state.bind(this);
1383
- this.view = this.view.bind(this);
1384
- this.interactions = this.interactions.bind(this);
1385
- this.explain = this.explain.bind(this);
1386
- }
1387
- get checkpointDigest() {
1388
- return digestPluginRuntimeJson({
1389
- digestVersion: "scenario-checkpoint@1",
1390
- scenarioId: this.scenarioId,
1391
- checkpoint: this.checkpoint,
1392
- reducerState: this.reducerState,
1393
- trace: this.trace
1394
- });
1395
- }
1396
- get complete() {
1397
- return this.checkpoint.segment === "when" && this.checkpoint.completed === this.scenario.when.length;
1398
- }
1399
- get diagnostics() {
1400
- const flow = readFlowState(this.reducerState);
1401
- const projection = this.project(this.playerIds[0]);
1402
- return {
1403
- events: structuredClone(this.events),
1404
- lastDispatch: lastAcceptedDispatch(this.events),
1405
- flow: {
1406
- currentPhase: flow.currentPhase,
1407
- currentStage: projection.currentStage ?? null,
1408
- activeSeats: flow.activePlayers.flatMap((playerId) => {
1409
- const seat = this.playerIds.indexOf(playerId);
1410
- return seat < 0 ? [] : [{ seat }];
1411
- })
1412
- }
1413
- };
1414
- }
1415
- state() {
1416
- return structuredClone(this.reducerState.domain);
1417
- }
1418
- view(seat) {
1419
- const playerId = this.playerId(seat, "seat");
1420
- const projection = this.project(playerId);
1421
- return structuredClone(projection.seats?.[playerId]?.view ?? null);
1422
- }
1423
- interactions(seat) {
1424
- const playerId = this.playerId(seat, "seat");
1425
- const projection = this.project(playerId);
1426
- const refs = projection.seats?.[playerId]?.availableInteractionRefs ?? [];
1427
- const interactionsByRef = projection.interactionsByRef ?? {};
1428
- return structuredClone(
1429
- refs.flatMap((ref) => {
1430
- const descriptor = interactionsByRef[ref];
1431
- return descriptor ? [descriptor] : [];
1432
- })
1433
- );
1434
- }
1435
- explain(seat, interactionId) {
1436
- return structuredClone(
1437
- this.bundle.explainInteraction({
1438
- state: this.reducerState,
1439
- playerId: this.playerId(seat, "seat"),
1440
- interactionId
1441
- })
1442
- );
1443
- }
1444
- clone() {
1445
- return new _ScenarioReplayImplementation({
1446
- game: this.game,
1447
- scenario: this.scenario,
1448
- playerIds: this.playerIds,
1449
- reducerState: this.reducerState,
1450
- checkpoint: this.checkpoint,
1451
- trace: this.trace,
1452
- events: this.events
1453
- });
1454
- }
1455
- inspectionAuthority(perspective) {
1456
- const selectedPlayerId = perspective.kind === "player" ? this.playerId({ seat: perspective.seat }, "perspective.seat") : null;
1457
- const projection = this.bundle.project({
1458
- state: this.reducerState,
1459
- playerIds: selectedPlayerId ? [selectedPlayerId] : []
1460
- });
1461
- const descriptors = selectedPlayerId ? descriptorsForPlayer(projection, selectedPlayerId) : [];
1462
- const flow = readFlowState(this.reducerState);
1463
- return {
1464
- game: this.game,
1465
- scenario: structuredClone(this.scenario),
1466
- playerIds: [...this.playerIds],
1467
- checkpoint: structuredClone(this.checkpoint),
1468
- checkpointDigest: this.checkpointDigest,
1469
- publicState: structuredClone(this.reducerState.domain.publicState),
1470
- view: structuredClone(
1471
- selectedPlayerId ? projection.seats[selectedPlayerId]?.view ?? null : projection.sharedView ?? null
1472
- ),
1473
- scheduler: {
1474
- phase: flow.currentPhase ?? "",
1475
- step: readPhaseStep(this.reducerState.domain.phase),
1476
- activePlayerIds: [
1477
- ...projection.schedulerFlow?.activePlayerIds ?? projection.stageSeats ?? []
1478
- ],
1479
- pendingPlayerIds: [
1480
- ...projection.schedulerFlow?.pendingPlayerIds ?? projection.simultaneousPhase?.pendingPlayerIds ?? []
1481
- ],
1482
- continuationDependencies: structuredClone(
1483
- projection.schedulerFlow?.continuationDependencies ?? []
1484
- )
1485
- },
1486
- interactions: descriptors.map((descriptor) => ({
1487
- descriptor: structuredClone(descriptor),
1488
- explanation: structuredClone(
1489
- this.bundle.explainInteraction({
1490
- state: this.reducerState,
1491
- playerId: selectedPlayerId,
1492
- interactionId: String(descriptor.interactionId ?? "")
1493
- })
1494
- ),
1495
- actionability: structuredClone(
1496
- this.bundle.resolveInteractionActionability({
1497
- state: this.reducerState,
1498
- playerId: selectedPlayerId,
1499
- interactionId: String(descriptor.interactionId ?? "")
1500
- })
1501
- )
1502
- })),
1503
- entropy: {
1504
- seed: this.reducerState.runtime.rng.seed,
1505
- draws: structuredClone(this.reducerState.runtime.rng.draws ?? [])
1506
- },
1507
- commandTrace: structuredClone(this.trace)
1508
- };
1509
- }
1510
- enumerateInteractionParams(options) {
1511
- return structuredClone(
1512
- this.bundle.enumerateInteractionParams({
1513
- state: this.reducerState,
1514
- playerId: this.playerId({ seat: options.seat }, "perspective.seat"),
1515
- interactionId: options.interactionId,
1516
- maxEvaluations: options.maxEvaluations
1517
- })
1518
- );
1519
- }
1520
- async advance(command) {
1521
- const clone = new _ScenarioReplayImplementation({
1522
- game: this.game,
1523
- scenario: this.scenario,
1524
- playerIds: this.playerIds,
1525
- reducerState: this.reducerState,
1526
- checkpoint: this.checkpoint,
1527
- trace: this.trace,
1528
- events: this.events
1529
- });
1530
- const result = await clone.dispatch(command, "probe", 0);
1531
- if (result.kind === "accept") {
1532
- clone.trace.push({
1533
- segment: this.checkpoint.segment === "when" ? "when" : "given",
1534
- index: this.checkpoint.completed,
1535
- command: structuredClone(command),
1536
- trace: structuredClone(result.trace)
1537
- });
1538
- return {
1539
- kind: "accepted",
1540
- replay: clone,
1541
- trace: structuredClone(result.trace)
1542
- };
1543
- }
1544
- return {
1545
- kind: "rejected",
1546
- errorCode: result.errorCode,
1547
- ...result.message === void 0 ? {} : { message: result.message },
1548
- trace: structuredClone(result.trace)
1549
- };
1550
- }
1551
- async replayTo(target) {
1552
- const givenCount = target.segment === "setup" ? 0 : target.segment === "given" ? target.completed : this.scenario.given.length;
1553
- const whenCount = target.segment === "when" ? target.completed : 0;
1554
- for (let index = 0; index < givenCount; index += 1) {
1555
- await this.dispatchRequired("given", index, this.scenario.given[index]);
1556
- }
1557
- this.checkpoint = { segment: "given", completed: givenCount };
1558
- for (let index = 0; index < whenCount; index += 1) {
1559
- await this.dispatchRequired("when", index, this.scenario.when[index]);
1560
- }
1561
- this.checkpoint = structuredClone(target);
1562
- }
1563
- async probe(command) {
1564
- const clone = requireScenarioReplayImplementation(this.clone());
1565
- const result = await clone.dispatch(command, "probe", 0);
1566
- if (result.kind === "accept") {
1567
- const accepted = {
1568
- kind: "accepted",
1569
- command,
1570
- checkpointDigest: digestPluginRuntimeJson({
1571
- digestVersion: "scenario-probe@1",
1572
- from: this.checkpointDigest,
1573
- command,
1574
- reducerState: clone.reducerState,
1575
- trace: result.trace
1576
- }),
1577
- trace: result.trace,
1578
- toBeAccepted() {
1579
- },
1580
- toRejectWith(expected) {
1581
- scenarioAssertionFailure(
1582
- `Expected probe to reject${expected.errorCode ? ` with ${expected.errorCode}` : ""}, but it was accepted.`
1583
- );
1584
- }
1585
- };
1586
- return accepted;
1587
- }
1588
- const rejected = {
1589
- kind: "rejected",
1590
- command,
1591
- errorCode: result.errorCode,
1592
- ...result.message === void 0 ? {} : { message: result.message },
1593
- trace: result.trace,
1594
- toBeAccepted() {
1595
- scenarioAssertionFailure(
1596
- `Expected probe to be accepted, but it rejected with ${result.errorCode}.`
1597
- );
1598
- },
1599
- toRejectWith(expected) {
1600
- assertRejectionMatches(result, expected);
1601
- }
1602
- };
1603
- return rejected;
1604
- }
1605
- async dispatchRequired(segment, index, command) {
1606
- const result = await this.dispatch(command, segment, index);
1607
- if (result.kind === "reject") {
1608
- throw new ScenarioReplayError({
1609
- scenarioId: this.scenarioId,
1610
- segment,
1611
- index,
1612
- interactionId: command.interactionId,
1613
- errorCode: result.errorCode,
1614
- reducerMessage: result.message,
1615
- trace: result.trace
1616
- });
1617
- }
1618
- this.trace.push({
1619
- segment,
1620
- index,
1621
- command: structuredClone(command),
1622
- trace: result.trace
1623
- });
1624
- }
1625
- async dispatch(command, segment, index) {
1626
- const phase = readFlowState(this.reducerState).currentPhase ?? "";
1627
- const playerId = resolveScenarioSeatRef({
1628
- ref: command.actor,
1629
- playerIds: this.playerIds,
1630
- path: `scenario.${segment}[${index}].actor`
1631
- });
1632
- const params = resolveScenarioCommandParams({
1633
- game: this.game,
1634
- phase,
1635
- interactionId: command.interactionId,
1636
- params: command.params,
1637
- playerIds: this.playerIds,
1638
- path: `scenario.${segment}[${index}]`
1639
- });
1640
- const result = await this.bundle.dispatch({
1641
- state: this.reducerState,
1642
- input: {
1643
- kind: "interaction",
1644
- playerId,
1645
- interactionId: command.interactionId,
1646
- params
1647
- }
1648
- });
1649
- if (result.kind === "reject") {
1650
- return {
1651
- kind: "reject",
1652
- errorCode: result.errorCode,
1653
- ...result.message === void 0 ? {} : { message: result.message },
1654
- trace: []
1655
- };
1656
- }
1657
- this.reducerState = structuredClone(result.state);
1658
- return {
1659
- kind: "accept",
1660
- trace: summarizeWireTrace(result.trace)
1661
- };
1662
- }
1663
- playerId(seat, path) {
1664
- return resolveScenarioSeatRef({
1665
- ref: seat,
1666
- playerIds: this.playerIds,
1667
- path
1668
- });
1669
- }
1670
- project(playerId) {
1671
- return this.bundle.project({
1672
- state: this.reducerState,
1673
- playerIds: [playerId]
1674
- });
1675
- }
1676
- };
1677
- function inspectScenarioReplayAuthority(options) {
1678
- return requireScenarioReplayImplementation(
1679
- options.replay
1680
- ).inspectionAuthority(options.perspective);
1681
- }
1682
- function enumerateScenarioInteractionParams(options) {
1683
- return requireScenarioReplayImplementation(
1684
- options.replay
1685
- ).enumerateInteractionParams(options);
1686
- }
1687
- function advanceScenarioReplay(options) {
1688
- return requireScenarioReplayImplementation(options.replay).advance(
1689
- options.command
1690
- );
1691
- }
1692
- function descriptorsForPlayer(projection, playerId) {
1693
- const refs = projection.seats[playerId]?.availableInteractionRefs ?? [];
1694
- const interactionsByRef = projection.interactionsByRef ?? {};
1695
- return refs.flatMap((ref) => {
1696
- const descriptor = interactionsByRef[ref];
1697
- return descriptor ? [descriptor] : [];
1698
- });
1699
- }
1700
- function createScenarioBundle(game, events) {
1701
- return createReducerTestingBundle(game, {
1702
- diagnostics: {
1703
- event(event) {
1704
- events.push(structuredClone(event));
1705
- }
1706
- }
1707
- });
1708
- }
1709
- function validateReplayDefinition(game, replay) {
1710
- validateScenarioDefinition(game, {
1711
- ...replay,
1712
- then: () => void 0
1713
- });
1714
- }
1715
- function resolvePlayerIds(game, count) {
1716
- const declared = game.contract.manifest.literals.playerIds;
1717
- return Array.from(
1718
- { length: count },
1719
- (_, index) => declared[index] ?? `player-${index + 1}`
1720
- );
1721
- }
1722
- function normalizeCheckpoint(scenario, checkpoint) {
1723
- if (checkpoint === void 0) {
1724
- return { segment: "when", completed: scenario.when.length };
1725
- }
1726
- const maximum = checkpoint.segment === "setup" ? 0 : checkpoint.segment === "given" ? scenario.given.length : scenario.when.length;
1727
- if (!Number.isSafeInteger(checkpoint.completed) || checkpoint.completed < 0 || checkpoint.completed > maximum || checkpoint.segment === "setup" && checkpoint.completed !== 0) {
1728
- throw new ScenarioDefinitionValidationError({
1729
- code: "OUT_OF_RANGE",
1730
- path: "at.completed",
1731
- reason: `expected 0 through ${maximum} for ${checkpoint.segment}`
1732
- });
1733
- }
1734
- return structuredClone(checkpoint);
1735
- }
1736
- function readFlowState(state) {
1737
- const flow = state.domain?.flow;
1738
- if (typeof flow !== "object" || flow === null) {
1739
- return { currentPhase: null, activePlayers: [] };
1740
- }
1741
- const record = flow;
1742
- return {
1743
- currentPhase: typeof record.currentPhase === "string" ? record.currentPhase : null,
1744
- activePlayers: Array.isArray(record.activePlayers) ? record.activePlayers.filter(
1745
- (playerId) => typeof playerId === "string"
1746
- ) : []
1747
- };
1748
- }
1749
- function readPhaseStep(phase) {
1750
- if (!phase || typeof phase !== "object") return null;
1751
- const step = phase.step;
1752
- return typeof step === "string" ? step : null;
1753
- }
1754
- function summarizeWireTrace(trace) {
1755
- return trace.flatMap((entry) => {
1756
- switch (entry.kind) {
1757
- case "acceptedClientInput":
1758
- return [
1759
- {
1760
- kind: "acceptedClientInput",
1761
- interactionId: entry.input.interactionId,
1762
- playerId: entry.input.playerId
1763
- }
1764
- ];
1765
- case "appliedEffect":
1766
- return [
1767
- {
1768
- kind: "appliedInstruction",
1769
- instruction: String(
1770
- entry.effect.kind ?? entry.effect.type ?? "effect"
1771
- )
1772
- }
1773
- ];
1774
- case "rngConsumption":
1775
- return [
1776
- {
1777
- kind: "rngConsumption",
1778
- version: entry.version,
1779
- operation: entry.operation,
1780
- drawIndex: entry.drawIndex
1781
- }
1782
- ];
1783
- }
1784
- });
1785
- }
1786
- function lastAcceptedDispatch(events) {
1787
- for (let index = events.length - 1; index >= 0; index -= 1) {
1788
- const event = events[index];
1789
- if (event?.type === "submitAccepted") {
1790
- return {
1791
- submissionId: event.submissionId,
1792
- trace: structuredClone(event.trace)
1793
- };
1794
- }
1795
- }
1796
- return null;
1797
- }
1798
- function lastRejectedEvent(diagnostics) {
1799
- for (let index = diagnostics.events.length - 1; index >= 0; index -= 1) {
1800
- const event = diagnostics.events[index];
1801
- if (event?.type === "submitRejected") return event;
1802
- }
1803
- return null;
1804
- }
1805
- function assertRejectionMatches(rejection, expected) {
1806
- if (expected.errorCode !== void 0 && rejection.errorCode !== expected.errorCode) {
1807
- scenarioAssertionFailure(
1808
- `Expected rejection errorCode '${expected.errorCode}', received '${rejection.errorCode}'.`
1809
- );
1810
- }
1811
- if (typeof expected.message === "string" && rejection.message !== expected.message) {
1812
- scenarioAssertionFailure(
1813
- `Expected rejection message '${expected.message}', received '${rejection.message ?? "undefined"}'.`
1814
- );
1815
- }
1816
- if (expected.message instanceof RegExp && !expected.message.test(rejection.message ?? "")) {
1817
- scenarioAssertionFailure(
1818
- `Expected rejection message '${rejection.message ?? ""}' to match ${String(expected.message)}.`
1819
- );
1820
- }
1821
- }
1822
- function requireScenarioReplayImplementation(replay) {
1823
- if (!(replay instanceof ScenarioReplayImplementation)) {
1824
- throw new Error(
1825
- "Scenario replay was not created by replayScenario(); alternate replay implementations are not supported."
1826
- );
1827
- }
1828
- return replay;
1829
- }
1830
-
1831
- // src/testing/canonical.ts
1832
- function canonicalScenarioJson(value) {
1833
- return encodeCanonicalPluginRuntimeJson(value);
1834
- }
1835
- function digestScenarioJson(value) {
1836
- return digestPluginRuntimeJson(value);
1837
- }
1838
- function compareCanonicalScenarioJson(left, right) {
1839
- const leftJson = canonicalScenarioJson(left);
1840
- const rightJson = canonicalScenarioJson(right);
1841
- return leftJson === rightJson ? 0 : leftJson < rightJson ? -1 : 1;
1842
- }
1843
-
1844
- export {
1845
- ScenarioDefinitionValidationError,
1846
- resolveScenarioCheckpoint,
1847
- ScenarioReplayError,
1848
- createScenarioAuthoring,
1849
- toScenarioReplayDefinition,
1850
- ScenarioAssertionError,
1851
- createExpectApi,
1852
- resolveScenarioCommandParams,
1853
- projectScenarioCommandParams,
1854
- replayScenario,
1855
- materializeScenarioRuntimeCheckpoint,
1856
- assertScenario,
1857
- probeScenarioCommand,
1858
- inspectScenarioReplayAuthority,
1859
- enumerateScenarioInteractionParams,
1860
- advanceScenarioReplay,
1861
- digestScenarioJson,
1862
- compareCanonicalScenarioJson
1863
- };
1864
- //# sourceMappingURL=chunk-BI4G3PM2.js.map