@drmxrcy/tcg-core 0.0.0-202602060542

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 (157) hide show
  1. package/README.md +882 -0
  2. package/package.json +58 -0
  3. package/src/__tests__/alpha-clash-engine-definition.test.ts +319 -0
  4. package/src/__tests__/createMockAlphaClashGame.ts +462 -0
  5. package/src/__tests__/createMockGrandArchiveGame.ts +373 -0
  6. package/src/__tests__/createMockGundamGame.ts +379 -0
  7. package/src/__tests__/createMockLorcanaGame.ts +328 -0
  8. package/src/__tests__/createMockOnePieceGame.ts +429 -0
  9. package/src/__tests__/createMockRiftboundGame.ts +462 -0
  10. package/src/__tests__/grand-archive-engine-definition.test.ts +118 -0
  11. package/src/__tests__/gundam-engine-definition.test.ts +110 -0
  12. package/src/__tests__/integration-complete-game.test.ts +508 -0
  13. package/src/__tests__/integration-network-sync.test.ts +469 -0
  14. package/src/__tests__/lorcana-engine-definition.test.ts +100 -0
  15. package/src/__tests__/move-enumeration.test.ts +725 -0
  16. package/src/__tests__/multiplayer-engine.test.ts +555 -0
  17. package/src/__tests__/one-piece-engine-definition.test.ts +114 -0
  18. package/src/__tests__/riftbound-engine-definition.test.ts +124 -0
  19. package/src/actions/action-definition.test.ts +201 -0
  20. package/src/actions/action-definition.ts +122 -0
  21. package/src/actions/action-timing.test.ts +490 -0
  22. package/src/actions/action-timing.ts +257 -0
  23. package/src/cards/card-definition.test.ts +268 -0
  24. package/src/cards/card-definition.ts +27 -0
  25. package/src/cards/card-instance.test.ts +422 -0
  26. package/src/cards/card-instance.ts +49 -0
  27. package/src/cards/computed-properties.test.ts +530 -0
  28. package/src/cards/computed-properties.ts +84 -0
  29. package/src/cards/conditional-modifiers.test.ts +390 -0
  30. package/src/cards/modifiers.test.ts +286 -0
  31. package/src/cards/modifiers.ts +51 -0
  32. package/src/engine/MULTIPLAYER.md +425 -0
  33. package/src/engine/__tests__/rule-engine-flow.test.ts +348 -0
  34. package/src/engine/__tests__/rule-engine-history.test.ts +535 -0
  35. package/src/engine/__tests__/rule-engine-moves.test.ts +488 -0
  36. package/src/engine/__tests__/rule-engine.test.ts +366 -0
  37. package/src/engine/index.ts +14 -0
  38. package/src/engine/multiplayer-engine.example.ts +571 -0
  39. package/src/engine/multiplayer-engine.ts +409 -0
  40. package/src/engine/rule-engine.test.ts +286 -0
  41. package/src/engine/rule-engine.ts +1539 -0
  42. package/src/engine/tracker-system.ts +172 -0
  43. package/src/examples/__tests__/coin-flip-game.test.ts +641 -0
  44. package/src/filtering/card-filter.test.ts +230 -0
  45. package/src/filtering/card-filter.ts +91 -0
  46. package/src/filtering/card-query.test.ts +901 -0
  47. package/src/filtering/card-query.ts +273 -0
  48. package/src/filtering/filter-matching.test.ts +944 -0
  49. package/src/filtering/filter-matching.ts +315 -0
  50. package/src/flow/SERIALIZATION.md +428 -0
  51. package/src/flow/__tests__/flow-definition.test.ts +427 -0
  52. package/src/flow/__tests__/flow-manager.test.ts +756 -0
  53. package/src/flow/__tests__/flow-serialization.test.ts +565 -0
  54. package/src/flow/flow-definition.ts +453 -0
  55. package/src/flow/flow-manager.ts +1044 -0
  56. package/src/flow/index.ts +35 -0
  57. package/src/game-definition/__tests__/game-definition-validation.test.ts +359 -0
  58. package/src/game-definition/__tests__/game-definition.test.ts +291 -0
  59. package/src/game-definition/__tests__/move-definitions.test.ts +328 -0
  60. package/src/game-definition/game-definition.ts +261 -0
  61. package/src/game-definition/index.ts +28 -0
  62. package/src/game-definition/move-definitions.ts +188 -0
  63. package/src/game-definition/validation.ts +183 -0
  64. package/src/history/history-manager.test.ts +497 -0
  65. package/src/history/history-manager.ts +312 -0
  66. package/src/history/history-operations.ts +122 -0
  67. package/src/history/index.ts +9 -0
  68. package/src/history/types.ts +255 -0
  69. package/src/index.ts +32 -0
  70. package/src/logging/index.ts +27 -0
  71. package/src/logging/log-formatter.ts +187 -0
  72. package/src/logging/logger.ts +276 -0
  73. package/src/logging/types.ts +148 -0
  74. package/src/moves/create-move.test.ts +331 -0
  75. package/src/moves/create-move.ts +64 -0
  76. package/src/moves/move-enumeration.ts +228 -0
  77. package/src/moves/move-executor.test.ts +431 -0
  78. package/src/moves/move-executor.ts +195 -0
  79. package/src/moves/move-system.test.ts +380 -0
  80. package/src/moves/move-system.ts +463 -0
  81. package/src/moves/standard-moves.ts +231 -0
  82. package/src/operations/card-operations.test.ts +236 -0
  83. package/src/operations/card-operations.ts +116 -0
  84. package/src/operations/card-registry-impl.test.ts +251 -0
  85. package/src/operations/card-registry-impl.ts +70 -0
  86. package/src/operations/card-registry.test.ts +234 -0
  87. package/src/operations/card-registry.ts +106 -0
  88. package/src/operations/counter-operations.ts +152 -0
  89. package/src/operations/game-operations.test.ts +280 -0
  90. package/src/operations/game-operations.ts +140 -0
  91. package/src/operations/index.ts +24 -0
  92. package/src/operations/operations-impl.test.ts +354 -0
  93. package/src/operations/operations-impl.ts +468 -0
  94. package/src/operations/zone-operations.test.ts +295 -0
  95. package/src/operations/zone-operations.ts +223 -0
  96. package/src/rng/seeded-rng.test.ts +339 -0
  97. package/src/rng/seeded-rng.ts +123 -0
  98. package/src/targeting/index.ts +48 -0
  99. package/src/targeting/target-definition.test.ts +273 -0
  100. package/src/targeting/target-definition.ts +37 -0
  101. package/src/targeting/target-dsl.ts +279 -0
  102. package/src/targeting/target-resolver.ts +486 -0
  103. package/src/targeting/target-validation.test.ts +994 -0
  104. package/src/targeting/target-validation.ts +286 -0
  105. package/src/telemetry/events.ts +202 -0
  106. package/src/telemetry/index.ts +21 -0
  107. package/src/telemetry/telemetry-manager.ts +127 -0
  108. package/src/telemetry/types.ts +68 -0
  109. package/src/testing/__tests__/testing-utilities-integration.test.ts +161 -0
  110. package/src/testing/index.ts +88 -0
  111. package/src/testing/test-assertions.test.ts +341 -0
  112. package/src/testing/test-assertions.ts +256 -0
  113. package/src/testing/test-card-factory.test.ts +228 -0
  114. package/src/testing/test-card-factory.ts +111 -0
  115. package/src/testing/test-context-factory.ts +187 -0
  116. package/src/testing/test-end-assertions.test.ts +262 -0
  117. package/src/testing/test-end-assertions.ts +95 -0
  118. package/src/testing/test-engine-builder.test.ts +389 -0
  119. package/src/testing/test-engine-builder.ts +46 -0
  120. package/src/testing/test-flow-assertions.test.ts +284 -0
  121. package/src/testing/test-flow-assertions.ts +115 -0
  122. package/src/testing/test-player-builder.test.ts +132 -0
  123. package/src/testing/test-player-builder.ts +46 -0
  124. package/src/testing/test-replay-assertions.test.ts +356 -0
  125. package/src/testing/test-replay-assertions.ts +164 -0
  126. package/src/testing/test-rng-helpers.test.ts +260 -0
  127. package/src/testing/test-rng-helpers.ts +190 -0
  128. package/src/testing/test-state-builder.test.ts +373 -0
  129. package/src/testing/test-state-builder.ts +99 -0
  130. package/src/testing/test-zone-factory.test.ts +295 -0
  131. package/src/testing/test-zone-factory.ts +224 -0
  132. package/src/types/branded-utils.ts +54 -0
  133. package/src/types/branded.test.ts +175 -0
  134. package/src/types/branded.ts +33 -0
  135. package/src/types/index.ts +8 -0
  136. package/src/types/state.test.ts +198 -0
  137. package/src/types/state.ts +154 -0
  138. package/src/validation/card-type-guards.test.ts +242 -0
  139. package/src/validation/card-type-guards.ts +179 -0
  140. package/src/validation/index.ts +40 -0
  141. package/src/validation/schema-builders.test.ts +403 -0
  142. package/src/validation/schema-builders.ts +345 -0
  143. package/src/validation/type-guard-builder.test.ts +216 -0
  144. package/src/validation/type-guard-builder.ts +109 -0
  145. package/src/validation/validator-builder.test.ts +375 -0
  146. package/src/validation/validator-builder.ts +273 -0
  147. package/src/zones/index.ts +28 -0
  148. package/src/zones/zone-factory.test.ts +183 -0
  149. package/src/zones/zone-factory.ts +44 -0
  150. package/src/zones/zone-operations.test.ts +800 -0
  151. package/src/zones/zone-operations.ts +306 -0
  152. package/src/zones/zone-state-helpers.test.ts +337 -0
  153. package/src/zones/zone-state-helpers.ts +128 -0
  154. package/src/zones/zone-visibility.test.ts +156 -0
  155. package/src/zones/zone-visibility.ts +36 -0
  156. package/src/zones/zone.test.ts +186 -0
  157. package/src/zones/zone.ts +66 -0
