@joystyk/testing 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +36 -0
  4. package/README.md +191 -2
  5. package/dist/builders.d.ts +59 -0
  6. package/dist/builders.js +129 -0
  7. package/dist/builders.js.map +1 -0
  8. package/dist/clock.d.ts +30 -0
  9. package/dist/clock.js +58 -0
  10. package/dist/clock.js.map +1 -0
  11. package/dist/deep.d.ts +25 -0
  12. package/dist/deep.js +105 -0
  13. package/dist/deep.js.map +1 -0
  14. package/dist/engine-conformance.d.ts +166 -0
  15. package/dist/engine-conformance.js +995 -0
  16. package/dist/engine-conformance.js.map +1 -0
  17. package/dist/fixtures/dice-race.d.ts +41 -0
  18. package/dist/fixtures/dice-race.js +153 -0
  19. package/dist/fixtures/dice-race.js.map +1 -0
  20. package/dist/fixtures/draw-play.d.ts +54 -0
  21. package/dist/fixtures/draw-play.js +188 -0
  22. package/dist/fixtures/draw-play.js.map +1 -0
  23. package/dist/fixtures/drift.d.ts +34 -0
  24. package/dist/fixtures/drift.js +109 -0
  25. package/dist/fixtures/drift.js.map +1 -0
  26. package/dist/fixtures/guards.d.ts +11 -0
  27. package/dist/fixtures/guards.js +16 -0
  28. package/dist/fixtures/guards.js.map +1 -0
  29. package/dist/fixtures/hint-puzzle.d.ts +47 -0
  30. package/dist/fixtures/hint-puzzle.js +129 -0
  31. package/dist/fixtures/hint-puzzle.js.map +1 -0
  32. package/dist/fixtures/index.d.ts +6 -0
  33. package/dist/fixtures/index.js +7 -0
  34. package/dist/fixtures/nim.d.ts +34 -0
  35. package/dist/fixtures/nim.js +141 -0
  36. package/dist/fixtures/nim.js.map +1 -0
  37. package/dist/fixtures/sealed-bids.d.ts +46 -0
  38. package/dist/fixtures/sealed-bids.js +141 -0
  39. package/dist/fixtures/sealed-bids.js.map +1 -0
  40. package/dist/index.d.ts +13 -0
  41. package/dist/index.js +14 -0
  42. package/dist/replay.d.ts +58 -0
  43. package/dist/replay.js +110 -0
  44. package/dist/replay.js.map +1 -0
  45. package/dist/report.d.ts +68 -0
  46. package/dist/report.js +104 -0
  47. package/dist/report.js.map +1 -0
  48. package/dist/rng.d.ts +18 -0
  49. package/dist/rng.js +25 -0
  50. package/dist/rng.js.map +1 -0
  51. package/dist/schema.d.ts +13 -0
  52. package/dist/schema.js +21 -0
  53. package/dist/schema.js.map +1 -0
  54. package/dist/session-ports.d.ts +16 -0
  55. package/dist/session-ports.js +32 -0
  56. package/dist/session-ports.js.map +1 -0
  57. package/dist/simulation-conformance.d.ts +71 -0
  58. package/dist/simulation-conformance.js +312 -0
  59. package/dist/simulation-conformance.js.map +1 -0
  60. package/dist/storage.d.ts +29 -0
  61. package/dist/storage.js +63 -0
  62. package/dist/storage.js.map +1 -0
  63. package/dist/vitest.d.ts +32 -0
  64. package/dist/vitest.js +45 -0
  65. package/dist/vitest.js.map +1 -0
  66. package/dist/waiting.d.ts +58 -0
  67. package/dist/waiting.js +97 -0
  68. package/dist/waiting.js.map +1 -0
  69. package/package.json +55 -3
