@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,115 @@
1
+ import type { RuleEngine } from "../engine/rule-engine";
2
+ import type { MoveContext, MoveContextInput } from "../moves/move-system";
3
+
4
+ /**
5
+ * Test Flow Assertions
6
+ *
7
+ * Assertion helpers for testing game flow and phase transitions
8
+ */
9
+
10
+ /**
11
+ * Assert that a move causes a phase transition
12
+ *
13
+ * Verifies that executing a move transitions from one phase to another.
14
+ * Supports custom phase paths for games that store phase in nested state.
15
+ *
16
+ * @param engine - Rule engine instance
17
+ * @param moveId - Move to execute
18
+ * @param context - Move context
19
+ * @param fromPhase - Expected initial phase
20
+ * @param toPhase - Expected final phase
21
+ * @param phasePath - Optional path to phase in state (default: 'phase')
22
+ * @throws Error if phase transition doesn't match expectations
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * // Test phase transition
27
+ * expectPhaseTransition(
28
+ * engine,
29
+ * 'endPhase',
30
+ * { playerId: 'p1' },
31
+ * 'main',
32
+ * 'end'
33
+ * );
34
+ *
35
+ * // Test with custom phase path
36
+ * expectPhaseTransition(
37
+ * engine,
38
+ * 'advance',
39
+ * { playerId: 'p1' },
40
+ * 'draw',
41
+ * 'main',
42
+ * 'gameState.currentPhase'
43
+ * );
44
+ * ```
45
+ */
46
+ export function expectPhaseTransition<
47
+ TState,
48
+ TMoves extends Record<string, any>,
49
+ >(
50
+ engine: RuleEngine<TState, TMoves>,
51
+ moveId: string,
52
+ context: MoveContextInput,
53
+ fromPhase: string,
54
+ toPhase: string,
55
+ phasePath = "phase",
56
+ ): void {
57
+ // Get initial state
58
+ const initialState = engine.getState();
59
+ const initialPhase = getPropertyByPath(initialState, phasePath);
60
+
61
+ // Verify initial phase
62
+ if (initialPhase !== fromPhase) {
63
+ throw new Error(
64
+ `Expected initial phase to be '${fromPhase}', but found '${initialPhase}'`,
65
+ );
66
+ }
67
+
68
+ // Execute move
69
+ const result = engine.executeMove(moveId, context);
70
+
71
+ // Check if move succeeded
72
+ if (!result.success) {
73
+ throw new Error(
74
+ `Move failed during phase transition test: ${result.error}` +
75
+ (result.errorCode ? ` (code: ${result.errorCode})` : ""),
76
+ );
77
+ }
78
+
79
+ // Get final state
80
+ const finalState = engine.getState();
81
+ const finalPhase = getPropertyByPath(finalState, phasePath);
82
+
83
+ // Verify final phase
84
+ if (finalPhase !== toPhase) {
85
+ throw new Error(
86
+ `Expected final phase to be '${toPhase}', but found '${finalPhase}'`,
87
+ );
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Get property value by path with dot notation
93
+ *
94
+ * Supports paths like:
95
+ * - 'phase'
96
+ * - 'gameState.currentPhase'
97
+ * - 'flow.phase'
98
+ *
99
+ * @param obj - Object to traverse
100
+ * @param path - Property path
101
+ * @returns Property value or undefined
102
+ */
103
+ function getPropertyByPath(obj: any, path: string): any {
104
+ const parts = path.split(".");
105
+ let current = obj;
106
+
107
+ for (const part of parts) {
108
+ if (current === null || current === undefined) {
109
+ return undefined;
110
+ }
111
+ current = current[part];
112
+ }
113
+
114
+ return current;
115
+ }
@@ -0,0 +1,132 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createTestPlayers } from "./test-player-builder";
3
+
4
+ /**
5
+ * Tests for createTestPlayers - Player builder for tests
6
+ *
7
+ * Task 2.1: Write tests for test builders (createTestPlayers)
8
+ *
9
+ * Tests verify:
10
+ * - Creating multiple players with default names
11
+ * - Creating players with custom names
12
+ * - Player IDs are unique and properly formatted
13
+ * - Type safety for Player objects
14
+ */
15
+
16
+ describe("createTestPlayers", () => {
17
+ describe("Basic Functionality", () => {
18
+ it("should create specified number of players with default names", () => {
19
+ const players = createTestPlayers(2);
20
+
21
+ expect(players).toHaveLength(2);
22
+ expect(players[0]?.name).toBe("Player 1");
23
+ expect(players[1]?.name).toBe("Player 2");
24
+ });
25
+
26
+ it("should create players with custom names", () => {
27
+ const players = createTestPlayers(3, ["Alice", "Bob", "Charlie"]);
28
+
29
+ expect(players).toHaveLength(3);
30
+ expect(players[0]?.name).toBe("Alice");
31
+ expect(players[1]?.name).toBe("Bob");
32
+ expect(players[2]?.name).toBe("Charlie");
33
+ });
34
+
35
+ it("should handle partial custom names with defaults for remaining", () => {
36
+ const players = createTestPlayers(4, ["Alice", "Bob"]);
37
+
38
+ expect(players).toHaveLength(4);
39
+ expect(players[0]?.name).toBe("Alice");
40
+ expect(players[1]?.name).toBe("Bob");
41
+ expect(players[2]?.name).toBe("Player 3");
42
+ expect(players[3]?.name).toBe("Player 4");
43
+ });
44
+
45
+ it("should create single player", () => {
46
+ const players = createTestPlayers(1);
47
+
48
+ expect(players).toHaveLength(1);
49
+ expect(players[0]?.name).toBe("Player 1");
50
+ });
51
+
52
+ it("should create single player with custom name", () => {
53
+ const players = createTestPlayers(1, ["Alice"]);
54
+
55
+ expect(players).toHaveLength(1);
56
+ expect(players[0]?.name).toBe("Alice");
57
+ });
58
+ });
59
+
60
+ describe("Player IDs", () => {
61
+ it("should generate unique IDs for each player", () => {
62
+ const players = createTestPlayers(3);
63
+
64
+ const ids = players.map((p) => p.id);
65
+ const uniqueIds = new Set(ids);
66
+
67
+ expect(uniqueIds.size).toBe(3);
68
+ });
69
+
70
+ it("should generate properly formatted PlayerId brand", () => {
71
+ const players = createTestPlayers(2);
72
+
73
+ // IDs should be non-empty strings
74
+ for (const player of players) {
75
+ expect(player.id).toBeTypeOf("string");
76
+ expect(player.id.length).toBeGreaterThan(0);
77
+ }
78
+ });
79
+
80
+ it("should generate deterministic IDs based on index", () => {
81
+ const players1 = createTestPlayers(2);
82
+ const players2 = createTestPlayers(2);
83
+
84
+ // IDs should be based on player index, so they should match
85
+ expect(players1[0]?.id).toBe(players2[0]?.id);
86
+ expect(players1[1]?.id).toBe(players2[1]?.id);
87
+ });
88
+ });
89
+
90
+ describe("Edge Cases", () => {
91
+ it("should handle 0 players by returning empty array", () => {
92
+ const players = createTestPlayers(0);
93
+
94
+ expect(players).toHaveLength(0);
95
+ });
96
+
97
+ it("should handle large number of players", () => {
98
+ const players = createTestPlayers(10);
99
+
100
+ expect(players).toHaveLength(10);
101
+ expect(players[9]?.name).toBe("Player 10");
102
+ });
103
+
104
+ it("should ignore extra names beyond count", () => {
105
+ const players = createTestPlayers(2, ["Alice", "Bob", "Charlie"]);
106
+
107
+ expect(players).toHaveLength(2);
108
+ expect(players[0]?.name).toBe("Alice");
109
+ expect(players[1]?.name).toBe("Bob");
110
+ });
111
+ });
112
+
113
+ describe("Type Safety", () => {
114
+ it("should return Player objects with correct structure", () => {
115
+ const players = createTestPlayers(1);
116
+
117
+ expect(players[0]).toHaveProperty("id");
118
+ expect(players[0]).toHaveProperty("name");
119
+ });
120
+
121
+ it("should have PlayerId branded type", () => {
122
+ const players = createTestPlayers(1);
123
+
124
+ // Runtime check - ID should be a string
125
+ expect(typeof players[0]?.id).toBe("string");
126
+
127
+ // Type-level check happens at compile time
128
+ // The following would fail TypeScript compilation if Player.id wasn't PlayerId:
129
+ // const playerId: PlayerId = players[0].id;
130
+ });
131
+ });
132
+ });
@@ -0,0 +1,46 @@
1
+ import type { Player } from "../game-definition/game-definition";
2
+ import { createPlayerId } from "../types";
3
+
4
+ /**
5
+ * Test Player Builder
6
+ *
7
+ * Task 2.4: Implement createTestPlayers(count, names?)
8
+ *
9
+ * Creates an array of test players with unique IDs and names.
10
+ * Simplifies test setup by reducing boilerplate player creation.
11
+ *
12
+ * Features:
13
+ * - Generates unique PlayerId for each player
14
+ * - Uses custom names if provided, otherwise generates default names
15
+ * - Deterministic IDs based on player index for reproducible tests
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * // Create 2 players with default names
20
+ * const players = createTestPlayers(2);
21
+ * // [{ id: 'test-p1', name: 'Player 1' }, { id: 'test-p2', name: 'Player 2' }]
22
+ *
23
+ * // Create players with custom names
24
+ * const players = createTestPlayers(3, ['Alice', 'Bob', 'Charlie']);
25
+ * // [{ id: 'test-p1', name: 'Alice' }, ...]
26
+ *
27
+ * // Partial custom names
28
+ * const players = createTestPlayers(3, ['Alice']);
29
+ * // [{ id: 'test-p1', name: 'Alice' }, { id: 'test-p2', name: 'Player 2' }, ...]
30
+ * ```
31
+ */
32
+ export function createTestPlayers(count: number, names?: string[]): Player[] {
33
+ const players: Player[] = [];
34
+
35
+ for (let i = 0; i < count; i++) {
36
+ const playerId = createPlayerId(`test-p${i + 1}`);
37
+ const playerName = names?.[i] ?? `Player ${i + 1}`;
38
+
39
+ players.push({
40
+ id: playerId,
41
+ name: playerName,
42
+ });
43
+ }
44
+
45
+ return players;
46
+ }
@@ -0,0 +1,356 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { RuleEngine } from "../engine/rule-engine";
3
+ import type { GameDefinition } from "../game-definition/game-definition";
4
+ import type { GameMoveDefinitions } from "../game-definition/move-definitions";
5
+ import { createPlayerId, type PlayerId } from "../types";
6
+ import { expectDeterministicReplay } from "./test-replay-assertions";
7
+
8
+ /**
9
+ * Test state for replay assertions
10
+ */
11
+ type ReplayTestState = {
12
+ players: Array<{
13
+ id: PlayerId;
14
+ name: string;
15
+ score: number;
16
+ }>;
17
+ turnNumber: number;
18
+ randomValues: number[];
19
+ };
20
+
21
+ type ReplayTestMoves = {
22
+ addRandom: Record<string, never>;
23
+ incrementScore: { amount: number };
24
+ };
25
+
26
+ describe("test-replay-assertions", () => {
27
+ function createTestEngine(seed?: string) {
28
+ const moves: GameMoveDefinitions<ReplayTestState, ReplayTestMoves> = {
29
+ addRandom: {
30
+ reducer: (draft, context) => {
31
+ // Use RNG to add random value
32
+ const value = context.rng?.randomInt(1, 100) ?? 0;
33
+ draft.randomValues.push(value);
34
+ },
35
+ },
36
+ incrementScore: {
37
+ reducer: (draft, context) => {
38
+ const player = draft.players.find((p) => p.id === context.playerId);
39
+ if (player && context.params?.amount) {
40
+ player.score += context.params.amount as number;
41
+ }
42
+ },
43
+ },
44
+ };
45
+
46
+ const gameDefinition: GameDefinition<ReplayTestState, ReplayTestMoves> = {
47
+ name: "Replay Test Game",
48
+ setup: (players) => ({
49
+ players: players.map((p) => ({
50
+ id: p.id as PlayerId,
51
+ name: p.name || "Player",
52
+ score: 0,
53
+ })),
54
+ turnNumber: 1,
55
+ randomValues: [],
56
+ }),
57
+ moves,
58
+ };
59
+
60
+ const players = [
61
+ { id: createPlayerId("p1"), name: "Alice" },
62
+ { id: createPlayerId("p2"), name: "Bob" },
63
+ ];
64
+
65
+ return new RuleEngine(gameDefinition, players, { seed });
66
+ }
67
+
68
+ describe("expectDeterministicReplay", () => {
69
+ it("should pass when replay produces same state", () => {
70
+ const engine = createTestEngine("replay-seed");
71
+
72
+ // Execute some moves
73
+ engine.executeMove("incrementScore", {
74
+ playerId: createPlayerId("p1"),
75
+ params: { amount: 5 },
76
+ });
77
+ engine.executeMove("addRandom", {
78
+ playerId: createPlayerId("p1"),
79
+ params: {},
80
+ });
81
+ engine.executeMove("incrementScore", {
82
+ playerId: createPlayerId("p2"),
83
+ params: { amount: 3 },
84
+ });
85
+
86
+ // Should not throw
87
+ expectDeterministicReplay(engine);
88
+ });
89
+
90
+ it("should verify replay with RNG operations", () => {
91
+ const engine = createTestEngine("rng-replay-seed");
92
+
93
+ // Execute moves that use RNG
94
+ for (let i = 0; i < 10; i++) {
95
+ engine.executeMove("addRandom", {
96
+ playerId: createPlayerId("p1"),
97
+ params: {},
98
+ });
99
+ }
100
+
101
+ // Replay should produce exact same random values
102
+ expectDeterministicReplay(engine);
103
+ });
104
+
105
+ it("should work with complex move sequences", () => {
106
+ const engine = createTestEngine("complex-replay");
107
+
108
+ // Complex sequence
109
+ engine.executeMove("incrementScore", {
110
+ playerId: createPlayerId("p1"),
111
+ params: { amount: 1 },
112
+ });
113
+ engine.executeMove("addRandom", {
114
+ playerId: createPlayerId("p1"),
115
+ params: {},
116
+ });
117
+ engine.executeMove("incrementScore", {
118
+ playerId: createPlayerId("p2"),
119
+ params: { amount: 2 },
120
+ });
121
+ engine.executeMove("addRandom", {
122
+ playerId: createPlayerId("p2"),
123
+ params: {},
124
+ });
125
+ engine.executeMove("incrementScore", {
126
+ playerId: createPlayerId("p1"),
127
+ params: { amount: 3 },
128
+ });
129
+
130
+ expectDeterministicReplay(engine);
131
+ });
132
+
133
+ it("should detect non-deterministic behavior", () => {
134
+ // Create engine with non-deterministic move
135
+ let callCount = 0;
136
+ const moves: GameMoveDefinitions<ReplayTestState, ReplayTestMoves> = {
137
+ addRandom: {
138
+ reducer: (draft) => {
139
+ // Add non-deterministic behavior
140
+ callCount++;
141
+ draft.randomValues.push(callCount); // Different each time
142
+ },
143
+ },
144
+ incrementScore: {
145
+ reducer: () => {},
146
+ },
147
+ };
148
+
149
+ const gameDefinition: GameDefinition<ReplayTestState, ReplayTestMoves> = {
150
+ name: "Non-deterministic Test",
151
+ setup: (players) => ({
152
+ players: players.map((p) => ({
153
+ id: p.id as PlayerId,
154
+ name: p.name || "Player",
155
+ score: 0,
156
+ })),
157
+ turnNumber: 1,
158
+ randomValues: [],
159
+ }),
160
+ moves,
161
+ };
162
+
163
+ const engine = new RuleEngine(
164
+ gameDefinition,
165
+ [{ id: createPlayerId("p1"), name: "Alice" }],
166
+ { seed: "non-deterministic" },
167
+ );
168
+
169
+ engine.executeMove("addRandom", {
170
+ playerId: createPlayerId("p1"),
171
+ params: {},
172
+ });
173
+
174
+ // Should throw because replay will have different state
175
+ expect(() => {
176
+ expectDeterministicReplay(engine);
177
+ }).toThrow(/Replay produced different state/);
178
+ });
179
+
180
+ it("should work with empty history", () => {
181
+ const engine = createTestEngine("empty-seed");
182
+
183
+ // No moves executed
184
+ expectDeterministicReplay(engine);
185
+ });
186
+
187
+ it("should work with single move", () => {
188
+ const engine = createTestEngine("single-move");
189
+
190
+ engine.executeMove("incrementScore", {
191
+ playerId: createPlayerId("p1"),
192
+ params: { amount: 10 },
193
+ });
194
+
195
+ expectDeterministicReplay(engine);
196
+ });
197
+
198
+ it("should provide helpful error message on mismatch", () => {
199
+ // Create problematic engine
200
+ let isReplay = false;
201
+ const moves: GameMoveDefinitions<ReplayTestState, ReplayTestMoves> = {
202
+ addRandom: {
203
+ reducer: (draft, context) => {
204
+ if (isReplay) {
205
+ // Different behavior on replay
206
+ draft.randomValues.push(999);
207
+ } else {
208
+ const value = context.rng?.randomInt(1, 100) ?? 0;
209
+ draft.randomValues.push(value);
210
+ isReplay = true; // Flag for next call
211
+ }
212
+ },
213
+ },
214
+ incrementScore: { reducer: () => {} },
215
+ };
216
+
217
+ const gameDefinition: GameDefinition<ReplayTestState, ReplayTestMoves> = {
218
+ name: "Mismatch Test",
219
+ setup: (players) => ({
220
+ players: players.map((p) => ({
221
+ id: p.id as PlayerId,
222
+ name: p.name || "Player",
223
+ score: 0,
224
+ })),
225
+ turnNumber: 1,
226
+ randomValues: [],
227
+ }),
228
+ moves,
229
+ };
230
+
231
+ const engine = new RuleEngine(
232
+ gameDefinition,
233
+ [{ id: createPlayerId("p1"), name: "Alice" }],
234
+ { seed: "mismatch" },
235
+ );
236
+
237
+ engine.executeMove("addRandom", {
238
+ playerId: createPlayerId("p1"),
239
+ params: {},
240
+ });
241
+
242
+ expect(() => {
243
+ expectDeterministicReplay(engine);
244
+ }).toThrow(/Replay produced different state/);
245
+ });
246
+ });
247
+
248
+ describe("integration with real game scenarios", () => {
249
+ it("should verify card shuffling is deterministic", () => {
250
+ type CardGameState = {
251
+ players: Array<{ id: PlayerId; name: string }>;
252
+ deck: string[];
253
+ };
254
+
255
+ type CardMoves = {
256
+ shuffle: Record<string, never>;
257
+ };
258
+
259
+ const moves: GameMoveDefinitions<CardGameState, CardMoves> = {
260
+ shuffle: {
261
+ reducer: (draft, context) => {
262
+ if (context.rng) {
263
+ draft.deck = context.rng.shuffle(draft.deck);
264
+ }
265
+ },
266
+ },
267
+ };
268
+
269
+ const gameDefinition: GameDefinition<CardGameState, CardMoves> = {
270
+ name: "Card Shuffle Test",
271
+ setup: (players) => ({
272
+ players: players.map((p) => ({
273
+ id: p.id as PlayerId,
274
+ name: p.name || "Player",
275
+ })),
276
+ deck: ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"],
277
+ }),
278
+ moves,
279
+ };
280
+
281
+ const engine = new RuleEngine(
282
+ gameDefinition,
283
+ [{ id: createPlayerId("p1"), name: "Alice" }],
284
+ { seed: "shuffle-test" },
285
+ );
286
+
287
+ // Shuffle multiple times
288
+ engine.executeMove("shuffle", {
289
+ playerId: createPlayerId("p1"),
290
+ params: {},
291
+ });
292
+ engine.executeMove("shuffle", {
293
+ playerId: createPlayerId("p1"),
294
+ params: {},
295
+ });
296
+ engine.executeMove("shuffle", {
297
+ playerId: createPlayerId("p1"),
298
+ params: {},
299
+ });
300
+
301
+ // Replay should produce same sequence of shuffles
302
+ expectDeterministicReplay(engine);
303
+ });
304
+
305
+ it("should verify dice rolls are deterministic", () => {
306
+ type DiceGameState = {
307
+ players: Array<{ id: PlayerId; name: string }>;
308
+ rolls: number[];
309
+ };
310
+
311
+ type DiceMoves = {
312
+ roll: Record<string, never>;
313
+ };
314
+
315
+ const moves: GameMoveDefinitions<DiceGameState, DiceMoves> = {
316
+ roll: {
317
+ reducer: (draft, context) => {
318
+ if (context.rng) {
319
+ const roll = context.rng.rollDice(20) as number;
320
+ draft.rolls.push(roll);
321
+ }
322
+ },
323
+ },
324
+ };
325
+
326
+ const gameDefinition: GameDefinition<DiceGameState, DiceMoves> = {
327
+ name: "Dice Roll Test",
328
+ setup: (players) => ({
329
+ players: players.map((p) => ({
330
+ id: p.id as PlayerId,
331
+ name: p.name || "Player",
332
+ })),
333
+ rolls: [],
334
+ }),
335
+ moves,
336
+ };
337
+
338
+ const engine = new RuleEngine(
339
+ gameDefinition,
340
+ [{ id: createPlayerId("p1"), name: "Alice" }],
341
+ { seed: "dice-test" },
342
+ );
343
+
344
+ // Roll dice multiple times
345
+ for (let i = 0; i < 20; i++) {
346
+ engine.executeMove("roll", {
347
+ playerId: createPlayerId("p1"),
348
+ params: {},
349
+ });
350
+ }
351
+
352
+ // Replay should produce same dice rolls
353
+ expectDeterministicReplay(engine);
354
+ });
355
+ });
356
+ });