@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
@@ -0,0 +1,34 @@
1
+ import { type Migration, type SeatId } from '@joystyk/contracts';
2
+ import type { EngineConformanceOptions } from '../engine-conformance.js';
3
+ export type NimState = {
4
+ readonly heap: number;
5
+ readonly turn: SeatId;
6
+ readonly winner: SeatId | null;
7
+ readonly moves: number;
8
+ };
9
+ export type NimCommand = {
10
+ readonly type: 'take';
11
+ readonly count: number;
12
+ };
13
+ export type NimView = NimState;
14
+ export type NimEvent = {
15
+ readonly type: 'took';
16
+ readonly seat: SeatId;
17
+ readonly count: number;
18
+ } | {
19
+ readonly type: 'resigned';
20
+ readonly seat: SeatId;
21
+ };
22
+ export type NimConfig = {
23
+ readonly heap: number;
24
+ };
25
+ export declare const isNimState: (v: unknown) => v is NimState;
26
+ /** The current save version of Nim's state. */
27
+ export declare const NIM_SAVE_VERSION = 2;
28
+ /** v1 → v2 added `moves`; a v1 save restarts the count at 0. */
29
+ export declare const nimMigrations: readonly Migration[];
30
+ /** Stored states for every shipped save version. */
31
+ export declare const nimSaveFixtures: import("../builders.js").SaveFixture[];
32
+ export declare const nim: import("@joystyk/contracts").DiscreteEngine<NimState, NimCommand, NimState, NimEvent, NimConfig>;
33
+ /** Conformance options for the Nim fixture. */
34
+ export declare const nimConformance: EngineConformanceOptions<NimState, NimCommand, NimConfig>;
@@ -0,0 +1,141 @@
1
+ import { guardSchema, saveFixture } from "../builders.js";
2
+ import { isInt, isRecord, isSeat, isSeatOrNull } from "./guards.js";
3
+ import { defineEngine, err, gameError, ok } from "@joystyk/contracts";
4
+ //#region src/fixtures/nim.ts
5
+ var isNimState = (v) => isRecord(v) && isInt(v["heap"], 0) && isSeat(v["turn"]) && v["turn"] < 2 && isSeatOrNull(v["winner"]) && isInt(v["moves"], 0);
6
+ var isNimCommand = (v) => isRecord(v) && v["type"] === "take" && isInt(v["count"]);
7
+ var isNimEvent = (v) => isRecord(v) && (v["type"] === "took" && isSeat(v["seat"]) && isInt(v["count"]) || v["type"] === "resigned" && isSeat(v["seat"]));
8
+ var isNimConfig = (v) => isRecord(v) && isInt(v["heap"], 1, 1e3);
9
+ /** The current save version of Nim's state. */
10
+ var NIM_SAVE_VERSION = 2;
11
+ /** v1 → v2 added `moves`; a v1 save restarts the count at 0. */
12
+ var nimMigrations = [{
13
+ from: 1,
14
+ to: 2,
15
+ migrate: (old) => isRecord(old) ? ok({
16
+ ...old,
17
+ moves: 0
18
+ }) : err(gameError("BadSave", "This saved game is damaged."))
19
+ }];
20
+ /** Stored states for every shipped save version. */
21
+ var nimSaveFixtures = [saveFixture("v1 mid-game", 1, {
22
+ heap: 7,
23
+ turn: 1,
24
+ winner: null
25
+ }), saveFixture("v2 finished", 2, {
26
+ heap: 0,
27
+ turn: 0,
28
+ winner: 1,
29
+ moves: 6
30
+ })];
31
+ var legalCounts = (heap) => [
32
+ 1,
33
+ 2,
34
+ 3
35
+ ].filter((n) => n <= heap);
36
+ var nim = defineEngine({
37
+ rulesVersion: 1,
38
+ schemas: {
39
+ config: guardSchema(isNimConfig, "Expected { heap }."),
40
+ command: guardSchema(isNimCommand, "Expected { type: \"take\", count }."),
41
+ state: guardSchema(isNimState, "Not a Nim state."),
42
+ view: guardSchema(isNimState, "Not a Nim view."),
43
+ event: guardSchema(isNimEvent, "Not a Nim event.")
44
+ },
45
+ init: (config, seats, rng) => seats.length === 2 ? ok({
46
+ heap: config.heap,
47
+ turn: rng.int(0, 2),
48
+ winner: null,
49
+ moves: 0
50
+ }) : err(gameError("Seats", "Nim needs two players.")),
51
+ apply: (state, input) => {
52
+ if (state.winner !== null) return err(gameError("GameOver", "The game is over."));
53
+ switch (input.kind) {
54
+ case "command": {
55
+ if (input.seat !== state.turn) return err(gameError("NotYourTurn", "It's not your turn."));
56
+ const { count } = input.command;
57
+ if (!legalCounts(state.heap).includes(count)) return err(gameError("IllegalMove", "Take one, two or three.", { count }));
58
+ const heap = state.heap - count;
59
+ const next = {
60
+ heap,
61
+ turn: 1 - state.turn,
62
+ winner: heap === 0 ? input.seat : null,
63
+ moves: state.moves + 1
64
+ };
65
+ return ok({
66
+ state: next,
67
+ beats: [{ event: {
68
+ type: "took",
69
+ seat: input.seat,
70
+ count
71
+ } }]
72
+ });
73
+ }
74
+ case "resign":
75
+ if (input.seat !== 0 && input.seat !== 1) return err(gameError("NoSuchSeat", "There is no such seat."));
76
+ return ok({
77
+ state: {
78
+ ...state,
79
+ winner: 1 - input.seat
80
+ },
81
+ beats: [{ event: {
82
+ type: "resigned",
83
+ seat: input.seat
84
+ } }]
85
+ });
86
+ default: return err(gameError("Unsupported", "Nim has no clocks, seat changes or jobs."));
87
+ }
88
+ },
89
+ pending: (state) => state.winner === null ? [state.turn] : [],
90
+ viewFor: (state) => state,
91
+ eventFor: (event) => event,
92
+ outcome: (state) => state.winner === null ? null : {
93
+ result: "won",
94
+ placements: [{
95
+ seat: state.winner,
96
+ place: 1
97
+ }, {
98
+ seat: 1 - state.winner,
99
+ place: 2
100
+ }],
101
+ summary: `Seat ${state.winner + 1} wins.`,
102
+ stats: { moves: state.moves }
103
+ },
104
+ ai: {
105
+ levels: ["random", "perfect"],
106
+ choose: (view, _seat, level, rng) => {
107
+ const counts = legalCounts(view.heap);
108
+ const perfect = view.heap % 4;
109
+ return {
110
+ type: "take",
111
+ count: level === "perfect" && perfect > 0 ? perfect : counts[rng.int(0, counts.length)] ?? 1
112
+ };
113
+ }
114
+ }
115
+ });
116
+ /** Conformance options for the Nim fixture. */
117
+ var nimConformance = {
118
+ name: "nim",
119
+ configs: [{ heap: 5 }, { heap: 21 }],
120
+ seatCounts: [2],
121
+ commands: () => [
122
+ 0,
123
+ 1,
124
+ 2,
125
+ 3,
126
+ 4
127
+ ].map((count) => ({
128
+ type: "take",
129
+ count
130
+ })),
131
+ invariants: (s) => s.heap < 0 ? "the heap is negative" : s.heap === 0 && s.winner === null ? "an empty heap without a winner" : true,
132
+ saves: {
133
+ version: 2,
134
+ migrations: nimMigrations,
135
+ fixtures: nimSaveFixtures
136
+ }
137
+ };
138
+ //#endregion
139
+ export { NIM_SAVE_VERSION, isNimState, nim, nimConformance, nimMigrations, nimSaveFixtures };
140
+
141
+ //# sourceMappingURL=nim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nim.js","names":[],"sources":["../../src/fixtures/nim.ts"],"sourcesContent":["// Nim: two players take one to three from a heap; whoever takes the last one wins. All state\n// is public. The fixture also has a save history (v1 lacked the move counter) and an AI.\n\nimport { defineEngine, err, gameError, ok, type Json, type Migration, type SeatId } from '@joystyk/contracts';\nimport { guardSchema, saveFixture } from '../builders.ts';\nimport type { EngineConformanceOptions } from '../engine-conformance.ts';\nimport { isInt, isRecord, isSeat, isSeatOrNull } from './guards.ts';\n\nexport type NimState = {\n readonly heap: number;\n readonly turn: SeatId;\n readonly winner: SeatId | null;\n readonly moves: number;\n};\nexport type NimCommand = { readonly type: 'take'; readonly count: number };\nexport type NimView = NimState;\nexport type NimEvent =\n | { readonly type: 'took'; readonly seat: SeatId; readonly count: number }\n | { readonly type: 'resigned'; readonly seat: SeatId };\nexport type NimConfig = { readonly heap: number };\n\nexport const isNimState = (v: unknown): v is NimState =>\n isRecord(v) &&\n isInt(v['heap'], 0) &&\n isSeat(v['turn']) &&\n v['turn'] < 2 &&\n isSeatOrNull(v['winner']) &&\n isInt(v['moves'], 0);\nconst isNimCommand = (v: unknown): v is NimCommand => isRecord(v) && v['type'] === 'take' && isInt(v['count']);\nconst isNimEvent = (v: unknown): v is NimEvent =>\n isRecord(v) &&\n ((v['type'] === 'took' && isSeat(v['seat']) && isInt(v['count'])) || (v['type'] === 'resigned' && isSeat(v['seat'])));\nconst isNimConfig = (v: unknown): v is NimConfig => isRecord(v) && isInt(v['heap'], 1, 1000);\n\n/** The current save version of Nim's state. */\nexport const NIM_SAVE_VERSION = 2;\n\n/** v1 → v2 added `moves`; a v1 save restarts the count at 0. */\nexport const nimMigrations: readonly Migration[] = [\n {\n from: 1,\n to: 2,\n migrate: (old: Json) =>\n isRecord(old) ? ok({ ...old, moves: 0 }) : err(gameError('BadSave', 'This saved game is damaged.')),\n },\n];\n\n/** Stored states for every shipped save version. */\nexport const nimSaveFixtures = [\n saveFixture('v1 mid-game', 1, { heap: 7, turn: 1, winner: null }),\n saveFixture('v2 finished', 2, { heap: 0, turn: 0, winner: 1, moves: 6 }),\n];\n\nconst legalCounts = (heap: number): number[] => [1, 2, 3].filter((n) => n <= heap);\n\nexport const nim = defineEngine<NimState, NimCommand, NimView, NimEvent, NimConfig>({\n rulesVersion: 1,\n schemas: {\n config: guardSchema(isNimConfig, 'Expected { heap }.'),\n command: guardSchema(isNimCommand, 'Expected { type: \"take\", count }.'),\n state: guardSchema(isNimState, 'Not a Nim state.'),\n view: guardSchema(isNimState, 'Not a Nim view.'),\n event: guardSchema(isNimEvent, 'Not a Nim event.'),\n },\n init: (config: NimConfig, seats, rng) =>\n seats.length === 2\n ? ok({ heap: config.heap, turn: rng.int(0, 2), winner: null, moves: 0 })\n : err(gameError('Seats', 'Nim needs two players.')),\n apply: (state: NimState, input) => {\n if (state.winner !== null) return err(gameError('GameOver', 'The game is over.'));\n switch (input.kind) {\n case 'command': {\n if (input.seat !== state.turn) return err(gameError('NotYourTurn', \"It's not your turn.\"));\n const { count } = input.command;\n if (!legalCounts(state.heap).includes(count))\n return err(gameError('IllegalMove', 'Take one, two or three.', { count }));\n const heap = state.heap - count;\n const next: NimState = {\n heap,\n turn: 1 - state.turn,\n winner: heap === 0 ? input.seat : null,\n moves: state.moves + 1,\n };\n return ok({ state: next, beats: [{ event: { type: 'took', seat: input.seat, count } }] });\n }\n case 'resign': {\n if (input.seat !== 0 && input.seat !== 1) return err(gameError('NoSuchSeat', 'There is no such seat.'));\n return ok({\n state: { ...state, winner: 1 - input.seat },\n beats: [{ event: { type: 'resigned', seat: input.seat } }],\n });\n }\n default:\n return err(gameError('Unsupported', 'Nim has no clocks, seat changes or jobs.'));\n }\n },\n pending: (state) => (state.winner === null ? [state.turn] : []),\n viewFor: (state) => state,\n eventFor: (event) => event,\n outcome: (state) =>\n state.winner === null\n ? null\n : {\n result: 'won',\n placements: [\n { seat: state.winner, place: 1 },\n { seat: 1 - state.winner, place: 2 },\n ],\n summary: `Seat ${state.winner + 1} wins.`,\n stats: { moves: state.moves },\n },\n ai: {\n levels: ['random', 'perfect'],\n choose: (view, _seat, level, rng) => {\n const counts = legalCounts(view.heap);\n const perfect = view.heap % 4;\n const count = level === 'perfect' && perfect > 0 ? perfect : (counts[rng.int(0, counts.length)] ?? 1);\n return { type: 'take', count };\n },\n },\n});\n\n/** Conformance options for the Nim fixture. */\nexport const nimConformance: EngineConformanceOptions<NimState, NimCommand, NimConfig> = {\n name: 'nim',\n configs: [{ heap: 5 }, { heap: 21 }],\n seatCounts: [2],\n commands: () => [0, 1, 2, 3, 4].map((count) => ({ type: 'take', count })),\n invariants: (s) =>\n s.heap < 0 ? 'the heap is negative' : s.heap === 0 && s.winner === null ? 'an empty heap without a winner' : true,\n saves: { version: NIM_SAVE_VERSION, migrations: nimMigrations, fixtures: nimSaveFixtures },\n};\n"],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { type SeatId } from '@joystyk/contracts';
2
+ import type { EngineConformanceOptions } from '../engine-conformance.js';
3
+ export type SealedBidsState = {
4
+ readonly round: number;
5
+ readonly rounds: number;
6
+ /** This round's bids; null until the seat bids. Hidden from everyone else. */
7
+ readonly bids: readonly (number | null)[];
8
+ readonly scores: readonly number[];
9
+ /** Revealed bids of finished rounds. */
10
+ readonly history: readonly (readonly number[])[];
11
+ readonly resigned: SeatId | null;
12
+ };
13
+ export type SealedBidsCommand = {
14
+ readonly type: 'bid';
15
+ readonly amount: number;
16
+ };
17
+ export type SealedBidsEvent = {
18
+ readonly type: 'bid';
19
+ readonly seat: SeatId;
20
+ readonly amount: number | null;
21
+ } | {
22
+ readonly type: 'revealed';
23
+ readonly bids: readonly number[];
24
+ } | {
25
+ readonly type: 'resigned';
26
+ readonly seat: SeatId;
27
+ };
28
+ export type SealedBidsView = {
29
+ readonly round: number;
30
+ readonly rounds: number;
31
+ readonly myBid: number | null;
32
+ readonly submitted: readonly boolean[];
33
+ readonly scores: readonly number[];
34
+ readonly history: readonly (readonly number[])[];
35
+ readonly resigned: SeatId | null;
36
+ };
37
+ export type SealedBidsConfig = {
38
+ readonly rounds: number;
39
+ };
40
+ export declare const sealedBids: import("@joystyk/contracts").DiscreteEngine<SealedBidsState, SealedBidsCommand, SealedBidsView, SealedBidsEvent, SealedBidsConfig>;
41
+ /** Re-draws every bid `viewer` can't see. */
42
+ export declare const perturbSealedBids: (state: SealedBidsState, viewer: SeatId | null, rng: {
43
+ int(min: number, max: number): number;
44
+ }) => SealedBidsState;
45
+ /** Conformance options for the Sealed Bids fixture. */
46
+ export declare const sealedBidsConformance: EngineConformanceOptions<SealedBidsState, SealedBidsCommand, SealedBidsConfig>;
@@ -0,0 +1,141 @@
1
+ import { guardSchema } from "../builders.js";
2
+ import { isArrayOf, isInt, isRecord, isSeat, isSeatOrNull, rankPlaces } from "./guards.js";
3
+ import { defineEngine, err, gameError, ok } from "@joystyk/contracts";
4
+ //#region src/fixtures/sealed-bids.ts
5
+ var isBid = (v) => isInt(v, 1, 3);
6
+ var isScores = (v) => isArrayOf(v, (n) => isInt(n, 0));
7
+ var isHistory = (v) => isArrayOf(v, (r) => isArrayOf(r, isBid));
8
+ var isState = (v) => isRecord(v) && isInt(v["round"], 1) && isInt(v["rounds"], 1) && isArrayOf(v["bids"], (b) => b === null || isBid(b)) && isScores(v["scores"]) && isHistory(v["history"]) && isSeatOrNull(v["resigned"]);
9
+ var isView = (v) => isRecord(v) && isInt(v["round"], 1) && isInt(v["rounds"], 1) && (v["myBid"] === null || isBid(v["myBid"])) && isArrayOf(v["submitted"], (b) => typeof b === "boolean") && isScores(v["scores"]) && isHistory(v["history"]) && isSeatOrNull(v["resigned"]);
10
+ var isCommand = (v) => isRecord(v) && v["type"] === "bid" && isInt(v["amount"]);
11
+ var isEvent = (v) => isRecord(v) && (v["type"] === "bid" && isSeat(v["seat"]) && (v["amount"] === null || isBid(v["amount"])) || v["type"] === "revealed" && isArrayOf(v["bids"], isBid) || v["type"] === "resigned" && isSeat(v["seat"]));
12
+ var isConfig = (v) => isRecord(v) && isInt(v["rounds"], 1, 20);
13
+ var over = (s) => s.resigned !== null || s.round > s.rounds;
14
+ var sealedBids = defineEngine({
15
+ rulesVersion: 1,
16
+ schemas: {
17
+ config: guardSchema(isConfig, "Expected { rounds } from 1 to 20."),
18
+ command: guardSchema(isCommand, "Expected { type: \"bid\", amount }."),
19
+ state: guardSchema(isState, "Not a Sealed Bids state."),
20
+ view: guardSchema(isView, "Not a Sealed Bids view."),
21
+ event: guardSchema(isEvent, "Not a Sealed Bids event.")
22
+ },
23
+ init: (config, seats) => seats.length < 2 || seats.length > 4 ? err(gameError("Seats", "Sealed Bids needs two to four players.")) : ok({
24
+ round: 1,
25
+ rounds: config.rounds,
26
+ bids: seats.map(() => null),
27
+ scores: seats.map(() => 0),
28
+ history: [],
29
+ resigned: null
30
+ }),
31
+ apply: (state, input) => {
32
+ if (over(state)) return err(gameError("GameOver", "The game is over."));
33
+ if (input.kind === "resign") {
34
+ if (state.bids[input.seat] === void 0) return err(gameError("NoSuchSeat", "There is no such seat."));
35
+ return ok({
36
+ state: {
37
+ ...state,
38
+ resigned: input.seat
39
+ },
40
+ beats: [{ event: {
41
+ type: "resigned",
42
+ seat: input.seat
43
+ } }]
44
+ });
45
+ }
46
+ if (input.kind !== "command") return err(gameError("Unsupported", "Sealed Bids has no clocks, seat changes or jobs."));
47
+ if (state.bids[input.seat] !== null) return err(gameError("NotYourTurn", "You have already bid this round."));
48
+ const { amount } = input.command;
49
+ if (!isBid(amount)) return err(gameError("IllegalBid", "Bid 1, 2 or 3."));
50
+ const bids = state.bids.map((b, s) => s === input.seat ? amount : b);
51
+ const placed = { event: {
52
+ type: "bid",
53
+ seat: input.seat,
54
+ amount
55
+ } };
56
+ if (bids.some((b) => b === null)) return ok({
57
+ state: {
58
+ ...state,
59
+ bids
60
+ },
61
+ beats: [placed]
62
+ });
63
+ const revealed = bids;
64
+ const scores = state.scores.map((score, s) => {
65
+ const bid = revealed[s] ?? 0;
66
+ return revealed.filter((b) => b === bid).length === 1 ? score + bid : score;
67
+ });
68
+ const next = {
69
+ ...state,
70
+ round: state.round + 1,
71
+ bids: bids.map(() => null),
72
+ scores,
73
+ history: [...state.history, revealed]
74
+ };
75
+ return ok({
76
+ state: next,
77
+ beats: [placed, { event: {
78
+ type: "revealed",
79
+ bids: revealed
80
+ } }]
81
+ });
82
+ },
83
+ pending: (state) => over(state) ? [] : state.bids.flatMap((b, s) => b === null ? [s] : []),
84
+ viewFor: (state, viewer) => ({
85
+ round: state.round,
86
+ rounds: state.rounds,
87
+ myBid: viewer === null ? null : state.bids[viewer] ?? null,
88
+ submitted: state.bids.map((b) => b !== null),
89
+ scores: state.scores,
90
+ history: state.history,
91
+ resigned: state.resigned
92
+ }),
93
+ eventFor: (event, viewer) => event.type === "bid" && event.seat !== viewer ? {
94
+ ...event,
95
+ amount: null
96
+ } : event,
97
+ outcome: (state) => over(state) ? {
98
+ result: "won",
99
+ placements: rankPlaces(state.scores, state.resigned),
100
+ summary: state.resigned === null ? `After ${state.rounds} rounds.` : `Seat ${state.resigned + 1} resigned.`
101
+ } : null,
102
+ decisionKey: (state, seat) => ({
103
+ round: state.round,
104
+ myBid: state.bids[seat] ?? null,
105
+ scores: state.scores,
106
+ history: state.history,
107
+ resigned: state.resigned
108
+ })
109
+ });
110
+ /** Re-draws every bid `viewer` can't see. */
111
+ var perturbSealedBids = (state, viewer, rng) => ({
112
+ ...state,
113
+ bids: state.bids.map((b, s) => b === null || s === viewer ? b : rng.int(1, 4))
114
+ });
115
+ /** Conformance options for the Sealed Bids fixture. */
116
+ var sealedBidsConformance = {
117
+ name: "sealed-bids",
118
+ configs: [{ rounds: 4 }],
119
+ seatCounts: [
120
+ 2,
121
+ 3,
122
+ 4
123
+ ],
124
+ games: 8,
125
+ commands: () => [
126
+ 0,
127
+ 1,
128
+ 2,
129
+ 3,
130
+ 4
131
+ ].map((amount) => ({
132
+ type: "bid",
133
+ amount
134
+ })),
135
+ perturbHidden: perturbSealedBids,
136
+ invariants: (s) => s.history.length === s.round - 1 ? true : "history and round disagree"
137
+ };
138
+ //#endregion
139
+ export { perturbSealedBids, sealedBids, sealedBidsConformance };
140
+
141
+ //# sourceMappingURL=sealed-bids.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealed-bids.js","names":[],"sources":["../../src/fixtures/sealed-bids.ts"],"sourcesContent":["// Sealed Bids: a simultaneous-move game for two to four players. Every round, all players\n// secretly bid 1, 2 or 3 at the same time. When the last bid arrives, the bids are revealed\n// and each unique bid scores its value. After the configured rounds the highest score wins.\n// It exercises several pending seats at once, hidden choices, redacted events and a\n// decisionKey that ignores who has already bid, so simultaneous bids are never stale.\n\nimport { defineEngine, err, gameError, ok, type SeatId } from '@joystyk/contracts';\nimport { guardSchema } from '../builders.ts';\nimport type { EngineConformanceOptions } from '../engine-conformance.ts';\nimport { isArrayOf, isInt, isRecord, isSeat, isSeatOrNull, rankPlaces } from './guards.ts';\n\nexport type SealedBidsState = {\n readonly round: number;\n readonly rounds: number;\n /** This round's bids; null until the seat bids. Hidden from everyone else. */\n readonly bids: readonly (number | null)[];\n readonly scores: readonly number[];\n /** Revealed bids of finished rounds. */\n readonly history: readonly (readonly number[])[];\n readonly resigned: SeatId | null;\n};\nexport type SealedBidsCommand = { readonly type: 'bid'; readonly amount: number };\nexport type SealedBidsEvent =\n | { readonly type: 'bid'; readonly seat: SeatId; readonly amount: number | null }\n | { readonly type: 'revealed'; readonly bids: readonly number[] }\n | { readonly type: 'resigned'; readonly seat: SeatId };\nexport type SealedBidsView = {\n readonly round: number;\n readonly rounds: number;\n readonly myBid: number | null;\n readonly submitted: readonly boolean[];\n readonly scores: readonly number[];\n readonly history: readonly (readonly number[])[];\n readonly resigned: SeatId | null;\n};\nexport type SealedBidsConfig = { readonly rounds: number };\n\nconst isBid = (v: unknown): v is number => isInt(v, 1, 3);\nconst isScores = (v: unknown): v is number[] => isArrayOf(v, (n): n is number => isInt(n, 0));\nconst isHistory = (v: unknown): v is number[][] => isArrayOf(v, (r): r is number[] => isArrayOf(r, isBid));\nconst isState = (v: unknown): v is SealedBidsState =>\n isRecord(v) &&\n isInt(v['round'], 1) &&\n isInt(v['rounds'], 1) &&\n isArrayOf(v['bids'], (b): b is number | null => b === null || isBid(b)) &&\n isScores(v['scores']) &&\n isHistory(v['history']) &&\n isSeatOrNull(v['resigned']);\nconst isView = (v: unknown): v is SealedBidsView =>\n isRecord(v) &&\n isInt(v['round'], 1) &&\n isInt(v['rounds'], 1) &&\n (v['myBid'] === null || isBid(v['myBid'])) &&\n isArrayOf(v['submitted'], (b): b is boolean => typeof b === 'boolean') &&\n isScores(v['scores']) &&\n isHistory(v['history']) &&\n isSeatOrNull(v['resigned']);\nconst isCommand = (v: unknown): v is SealedBidsCommand => isRecord(v) && v['type'] === 'bid' && isInt(v['amount']);\nconst isEvent = (v: unknown): v is SealedBidsEvent =>\n isRecord(v) &&\n ((v['type'] === 'bid' && isSeat(v['seat']) && (v['amount'] === null || isBid(v['amount']))) ||\n (v['type'] === 'revealed' && isArrayOf(v['bids'], isBid)) ||\n (v['type'] === 'resigned' && isSeat(v['seat'])));\nconst isConfig = (v: unknown): v is SealedBidsConfig => isRecord(v) && isInt(v['rounds'], 1, 20);\n\nconst over = (s: SealedBidsState): boolean => s.resigned !== null || s.round > s.rounds;\n\nexport const sealedBids = defineEngine<\n SealedBidsState,\n SealedBidsCommand,\n SealedBidsView,\n SealedBidsEvent,\n SealedBidsConfig\n>({\n rulesVersion: 1,\n schemas: {\n config: guardSchema(isConfig, 'Expected { rounds } from 1 to 20.'),\n command: guardSchema(isCommand, 'Expected { type: \"bid\", amount }.'),\n state: guardSchema(isState, 'Not a Sealed Bids state.'),\n view: guardSchema(isView, 'Not a Sealed Bids view.'),\n event: guardSchema(isEvent, 'Not a Sealed Bids event.'),\n },\n init: (config: SealedBidsConfig, seats) =>\n seats.length < 2 || seats.length > 4\n ? err(gameError('Seats', 'Sealed Bids needs two to four players.'))\n : ok({\n round: 1,\n rounds: config.rounds,\n bids: seats.map(() => null),\n scores: seats.map(() => 0),\n history: [],\n resigned: null,\n }),\n apply: (state: SealedBidsState, input) => {\n if (over(state)) return err(gameError('GameOver', 'The game is over.'));\n if (input.kind === 'resign') {\n if (state.bids[input.seat] === undefined) return err(gameError('NoSuchSeat', 'There is no such seat.'));\n return ok({\n state: { ...state, resigned: input.seat },\n beats: [{ event: { type: 'resigned', seat: input.seat } }],\n });\n }\n if (input.kind !== 'command')\n return err(gameError('Unsupported', 'Sealed Bids has no clocks, seat changes or jobs.'));\n if (state.bids[input.seat] !== null) return err(gameError('NotYourTurn', 'You have already bid this round.'));\n const { amount } = input.command;\n if (!isBid(amount)) return err(gameError('IllegalBid', 'Bid 1, 2 or 3.'));\n const bids = state.bids.map((b, s) => (s === input.seat ? amount : b));\n const placed = { event: { type: 'bid', seat: input.seat, amount } } as const;\n if (bids.some((b) => b === null)) return ok({ state: { ...state, bids }, beats: [placed] });\n const revealed = bids as number[];\n const scores = state.scores.map((score, s) => {\n const bid = revealed[s] ?? 0;\n return revealed.filter((b) => b === bid).length === 1 ? score + bid : score;\n });\n const next: SealedBidsState = {\n ...state,\n round: state.round + 1,\n bids: bids.map(() => null),\n scores,\n history: [...state.history, revealed],\n };\n return ok({ state: next, beats: [placed, { event: { type: 'revealed', bids: revealed } }] });\n },\n pending: (state) => (over(state) ? [] : state.bids.flatMap((b, s) => (b === null ? [s] : []))),\n viewFor: (state, viewer): SealedBidsView => ({\n round: state.round,\n rounds: state.rounds,\n myBid: viewer === null ? null : (state.bids[viewer] ?? null),\n submitted: state.bids.map((b) => b !== null),\n scores: state.scores,\n history: state.history,\n resigned: state.resigned,\n }),\n eventFor: (event, viewer) => (event.type === 'bid' && event.seat !== viewer ? { ...event, amount: null } : event),\n outcome: (state) =>\n over(state)\n ? {\n result: 'won',\n placements: rankPlaces(state.scores, state.resigned),\n summary: state.resigned === null ? `After ${state.rounds} rounds.` : `Seat ${state.resigned + 1} resigned.`,\n }\n : null,\n // Who has already bid doesn't change anyone else's decision.\n decisionKey: (state, seat) => ({\n round: state.round,\n myBid: state.bids[seat] ?? null,\n scores: state.scores,\n history: state.history,\n resigned: state.resigned,\n }),\n});\n\n/** Re-draws every bid `viewer` can't see. */\nexport const perturbSealedBids = (\n state: SealedBidsState,\n viewer: SeatId | null,\n rng: { int(min: number, max: number): number },\n): SealedBidsState => ({\n ...state,\n bids: state.bids.map((b, s) => (b === null || s === viewer ? b : rng.int(1, 4))),\n});\n\n/** Conformance options for the Sealed Bids fixture. */\nexport const sealedBidsConformance: EngineConformanceOptions<SealedBidsState, SealedBidsCommand, SealedBidsConfig> = {\n name: 'sealed-bids',\n configs: [{ rounds: 4 }],\n seatCounts: [2, 3, 4],\n games: 8,\n commands: () => [0, 1, 2, 3, 4].map((amount) => ({ type: 'bid', amount })),\n perturbHidden: perturbSealedBids,\n invariants: (s) => (s.history.length === s.round - 1 ? true : 'history and round disagree'),\n};\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export { checkEngine, ENGINE_CHECKS, type EngineCheckId, type EngineConformanceOptions, type EngineSetup, type HiddenTransition, type InvariantResult, type SaveOptions, } from './engine-conformance.js';
2
+ export { checkSimulation, SIMULATION_CHECKS, type SimulationCheckId, type SimulationConformanceOptions, } from './simulation-conformance.js';
3
+ export { ConformanceError, expectConformance, runEngineConformance, runSimulationConformance, type VitestOptions, } from './vitest.js';
4
+ export { checkById, failedChecks, formatCheck, formatFailure, formatReport, type CheckResult, type CheckStatus, type ConformanceReport, type Failure, } from './report.js';
5
+ export { replayEngine, replaySimulation, undoByReplay, type EngineReplayResult, type ReplayedInput, type SimulationReplayResult, type UndoResult, } from './replay.js';
6
+ export { cloneRng, testRng, testSeed, testSeeds } from './rng.js';
7
+ export { createFakeClock, type FakeClock, type TimerHandle } from './clock.js';
8
+ export { manualFrames, nextTask, pause, WAIT_BUDGET_MS, waitUntil, type FrameWaitOptions, type ManualFrames, type WaitOptions, } from './waiting.js';
9
+ export { createFakeStorage, type FakeStorage, type StorageCall, type StorageOperation } from './storage.js';
10
+ export { createMemorySaveStore, runnerClock, type MemorySaveStore } from './session-ports.js';
11
+ export { anySchema, commandInput, engineReplay, guardSchema, isEngineReplay, jobFailure, jobResult, resignInput, saveEnvelope, saveFixture, seatInput, seats, simulationReplay, timeoutInput, type SaveFixture, } from './builders.js';
12
+ export { formatIssues, validate, type Validation } from './schema.js';
13
+ export { deepEqual, deepFreeze, firstDifference, jsonRoundTrip } from './deep.js';
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import { anySchema, commandInput, engineReplay, guardSchema, isEngineReplay, jobFailure, jobResult, resignInput, saveEnvelope, saveFixture, seatInput, seats, simulationReplay, timeoutInput } from "./builders.js";
2
+ import { createFakeClock } from "./clock.js";
3
+ import { deepEqual, deepFreeze, firstDifference, jsonRoundTrip } from "./deep.js";
4
+ import { replayEngine, replaySimulation, undoByReplay } from "./replay.js";
5
+ import { checkById, failedChecks, formatCheck, formatFailure, formatReport } from "./report.js";
6
+ import { cloneRng, testRng, testSeed, testSeeds } from "./rng.js";
7
+ import { formatIssues, validate } from "./schema.js";
8
+ import { ENGINE_CHECKS, checkEngine } from "./engine-conformance.js";
9
+ import { SIMULATION_CHECKS, checkSimulation } from "./simulation-conformance.js";
10
+ import { ConformanceError, expectConformance, runEngineConformance, runSimulationConformance } from "./vitest.js";
11
+ import { WAIT_BUDGET_MS, manualFrames, nextTask, pause, waitUntil } from "./waiting.js";
12
+ import { createFakeStorage } from "./storage.js";
13
+ import { createMemorySaveStore, runnerClock } from "./session-ports.js";
14
+ export { ConformanceError, ENGINE_CHECKS, SIMULATION_CHECKS, WAIT_BUDGET_MS, anySchema, checkById, checkEngine, checkSimulation, cloneRng, commandInput, createFakeClock, createFakeStorage, createMemorySaveStore, deepEqual, deepFreeze, engineReplay, expectConformance, failedChecks, firstDifference, formatCheck, formatFailure, formatIssues, formatReport, guardSchema, isEngineReplay, jobFailure, jobResult, jsonRoundTrip, manualFrames, nextTask, pause, replayEngine, replaySimulation, resignInput, runEngineConformance, runSimulationConformance, runnerClock, saveEnvelope, saveFixture, seatInput, seats, simulationReplay, testRng, testSeed, testSeeds, timeoutInput, undoByReplay, validate, waitUntil };
@@ -0,0 +1,58 @@
1
+ import { type AssistKind, type DiscreteEngine, type EngineAssist, type EngineReplay, type FixedStepSimulation, type Json, type Outcome, type Result, type SimulationReplay } from '@joystyk/contracts';
2
+ /** What one replayed input's transition flagged. */
3
+ export type ReplayedInput = {
4
+ readonly barrier: boolean;
5
+ readonly assist: EngineAssist | null;
6
+ /** Whether the input was a command from a seat a human controlled at the time. */
7
+ readonly humanCommand: boolean;
8
+ };
9
+ /** The end of a Layer 2 replay. */
10
+ export type EngineReplayResult<State> = {
11
+ readonly state: State;
12
+ readonly outcome: Outcome | null;
13
+ /** One entry per input, in order. */
14
+ readonly inputs: readonly ReplayedInput[];
15
+ /** One flag per input: whether its transition set `undoBarrier`. */
16
+ readonly barriers: readonly boolean[];
17
+ };
18
+ /**
19
+ * Replays a Layer 2 log: `init` with the seed, then every input in order with the same
20
+ * generator. Fails with `ReplayInitFailed` or `ReplayInputRejected` (detail: the input's
21
+ * index and the engine's error) when the log no longer applies, as it won't after a rules
22
+ * change. An engine that throws is a bug, so the exception propagates.
23
+ */
24
+ export declare const replayEngine: <State extends Json, Command extends Json, View extends Json, Event extends Json, Config extends Json>(engine: DiscreteEngine<State, Command, View, Event, Config>, replay: EngineReplay<Command, Config>) => Result<EngineReplayResult<State>>;
25
+ /** The result of one undo by replay. */
26
+ export type UndoResult<State, Command extends Json, Config extends Json> = {
27
+ /** The replay without the undone inputs. */
28
+ readonly replay: EngineReplay<Command, Config>;
29
+ readonly state: State;
30
+ /** How many inputs were dropped: the human's command and everything after it. */
31
+ readonly dropped: number;
32
+ /**
33
+ * Every assistance kind used in the game before the undo, in `ASSIST_KINDS` order, `'undo'`
34
+ * included. The record only grows: undoing a hint doesn't remove `'hint'`.
35
+ */
36
+ readonly assists: readonly AssistKind[];
37
+ };
38
+ /**
39
+ * Undo by replay, as the local runner performs it (D-32): it returns to the start of the
40
+ * human seat's previous decision, dropping the latest command from a human-controlled seat
41
+ * and everything after it (the AI's replies, job outcomes), then replays the rest from the
42
+ * seed. It refuses with `NothingToUndo` when no human command is left and with
43
+ * `UndoBarrier` when any dropped input's transition set `undoBarrier`, so undo never steps
44
+ * back past a barrier nor past anything before it. Barrier and assistance flags come from
45
+ * replaying the log, never from a separate record. Undo policies and limits are the
46
+ * runner's business, not this helper's.
47
+ */
48
+ export declare const undoByReplay: <State extends Json, Command extends Json, View extends Json, Event extends Json, Config extends Json>(engine: DiscreteEngine<State, Command, View, Event, Config>, replay: EngineReplay<Command, Config>) => Result<UndoResult<State, Command, Config>>;
49
+ /** The end of a Layer 3 replay. */
50
+ export type SimulationReplayResult<State> = {
51
+ readonly state: State;
52
+ readonly outcome: Outcome | null;
53
+ };
54
+ /**
55
+ * Replays a Layer 3 log: `init` with the seed, then `step` from tick 0 up to `endTick`
56
+ * (exclusive), giving each tick the inputs assigned to it in array order.
57
+ */
58
+ export declare const replaySimulation: <State extends Json, Input extends Json, View, Event extends Json, Config extends Json>(simulation: FixedStepSimulation<State, Input, View, Event, Config>, replay: SimulationReplay<Input, Config>) => Result<SimulationReplayResult<State>>;
package/dist/replay.js ADDED
@@ -0,0 +1,110 @@
1
+ import { ASSIST_KINDS, err, gameError, ok } from "@joystyk/contracts";
2
+ import { restoreRng } from "@joystyk/kit";
3
+ //#region src/replay.ts
4
+ var ENGINE_ASSISTS = [
5
+ "hint",
6
+ "check",
7
+ "reveal",
8
+ "shuffle"
9
+ ];
10
+ var seatsAfter = (seats, input) => {
11
+ if (input.kind !== "seat" || input.change.type !== "controller") return seats;
12
+ const { change } = input;
13
+ return seats.map((seat) => seat.id !== input.seat ? seat : change.aiLevel === void 0 ? {
14
+ id: seat.id,
15
+ name: change.name,
16
+ controller: change.controller
17
+ } : {
18
+ id: seat.id,
19
+ name: change.name,
20
+ controller: change.controller,
21
+ aiLevel: change.aiLevel
22
+ });
23
+ };
24
+ /**
25
+ * Replays a Layer 2 log: `init` with the seed, then every input in order with the same
26
+ * generator. Fails with `ReplayInitFailed` or `ReplayInputRejected` (detail: the input's
27
+ * index and the engine's error) when the log no longer applies, as it won't after a rules
28
+ * change. An engine that throws is a bug, so the exception propagates.
29
+ */
30
+ var replayEngine = (engine, replay) => {
31
+ const rng = restoreRng(replay.seed);
32
+ const started = engine.init(replay.config, replay.seats, rng);
33
+ if (!started.ok) return err(gameError("ReplayInitFailed", "The replay could not start.", { error: started.error }));
34
+ let state = started.value;
35
+ let seats = replay.seats;
36
+ const inputs = [];
37
+ for (const [index, input] of replay.inputs.entries()) {
38
+ const applied = engine.apply(state, input, rng);
39
+ if (!applied.ok) return err(gameError("ReplayInputRejected", "The replay no longer applies.", {
40
+ index,
41
+ error: applied.error
42
+ }));
43
+ const { assist } = applied.value;
44
+ inputs.push({
45
+ barrier: applied.value.undoBarrier === true,
46
+ assist: ENGINE_ASSISTS.includes(assist) ? assist : null,
47
+ humanCommand: input.kind === "command" && seats.find((s) => s.id === input.seat)?.controller === "human"
48
+ });
49
+ seats = seatsAfter(seats, input);
50
+ state = applied.value.state;
51
+ }
52
+ return ok({
53
+ state,
54
+ outcome: engine.outcome(state),
55
+ inputs,
56
+ barriers: inputs.map((i) => i.barrier)
57
+ });
58
+ };
59
+ /**
60
+ * Undo by replay, as the local runner performs it (D-32): it returns to the start of the
61
+ * human seat's previous decision, dropping the latest command from a human-controlled seat
62
+ * and everything after it (the AI's replies, job outcomes), then replays the rest from the
63
+ * seed. It refuses with `NothingToUndo` when no human command is left and with
64
+ * `UndoBarrier` when any dropped input's transition set `undoBarrier`, so undo never steps
65
+ * back past a barrier nor past anything before it. Barrier and assistance flags come from
66
+ * replaying the log, never from a separate record. Undo policies and limits are the
67
+ * runner's business, not this helper's.
68
+ */
69
+ var undoByReplay = (engine, replay) => {
70
+ const full = replayEngine(engine, replay);
71
+ if (!full.ok) return full;
72
+ const cut = full.value.inputs.findLastIndex((input) => input.humanCommand);
73
+ if (cut < 0) return err(gameError("NothingToUndo", "There is nothing to undo."));
74
+ const barrier = full.value.inputs.findIndex((input, index) => index >= cut && input.barrier);
75
+ if (barrier >= 0) return err(gameError("UndoBarrier", "That move can't be undone.", { index: barrier }));
76
+ const shorter = {
77
+ ...replay,
78
+ inputs: replay.inputs.slice(0, cut)
79
+ };
80
+ const previous = replayEngine(engine, shorter);
81
+ if (!previous.ok) return previous;
82
+ const used = /* @__PURE__ */ new Set(["undo", ...full.value.inputs.flatMap((input) => input.assist === null ? [] : [input.assist])]);
83
+ return ok({
84
+ replay: shorter,
85
+ state: previous.value.state,
86
+ dropped: replay.inputs.length - cut,
87
+ assists: ASSIST_KINDS.filter((kind) => used.has(kind))
88
+ });
89
+ };
90
+ /**
91
+ * Replays a Layer 3 log: `init` with the seed, then `step` from tick 0 up to `endTick`
92
+ * (exclusive), giving each tick the inputs assigned to it in array order.
93
+ */
94
+ var replaySimulation = (simulation, replay) => {
95
+ const rng = restoreRng(replay.seed);
96
+ const started = simulation.init(replay.config, rng);
97
+ if (!started.ok) return err(gameError("ReplayInitFailed", "The replay could not start.", { error: started.error }));
98
+ const byTick = /* @__PURE__ */ new Map();
99
+ for (const { tick, input } of replay.inputs) byTick.set(tick, [...byTick.get(tick) ?? [], input]);
100
+ let state = started.value;
101
+ for (let tick = 0; tick < replay.endTick; tick += 1) state = simulation.step(state, byTick.get(tick) ?? [], rng).state;
102
+ return ok({
103
+ state,
104
+ outcome: simulation.outcome(state)
105
+ });
106
+ };
107
+ //#endregion
108
+ export { replayEngine, replaySimulation, undoByReplay };
109
+
110
+ //# sourceMappingURL=replay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.js","names":[],"sources":["../src/replay.ts"],"sourcesContent":["// Replay and undo by replay, as the platform performs them (game-contract.md, \"Replays\" and\n// \"Undo, barriers and assistance\"). The session package's runners do the same in production;\n// these versions also report what each input's transition flagged, which tests need.\n\nimport {\n ASSIST_KINDS,\n err,\n gameError,\n ok,\n type AssistKind,\n type DiscreteEngine,\n type EngineAssist,\n type EngineInput,\n type EngineReplay,\n type FixedStepSimulation,\n type Json,\n type Outcome,\n type Result,\n type SeatInfo,\n type SimulationReplay,\n} from '@joystyk/contracts';\nimport { restoreRng } from '@joystyk/kit';\n\n/** What one replayed input's transition flagged. */\nexport type ReplayedInput = {\n readonly barrier: boolean;\n readonly assist: EngineAssist | null;\n /** Whether the input was a command from a seat a human controlled at the time. */\n readonly humanCommand: boolean;\n};\n\n/** The end of a Layer 2 replay. */\nexport type EngineReplayResult<State> = {\n readonly state: State;\n readonly outcome: Outcome | null;\n /** One entry per input, in order. */\n readonly inputs: readonly ReplayedInput[];\n /** One flag per input: whether its transition set `undoBarrier`. */\n readonly barriers: readonly boolean[];\n};\n\nconst ENGINE_ASSISTS: readonly unknown[] = ['hint', 'check', 'reveal', 'shuffle'];\n\nconst seatsAfter = (seats: readonly SeatInfo[], input: EngineInput<unknown>): readonly SeatInfo[] => {\n if (input.kind !== 'seat' || input.change.type !== 'controller') return seats;\n const { change } = input;\n return seats.map((seat) =>\n seat.id !== input.seat\n ? seat\n : change.aiLevel === undefined\n ? { id: seat.id, name: change.name, controller: change.controller }\n : { id: seat.id, name: change.name, controller: change.controller, aiLevel: change.aiLevel },\n );\n};\n\n/**\n * Replays a Layer 2 log: `init` with the seed, then every input in order with the same\n * generator. Fails with `ReplayInitFailed` or `ReplayInputRejected` (detail: the input's\n * index and the engine's error) when the log no longer applies, as it won't after a rules\n * change. An engine that throws is a bug, so the exception propagates.\n */\nexport const replayEngine = <\n State extends Json,\n Command extends Json,\n View extends Json,\n Event extends Json,\n Config extends Json,\n>(\n engine: DiscreteEngine<State, Command, View, Event, Config>,\n replay: EngineReplay<Command, Config>,\n): Result<EngineReplayResult<State>> => {\n const rng = restoreRng(replay.seed);\n const started = engine.init(replay.config, replay.seats, rng);\n if (!started.ok) return err(gameError('ReplayInitFailed', 'The replay could not start.', { error: started.error }));\n let state = started.value;\n let seats = replay.seats;\n const inputs: ReplayedInput[] = [];\n for (const [index, input] of replay.inputs.entries()) {\n const applied = engine.apply(state, input, rng);\n if (!applied.ok) {\n return err(gameError('ReplayInputRejected', 'The replay no longer applies.', { index, error: applied.error }));\n }\n const { assist } = applied.value;\n inputs.push({\n barrier: applied.value.undoBarrier === true,\n assist: ENGINE_ASSISTS.includes(assist) ? (assist as EngineAssist) : null,\n humanCommand: input.kind === 'command' && seats.find((s) => s.id === input.seat)?.controller === 'human',\n });\n seats = seatsAfter(seats, input);\n state = applied.value.state;\n }\n return ok({ state, outcome: engine.outcome(state), inputs, barriers: inputs.map((i) => i.barrier) });\n};\n\n/** The result of one undo by replay. */\nexport type UndoResult<State, Command extends Json, Config extends Json> = {\n /** The replay without the undone inputs. */\n readonly replay: EngineReplay<Command, Config>;\n readonly state: State;\n /** How many inputs were dropped: the human's command and everything after it. */\n readonly dropped: number;\n /**\n * Every assistance kind used in the game before the undo, in `ASSIST_KINDS` order, `'undo'`\n * included. The record only grows: undoing a hint doesn't remove `'hint'`.\n */\n readonly assists: readonly AssistKind[];\n};\n\n/**\n * Undo by replay, as the local runner performs it (D-32): it returns to the start of the\n * human seat's previous decision, dropping the latest command from a human-controlled seat\n * and everything after it (the AI's replies, job outcomes), then replays the rest from the\n * seed. It refuses with `NothingToUndo` when no human command is left and with\n * `UndoBarrier` when any dropped input's transition set `undoBarrier`, so undo never steps\n * back past a barrier nor past anything before it. Barrier and assistance flags come from\n * replaying the log, never from a separate record. Undo policies and limits are the\n * runner's business, not this helper's.\n */\nexport const undoByReplay = <\n State extends Json,\n Command extends Json,\n View extends Json,\n Event extends Json,\n Config extends Json,\n>(\n engine: DiscreteEngine<State, Command, View, Event, Config>,\n replay: EngineReplay<Command, Config>,\n): Result<UndoResult<State, Command, Config>> => {\n const full = replayEngine(engine, replay);\n if (!full.ok) return full;\n const cut = full.value.inputs.findLastIndex((input) => input.humanCommand);\n if (cut < 0) return err(gameError('NothingToUndo', 'There is nothing to undo.'));\n const barrier = full.value.inputs.findIndex((input, index) => index >= cut && input.barrier);\n if (barrier >= 0) return err(gameError('UndoBarrier', \"That move can't be undone.\", { index: barrier }));\n const shorter: EngineReplay<Command, Config> = { ...replay, inputs: replay.inputs.slice(0, cut) };\n const previous = replayEngine(engine, shorter);\n if (!previous.ok) return previous;\n const used = new Set<AssistKind>([\n 'undo',\n ...full.value.inputs.flatMap((input) => (input.assist === null ? [] : [input.assist])),\n ]);\n return ok({\n replay: shorter,\n state: previous.value.state,\n dropped: replay.inputs.length - cut,\n assists: ASSIST_KINDS.filter((kind) => used.has(kind)),\n });\n};\n\n/** The end of a Layer 3 replay. */\nexport type SimulationReplayResult<State> = {\n readonly state: State;\n readonly outcome: Outcome | null;\n};\n\n/**\n * Replays a Layer 3 log: `init` with the seed, then `step` from tick 0 up to `endTick`\n * (exclusive), giving each tick the inputs assigned to it in array order.\n */\nexport const replaySimulation = <State extends Json, Input extends Json, View, Event extends Json, Config extends Json>(\n simulation: FixedStepSimulation<State, Input, View, Event, Config>,\n replay: SimulationReplay<Input, Config>,\n): Result<SimulationReplayResult<State>> => {\n const rng = restoreRng(replay.seed);\n const started = simulation.init(replay.config, rng);\n if (!started.ok) return err(gameError('ReplayInitFailed', 'The replay could not start.', { error: started.error }));\n const byTick = new Map<number, Input[]>();\n for (const { tick, input } of replay.inputs) byTick.set(tick, [...(byTick.get(tick) ?? []), input]);\n let state = started.value;\n for (let tick = 0; tick < replay.endTick; tick += 1) {\n state = simulation.step(state, byTick.get(tick) ?? [], rng).state;\n }\n return ok({ state, outcome: simulation.outcome(state) });\n};\n"],"mappings":"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"}