package/dist/deep.js ADDED
@@ -0,0 +1,105 @@
1
+ //#region src/deep.ts
2
+ var describeValue = (value) => {
3
+ if (value === void 0) return "undefined";
4
+ if (typeof value === "number" && Object.is(value, -0)) return "-0";
5
+ if (typeof value === "function") return "a function";
6
+ try {
7
+ const text = JSON.stringify(value);
8
+ if (text === void 0) return String(value);
9
+ return text.length > 80 ? `${text.slice(0, 77)}...` : text;
10
+ } catch {
11
+ return String(value);
12
+ }
13
+ };
14
+ var isObject = (value) => typeof value === "object" && value !== null;
15
+ var pathKey = (path, key) => /^[A-Za-z_$][\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
16
+ /**
17
+ * The first place where `a` and `b` differ, as a readable path with both values, or null when
18
+ * they are structurally equal. Numbers compare with `Object.is`, so `0` and `-0` differ, and
19
+ * a missing key differs from a key holding `undefined`.
20
+ */
21
+ var firstDifference = (a, b, path = "$") => {
22
+ if (Object.is(a, b)) return null;
23
+ if (!isObject(a) || !isObject(b)) return `${path}: ${describeValue(a)} vs ${describeValue(b)}`;
24
+ if (Array.isArray(a) !== Array.isArray(b)) return `${path}: ${describeValue(a)} vs ${describeValue(b)}`;
25
+ if (Array.isArray(a) && Array.isArray(b)) {
26
+ if (a.length !== b.length) return `${path}.length: ${a.length} vs ${b.length}`;
27
+ for (let i = 0; i < a.length; i += 1) {
28
+ const d = firstDifference(a[i], b[i], `${path}[${i}]`);
29
+ if (d !== null) return d;
30
+ }
31
+ return null;
32
+ }
33
+ const keysA = Object.keys(a);
34
+ const keysB = Object.keys(b);
35
+ for (const key of keysA) if (!Object.hasOwn(b, key)) return `${pathKey(path, key)}: ${describeValue(a[key])} vs (missing)`;
36
+ for (const key of keysB) if (!Object.hasOwn(a, key)) return `${pathKey(path, key)}: (missing) vs ${describeValue(b[key])}`;
37
+ for (const key of keysA) {
38
+ const d = firstDifference(a[key], b[key], pathKey(path, key));
39
+ if (d !== null) return d;
40
+ }
41
+ return null;
42
+ };
43
+ /** True when `a` and `b` are structurally equal (see `firstDifference`). */
44
+ var deepEqual = (a, b) => firstDifference(a, b) === null;
45
+ /**
46
+ * Freezes `value` and everything reachable from it, then returns it. The suites freeze every
47
+ * state, command and input they hand to an engine, so an engine that mutates its input
48
+ * throws instead of silently corrupting the game.
49
+ */
50
+ var deepFreeze = (value) => {
51
+ if (isObject(value) && !Object.isFrozen(value)) {
52
+ Object.freeze(value);
53
+ for (const key of Object.keys(value)) deepFreeze(value[key]);
54
+ }
55
+ return value;
56
+ };
57
+ /**
58
+ * Why `value` isn't JSON, with the path to the first offending part, or null when it is.
59
+ * The contract's `isJson` answers yes or no; failure messages need the where and why.
60
+ */
61
+ var jsonProblem = (value, path = "$", seen = /* @__PURE__ */ new Set()) => {
62
+ switch (typeof value) {
63
+ case "string":
64
+ case "boolean": return null;
65
+ case "number":
66
+ if (Object.is(value, -0)) return `-0 at ${path} (JSON turns it into 0)`;
67
+ return Number.isFinite(value) ? null : `${value} at ${path}`;
68
+ case "object": {
69
+ if (value === null) return null;
70
+ if (seen.has(value)) return `a cycle at ${path}`;
71
+ const proto = Object.getPrototypeOf(value);
72
+ if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return `a ${value.constructor?.name ?? "class"} instance at ${path}`;
73
+ if (Object.getOwnPropertySymbols(value).length > 0) return `a symbol key at ${path}`;
74
+ seen.add(value);
75
+ try {
76
+ if (Array.isArray(value)) {
77
+ for (let i = 0; i < value.length; i += 1) {
78
+ if (!(i in value)) return `a hole at ${path}[${i}]`;
79
+ const problem = jsonProblem(value[i], `${path}[${i}]`, seen);
80
+ if (problem !== null) return problem;
81
+ }
82
+ return null;
83
+ }
84
+ for (const key of Object.keys(value)) {
85
+ const problem = jsonProblem(value[key], pathKey(path, key), seen);
86
+ if (problem !== null) return problem;
87
+ }
88
+ return null;
89
+ } finally {
90
+ seen.delete(value);
91
+ }
92
+ }
93
+ default: return `${value === void 0 ? "undefined" : typeof value} at ${path}`;
94
+ }
95
+ };
96
+ /** A JSON round trip: what a value looks like after a socket, a save or a replay file. */
97
+ var jsonRoundTrip = (value) => JSON.parse(JSON.stringify(value));
98
+ /** A short, single-line rendering of any value for failure messages. */
99
+ var show = describeValue;
100
+ /** The message of anything thrown. */
101
+ var errorMessage = (error) => error instanceof Error ? `${error.name}: ${error.message}` : `thrown value ${describeValue(error)}`;
102
+ //#endregion
103
+ export { deepEqual, deepFreeze, errorMessage, firstDifference, jsonProblem, jsonRoundTrip, show };
104
+
105
+ //# sourceMappingURL=deep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deep.js","names":[],"sources":["../src/deep.ts"],"sourcesContent":["// Structural helpers for comparing and protecting values the suites pass around.\n\nconst describeValue = (value: unknown): string => {\n if (value === undefined) return 'undefined';\n if (typeof value === 'number' && Object.is(value, -0)) return '-0';\n if (typeof value === 'function') return 'a function';\n try {\n const text = JSON.stringify(value);\n if (text === undefined) return String(value);\n return text.length > 80 ? `${text.slice(0, 77)}...` : text;\n } catch {\n return String(value);\n }\n};\n\nconst isObject = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null;\n\nconst pathKey = (path: string, key: string): string =>\n /^[A-Za-z_$][\\w$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;\n\n/**\n * The first place where `a` and `b` differ, as a readable path with both values, or null when\n * they are structurally equal. Numbers compare with `Object.is`, so `0` and `-0` differ, and\n * a missing key differs from a key holding `undefined`.\n */\nexport const firstDifference = (a: unknown, b: unknown, path = '$'): string | null => {\n if (Object.is(a, b)) return null;\n if (!isObject(a) || !isObject(b)) return `${path}: ${describeValue(a)} vs ${describeValue(b)}`;\n if (Array.isArray(a) !== Array.isArray(b)) return `${path}: ${describeValue(a)} vs ${describeValue(b)}`;\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return `${path}.length: ${a.length} vs ${b.length}`;\n for (let i = 0; i < a.length; i += 1) {\n const d = firstDifference(a[i], b[i], `${path}[${i}]`);\n if (d !== null) return d;\n }\n return null;\n }\n const keysA = Object.keys(a);\n const keysB = Object.keys(b);\n for (const key of keysA) {\n if (!Object.hasOwn(b, key)) return `${pathKey(path, key)}: ${describeValue(a[key])} vs (missing)`;\n }\n for (const key of keysB) {\n if (!Object.hasOwn(a, key)) return `${pathKey(path, key)}: (missing) vs ${describeValue(b[key])}`;\n }\n for (const key of keysA) {\n const d = firstDifference(a[key], b[key], pathKey(path, key));\n if (d !== null) return d;\n }\n return null;\n};\n\n/** True when `a` and `b` are structurally equal (see `firstDifference`). */\nexport const deepEqual = (a: unknown, b: unknown): boolean => firstDifference(a, b) === null;\n\n/**\n * Freezes `value` and everything reachable from it, then returns it. The suites freeze every\n * state, command and input they hand to an engine, so an engine that mutates its input\n * throws instead of silently corrupting the game.\n */\nexport const deepFreeze = <T>(value: T): T => {\n if (isObject(value) && !Object.isFrozen(value)) {\n Object.freeze(value);\n for (const key of Object.keys(value)) deepFreeze(value[key]);\n }\n return value;\n};\n\n/**\n * Why `value` isn't JSON, with the path to the first offending part, or null when it is.\n * The contract's `isJson` answers yes or no; failure messages need the where and why.\n */\nexport const jsonProblem = (value: unknown, path = '$', seen: Set<object> = new Set()): string | null => {\n switch (typeof value) {\n case 'string':\n case 'boolean':\n return null;\n case 'number':\n if (Object.is(value, -0)) return `-0 at ${path} (JSON turns it into 0)`;\n return Number.isFinite(value) ? null : `${value} at ${path}`;\n case 'object': {\n if (value === null) return null;\n if (seen.has(value)) return `a cycle at ${path}`;\n const proto: unknown = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) {\n return `a ${(value as object).constructor?.name ?? 'class'} instance at ${path}`;\n }\n if (Object.getOwnPropertySymbols(value).length > 0) return `a symbol key at ${path}`;\n seen.add(value);\n try {\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i += 1) {\n if (!(i in value)) return `a hole at ${path}[${i}]`;\n const problem = jsonProblem(value[i], `${path}[${i}]`, seen);\n if (problem !== null) return problem;\n }\n return null;\n }\n for (const key of Object.keys(value)) {\n const problem = jsonProblem((value as Record<string, unknown>)[key], pathKey(path, key), seen);\n if (problem !== null) return problem;\n }\n return null;\n } finally {\n seen.delete(value);\n }\n }\n default:\n return `${value === undefined ? 'undefined' : typeof value} at ${path}`;\n }\n};\n\n/** A JSON round trip: what a value looks like after a socket, a save or a replay file. */\nexport const jsonRoundTrip = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T;\n\n/** A short, single-line rendering of any value for failure messages. */\nexport const show = describeValue;\n\n/** The message of anything thrown. */\nexport const errorMessage = (error: unknown): string =>\n error instanceof Error ? `${error.name}: ${error.message}` : `thrown value ${describeValue(error)}`;\n"],"mappings":";AAEA,IAAM,iBAAiB,UAA2B;CAChD,IAAI,UAAU,KAAA,GAAW,OAAO;CAChC,IAAI,OAAO,UAAU,YAAY,OAAO,GAAG,OAAO,EAAE,GAAG,OAAO;CAC9D,IAAI,OAAO,UAAU,YAAY,OAAO;CACxC,IAAI;EACF,MAAM,OAAO,KAAK,UAAU,KAAK;EACjC,IAAI,SAAS,KAAA,GAAW,OAAO,OAAO,KAAK;EAC3C,OAAO,KAAK,SAAS,KAAK,GAAG,KAAK,MAAM,GAAG,EAAE,EAAE,OAAO;CACxD,QAAQ;EACN,OAAO,OAAO,KAAK;CACrB;AACF;AAEA,IAAM,YAAY,UAAqD,OAAO,UAAU,YAAY,UAAU;AAE9G,IAAM,WAAW,MAAc,QAC7B,qBAAqB,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,QAAQ,GAAG,KAAK,GAAG,KAAK,UAAU,GAAG,EAAE;;;;;;AAOrF,IAAa,mBAAmB,GAAY,GAAY,OAAO,QAAuB;CACpF,IAAI,OAAO,GAAG,GAAG,CAAC,GAAG,OAAO;CAC5B,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,OAAO,GAAG,KAAK,IAAI,cAAc,CAAC,EAAE,MAAM,cAAc,CAAC;CAC3F,IAAI,MAAM,QAAQ,CAAC,MAAM,MAAM,QAAQ,CAAC,GAAG,OAAO,GAAG,KAAK,IAAI,cAAc,CAAC,EAAE,MAAM,cAAc,CAAC;CACpG,IAAI,MAAM,QAAQ,CAAC,KAAK,MAAM,QAAQ,CAAC,GAAG;EACxC,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO,GAAG,KAAK,WAAW,EAAE,OAAO,MAAM,EAAE;EACtE,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,QAAQ,KAAK,GAAG;GACpC,MAAM,IAAI,gBAAgB,EAAE,IAAI,EAAE,IAAI,GAAG,KAAK,GAAG,EAAE,EAAE;GACrD,IAAI,MAAM,MAAM,OAAO;EACzB;EACA,OAAO;CACT;CACA,MAAM,QAAQ,OAAO,KAAK,CAAC;CAC3B,MAAM,QAAQ,OAAO,KAAK,CAAC;CAC3B,KAAK,MAAM,OAAO,OAChB,IAAI,CAAC,OAAO,OAAO,GAAG,GAAG,GAAG,OAAO,GAAG,QAAQ,MAAM,GAAG,EAAE,IAAI,cAAc,EAAE,IAAI,EAAE;CAErF,KAAK,MAAM,OAAO,OAChB,IAAI,CAAC,OAAO,OAAO,GAAG,GAAG,GAAG,OAAO,GAAG,QAAQ,MAAM,GAAG,EAAE,iBAAiB,cAAc,EAAE,IAAI;CAEhG,KAAK,MAAM,OAAO,OAAO;EACvB,MAAM,IAAI,gBAAgB,EAAE,MAAM,EAAE,MAAM,QAAQ,MAAM,GAAG,CAAC;EAC5D,IAAI,MAAM,MAAM,OAAO;CACzB;CACA,OAAO;AACT;;AAGA,IAAa,aAAa,GAAY,MAAwB,gBAAgB,GAAG,CAAC,MAAM;;;;;;AAOxF,IAAa,cAAiB,UAAgB;CAC5C,IAAI,SAAS,KAAK,KAAK,CAAC,OAAO,SAAS,KAAK,GAAG;EAC9C,OAAO,OAAO,KAAK;EACnB,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAG,WAAW,MAAM,IAAI;CAC7D;CACA,OAAO;AACT;;;;;AAMA,IAAa,eAAe,OAAgB,OAAO,KAAK,uBAAoB,IAAI,IAAI,MAAqB;CACvG,QAAQ,OAAO,OAAf;EACE,KAAK;EACL,KAAK,WACH,OAAO;EACT,KAAK;GACH,IAAI,OAAO,GAAG,OAAO,EAAE,GAAG,OAAO,SAAS,KAAK;GAC/C,OAAO,OAAO,SAAS,KAAK,IAAI,OAAO,GAAG,MAAM,MAAM;EACxD,KAAK,UAAU;GACb,IAAI,UAAU,MAAM,OAAO;GAC3B,IAAI,KAAK,IAAI,KAAK,GAAG,OAAO,cAAc;GAC1C,MAAM,QAAiB,OAAO,eAAe,KAAK;GAClD,IAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,UAAU,OAAO,aAAa,UAAU,MACnE,OAAO,KAAM,MAAiB,aAAa,QAAQ,QAAQ,eAAe;GAE5E,IAAI,OAAO,sBAAsB,KAAK,CAAC,CAAC,SAAS,GAAG,OAAO,mBAAmB;GAC9E,KAAK,IAAI,KAAK;GACd,IAAI;IACF,IAAI,MAAM,QAAQ,KAAK,GAAG;KACxB,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;MACxC,IAAI,EAAE,KAAK,QAAQ,OAAO,aAAa,KAAK,GAAG,EAAE;MACjD,MAAM,UAAU,YAAY,MAAM,IAAI,GAAG,KAAK,GAAG,EAAE,IAAI,IAAI;MAC3D,IAAI,YAAY,MAAM,OAAO;KAC/B;KACA,OAAO;IACT;IACA,KAAK,MAAM,OAAO,OAAO,KAAK,KAAK,GAAG;KACpC,MAAM,UAAU,YAAa,MAAkC,MAAM,QAAQ,MAAM,GAAG,GAAG,IAAI;KAC7F,IAAI,YAAY,MAAM,OAAO;IAC/B;IACA,OAAO;GACT,UAAU;IACR,KAAK,OAAO,KAAK;GACnB;EACF;EACA,SACE,OAAO,GAAG,UAAU,KAAA,IAAY,cAAc,OAAO,MAAM,MAAM;CACrE;AACF;;AAGA,IAAa,iBAAoB,UAAgB,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;;AAGjF,IAAa,OAAO;;AAGpB,IAAa,gBAAgB,UAC3B,iBAAiB,QAAQ,GAAG,MAAM,KAAK,IAAI,MAAM,YAAY,gBAAgB,cAAc,KAAK"}
@@ -0,0 +1,166 @@
1
+ import { type DiscreteEngine, type EngineAssist, type EngineInput, type Json, type Migration, type Rng, type SeatId, type SeatInfo } from '@joystyk/contracts';
2
+ import { type SaveFixture } from './builders.js';
3
+ import { type ConformanceReport } from './report.js';
4
+ /** One way to start a game: a config and the seats (a count, or explicit `SeatInfo`s). */
5
+ export type EngineSetup<Config> = {
6
+ readonly config: Config;
7
+ readonly seats: number | readonly SeatInfo[];
8
+ readonly label?: string;
9
+ };
10
+ /**
11
+ * What an invariant hook returns: nothing or `true` when the state is fine; `false` or a
12
+ * message when it isn't. Throwing (for example from `expect`) also counts as a violation.
13
+ */
14
+ export type InvariantResult = void | boolean | string;
15
+ /** Save-related inputs for the `saves` check. */
16
+ export type SaveOptions = {
17
+ /** The current save version (`GameVersions.save`). */
18
+ readonly version: number;
19
+ /** The oldest save version that was ever shipped. Default 1. */
20
+ readonly firstVersion?: number;
21
+ readonly migrations: readonly Migration[];
22
+ /** At least one stored state for every shipped save version, the current one included. */
23
+ readonly fixtures: readonly SaveFixture[];
24
+ };
25
+ /** Original successor snapshots: preserve what the viewer legitimately learns in them. */
26
+ export type HiddenTransition<State, Command> = {
27
+ readonly input: EngineInput<Command>;
28
+ readonly states: readonly State[];
29
+ };
30
+ /** Options for `checkEngine` and `runEngineConformance`. */
31
+ export type EngineConformanceOptions<State, Command, Config> = {
32
+ /** A name for reports and the vitest `describe` block. */
33
+ readonly name?: string;
34
+ /** The game id written into replays and saves. Default "test-game". */
35
+ readonly gameId?: string;
36
+ /** How to start games. Or give `configs` and `seatCounts`, and every pair is used. */
37
+ readonly setups?: readonly EngineSetup<Config>[];
38
+ readonly configs?: readonly Config[];
39
+ readonly seatCounts?: readonly number[];
40
+ /** Random games per setup. Default 12. */
41
+ readonly games?: number;
42
+ /** The base seed. Game `g` of setup `s` uses the seed string `${seed}/${s}/${g}`. Default "conformance". */
43
+ readonly seed?: string;
44
+ /** The most inputs one game may take. Default 400. */
45
+ readonly maxSteps?: number;
46
+ /** Fail a game that has no outcome after `maxSteps` inputs. Default true. */
47
+ readonly requireOutcome?: boolean;
48
+ /**
49
+ * Candidate commands for `seat` in `state`: every legal command, or a sample of them.
50
+ * Illegal candidates are fine; the engine's answer decides. Without it, the suite asks
51
+ * `engine.ai` for one command per pending seat.
52
+ */
53
+ readonly commands?: (state: State, seat: SeatId, rng: Rng) => readonly Command[];
54
+ /**
55
+ * Changes only what `viewer` may not see (other hands, the deck, a solution), usually with
56
+ * the kit's `perturbHidden`. Without it the no-leak check is skipped, which is right only
57
+ * for games whose state is entirely public.
58
+ */
59
+ readonly perturbHidden?: (state: State, viewer: SeatId | null, rng: Rng) => State;
60
+ /**
61
+ * Optional event-check retry: preserve the viewer's current and newly learned facts,
62
+ * including the input's required cards, while varying information that stays hidden.
63
+ * Separate from perturbHidden so legacy position helpers never receive transition work.
64
+ */
65
+ readonly perturbTransition?: (state: State, viewer: SeatId | null, rng: Rng, transition: HiddenTransition<State, Command>) => State;
66
+ /** Perturbations per viewer per position. Default 2. */
67
+ readonly perturbations?: number;
68
+ /** Called on every state the game reaches (after `init` and after every applied input). */
69
+ readonly invariants?: (state: State) => InvariantResult;
70
+ /**
71
+ * Which platform inputs the random games send. `resign` (default true) is accepted by every
72
+ * engine while the game is in progress; turn on `timeout` and `seatChanges` for engines
73
+ * that support clocks and seat hand-overs.
74
+ */
75
+ readonly platformInputs?: {
76
+ readonly resign?: boolean;
77
+ readonly timeout?: boolean;
78
+ readonly seatChanges?: boolean;
79
+ };
80
+ /** Fake job runners by job kind: they return the outcome's `value`. */
81
+ readonly jobs?: {
82
+ readonly [kind: string]: (payload: Json, rng: Rng) => Json;
83
+ };
84
+ /** How often a job fails instead (`{ ok: false }`). Default 0.1. */
85
+ readonly jobFailureRate?: number;
86
+ /**
87
+ * Which inputs the game's spec makes undo barriers. When given, every transition's
88
+ * `undoBarrier` must match it.
89
+ */
90
+ readonly undoBarrier?: (input: EngineInput<Command>, before: State, after: State) => boolean;
91
+ /**
92
+ * Which inputs the game's spec counts as assistance (a hint, a check, a reveal). When
93
+ * given, every transition's `assist` must match it.
94
+ */
95
+ readonly assist?: (input: EngineInput<Command>, before: State, after: State) => EngineAssist | null;
96
+ /** Save fixtures and migrations. Without them only the save round trip is checked. */
97
+ readonly saves?: SaveOptions;
98
+ /** Run only one game, to reproduce a reported failure. */
99
+ readonly only?: {
100
+ readonly setup: number;
101
+ readonly game: number;
102
+ };
103
+ /** Failures kept per check in the report. Default 3. */
104
+ readonly maxFailuresPerCheck?: number;
105
+ };
106
+ /** The checks this suite runs, in report order. */
107
+ export declare const ENGINE_CHECKS: readonly [{
108
+ readonly id: "determinism";
109
+ readonly title: "Determinism: the same seed and inputs give the same states, beats, barriers and outcomes";
110
+ }, {
111
+ readonly id: "replay";
112
+ readonly title: "Replay: the recorded log reproduces the final state";
113
+ }, {
114
+ readonly id: "json";
115
+ readonly title: "JSON safety of configs, states, views, events, commands, jobs and decision keys";
116
+ }, {
117
+ readonly id: "schemas";
118
+ readonly title: "Schema agreement: every value passes the engine's own schemas";
119
+ }, {
120
+ readonly id: "no-leak";
121
+ readonly title: "No leaks: viewFor, eventFor and decisionKey ignore what the viewer may not see";
122
+ }, {
123
+ readonly id: "rejections";
124
+ readonly title: "Rejected inputs change nothing: apply never throws or mutates, and rejects with a GameError";
125
+ }, {
126
+ readonly id: "turns";
127
+ readonly title: "Turn discipline: only pending decisions and declared untimed side commands are allowed";
128
+ }, {
129
+ readonly id: "resign";
130
+ readonly title: "Resignation is accepted from any seat, in turn or not, while the game is in progress";
131
+ }, {
132
+ readonly id: "invariants";
133
+ readonly title: "Invariants hold on every state the game reaches";
134
+ }, {
135
+ readonly id: "ai";
136
+ readonly title: "AI legality: ai.choose never returns a rejected command";
137
+ }, {
138
+ readonly id: "saves";
139
+ readonly title: "Saves: round trip, restore and continue, and the migration chain from fixtures";
140
+ }, {
141
+ readonly id: "jobs";
142
+ readonly title: "Job hygiene: job outcomes are accepted only while their job is awaited";
143
+ }, {
144
+ readonly id: "stale";
145
+ readonly title: "Staleness: while a seat's decisionKey is unchanged, the commands it may send are unchanged";
146
+ }, {
147
+ readonly id: "undo";
148
+ readonly title: "Undo by replay returns to the human's previous decision and never steps back past a barrier";
149
+ }, {
150
+ readonly id: "assist";
151
+ readonly title: "Assistance: transitions flag hints, checks and reveals, and replay reproduces the flags";
152
+ }, {
153
+ readonly id: "session-hooks";
154
+ readonly title: "Session hooks: startsClock is declared and set at most once a game, and worthSaving is pure";
155
+ }, {
156
+ readonly id: "outcome";
157
+ readonly title: "Every game reaches a valid outcome in time, and a finished game rejects commands and resignations";
158
+ }];
159
+ /** The id of one engine check. */
160
+ export type EngineCheckId = (typeof ENGINE_CHECKS)[number]['id'];
161
+ /**
162
+ * Runs the Layer 2 conformance suite against `engine` and returns a report. It never throws
163
+ * for a failing check; it throws only for unusable options. Use `runEngineConformance` to get
164
+ * one vitest case per check instead.
165
+ */
166
+ export declare const checkEngine: <State extends Json, Command extends Json, View extends Json, Event extends Json, Config extends Json>(engine: DiscreteEngine<State, Command, View, Event, Config>, options: EngineConformanceOptions<State, Command, Config>) => Promise<ConformanceReport>;