@@ -0,0 +1,164 @@
1
+ import type { RuleEngine } from "../engine/rule-engine";
2
+
3
+ /**
4
+ * Test Replay Assertions
5
+ *
6
+ * Assertion helpers for testing deterministic replay behavior
7
+ */
8
+
9
+ /**
10
+ * Assert that replaying the game produces the same final state
11
+ *
12
+ * This verifies that the game is deterministic by:
13
+ * 1. Capturing the current game state
14
+ * 2. Replaying all moves from the beginning
15
+ * 3. Comparing the final states
16
+ *
17
+ * This is crucial for:
18
+ * - Network synchronization
19
+ * - Game recordings/replays
20
+ * - Bug reproduction
21
+ * - Ensuring RNG is properly seeded
22
+ *
23
+ * @param engine - Rule engine instance with move history
24
+ * @throws Error if replay produces different state
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const engine = new RuleEngine(gameDefinition, players, {
29
+ * seed: 'test-seed'
30
+ * });
31
+ *
32
+ * // Execute some moves
33
+ * engine.executeMove('shuffle', { playerId: 'p1' });
34
+ * engine.executeMove('draw', { playerId: 'p1' });
35
+ * engine.executeMove('play', { playerId: 'p1', data: { cardId: 'card1' } });
36
+ *
37
+ * // Verify replay is deterministic
38
+ * expectDeterministicReplay(engine);
39
+ * ```
40
+ */
41
+ export function expectDeterministicReplay<
42
+ TState,
43
+ TMoves extends Record<string, any>,
44
+ >(engine: RuleEngine<TState, TMoves>): void {
45
+ // Get current state
46
+ const originalState = engine.getState();
47
+
48
+ // Replay from the beginning
49
+ const replayedState = engine.replay();
50
+
51
+ // Compare states
52
+ const originalJson = JSON.stringify(originalState);
53
+ const replayedJson = JSON.stringify(replayedState);
54
+
55
+ if (originalJson !== replayedJson) {
56
+ // Find differences for better error message
57
+ const diff = findStateDifferences(originalState, replayedState);
58
+
59
+ throw new Error(
60
+ "Replay produced different state than original execution.\n" +
61
+ "This indicates non-deterministic behavior in your game logic.\n\n" +
62
+ "Common causes:\n" +
63
+ "- Using Math.random() instead of context.rng\n" +
64
+ "- Using Date.now() or other time-based values\n" +
65
+ "- External state mutations\n" +
66
+ "- Non-deterministic array sorting\n\n" +
67
+ `Differences found:\n${diff}\n\n` +
68
+ `Original state: ${truncateString(originalJson, 500)}\n\n` +
69
+ `Replayed state: ${truncateString(replayedJson, 500)}`,
70
+ );
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Find differences between two states for error reporting
76
+ *
77
+ * @param original - Original state
78
+ * @param replayed - Replayed state
79
+ * @returns String describing differences
80
+ */
81
+ function findStateDifferences(original: any, replayed: any): string {
82
+ const differences: string[] = [];
83
+
84
+ const findDiffs = (obj1: any, obj2: any, path = "") => {
85
+ if (typeof obj1 !== typeof obj2) {
86
+ differences.push(
87
+ `${path || "root"}: type mismatch (${typeof obj1} vs ${typeof obj2})`,
88
+ );
89
+ return;
90
+ }
91
+
92
+ if (typeof obj1 !== "object" || obj1 === null || obj2 === null) {
93
+ if (obj1 !== obj2) {
94
+ differences.push(
95
+ `${path || "root"}: value mismatch (${JSON.stringify(obj1)} vs ${JSON.stringify(obj2)})`,
96
+ );
97
+ }
98
+ return;
99
+ }
100
+
101
+ // Check arrays
102
+ if (Array.isArray(obj1) && Array.isArray(obj2)) {
103
+ if (obj1.length !== obj2.length) {
104
+ differences.push(
105
+ `${path || "root"}: array length mismatch (${obj1.length} vs ${obj2.length})`,
106
+ );
107
+ return;
108
+ }
109
+
110
+ for (let i = 0; i < obj1.length; i++) {
111
+ findDiffs(obj1[i], obj2[i], `${path}[${i}]`);
112
+ }
113
+ return;
114
+ }
115
+
116
+ // Check objects
117
+ const keys1 = Object.keys(obj1).sort();
118
+ const keys2 = Object.keys(obj2).sort();
119
+
120
+ const allKeys = new Set([...keys1, ...keys2]);
121
+
122
+ for (const key of allKeys) {
123
+ if (!(key in obj1)) {
124
+ differences.push(`${path}.${key}: missing in original`);
125
+ } else if (key in obj2) {
126
+ findDiffs(obj1[key], obj2[key], path ? `${path}.${key}` : key);
127
+ } else {
128
+ differences.push(`${path}.${key}: missing in replay`);
129
+ }
130
+ }
131
+ };
132
+
133
+ findDiffs(original, replayed);
134
+
135
+ if (differences.length === 0) {
136
+ return "States are identical (this shouldn't happen)";
137
+ }
138
+
139
+ // Limit differences shown
140
+ const maxDiffs = 10;
141
+ const shown = differences.slice(0, maxDiffs);
142
+ const remaining = differences.length - maxDiffs;
143
+
144
+ let result = shown.join("\n");
145
+ if (remaining > 0) {
146
+ result += `\n... and ${remaining} more differences`;
147
+ }
148
+
149
+ return result;
150
+ }
151
+
152
+ /**
153
+ * Truncate a string for display
154
+ *
155
+ * @param str - String to truncate
156
+ * @param maxLength - Maximum length
157
+ * @returns Truncated string
158
+ */
159
+ function truncateString(str: string, maxLength: number): string {
160
+ if (str.length <= maxLength) {
161
+ return str;
162
+ }
163
+ return `${str.substring(0, maxLength)}... (${str.length - maxLength} more chars)`;
164
+ }
@@ -0,0 +1,260 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { SeededRNG } from "../rng/seeded-rng";
3
+ import {
4
+ createDeterministicRNG,
5
+ expectDeterministicBehavior,
6
+ withSeed,
7
+ } from "./test-rng-helpers";
8
+
9
+ describe("test-rng-helpers", () => {
10
+ describe("withSeed", () => {
11
+ it("should execute function with deterministic RNG", () => {
12
+ const result1 = withSeed("test-seed", (rng) => {
13
+ return rng.randomInt(1, 100);
14
+ });
15
+
16
+ const result2 = withSeed("test-seed", (rng) => {
17
+ return rng.randomInt(1, 100);
18
+ });
19
+
20
+ // Same seed should produce same results
21
+ expect(result1).toBe(result2);
22
+ });
23
+
24
+ it("should produce different results with different seeds", () => {
25
+ const result1 = withSeed("seed-1", (rng) => {
26
+ return rng.randomInt(1, 100);
27
+ });
28
+
29
+ const result2 = withSeed("seed-2", (rng) => {
30
+ return rng.randomInt(1, 100);
31
+ });
32
+
33
+ // Different seeds should (very likely) produce different results
34
+ // This test could theoretically fail but probability is very low
35
+ expect(result1).not.toBe(result2);
36
+ });
37
+
38
+ it("should work with multiple RNG operations", () => {
39
+ const results1 = withSeed("test-seed", (rng) => {
40
+ return [
41
+ rng.randomInt(1, 10),
42
+ rng.randomInt(1, 10),
43
+ rng.randomInt(1, 10),
44
+ ];
45
+ });
46
+
47
+ const results2 = withSeed("test-seed", (rng) => {
48
+ return [
49
+ rng.randomInt(1, 10),
50
+ rng.randomInt(1, 10),
51
+ rng.randomInt(1, 10),
52
+ ];
53
+ });
54
+
55
+ // Same seed should produce same sequence
56
+ expect(results1).toEqual(results2);
57
+ });
58
+
59
+ it("should work with shuffle operations", () => {
60
+ const array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
61
+
62
+ const shuffled1 = withSeed("shuffle-seed", (rng) => {
63
+ return rng.shuffle(array);
64
+ });
65
+
66
+ const shuffled2 = withSeed("shuffle-seed", (rng) => {
67
+ return rng.shuffle(array);
68
+ });
69
+
70
+ // Same seed should produce same shuffle
71
+ expect(shuffled1).toEqual(shuffled2);
72
+ });
73
+
74
+ it("should work with pick operations", () => {
75
+ const array = ["a", "b", "c", "d", "e"];
76
+
77
+ const picked1 = withSeed("pick-seed", (rng) => {
78
+ return [rng.pick(array), rng.pick(array), rng.pick(array)];
79
+ });
80
+
81
+ const picked2 = withSeed("pick-seed", (rng) => {
82
+ return [rng.pick(array), rng.pick(array), rng.pick(array)];
83
+ });
84
+
85
+ // Same seed should produce same picks
86
+ expect(picked1).toEqual(picked2);
87
+ });
88
+
89
+ it("should return function result", () => {
90
+ const result = withSeed("test", (rng) => {
91
+ return { value: rng.randomInt(1, 10), text: "hello" };
92
+ });
93
+
94
+ expect(result).toHaveProperty("value");
95
+ expect(result).toHaveProperty("text");
96
+ expect(result.text).toBe("hello");
97
+ });
98
+ });
99
+
100
+ describe("createDeterministicRNG", () => {
101
+ it("should create RNG with specified seed", () => {
102
+ const rng = createDeterministicRNG("my-seed");
103
+
104
+ expect(rng.getSeed()).toBe("my-seed");
105
+ });
106
+
107
+ it("should use default seed if none provided", () => {
108
+ const rng = createDeterministicRNG();
109
+
110
+ expect(rng.getSeed()).toBe("test-seed");
111
+ });
112
+
113
+ it("should produce deterministic results", () => {
114
+ const rng1 = createDeterministicRNG("same-seed");
115
+ const rng2 = createDeterministicRNG("same-seed");
116
+
117
+ const value1 = rng1.randomInt(1, 1000);
118
+ const value2 = rng2.randomInt(1, 1000);
119
+
120
+ expect(value1).toBe(value2);
121
+ });
122
+
123
+ it("should be reusable", () => {
124
+ const rng = createDeterministicRNG("reuse-seed");
125
+
126
+ const results1 = [rng.randomInt(1, 10), rng.randomInt(1, 10)];
127
+
128
+ // Reset by creating new RNG with same seed
129
+ const rng2 = createDeterministicRNG("reuse-seed");
130
+ const results2 = [rng2.randomInt(1, 10), rng2.randomInt(1, 10)];
131
+
132
+ expect(results1).toEqual(results2);
133
+ });
134
+ });
135
+
136
+ describe("expectDeterministicBehavior", () => {
137
+ it("should pass when function produces same results", () => {
138
+ const fn = (rng: SeededRNG) => rng.randomInt(1, 100);
139
+
140
+ // Should not throw
141
+ expectDeterministicBehavior(fn, "test-seed");
142
+ });
143
+
144
+ it("should throw when function produces different results", () => {
145
+ let counter = 0;
146
+ const fn = (rng: SeededRNG) => {
147
+ // Add non-deterministic behavior
148
+ counter++;
149
+ return rng.randomInt(1, 100) + counter;
150
+ };
151
+
152
+ expect(() => {
153
+ expectDeterministicBehavior(fn, "test-seed");
154
+ }).toThrow(/deterministic/);
155
+ });
156
+
157
+ it("should work with complex return values", () => {
158
+ const fn = (rng: SeededRNG) => ({
159
+ roll: rng.randomInt(1, 6),
160
+ flip: rng.flipCoin(),
161
+ pick: rng.pick(["a", "b", "c"]),
162
+ });
163
+
164
+ // Should not throw
165
+ expectDeterministicBehavior(fn, "complex-seed");
166
+ });
167
+
168
+ it("should work with array returns", () => {
169
+ const fn = (rng: SeededRNG) => {
170
+ return Array.from({ length: 10 }, () => rng.randomInt(1, 100));
171
+ };
172
+
173
+ // Should not throw
174
+ expectDeterministicBehavior(fn, "array-seed");
175
+ });
176
+
177
+ it("should detect non-determinism from external state", () => {
178
+ let callCount = 0;
179
+ const fn = (rng: SeededRNG) => {
180
+ // Each call uses a different RNG seed due to external state
181
+ callCount++;
182
+ const tempRng = new (
183
+ rng.constructor as new (
184
+ seed: string,
185
+ ) => SeededRNG
186
+ )(`seed-${callCount}`);
187
+ return tempRng.randomInt(1, 100);
188
+ };
189
+
190
+ callCount = 0;
191
+ expect(() => {
192
+ expectDeterministicBehavior(fn, "shuffle-seed");
193
+ }).toThrow(/deterministic/);
194
+ });
195
+
196
+ it("should use default seed if none provided", () => {
197
+ const fn = (rng: SeededRNG) => rng.randomInt(1, 10);
198
+
199
+ // Should not throw
200
+ expectDeterministicBehavior(fn);
201
+ });
202
+
203
+ it("should provide helpful error message", () => {
204
+ let callCount = 0;
205
+ const fn = (_rng: SeededRNG) => {
206
+ callCount++;
207
+ return callCount; // Always different
208
+ };
209
+
210
+ expect(() => {
211
+ expectDeterministicBehavior(fn, "test");
212
+ }).toThrow(/Expected function to be deterministic/);
213
+ });
214
+ });
215
+
216
+ describe("integration", () => {
217
+ it("should help test game mechanics", () => {
218
+ // Simulate drawing cards deterministically
219
+ const drawCards = (rng: SeededRNG, count: number) => {
220
+ const deck = ["card1", "card2", "card3", "card4", "card5"];
221
+ const shuffled = rng.shuffle(deck);
222
+ return shuffled.slice(0, count);
223
+ };
224
+
225
+ const hand1 = withSeed("game-seed", (rng) => drawCards(rng, 3));
226
+ const hand2 = withSeed("game-seed", (rng) => drawCards(rng, 3));
227
+
228
+ expect(hand1).toEqual(hand2);
229
+ });
230
+
231
+ it("should help test dice rolls", () => {
232
+ const rollAttack = (rng: SeededRNG) => {
233
+ const attack = rng.rollDice(20) as number; // d20
234
+ const damage = rng.rollDice(6, 2) as number[]; // 2d6
235
+ return { attack, damage };
236
+ };
237
+
238
+ const result1 = withSeed("combat-seed", rollAttack);
239
+ const result2 = withSeed("combat-seed", rollAttack);
240
+
241
+ expect(result1).toEqual(result2);
242
+ });
243
+
244
+ it("should help test probability distributions", () => {
245
+ const generateResults = (rng: SeededRNG) => {
246
+ return Array.from({ length: 100 }, () => rng.randomInt(1, 6));
247
+ };
248
+
249
+ const results1 = withSeed("distribution-seed", generateResults);
250
+ const results2 = withSeed("distribution-seed", generateResults);
251
+
252
+ // Should be identical
253
+ expect(results1).toEqual(results2);
254
+
255
+ // Should have good distribution (all values 1-6 appear)
256
+ const uniqueValues = new Set(results1);
257
+ expect(uniqueValues.size).toBeGreaterThan(4); // At least 5 different values
258
+ });
259
+ });
260
+ });
@@ -0,0 +1,190 @@
1
+ import { SeededRNG } from "../rng/seeded-rng";
2
+
3
+ /**
4
+ * Test RNG Helpers
5
+ *
6
+ * Utilities for testing with deterministic randomness
7
+ */
8
+
9
+ /**
10
+ * Execute a function with a seeded RNG
11
+ *
12
+ * Creates a temporary RNG instance with the specified seed,
13
+ * executes the function, and returns its result.
14
+ *
15
+ * @param seed - Seed for deterministic behavior
16
+ * @param fn - Function to execute with RNG
17
+ * @returns Result of the function
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const result = withSeed('test-seed', (rng) => {
22
+ * return rng.shuffle([1, 2, 3, 4, 5]);
23
+ * });
24
+ *
25
+ * // Same seed produces same result
26
+ * const result2 = withSeed('test-seed', (rng) => {
27
+ * return rng.shuffle([1, 2, 3, 4, 5]);
28
+ * });
29
+ * expect(result).toEqual(result2);
30
+ * ```
31
+ */
32
+ export function withSeed<T>(seed: string, fn: (rng: SeededRNG) => T): T {
33
+ const rng = new SeededRNG(seed);
34
+ return fn(rng);
35
+ }
36
+
37
+ /**
38
+ * Create a deterministic RNG for testing
39
+ *
40
+ * Creates an RNG instance with a known seed for predictable testing.
41
+ *
42
+ * @param seed - Optional seed (default: 'test-seed')
43
+ * @returns Seeded RNG instance
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * const rng = createDeterministicRNG('my-test');
48
+ * const value1 = rng.randomInt(1, 100);
49
+ *
50
+ * // Recreate with same seed for same results
51
+ * const rng2 = createDeterministicRNG('my-test');
52
+ * const value2 = rng2.randomInt(1, 100);
53
+ *
54
+ * expect(value1).toBe(value2);
55
+ * ```
56
+ */
57
+ export function createDeterministicRNG(seed = "test-seed"): SeededRNG {
58
+ return new SeededRNG(seed);
59
+ }
60
+
61
+ /**
62
+ * Assert that a function produces deterministic results with same seed
63
+ *
64
+ * Executes the function twice with the same seed and verifies
65
+ * that the results are identical.
66
+ *
67
+ * @param fn - Function to test for determinism
68
+ * @param seed - Optional seed (default: 'determinism-test')
69
+ * @throws Error if function produces different results
70
+ *
71
+ * @example
72
+ * ```typescript
73
+ * // Test that shuffle is deterministic
74
+ * expectDeterministicBehavior((rng) => {
75
+ * return rng.shuffle([1, 2, 3, 4, 5]);
76
+ * });
77
+ *
78
+ * // Test game mechanic
79
+ * expectDeterministicBehavior((rng) => {
80
+ * const deck = createDeck();
81
+ * return drawCards(deck, 5, rng);
82
+ * }, 'draw-test-seed');
83
+ * ```
84
+ */
85
+ export function expectDeterministicBehavior<T>(
86
+ fn: (rng: SeededRNG) => T,
87
+ seed = "determinism-test",
88
+ ): void {
89
+ const result1 = withSeed(seed, fn);
90
+ const result2 = withSeed(seed, fn);
91
+
92
+ // Deep equality check
93
+ if (JSON.stringify(result1) !== JSON.stringify(result2)) {
94
+ throw new Error(
95
+ `Expected function to be deterministic with seed '${seed}', but got different results:\n` +
96
+ `Run 1: ${JSON.stringify(result1)}\n` +
97
+ `Run 2: ${JSON.stringify(result2)}`,
98
+ );
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Create multiple RNG instances with related seeds
104
+ *
105
+ * Useful for testing scenarios with multiple independent RNG streams
106
+ * (e.g., one per player) that need to be reproducible.
107
+ *
108
+ * @param count - Number of RNG instances to create
109
+ * @param baseSeed - Base seed for determinism
110
+ * @returns Array of seeded RNG instances
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * // Create RNG for each player
115
+ * const [p1Rng, p2Rng] = createMultipleRNGs(2, 'game-seed');
116
+ *
117
+ * // Each player gets deterministic but independent randomness
118
+ * const p1Roll = p1Rng.rollDice(20);
119
+ * const p2Roll = p2Rng.rollDice(20);
120
+ * ```
121
+ */
122
+ export function createMultipleRNGs(
123
+ count: number,
124
+ baseSeed = "test",
125
+ ): SeededRNG[] {
126
+ return Array.from({ length: count }, (_, i) => {
127
+ return new SeededRNG(`${baseSeed}-${i}`);
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Test a random operation multiple times with different seeds
133
+ *
134
+ * Useful for ensuring that random operations cover a good range
135
+ * of possible outcomes.
136
+ *
137
+ * @param fn - Function to test
138
+ * @param iterations - Number of iterations to run
139
+ * @returns Array of results
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * // Test that shuffle produces variety
144
+ * const results = testWithMultipleSeeds(
145
+ * (rng) => rng.shuffle([1, 2, 3, 4, 5]),
146
+ * 100
147
+ * );
148
+ *
149
+ * // Verify we got different shuffles
150
+ * const uniqueResults = new Set(results.map(r => JSON.stringify(r)));
151
+ * expect(uniqueResults.size).toBeGreaterThan(10);
152
+ * ```
153
+ */
154
+ export function testWithMultipleSeeds<T>(
155
+ fn: (rng: SeededRNG) => T,
156
+ iterations = 10,
157
+ ): T[] {
158
+ return Array.from({ length: iterations }, (_, i) => {
159
+ return withSeed(`test-iteration-${i}`, fn);
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Create a predictable sequence of random values
165
+ *
166
+ * Useful for testing specific scenarios with known random values.
167
+ *
168
+ * @param seed - Seed for the sequence
169
+ * @param count - Number of values to generate
170
+ * @param min - Minimum value (inclusive)
171
+ * @param max - Maximum value (inclusive)
172
+ * @returns Array of random integers
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * // Generate predictable dice rolls for testing
177
+ * const rolls = createPredictableSequence('combat', 10, 1, 20);
178
+ * // Always gets same sequence of rolls with this seed
179
+ * ```
180
+ */
181
+ export function createPredictableSequence(
182
+ seed: string,
183
+ count: number,
184
+ min: number,
185
+ max: number,
186
+ ): number[] {
187
+ return withSeed(seed, (rng) => {
188
+ return Array.from({ length: count }, () => rng.randomInt(min, max));
189
+ });
190
+ }