@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.
- package/README.md +882 -0
- package/package.json +58 -0
- package/src/__tests__/alpha-clash-engine-definition.test.ts +319 -0
- package/src/__tests__/createMockAlphaClashGame.ts +462 -0
- package/src/__tests__/createMockGrandArchiveGame.ts +373 -0
- package/src/__tests__/createMockGundamGame.ts +379 -0
- package/src/__tests__/createMockLorcanaGame.ts +328 -0
- package/src/__tests__/createMockOnePieceGame.ts +429 -0
- package/src/__tests__/createMockRiftboundGame.ts +462 -0
- package/src/__tests__/grand-archive-engine-definition.test.ts +118 -0
- package/src/__tests__/gundam-engine-definition.test.ts +110 -0
- package/src/__tests__/integration-complete-game.test.ts +508 -0
- package/src/__tests__/integration-network-sync.test.ts +469 -0
- package/src/__tests__/lorcana-engine-definition.test.ts +100 -0
- package/src/__tests__/move-enumeration.test.ts +725 -0
- package/src/__tests__/multiplayer-engine.test.ts +555 -0
- package/src/__tests__/one-piece-engine-definition.test.ts +114 -0
- package/src/__tests__/riftbound-engine-definition.test.ts +124 -0
- package/src/actions/action-definition.test.ts +201 -0
- package/src/actions/action-definition.ts +122 -0
- package/src/actions/action-timing.test.ts +490 -0
- package/src/actions/action-timing.ts +257 -0
- package/src/cards/card-definition.test.ts +268 -0
- package/src/cards/card-definition.ts +27 -0
- package/src/cards/card-instance.test.ts +422 -0
- package/src/cards/card-instance.ts +49 -0
- package/src/cards/computed-properties.test.ts +530 -0
- package/src/cards/computed-properties.ts +84 -0
- package/src/cards/conditional-modifiers.test.ts +390 -0
- package/src/cards/modifiers.test.ts +286 -0
- package/src/cards/modifiers.ts +51 -0
- package/src/engine/MULTIPLAYER.md +425 -0
- package/src/engine/__tests__/rule-engine-flow.test.ts +348 -0
- package/src/engine/__tests__/rule-engine-history.test.ts +535 -0
- package/src/engine/__tests__/rule-engine-moves.test.ts +488 -0
- package/src/engine/__tests__/rule-engine.test.ts +366 -0
- package/src/engine/index.ts +14 -0
- package/src/engine/multiplayer-engine.example.ts +571 -0
- package/src/engine/multiplayer-engine.ts +409 -0
- package/src/engine/rule-engine.test.ts +286 -0
- package/src/engine/rule-engine.ts +1539 -0
- package/src/engine/tracker-system.ts +172 -0
- package/src/examples/__tests__/coin-flip-game.test.ts +641 -0
- package/src/filtering/card-filter.test.ts +230 -0
- package/src/filtering/card-filter.ts +91 -0
- package/src/filtering/card-query.test.ts +901 -0
- package/src/filtering/card-query.ts +273 -0
- package/src/filtering/filter-matching.test.ts +944 -0
- package/src/filtering/filter-matching.ts +315 -0
- package/src/flow/SERIALIZATION.md +428 -0
- package/src/flow/__tests__/flow-definition.test.ts +427 -0
- package/src/flow/__tests__/flow-manager.test.ts +756 -0
- package/src/flow/__tests__/flow-serialization.test.ts +565 -0
- package/src/flow/flow-definition.ts +453 -0
- package/src/flow/flow-manager.ts +1044 -0
- package/src/flow/index.ts +35 -0
- package/src/game-definition/__tests__/game-definition-validation.test.ts +359 -0
- package/src/game-definition/__tests__/game-definition.test.ts +291 -0
- package/src/game-definition/__tests__/move-definitions.test.ts +328 -0
- package/src/game-definition/game-definition.ts +261 -0
- package/src/game-definition/index.ts +28 -0
- package/src/game-definition/move-definitions.ts +188 -0
- package/src/game-definition/validation.ts +183 -0
- package/src/history/history-manager.test.ts +497 -0
- package/src/history/history-manager.ts +312 -0
- package/src/history/history-operations.ts +122 -0
- package/src/history/index.ts +9 -0
- package/src/history/types.ts +255 -0
- package/src/index.ts +32 -0
- package/src/logging/index.ts +27 -0
- package/src/logging/log-formatter.ts +187 -0
- package/src/logging/logger.ts +276 -0
- package/src/logging/types.ts +148 -0
- package/src/moves/create-move.test.ts +331 -0
- package/src/moves/create-move.ts +64 -0
- package/src/moves/move-enumeration.ts +228 -0
- package/src/moves/move-executor.test.ts +431 -0
- package/src/moves/move-executor.ts +195 -0
- package/src/moves/move-system.test.ts +380 -0
- package/src/moves/move-system.ts +463 -0
- package/src/moves/standard-moves.ts +231 -0
- package/src/operations/card-operations.test.ts +236 -0
- package/src/operations/card-operations.ts +116 -0
- package/src/operations/card-registry-impl.test.ts +251 -0
- package/src/operations/card-registry-impl.ts +70 -0
- package/src/operations/card-registry.test.ts +234 -0
- package/src/operations/card-registry.ts +106 -0
- package/src/operations/counter-operations.ts +152 -0
- package/src/operations/game-operations.test.ts +280 -0
- package/src/operations/game-operations.ts +140 -0
- package/src/operations/index.ts +24 -0
- package/src/operations/operations-impl.test.ts +354 -0
- package/src/operations/operations-impl.ts +468 -0
- package/src/operations/zone-operations.test.ts +295 -0
- package/src/operations/zone-operations.ts +223 -0
- package/src/rng/seeded-rng.test.ts +339 -0
- package/src/rng/seeded-rng.ts +123 -0
- package/src/targeting/index.ts +48 -0
- package/src/targeting/target-definition.test.ts +273 -0
- package/src/targeting/target-definition.ts +37 -0
- package/src/targeting/target-dsl.ts +279 -0
- package/src/targeting/target-resolver.ts +486 -0
- package/src/targeting/target-validation.test.ts +994 -0
- package/src/targeting/target-validation.ts +286 -0
- package/src/telemetry/events.ts +202 -0
- package/src/telemetry/index.ts +21 -0
- package/src/telemetry/telemetry-manager.ts +127 -0
- package/src/telemetry/types.ts +68 -0
- package/src/testing/__tests__/testing-utilities-integration.test.ts +161 -0
- package/src/testing/index.ts +88 -0
- package/src/testing/test-assertions.test.ts +341 -0
- package/src/testing/test-assertions.ts +256 -0
- package/src/testing/test-card-factory.test.ts +228 -0
- package/src/testing/test-card-factory.ts +111 -0
- package/src/testing/test-context-factory.ts +187 -0
- package/src/testing/test-end-assertions.test.ts +262 -0
- package/src/testing/test-end-assertions.ts +95 -0
- package/src/testing/test-engine-builder.test.ts +389 -0
- package/src/testing/test-engine-builder.ts +46 -0
- package/src/testing/test-flow-assertions.test.ts +284 -0
- package/src/testing/test-flow-assertions.ts +115 -0
- package/src/testing/test-player-builder.test.ts +132 -0
- package/src/testing/test-player-builder.ts +46 -0
- package/src/testing/test-replay-assertions.test.ts +356 -0
- package/src/testing/test-replay-assertions.ts +164 -0
- package/src/testing/test-rng-helpers.test.ts +260 -0
- package/src/testing/test-rng-helpers.ts +190 -0
- package/src/testing/test-state-builder.test.ts +373 -0
- package/src/testing/test-state-builder.ts +99 -0
- package/src/testing/test-zone-factory.test.ts +295 -0
- package/src/testing/test-zone-factory.ts +224 -0
- package/src/types/branded-utils.ts +54 -0
- package/src/types/branded.test.ts +175 -0
- package/src/types/branded.ts +33 -0
- package/src/types/index.ts +8 -0
- package/src/types/state.test.ts +198 -0
- package/src/types/state.ts +154 -0
- package/src/validation/card-type-guards.test.ts +242 -0
- package/src/validation/card-type-guards.ts +179 -0
- package/src/validation/index.ts +40 -0
- package/src/validation/schema-builders.test.ts +403 -0
- package/src/validation/schema-builders.ts +345 -0
- package/src/validation/type-guard-builder.test.ts +216 -0
- package/src/validation/type-guard-builder.ts +109 -0
- package/src/validation/validator-builder.test.ts +375 -0
- package/src/validation/validator-builder.ts +273 -0
- package/src/zones/index.ts +28 -0
- package/src/zones/zone-factory.test.ts +183 -0
- package/src/zones/zone-factory.ts +44 -0
- package/src/zones/zone-operations.test.ts +800 -0
- package/src/zones/zone-operations.ts +306 -0
- package/src/zones/zone-state-helpers.test.ts +337 -0
- package/src/zones/zone-state-helpers.ts +128 -0
- package/src/zones/zone-visibility.test.ts +156 -0
- package/src/zones/zone-visibility.ts +36 -0
- package/src/zones/zone.test.ts +186 -0
- 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
|
+
}
|