@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,535 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { GameDefinition } from "../../game-definition/game-definition";
3
+ import type { GameMoveDefinitions } from "../../game-definition/move-definitions";
4
+ import { createPlayerId } from "../../types";
5
+ import { RuleEngine } from "../rule-engine";
6
+
7
+ /**
8
+ * Task 11: Rule Engine History & Advanced Features Tests
9
+ *
10
+ * Tests for:
11
+ * - History tracking (11.17-11.18)
12
+ * - Undo/Redo (11.15-11.16)
13
+ * - Replay (11.19-11.20)
14
+ * - RNG integration (11.25-11.26)
15
+ * - Flow integration (11.27-11.28)
16
+ */
17
+
18
+ type TestGameState = {
19
+ players: Array<{ id: string; name: string; score: number }>;
20
+ currentPlayerIndex: number;
21
+ randomValue?: number;
22
+ };
23
+
24
+ type TestMoves = {
25
+ incrementScore: { amount: number };
26
+ randomMove: Record<string, never>;
27
+ nextPlayer: Record<string, never>;
28
+ };
29
+
30
+ describe("RuleEngine - History & Replay", () => {
31
+ describe("Task 11.17, 11.18: getHistory", () => {
32
+ it("should track move history", () => {
33
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
34
+ incrementScore: {
35
+ reducer: (draft, context) => {
36
+ const player = draft.players[draft.currentPlayerIndex];
37
+ if (player && context.params?.amount) {
38
+ player.score += context.params.amount as number;
39
+ }
40
+ },
41
+ },
42
+ randomMove: { reducer: () => {} },
43
+ nextPlayer: { reducer: () => {} },
44
+ };
45
+
46
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
47
+ name: "Test Game",
48
+ setup: (players) => ({
49
+ players: players.map((p) => ({
50
+ id: p.id,
51
+ name: p.name || "Player",
52
+ score: 0,
53
+ })),
54
+ currentPlayerIndex: 0,
55
+ }),
56
+ moves,
57
+ };
58
+
59
+ const players = [
60
+ { id: createPlayerId("p1"), name: "Alice" },
61
+ { id: createPlayerId("p2"), name: "Bob" },
62
+ ];
63
+
64
+ const engine = new RuleEngine(gameDef, players);
65
+
66
+ // Execute some moves
67
+ engine.executeMove("incrementScore", {
68
+ playerId: createPlayerId("p1"),
69
+ params: { amount: 5 },
70
+ });
71
+
72
+ engine.executeMove("incrementScore", {
73
+ playerId: createPlayerId("p1"),
74
+ params: { amount: 3 },
75
+ });
76
+
77
+ const history = engine.getReplayHistory();
78
+
79
+ expect(history).toHaveLength(2);
80
+ expect(history[0]?.moveId).toBe("incrementScore");
81
+ expect(history[0]?.context.params?.amount).toBe(5);
82
+ expect(history[1]?.moveId).toBe("incrementScore");
83
+ expect(history[1]?.context.params?.amount).toBe(3);
84
+ });
85
+
86
+ it("should include patches in history", () => {
87
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
88
+ incrementScore: {
89
+ reducer: (draft, context) => {
90
+ const player = draft.players[draft.currentPlayerIndex];
91
+ if (player && context.params?.amount) {
92
+ player.score += context.params.amount as number;
93
+ }
94
+ },
95
+ },
96
+ randomMove: { reducer: () => {} },
97
+ nextPlayer: { reducer: () => {} },
98
+ };
99
+
100
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
101
+ name: "Test Game",
102
+ setup: (players) => ({
103
+ players: players.map((p) => ({
104
+ id: p.id,
105
+ name: p.name || "Player",
106
+ score: 0,
107
+ })),
108
+ currentPlayerIndex: 0,
109
+ }),
110
+ moves,
111
+ };
112
+
113
+ const players = [
114
+ { id: createPlayerId("p1"), name: "Alice" },
115
+ { id: createPlayerId("p2"), name: "Bob" },
116
+ ];
117
+
118
+ const engine = new RuleEngine(gameDef, players);
119
+
120
+ engine.executeMove("incrementScore", {
121
+ playerId: createPlayerId("p1"),
122
+ params: { amount: 5 },
123
+ });
124
+
125
+ const history = engine.getReplayHistory();
126
+
127
+ expect(history[0]?.patches).toBeDefined();
128
+ expect(history[0]?.patches.length).toBeGreaterThan(0);
129
+ expect(history[0]?.inversePatches).toBeDefined();
130
+ expect(history[0]?.inversePatches.length).toBeGreaterThan(0);
131
+ });
132
+ });
133
+
134
+ describe("Task 11.15, 11.16: Undo/Redo", () => {
135
+ it("should undo last move", () => {
136
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
137
+ incrementScore: {
138
+ reducer: (draft, context) => {
139
+ const player = draft.players[draft.currentPlayerIndex];
140
+ if (player && context.params?.amount) {
141
+ player.score += context.params.amount as number;
142
+ }
143
+ },
144
+ },
145
+ randomMove: { reducer: () => {} },
146
+ nextPlayer: { reducer: () => {} },
147
+ };
148
+
149
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
150
+ name: "Test Game",
151
+ setup: (players) => ({
152
+ players: players.map((p) => ({
153
+ id: p.id,
154
+ name: p.name || "Player",
155
+ score: 0,
156
+ })),
157
+ currentPlayerIndex: 0,
158
+ }),
159
+ moves,
160
+ };
161
+
162
+ const players = [
163
+ { id: createPlayerId("p1"), name: "Alice" },
164
+ { id: createPlayerId("p2"), name: "Bob" },
165
+ ];
166
+
167
+ const engine = new RuleEngine(gameDef, players);
168
+
169
+ // Execute a move
170
+ engine.executeMove("incrementScore", {
171
+ playerId: createPlayerId("p1"),
172
+ params: { amount: 5 },
173
+ });
174
+
175
+ let state = engine.getState();
176
+ expect(state.players[0]?.score).toBe(5);
177
+
178
+ // Undo
179
+ const undoSuccess = engine.undo();
180
+ expect(undoSuccess).toBe(true);
181
+
182
+ state = engine.getState();
183
+ expect(state.players[0]?.score).toBe(0);
184
+ });
185
+
186
+ it("should redo previously undone move", () => {
187
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
188
+ incrementScore: {
189
+ reducer: (draft, context) => {
190
+ const player = draft.players[draft.currentPlayerIndex];
191
+ if (player && context.params?.amount) {
192
+ player.score += context.params.amount as number;
193
+ }
194
+ },
195
+ },
196
+ randomMove: { reducer: () => {} },
197
+ nextPlayer: { reducer: () => {} },
198
+ };
199
+
200
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
201
+ name: "Test Game",
202
+ setup: (players) => ({
203
+ players: players.map((p) => ({
204
+ id: p.id,
205
+ name: p.name || "Player",
206
+ score: 0,
207
+ })),
208
+ currentPlayerIndex: 0,
209
+ }),
210
+ moves,
211
+ };
212
+
213
+ const players = [
214
+ { id: createPlayerId("p1"), name: "Alice" },
215
+ { id: createPlayerId("p2"), name: "Bob" },
216
+ ];
217
+
218
+ const engine = new RuleEngine(gameDef, players);
219
+
220
+ // Execute, undo, redo
221
+ engine.executeMove("incrementScore", {
222
+ playerId: createPlayerId("p1"),
223
+ params: { amount: 5 },
224
+ });
225
+
226
+ engine.undo();
227
+ const redoSuccess = engine.redo();
228
+ expect(redoSuccess).toBe(true);
229
+
230
+ const state = engine.getState();
231
+ expect(state.players[0]?.score).toBe(5);
232
+ });
233
+
234
+ it("should return false when no moves to undo", () => {
235
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
236
+ incrementScore: { reducer: () => {} },
237
+ randomMove: { reducer: () => {} },
238
+ nextPlayer: { reducer: () => {} },
239
+ };
240
+
241
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
242
+ name: "Test Game",
243
+ setup: (players) => ({
244
+ players: players.map((p) => ({
245
+ id: p.id,
246
+ name: p.name || "Player",
247
+ score: 0,
248
+ })),
249
+ currentPlayerIndex: 0,
250
+ }),
251
+ moves,
252
+ };
253
+
254
+ const players = [
255
+ { id: createPlayerId("p1"), name: "Alice" },
256
+ { id: createPlayerId("p2"), name: "Bob" },
257
+ ];
258
+
259
+ const engine = new RuleEngine(gameDef, players);
260
+ const undoSuccess = engine.undo();
261
+
262
+ expect(undoSuccess).toBe(false);
263
+ });
264
+
265
+ it("should return false when no moves to redo", () => {
266
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
267
+ incrementScore: { reducer: () => {} },
268
+ randomMove: { reducer: () => {} },
269
+ nextPlayer: { reducer: () => {} },
270
+ };
271
+
272
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
273
+ name: "Test Game",
274
+ setup: (players) => ({
275
+ players: players.map((p) => ({
276
+ id: p.id,
277
+ name: p.name || "Player",
278
+ score: 0,
279
+ })),
280
+ currentPlayerIndex: 0,
281
+ }),
282
+ moves,
283
+ };
284
+
285
+ const players = [
286
+ { id: createPlayerId("p1"), name: "Alice" },
287
+ { id: createPlayerId("p2"), name: "Bob" },
288
+ ];
289
+
290
+ const engine = new RuleEngine(gameDef, players);
291
+ const redoSuccess = engine.redo();
292
+
293
+ expect(redoSuccess).toBe(false);
294
+ });
295
+
296
+ it("should truncate forward history on new move after undo", () => {
297
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
298
+ incrementScore: {
299
+ reducer: (draft, context) => {
300
+ const player = draft.players[draft.currentPlayerIndex];
301
+ if (player && context.params?.amount) {
302
+ player.score += context.params.amount as number;
303
+ }
304
+ },
305
+ },
306
+ randomMove: { reducer: () => {} },
307
+ nextPlayer: { reducer: () => {} },
308
+ };
309
+
310
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
311
+ name: "Test Game",
312
+ setup: (players) => ({
313
+ players: players.map((p) => ({
314
+ id: p.id,
315
+ name: p.name || "Player",
316
+ score: 0,
317
+ })),
318
+ currentPlayerIndex: 0,
319
+ }),
320
+ moves,
321
+ };
322
+
323
+ const players = [
324
+ { id: createPlayerId("p1"), name: "Alice" },
325
+ { id: createPlayerId("p2"), name: "Bob" },
326
+ ];
327
+
328
+ const engine = new RuleEngine(gameDef, players);
329
+
330
+ // Execute two moves
331
+ engine.executeMove("incrementScore", {
332
+ playerId: createPlayerId("p1"),
333
+ params: { amount: 5 },
334
+ });
335
+
336
+ engine.executeMove("incrementScore", {
337
+ playerId: createPlayerId("p1"),
338
+ params: { amount: 3 },
339
+ });
340
+
341
+ // Undo one
342
+ engine.undo();
343
+
344
+ // Execute new move
345
+ engine.executeMove("incrementScore", {
346
+ playerId: createPlayerId("p1"),
347
+ params: { amount: 7 },
348
+ });
349
+
350
+ // Should not be able to redo (history was truncated)
351
+ const redoSuccess = engine.redo();
352
+ expect(redoSuccess).toBe(false);
353
+
354
+ const state = engine.getState();
355
+ expect(state.players[0]?.score).toBe(12); // 5 + 7
356
+ });
357
+ });
358
+
359
+ describe("Task 11.21, 11.22, 11.23, 11.24: Patch Management", () => {
360
+ it("should get patches since specific index", () => {
361
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
362
+ incrementScore: {
363
+ reducer: (draft, context) => {
364
+ const player = draft.players[draft.currentPlayerIndex];
365
+ if (player && context.params?.amount) {
366
+ player.score += context.params.amount as number;
367
+ }
368
+ },
369
+ },
370
+ randomMove: { reducer: () => {} },
371
+ nextPlayer: { reducer: () => {} },
372
+ };
373
+
374
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
375
+ name: "Test Game",
376
+ setup: (players) => ({
377
+ players: players.map((p) => ({
378
+ id: p.id,
379
+ name: p.name || "Player",
380
+ score: 0,
381
+ })),
382
+ currentPlayerIndex: 0,
383
+ }),
384
+ moves,
385
+ };
386
+
387
+ const players = [
388
+ { id: createPlayerId("p1"), name: "Alice" },
389
+ { id: createPlayerId("p2"), name: "Bob" },
390
+ ];
391
+
392
+ const engine = new RuleEngine(gameDef, players);
393
+
394
+ // Execute three moves
395
+ engine.executeMove("incrementScore", {
396
+ playerId: createPlayerId("p1"),
397
+ params: { amount: 5 },
398
+ });
399
+
400
+ engine.executeMove("incrementScore", {
401
+ playerId: createPlayerId("p1"),
402
+ params: { amount: 3 },
403
+ });
404
+
405
+ engine.executeMove("incrementScore", {
406
+ playerId: createPlayerId("p1"),
407
+ params: { amount: 2 },
408
+ });
409
+
410
+ // Get patches since move 1
411
+ const patches = engine.getPatches(1);
412
+
413
+ // Should include patches from moves 2 and 3
414
+ expect(patches.length).toBeGreaterThan(0);
415
+ });
416
+
417
+ it("should get all patches when no index specified", () => {
418
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
419
+ incrementScore: {
420
+ reducer: (draft, context) => {
421
+ const player = draft.players[draft.currentPlayerIndex];
422
+ if (player && context.params?.amount) {
423
+ player.score += context.params.amount as number;
424
+ }
425
+ },
426
+ },
427
+ randomMove: { reducer: () => {} },
428
+ nextPlayer: { reducer: () => {} },
429
+ };
430
+
431
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
432
+ name: "Test Game",
433
+ setup: (players) => ({
434
+ players: players.map((p) => ({
435
+ id: p.id,
436
+ name: p.name || "Player",
437
+ score: 0,
438
+ })),
439
+ currentPlayerIndex: 0,
440
+ }),
441
+ moves,
442
+ };
443
+
444
+ const players = [
445
+ { id: createPlayerId("p1"), name: "Alice" },
446
+ { id: createPlayerId("p2"), name: "Bob" },
447
+ ];
448
+
449
+ const engine = new RuleEngine(gameDef, players);
450
+
451
+ engine.executeMove("incrementScore", {
452
+ playerId: createPlayerId("p1"),
453
+ params: { amount: 5 },
454
+ });
455
+
456
+ const patches = engine.getPatches();
457
+ expect(patches.length).toBeGreaterThan(0);
458
+ });
459
+ });
460
+
461
+ describe("Task 11.25, 11.26: RNG Integration", () => {
462
+ it("should provide access to seeded RNG", () => {
463
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
464
+ incrementScore: { reducer: () => {} },
465
+ randomMove: { reducer: () => {} },
466
+ nextPlayer: { reducer: () => {} },
467
+ };
468
+
469
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
470
+ name: "Test Game",
471
+ setup: (players) => ({
472
+ players: players.map((p) => ({
473
+ id: p.id,
474
+ name: p.name || "Player",
475
+ score: 0,
476
+ })),
477
+ currentPlayerIndex: 0,
478
+ }),
479
+ moves,
480
+ };
481
+
482
+ const players = [
483
+ { id: createPlayerId("p1"), name: "Alice" },
484
+ { id: createPlayerId("p2"), name: "Bob" },
485
+ ];
486
+
487
+ const engine = new RuleEngine(gameDef, players);
488
+ const rng = engine.getRNG();
489
+
490
+ expect(rng).toBeDefined();
491
+ expect(rng.random).toBeFunction();
492
+ });
493
+
494
+ it("should produce deterministic results with same seed", () => {
495
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
496
+ randomMove: {
497
+ reducer: (draft) => {
498
+ // Moves can access RNG through game state or context
499
+ draft.randomValue = 42; // Placeholder
500
+ },
501
+ },
502
+ incrementScore: { reducer: () => {} },
503
+ nextPlayer: { reducer: () => {} },
504
+ };
505
+
506
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
507
+ name: "Test Game",
508
+ setup: (players) => ({
509
+ players: players.map((p) => ({
510
+ id: p.id,
511
+ name: p.name || "Player",
512
+ score: 0,
513
+ })),
514
+ currentPlayerIndex: 0,
515
+ }),
516
+ moves,
517
+ };
518
+
519
+ const players = [
520
+ { id: createPlayerId("p1"), name: "Alice" },
521
+ { id: createPlayerId("p2"), name: "Bob" },
522
+ ];
523
+
524
+ const engine1 = new RuleEngine(gameDef, players, { seed: "test-123" });
525
+ const engine2 = new RuleEngine(gameDef, players, { seed: "test-123" });
526
+
527
+ const rng1 = engine1.getRNG();
528
+ const rng2 = engine2.getRNG();
529
+
530
+ // Same seed -> same random values
531
+ expect(rng1.random()).toBe(rng2.random());
532
+ expect(rng1.random()).toBe(rng2.random());
533
+ });
534
+ });
535
+ });