@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,508 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { RuleEngine } from "../engine/rule-engine";
3
+ import type { FlowDefinition } from "../flow/flow-definition";
4
+ import type { GameDefinition } from "../game-definition/game-definition";
5
+ import type { GameMoveDefinitions } from "../game-definition/move-definitions";
6
+ import { createPlayerId, type PlayerId } from "../types";
7
+
8
+ /**
9
+ * Task 16.3, 16.4: Integration Tests - Complete Game Flow
10
+ *
11
+ * Tests that all systems work together in a complete game:
12
+ * - Game setup → Initial state
13
+ * - Move execution with validation
14
+ * - Flow management (turns/phases)
15
+ * - End conditions
16
+ * - History and replay
17
+ * - Player views
18
+ *
19
+ * This validates the entire @drmxrcy/tcg-core framework works cohesively.
20
+ */
21
+
22
+ type CompleteGameState = {
23
+ players: Array<{
24
+ id: PlayerId;
25
+ name: string;
26
+ health: number;
27
+ hand: string[];
28
+ deck: string[];
29
+ field: string[];
30
+ }>;
31
+ currentPlayerIndex: number;
32
+ turnNumber: number;
33
+ phase: "draw" | "main" | "end" | "gameover";
34
+ winner?: PlayerId;
35
+ };
36
+
37
+ type CompleteGameMoves = {
38
+ drawCard: Record<string, never>;
39
+ playCard: { cardId: string };
40
+ attackPlayer: { targetPlayerId: PlayerId };
41
+ endPhase: Record<string, never>;
42
+ };
43
+
44
+ describe("Integration - Complete Game Flow", () => {
45
+ describe("Task 16.3: Setup → Moves → End", () => {
46
+ it("should play complete game from setup to victory", () => {
47
+ const moves: GameMoveDefinitions<CompleteGameState, CompleteGameMoves> = {
48
+ drawCard: {
49
+ condition: (state) => state.phase === "draw",
50
+ reducer: (draft) => {
51
+ const player = draft.players[draft.currentPlayerIndex];
52
+ if (player && player.deck.length > 0) {
53
+ const card = player.deck.pop();
54
+ if (card) {
55
+ player.hand.push(card);
56
+ }
57
+ }
58
+ },
59
+ },
60
+ playCard: {
61
+ condition: (state) => state.phase === "main",
62
+ reducer: (draft, context) => {
63
+ const player = draft.players[draft.currentPlayerIndex];
64
+ if (player && context.params?.cardId) {
65
+ const cardId = context.params.cardId as string;
66
+ const cardIndex = player.hand.indexOf(cardId);
67
+ if (cardIndex >= 0) {
68
+ player.hand.splice(cardIndex, 1);
69
+ player.field.push(cardId);
70
+ }
71
+ }
72
+ },
73
+ },
74
+ attackPlayer: {
75
+ condition: (state) => state.phase === "main",
76
+ reducer: (draft, context) => {
77
+ if (context.params?.targetPlayerId) {
78
+ const target = draft.players.find(
79
+ (p) => p.id === context.params?.targetPlayerId,
80
+ );
81
+ if (target) {
82
+ target.health -= 1;
83
+ if (target.health <= 0) {
84
+ draft.phase = "gameover";
85
+ // Find winner (player with health > 0)
86
+ const winner = draft.players.find((p) => p.health > 0);
87
+ if (winner) {
88
+ draft.winner = winner.id;
89
+ }
90
+ }
91
+ }
92
+ }
93
+ },
94
+ },
95
+ endPhase: {
96
+ reducer: (draft) => {
97
+ if (draft.phase === "draw") {
98
+ draft.phase = "main";
99
+ } else if (draft.phase === "main") {
100
+ draft.phase = "end";
101
+ } else if (draft.phase === "end") {
102
+ // Next player's turn
103
+ draft.currentPlayerIndex =
104
+ (draft.currentPlayerIndex + 1) % draft.players.length;
105
+ draft.turnNumber += 1;
106
+ draft.phase = "draw";
107
+ }
108
+ },
109
+ },
110
+ };
111
+
112
+ const flow: FlowDefinition<CompleteGameState> = {
113
+ turn: {
114
+ onBegin: (context) => {
115
+ context.state.phase = "draw";
116
+ },
117
+ phases: {
118
+ draw: {
119
+ order: 0,
120
+ next: "main",
121
+ },
122
+ main: {
123
+ order: 1,
124
+ next: "end",
125
+ },
126
+ end: {
127
+ order: 2,
128
+ next: undefined,
129
+ },
130
+ },
131
+ },
132
+ };
133
+
134
+ const gameDefinition: GameDefinition<
135
+ CompleteGameState,
136
+ CompleteGameMoves
137
+ > = {
138
+ name: "Complete Game Test",
139
+ setup: (players) => ({
140
+ players: players.map((p) => ({
141
+ id: p.id as PlayerId,
142
+ name: p.name || "Player",
143
+ health: 3,
144
+ hand: [] as string[],
145
+ deck: ["card1", "card2", "card3"] as string[],
146
+ field: [] as string[],
147
+ })),
148
+ currentPlayerIndex: 0,
149
+ turnNumber: 1,
150
+ phase: "draw",
151
+ }),
152
+ moves,
153
+ flow,
154
+ endIf: (state) => {
155
+ if (state.winner) {
156
+ return {
157
+ winner: state.winner,
158
+ reason: "Opponent eliminated",
159
+ };
160
+ }
161
+ return undefined;
162
+ },
163
+ };
164
+
165
+ const players = [
166
+ { id: createPlayerId("p1"), name: "Alice" },
167
+ { id: createPlayerId("p2"), name: "Bob" },
168
+ ];
169
+
170
+ const engine = new RuleEngine(gameDefinition, players, {
171
+ seed: "complete-game-123",
172
+ });
173
+
174
+ // Verify initial setup
175
+ let state = engine.getState();
176
+ expect(state.players).toHaveLength(2);
177
+ expect(state.players[0]?.health).toBe(3);
178
+ expect(state.phase).toBe("draw");
179
+ expect(state.turnNumber).toBe(1);
180
+
181
+ // Play through a complete turn sequence
182
+ // Turn 1 - Player 1
183
+ engine.executeMove("drawCard", {
184
+ playerId: createPlayerId("p1"),
185
+ params: {},
186
+ });
187
+ state = engine.getState();
188
+ expect(state.players[0]?.hand.length).toBe(1);
189
+
190
+ engine.executeMove("endPhase", {
191
+ playerId: createPlayerId("p1"),
192
+ params: {},
193
+ }); // draw -> main
194
+ state = engine.getState();
195
+ expect(state.phase).toBe("main");
196
+
197
+ engine.executeMove("playCard", {
198
+ playerId: createPlayerId("p1"),
199
+ params: { cardId: "card3" },
200
+ });
201
+ state = engine.getState();
202
+ expect(state.players[0]?.field.length).toBe(1);
203
+
204
+ engine.executeMove("attackPlayer", {
205
+ playerId: createPlayerId("p1"),
206
+ params: { targetPlayerId: createPlayerId("p2") },
207
+ });
208
+ state = engine.getState();
209
+ expect(state.players[1]?.health).toBe(2);
210
+
211
+ engine.executeMove("endPhase", {
212
+ playerId: createPlayerId("p1"),
213
+ params: {},
214
+ }); // main -> end
215
+ state = engine.getState();
216
+ expect(state.phase).toBe("end");
217
+
218
+ engine.executeMove("endPhase", {
219
+ playerId: createPlayerId("p1"),
220
+ params: {},
221
+ }); // end -> next turn
222
+ state = engine.getState();
223
+ expect(state.phase).toBe("draw");
224
+ expect(state.turnNumber).toBe(2);
225
+ expect(state.currentPlayerIndex).toBe(1);
226
+
227
+ // Continue until win condition
228
+ let turns = 0;
229
+ const maxTurns = 20;
230
+
231
+ while (!engine.checkGameEnd() && turns < maxTurns) {
232
+ const currentPlayer = state.players[state.currentPlayerIndex];
233
+ if (!currentPlayer) break;
234
+
235
+ // Draw phase
236
+ if (state.phase === "draw" && currentPlayer.deck.length > 0) {
237
+ engine.executeMove("drawCard", {
238
+ playerId: currentPlayer.id,
239
+ params: {},
240
+ });
241
+ }
242
+ engine.executeMove("endPhase", {
243
+ playerId: currentPlayer.id,
244
+ params: {},
245
+ });
246
+
247
+ // Main phase - attack if possible
248
+ state = engine.getState();
249
+ if (state.phase === "main") {
250
+ const opponentIndex =
251
+ (state.currentPlayerIndex + 1) % state.players.length;
252
+ const opponent = state.players[opponentIndex];
253
+ if (opponent) {
254
+ engine.executeMove("attackPlayer", {
255
+ playerId: currentPlayer.id,
256
+ params: { targetPlayerId: opponent.id },
257
+ });
258
+ }
259
+ }
260
+ engine.executeMove("endPhase", {
261
+ playerId: currentPlayer.id,
262
+ params: {},
263
+ });
264
+
265
+ // End phase
266
+ state = engine.getState();
267
+ if (state.phase === "end") {
268
+ engine.executeMove("endPhase", {
269
+ playerId: currentPlayer.id,
270
+ params: {},
271
+ });
272
+ }
273
+
274
+ state = engine.getState();
275
+ turns++;
276
+ }
277
+
278
+ // Game should have ended
279
+ const gameEnd = engine.checkGameEnd();
280
+ expect(gameEnd).toBeDefined();
281
+ expect(gameEnd?.winner).toBeDefined();
282
+ expect(turns).toBeLessThan(maxTurns);
283
+
284
+ // Verify winner has health > 0
285
+ const winner = state.players.find((p) => p.id === gameEnd?.winner);
286
+ expect(winner?.health).toBeGreaterThan(0);
287
+
288
+ // Verify loser has health <= 0
289
+ const loser = state.players.find((p) => p.id !== gameEnd?.winner);
290
+ expect(loser?.health).toBeLessThanOrEqual(0);
291
+ });
292
+ });
293
+
294
+ describe("Task 16.4: All Systems Working Together", () => {
295
+ it("should integrate zones, cards, moves, flow, RNG, and history", () => {
296
+ const moves: GameMoveDefinitions<CompleteGameState, CompleteGameMoves> = {
297
+ drawCard: {
298
+ reducer: (draft, context) => {
299
+ const player = draft.players[draft.currentPlayerIndex];
300
+ if (player && player.deck.length > 0) {
301
+ // Use RNG to draw random card (deterministic)
302
+ const rng = context.rng;
303
+ if (rng) {
304
+ const index = rng.randomInt(0, player.deck.length - 1);
305
+ const card = player.deck.splice(index, 1)[0];
306
+ if (card) {
307
+ player.hand.push(card);
308
+ }
309
+ }
310
+ }
311
+ },
312
+ },
313
+ playCard: { reducer: () => {} },
314
+ attackPlayer: { reducer: () => {} },
315
+ endPhase: { reducer: () => {} },
316
+ };
317
+
318
+ const gameDefinition: GameDefinition<
319
+ CompleteGameState,
320
+ CompleteGameMoves
321
+ > = {
322
+ name: "Integration Test",
323
+ setup: (players) => ({
324
+ players: players.map((p) => ({
325
+ id: p.id as PlayerId,
326
+ name: p.name || "Player",
327
+ health: 5,
328
+ hand: [] as string[],
329
+ deck: ["A", "B", "C", "D", "E"] as string[],
330
+ field: [] as string[],
331
+ })),
332
+ currentPlayerIndex: 0,
333
+ turnNumber: 1,
334
+ phase: "draw",
335
+ }),
336
+ moves,
337
+ playerView: (state, playerId) => ({
338
+ ...state,
339
+ players: state.players.map((p) => ({
340
+ ...p,
341
+ // Hide opponent's hand
342
+ hand: p.id === playerId ? p.hand : [],
343
+ // Hide opponent's deck
344
+ deck: p.id === playerId ? p.deck : [],
345
+ })),
346
+ }),
347
+ };
348
+
349
+ const players = [
350
+ { id: createPlayerId("p1"), name: "Alice" },
351
+ { id: createPlayerId("p2"), name: "Bob" },
352
+ ];
353
+
354
+ const engine = new RuleEngine(gameDefinition, players, {
355
+ seed: "integration-test-456",
356
+ });
357
+
358
+ // Test RNG integration
359
+ engine.executeMove("drawCard", {
360
+ playerId: createPlayerId("p1"),
361
+ params: {},
362
+ });
363
+ const state1 = engine.getState();
364
+ expect(state1.players[0]?.hand.length).toBe(1);
365
+
366
+ // Test history tracking
367
+ engine.executeMove("drawCard", {
368
+ playerId: createPlayerId("p1"),
369
+ params: {},
370
+ });
371
+ const history = engine.getHistory();
372
+ expect(history.length).toBe(2);
373
+
374
+ // Test undo/redo
375
+ engine.undo();
376
+ const stateAfterUndo = engine.getState();
377
+ expect(stateAfterUndo.players[0]?.hand.length).toBe(1);
378
+
379
+ engine.redo();
380
+ const stateAfterRedo = engine.getState();
381
+ expect(stateAfterRedo.players[0]?.hand.length).toBe(2);
382
+
383
+ // Test player views
384
+ const p1View = engine.getPlayerView(createPlayerId("p1"));
385
+ const p2View = engine.getPlayerView(createPlayerId("p2"));
386
+
387
+ // P1 can see their own hand
388
+ expect(p1View.players[0]?.hand.length).toBe(2);
389
+ // P1 cannot see P2's hand
390
+ expect(p1View.players[1]?.hand.length).toBe(0);
391
+
392
+ // P2 cannot see P1's hand
393
+ expect(p2View.players[0]?.hand.length).toBe(0);
394
+
395
+ // Test patches for network sync
396
+ const patches = engine.getPatches();
397
+ expect(patches.length).toBeGreaterThan(0);
398
+
399
+ // Test deterministic replay
400
+ const finalState = engine.getState();
401
+ const replayedState = engine.replay();
402
+ expect(replayedState).toEqual(finalState);
403
+ });
404
+
405
+ it("should handle complex game state with multiple systems", () => {
406
+ const moves: GameMoveDefinitions<CompleteGameState, CompleteGameMoves> = {
407
+ drawCard: {
408
+ condition: (state) => {
409
+ const player = state.players[state.currentPlayerIndex];
410
+ return player !== undefined && player.deck.length > 0;
411
+ },
412
+ reducer: (draft) => {
413
+ const player = draft.players[draft.currentPlayerIndex];
414
+ if (player) {
415
+ const card = player.deck.pop();
416
+ if (card) {
417
+ player.hand.push(card);
418
+ }
419
+ }
420
+ },
421
+ },
422
+ playCard: {
423
+ condition: (state, context) => {
424
+ const player = state.players[state.currentPlayerIndex];
425
+ if (!(player && context.params?.cardId)) return false;
426
+ return player.hand.includes(context.params.cardId as string);
427
+ },
428
+ reducer: (draft, context) => {
429
+ const player = draft.players[draft.currentPlayerIndex];
430
+ if (player && context.params?.cardId) {
431
+ const cardId = context.params.cardId as string;
432
+ const index = player.hand.indexOf(cardId);
433
+ if (index >= 0) {
434
+ player.hand.splice(index, 1);
435
+ player.field.push(cardId);
436
+ }
437
+ }
438
+ },
439
+ },
440
+ attackPlayer: { reducer: () => {} },
441
+ endPhase: { reducer: () => {} },
442
+ };
443
+
444
+ const gameDefinition: GameDefinition<
445
+ CompleteGameState,
446
+ CompleteGameMoves
447
+ > = {
448
+ name: "Complex State Test",
449
+ setup: (players) => ({
450
+ players: players.map((p) => ({
451
+ id: p.id as PlayerId,
452
+ name: p.name || "Player",
453
+ health: 10,
454
+ hand: [] as string[],
455
+ deck: Array.from({ length: 20 }, (_, i) => `card${i}`) as string[],
456
+ field: [] as string[],
457
+ })),
458
+ currentPlayerIndex: 0,
459
+ turnNumber: 1,
460
+ phase: "draw",
461
+ }),
462
+ moves,
463
+ };
464
+
465
+ const players = [
466
+ { id: createPlayerId("p1"), name: "Alice" },
467
+ { id: createPlayerId("p2"), name: "Bob" },
468
+ ];
469
+
470
+ const engine = new RuleEngine(gameDefinition, players);
471
+
472
+ // Execute multiple moves with validation
473
+ for (let i = 0; i < 5; i++) {
474
+ const result = engine.executeMove("drawCard", {
475
+ playerId: createPlayerId("p1"),
476
+ params: {},
477
+ });
478
+ expect(result.success).toBe(true);
479
+ }
480
+
481
+ const state = engine.getState();
482
+ expect(state.players[0]?.hand.length).toBe(5);
483
+ expect(state.players[0]?.deck.length).toBe(15);
484
+
485
+ // Try invalid move
486
+ const invalidResult = engine.executeMove("playCard", {
487
+ playerId: createPlayerId("p1"),
488
+ params: { cardId: "nonexistent" },
489
+ });
490
+ expect(invalidResult.success).toBe(false);
491
+
492
+ // Valid move
493
+ const validResult = engine.executeMove("playCard", {
494
+ playerId: createPlayerId("p1"),
495
+ params: { cardId: state.players[0]?.hand[0] },
496
+ });
497
+ expect(validResult.success).toBe(true);
498
+
499
+ const finalState = engine.getState();
500
+ expect(finalState.players[0]?.hand.length).toBe(4);
501
+ expect(finalState.players[0]?.field.length).toBe(1);
502
+
503
+ // Verify history captured all successful moves
504
+ const history = engine.getHistory({ includeFailures: false });
505
+ expect(history.length).toBe(6); // 5 draws + 1 successful play (failed moves filtered out)
506
+ });
507
+ });
508
+ });