@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,725 @@
1
+ /**
2
+ * Move Enumeration System Tests
3
+ *
4
+ * Tests the move enumeration functionality including:
5
+ * - Basic enumeration
6
+ * - Validation filtering
7
+ * - Metadata inclusion
8
+ * - Error handling
9
+ * - Complex parameter types
10
+ */
11
+
12
+ import { describe, expect, it } from "bun:test";
13
+ import { type GameDefinition, type Player, RuleEngine } from "../index";
14
+ import { createPlayerId } from "../types/branded-utils";
15
+
16
+ // Test game state
17
+ type TestGameState = {
18
+ players: Array<{
19
+ id: string;
20
+ name: string;
21
+ hand: string[];
22
+ field: string[];
23
+ mana: number;
24
+ }>;
25
+ currentPlayerIndex: number;
26
+ };
27
+
28
+ // Test move parameters
29
+ type PlayCardParams = {
30
+ cardId: string;
31
+ };
32
+
33
+ type AttackParams = {
34
+ attackerId: string;
35
+ targetId: string;
36
+ };
37
+
38
+ type PassTurnParams = Record<string, never>;
39
+
40
+ type TestMoves = {
41
+ playCard: PlayCardParams;
42
+ attack: AttackParams;
43
+ passTurn: PassTurnParams;
44
+ };
45
+
46
+ describe("Move Enumeration System", () => {
47
+ describe("Basic Enumeration", () => {
48
+ it("should enumerate moves with simple parameters", () => {
49
+ const players: Player[] = [
50
+ { id: "p1", name: "Player 1" },
51
+ { id: "p2", name: "Player 2" },
52
+ ];
53
+
54
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
55
+ name: "Test Game",
56
+ setup: (players) => ({
57
+ players: players.map((p) => ({
58
+ id: p.id,
59
+ name: p.name ?? "",
60
+ hand: ["card1", "card2", "card3"],
61
+ field: [],
62
+ mana: 5,
63
+ })),
64
+ currentPlayerIndex: 0,
65
+ }),
66
+ moves: {
67
+ playCard: {
68
+ enumerator: (
69
+ state: TestGameState,
70
+ context: import("../moves/move-enumeration").MoveEnumerationContext,
71
+ ) => {
72
+ const player = state.players.find(
73
+ (p) => p.id === context.playerId,
74
+ );
75
+ if (!player) return [];
76
+
77
+ // Return all cards in hand as possible parameters
78
+ return player.hand.map((cardId: string) => ({ cardId }));
79
+ },
80
+ condition: (state, context) => {
81
+ const player = state.players.find(
82
+ (p) => p.id === context.playerId,
83
+ );
84
+ if (!player) return false;
85
+
86
+ // Check if card is in hand
87
+ return player.hand.includes(context.params.cardId);
88
+ },
89
+ reducer: (draft, context) => {
90
+ const player = draft.players.find(
91
+ (p) => p.id === context.playerId,
92
+ );
93
+ if (!player) return;
94
+
95
+ // Move card from hand to field
96
+ const index = player.hand.indexOf(context.params.cardId);
97
+ if (index >= 0) {
98
+ player.hand.splice(index, 1);
99
+ player.field.push(context.params.cardId);
100
+ }
101
+ },
102
+ },
103
+ attack: {
104
+ enumerator: () => [],
105
+ condition: () => false,
106
+ reducer: () => {},
107
+ },
108
+ passTurn: {
109
+ enumerator: () => [{}],
110
+ condition: () => true,
111
+ reducer: () => {},
112
+ },
113
+ },
114
+ };
115
+
116
+ const engine = new RuleEngine(gameDefinition, players);
117
+ const playerId = createPlayerId("p1");
118
+
119
+ // Enumerate all moves
120
+ const moves = engine.enumerateMoves(playerId);
121
+
122
+ // Should have moves for: playCard (3 cards) + attack (0) + passTurn (1)
123
+ const playCardMoves = moves.filter((m) => m.moveId === "playCard");
124
+ const passMoves = moves.filter((m) => m.moveId === "passTurn");
125
+
126
+ expect(playCardMoves.length).toBe(3);
127
+ expect(passMoves.length).toBe(1);
128
+
129
+ // Check that all cards are enumerated
130
+ const cardIds = playCardMoves.map(
131
+ (m) => (m.params as PlayCardParams).cardId,
132
+ );
133
+ expect(cardIds).toContain("card1");
134
+ expect(cardIds).toContain("card2");
135
+ expect(cardIds).toContain("card3");
136
+
137
+ // All should be valid
138
+ for (const move of playCardMoves) {
139
+ expect(move.isValid).toBe(true);
140
+ }
141
+ });
142
+
143
+ it("should enumerate moves without parameters", () => {
144
+ const players: Player[] = [{ id: "p1", name: "Player 1" }];
145
+
146
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
147
+ name: "Test Game",
148
+ setup: () => ({
149
+ players: [
150
+ { id: "p1", name: "Player 1", hand: [], field: [], mana: 0 },
151
+ ],
152
+ currentPlayerIndex: 0,
153
+ }),
154
+ moves: {
155
+ playCard: {
156
+ enumerator: () => [],
157
+ condition: () => false,
158
+ reducer: () => {},
159
+ },
160
+ attack: {
161
+ enumerator: () => [],
162
+ condition: () => false,
163
+ reducer: () => {},
164
+ },
165
+ passTurn: {
166
+ // Enumerator returns single empty object for moves without params
167
+ enumerator: () => [{}],
168
+ condition: () => true,
169
+ reducer: () => {},
170
+ },
171
+ },
172
+ };
173
+
174
+ const engine = new RuleEngine(gameDefinition, players);
175
+ const playerId = createPlayerId("p1");
176
+
177
+ const moves = engine.enumerateMoves(playerId, { validOnly: true });
178
+
179
+ // Only passTurn should be valid
180
+ expect(moves.length).toBe(1);
181
+ expect(moves[0]?.moveId).toBe("passTurn");
182
+ expect(moves[0]?.params).toEqual({});
183
+ expect(moves[0]?.isValid).toBe(true);
184
+ });
185
+ });
186
+
187
+ describe("Validation Filtering", () => {
188
+ it("should filter by validOnly option", () => {
189
+ const players: Player[] = [{ id: "p1", name: "Player 1" }];
190
+
191
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
192
+ name: "Test Game",
193
+ setup: () => ({
194
+ players: [
195
+ { id: "p1", name: "Player 1", hand: ["card1"], field: [], mana: 0 },
196
+ ],
197
+ currentPlayerIndex: 0,
198
+ }),
199
+ moves: {
200
+ playCard: {
201
+ enumerator: (
202
+ state: TestGameState,
203
+ context: import("../moves/move-enumeration").MoveEnumerationContext,
204
+ ) => {
205
+ const player = state.players.find(
206
+ (p) => p.id === context.playerId,
207
+ );
208
+ return player
209
+ ? player.hand.map((cardId: string) => ({ cardId }))
210
+ : [];
211
+ },
212
+ // Condition requires mana (which player doesn't have)
213
+ condition: (state, context) => {
214
+ const player = state.players.find(
215
+ (p) => p.id === context.playerId,
216
+ );
217
+ return (player?.mana ?? 0) > 0;
218
+ },
219
+ reducer: () => {},
220
+ },
221
+ attack: {
222
+ enumerator: () => [],
223
+ condition: () => false,
224
+ reducer: () => {},
225
+ },
226
+ passTurn: {
227
+ enumerator: () => [{}],
228
+ condition: () => true,
229
+ reducer: () => {},
230
+ },
231
+ },
232
+ };
233
+
234
+ const engine = new RuleEngine(gameDefinition, players);
235
+ const playerId = createPlayerId("p1");
236
+
237
+ // Get all moves (including invalid)
238
+ const allMoves = engine.enumerateMoves(playerId, { validOnly: false });
239
+ expect(allMoves.length).toBeGreaterThan(1);
240
+
241
+ // Check that playCard exists but is invalid
242
+ const playCardMove = allMoves.find((m) => m.moveId === "playCard");
243
+ expect(playCardMove).toBeDefined();
244
+ expect(playCardMove?.isValid).toBe(false);
245
+ expect(playCardMove?.validationError).toBeDefined();
246
+
247
+ // Get only valid moves
248
+ const validMoves = engine.enumerateMoves(playerId, { validOnly: true });
249
+
250
+ // Only passTurn should be valid
251
+ expect(validMoves.length).toBe(1);
252
+ expect(validMoves[0]?.moveId).toBe("passTurn");
253
+ expect(validMoves[0]?.isValid).toBe(true);
254
+ });
255
+
256
+ it("should include validation error details", () => {
257
+ const players: Player[] = [{ id: "p1", name: "Player 1" }];
258
+
259
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
260
+ name: "Test Game",
261
+ setup: () => ({
262
+ players: [
263
+ { id: "p1", name: "Player 1", hand: ["card1"], field: [], mana: 0 },
264
+ ],
265
+ currentPlayerIndex: 0,
266
+ }),
267
+ moves: {
268
+ playCard: {
269
+ enumerator: (
270
+ state: TestGameState,
271
+ context: import("../moves/move-enumeration").MoveEnumerationContext,
272
+ ) => {
273
+ const player = state.players.find(
274
+ (p) => p.id === context.playerId,
275
+ );
276
+ return player
277
+ ? player.hand.map((cardId: string) => ({ cardId }))
278
+ : [];
279
+ },
280
+ // Return detailed failure information
281
+ condition: (state, context) => {
282
+ const player = state.players.find(
283
+ (p) => p.id === context.playerId,
284
+ );
285
+ const required = 5;
286
+ const available = player?.mana ?? 0;
287
+
288
+ if (available < required) {
289
+ return {
290
+ reason: `Not enough mana. Required: ${required}, Available: ${available}`,
291
+ errorCode: "INSUFFICIENT_MANA",
292
+ context: { required, available },
293
+ };
294
+ }
295
+
296
+ return true;
297
+ },
298
+ reducer: () => {},
299
+ },
300
+ attack: {
301
+ enumerator: () => [],
302
+ condition: () => false,
303
+ reducer: () => {},
304
+ },
305
+ passTurn: {
306
+ enumerator: () => [{}],
307
+ condition: () => true,
308
+ reducer: () => {},
309
+ },
310
+ },
311
+ };
312
+
313
+ const engine = new RuleEngine(gameDefinition, players);
314
+ const playerId = createPlayerId("p1");
315
+
316
+ const moves = engine.enumerateMoves(playerId, { validOnly: false });
317
+ const playCardMove = moves.find((m) => m.moveId === "playCard");
318
+
319
+ expect(playCardMove).toBeDefined();
320
+ expect(playCardMove?.isValid).toBe(false);
321
+ expect(playCardMove?.validationError).toBeDefined();
322
+ expect(playCardMove?.validationError?.errorCode).toBe(
323
+ "INSUFFICIENT_MANA",
324
+ );
325
+ expect(playCardMove?.validationError?.reason).toContain(
326
+ "Not enough mana",
327
+ );
328
+ expect(playCardMove?.validationError?.context).toEqual({
329
+ required: 5,
330
+ available: 0,
331
+ });
332
+ });
333
+ });
334
+
335
+ describe("Metadata Inclusion", () => {
336
+ it("should include metadata when requested", () => {
337
+ const players: Player[] = [{ id: "p1", name: "Player 1" }];
338
+
339
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
340
+ name: "Test Game",
341
+ setup: () => ({
342
+ players: [
343
+ { id: "p1", name: "Player 1", hand: ["card1"], field: [], mana: 5 },
344
+ ],
345
+ currentPlayerIndex: 0,
346
+ }),
347
+ moves: {
348
+ playCard: {
349
+ enumerator: (
350
+ state: TestGameState,
351
+ context: import("../moves/move-enumeration").MoveEnumerationContext,
352
+ ) => {
353
+ const player = state.players.find(
354
+ (p) => p.id === context.playerId,
355
+ );
356
+ return player
357
+ ? player.hand.map((cardId: string) => ({ cardId }))
358
+ : [];
359
+ },
360
+ condition: () => true,
361
+ reducer: () => {},
362
+ metadata: {
363
+ displayName: "Play Card",
364
+ description: "Play a card from your hand",
365
+ category: "action",
366
+ tags: ["card", "play"],
367
+ priority: 1,
368
+ },
369
+ },
370
+ attack: {
371
+ enumerator: () => [],
372
+ condition: () => false,
373
+ reducer: () => {},
374
+ },
375
+ passTurn: {
376
+ enumerator: () => [{}],
377
+ condition: () => true,
378
+ reducer: () => {},
379
+ },
380
+ },
381
+ };
382
+
383
+ const engine = new RuleEngine(gameDefinition, players);
384
+ const playerId = createPlayerId("p1");
385
+
386
+ // Without metadata
387
+ const movesWithoutMeta = engine.enumerateMoves(playerId, {
388
+ includeMetadata: false,
389
+ });
390
+ const playCardWithoutMeta = movesWithoutMeta.find(
391
+ (m) => m.moveId === "playCard",
392
+ );
393
+ expect(playCardWithoutMeta?.metadata).toBeUndefined();
394
+
395
+ // With metadata
396
+ const movesWithMeta = engine.enumerateMoves(playerId, {
397
+ includeMetadata: true,
398
+ });
399
+ const playCardWithMeta = movesWithMeta.find(
400
+ (m) => m.moveId === "playCard",
401
+ );
402
+
403
+ expect(playCardWithMeta?.metadata).toBeDefined();
404
+ expect(playCardWithMeta?.metadata?.displayName).toBe("Play Card");
405
+ expect(playCardWithMeta?.metadata?.description).toBe(
406
+ "Play a card from your hand",
407
+ );
408
+ expect(playCardWithMeta?.metadata?.category).toBe("action");
409
+ expect(playCardWithMeta?.metadata?.tags).toEqual(["card", "play"]);
410
+ expect(playCardWithMeta?.metadata?.priority).toBe(1);
411
+ });
412
+ });
413
+
414
+ describe("Moves Without Enumerators", () => {
415
+ it("should handle moves without enumerators", () => {
416
+ const players: Player[] = [{ id: "p1", name: "Player 1" }];
417
+
418
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
419
+ name: "Test Game",
420
+ setup: () => ({
421
+ players: [
422
+ { id: "p1", name: "Player 1", hand: [], field: [], mana: 0 },
423
+ ],
424
+ currentPlayerIndex: 0,
425
+ }),
426
+ moves: {
427
+ playCard: {
428
+ // No enumerator provided
429
+ condition: () => true,
430
+ reducer: () => {},
431
+ },
432
+ attack: {
433
+ enumerator: () => [],
434
+ condition: () => false,
435
+ reducer: () => {},
436
+ },
437
+ passTurn: {
438
+ enumerator: () => [{}],
439
+ condition: () => true,
440
+ reducer: () => {},
441
+ },
442
+ },
443
+ };
444
+
445
+ const engine = new RuleEngine(gameDefinition, players);
446
+ const playerId = createPlayerId("p1");
447
+
448
+ // With validOnly: false, should include move with error
449
+ const allMoves = engine.enumerateMoves(playerId, { validOnly: false });
450
+ const playCardMove = allMoves.find((m) => m.moveId === "playCard");
451
+
452
+ expect(playCardMove).toBeDefined();
453
+ expect(playCardMove?.isValid).toBe(false);
454
+ expect(playCardMove?.validationError?.errorCode).toBe("NO_ENUMERATOR");
455
+ expect(playCardMove?.validationError?.reason).toContain(
456
+ "no enumerator provided",
457
+ );
458
+
459
+ // With validOnly: true, should not include move
460
+ const validMoves = engine.enumerateMoves(playerId, { validOnly: true });
461
+ const playCardInValid = validMoves.find((m) => m.moveId === "playCard");
462
+ expect(playCardInValid).toBeUndefined();
463
+ });
464
+ });
465
+
466
+ describe("Error Handling", () => {
467
+ it("should handle enumerator errors gracefully", () => {
468
+ const players: Player[] = [{ id: "p1", name: "Player 1" }];
469
+
470
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
471
+ name: "Test Game",
472
+ setup: () => ({
473
+ players: [
474
+ { id: "p1", name: "Player 1", hand: [], field: [], mana: 0 },
475
+ ],
476
+ currentPlayerIndex: 0,
477
+ }),
478
+ moves: {
479
+ playCard: {
480
+ // Enumerator that throws an error
481
+ enumerator: () => {
482
+ throw new Error("Test enumerator error");
483
+ },
484
+ condition: () => true,
485
+ reducer: () => {},
486
+ },
487
+ attack: {
488
+ enumerator: () => [],
489
+ condition: () => false,
490
+ reducer: () => {},
491
+ },
492
+ passTurn: {
493
+ enumerator: () => [{}],
494
+ condition: () => true,
495
+ reducer: () => {},
496
+ },
497
+ },
498
+ };
499
+
500
+ const engine = new RuleEngine(gameDefinition, players, {
501
+ logger: { level: "SILENT" }, // Suppress error logs in test
502
+ });
503
+ const playerId = createPlayerId("p1");
504
+
505
+ // Should not throw, but include error result
506
+ const moves = engine.enumerateMoves(playerId, { validOnly: false });
507
+ const playCardMove = moves.find((m) => m.moveId === "playCard");
508
+
509
+ expect(playCardMove).toBeDefined();
510
+ expect(playCardMove?.isValid).toBe(false);
511
+ expect(playCardMove?.validationError?.errorCode).toBe("ENUMERATOR_ERROR");
512
+ expect(playCardMove?.validationError?.reason).toContain(
513
+ "Test enumerator error",
514
+ );
515
+ });
516
+ });
517
+
518
+ describe("Filtering Options", () => {
519
+ it("should filter by moveIds", () => {
520
+ const players: Player[] = [{ id: "p1", name: "Player 1" }];
521
+
522
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
523
+ name: "Test Game",
524
+ setup: () => ({
525
+ players: [
526
+ { id: "p1", name: "Player 1", hand: ["card1"], field: [], mana: 5 },
527
+ ],
528
+ currentPlayerIndex: 0,
529
+ }),
530
+ moves: {
531
+ playCard: {
532
+ enumerator: (
533
+ state: TestGameState,
534
+ context: import("../moves/move-enumeration").MoveEnumerationContext,
535
+ ) => {
536
+ const player = state.players.find(
537
+ (p) => p.id === context.playerId,
538
+ );
539
+ return player
540
+ ? player.hand.map((cardId: string) => ({ cardId }))
541
+ : [];
542
+ },
543
+ condition: () => true,
544
+ reducer: () => {},
545
+ },
546
+ attack: {
547
+ enumerator: () => [{ attackerId: "a1", targetId: "t1" }],
548
+ condition: () => true,
549
+ reducer: () => {},
550
+ },
551
+ passTurn: {
552
+ enumerator: () => [{}],
553
+ condition: () => true,
554
+ reducer: () => {},
555
+ },
556
+ },
557
+ };
558
+
559
+ const engine = new RuleEngine(gameDefinition, players);
560
+ const playerId = createPlayerId("p1");
561
+
562
+ // Filter to only playCard and passTurn
563
+ const filteredMoves = engine.enumerateMoves(playerId, {
564
+ moveIds: ["playCard", "passTurn"],
565
+ });
566
+
567
+ const moveIds = filteredMoves.map((m) => m.moveId);
568
+ expect(moveIds).toContain("playCard");
569
+ expect(moveIds).toContain("passTurn");
570
+ expect(moveIds).not.toContain("attack");
571
+ });
572
+
573
+ it("should limit results per move with maxPerMove", () => {
574
+ const players: Player[] = [{ id: "p1", name: "Player 1" }];
575
+
576
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
577
+ name: "Test Game",
578
+ setup: () => ({
579
+ players: [
580
+ {
581
+ id: "p1",
582
+ name: "Player 1",
583
+ hand: ["card1", "card2", "card3", "card4", "card5"],
584
+ field: [],
585
+ mana: 5,
586
+ },
587
+ ],
588
+ currentPlayerIndex: 0,
589
+ }),
590
+ moves: {
591
+ playCard: {
592
+ enumerator: (
593
+ state: TestGameState,
594
+ context: import("../moves/move-enumeration").MoveEnumerationContext,
595
+ ) => {
596
+ const player = state.players.find(
597
+ (p) => p.id === context.playerId,
598
+ );
599
+ return player
600
+ ? player.hand.map((cardId: string) => ({ cardId }))
601
+ : [];
602
+ },
603
+ condition: () => true,
604
+ reducer: () => {},
605
+ },
606
+ attack: {
607
+ enumerator: () => [],
608
+ condition: () => false,
609
+ reducer: () => {},
610
+ },
611
+ passTurn: {
612
+ enumerator: () => [{}],
613
+ condition: () => true,
614
+ reducer: () => {},
615
+ },
616
+ },
617
+ };
618
+
619
+ const engine = new RuleEngine(gameDefinition, players);
620
+ const playerId = createPlayerId("p1");
621
+
622
+ // Limit to 2 results per move
623
+ const limitedMoves = engine.enumerateMoves(playerId, {
624
+ maxPerMove: 2,
625
+ });
626
+
627
+ const playCardMoves = limitedMoves.filter((m) => m.moveId === "playCard");
628
+ expect(playCardMoves.length).toBe(2); // Limited to 2 instead of 5
629
+
630
+ // Without limit
631
+ const allMoves = engine.enumerateMoves(playerId);
632
+ const allPlayCardMoves = allMoves.filter((m) => m.moveId === "playCard");
633
+ expect(allPlayCardMoves.length).toBe(5); // All 5 cards
634
+ });
635
+ });
636
+
637
+ describe("Complex Parameter Types", () => {
638
+ it("should enumerate moves with multiple parameter fields", () => {
639
+ const players: Player[] = [
640
+ { id: "p1", name: "Player 1" },
641
+ { id: "p2", name: "Player 2" },
642
+ ];
643
+
644
+ const gameDefinition: GameDefinition<TestGameState, TestMoves> = {
645
+ name: "Test Game",
646
+ setup: (players) => ({
647
+ players: players.map((p) => ({
648
+ id: p.id,
649
+ name: p.name ?? "",
650
+ hand: [],
651
+ field: p.id === "p1" ? ["attacker1", "attacker2"] : ["target1"],
652
+ mana: 5,
653
+ })),
654
+ currentPlayerIndex: 0,
655
+ }),
656
+ moves: {
657
+ playCard: {
658
+ enumerator: () => [],
659
+ condition: () => false,
660
+ reducer: () => {},
661
+ },
662
+ attack: {
663
+ // Enumerate all attacker-target combinations
664
+ enumerator: (
665
+ state: TestGameState,
666
+ context: import("../moves/move-enumeration").MoveEnumerationContext,
667
+ ) => {
668
+ const results: AttackParams[] = [];
669
+ const player = state.players.find(
670
+ (p) => p.id === context.playerId,
671
+ );
672
+ if (!player) return [];
673
+
674
+ // Get all opponent creatures
675
+ const opponents = state.players.filter(
676
+ (p) => p.id !== context.playerId,
677
+ );
678
+
679
+ for (const attackerId of player.field) {
680
+ for (const opponent of opponents) {
681
+ for (const targetId of opponent.field) {
682
+ results.push({ attackerId, targetId });
683
+ }
684
+ }
685
+ }
686
+
687
+ return results;
688
+ },
689
+ condition: () => true,
690
+ reducer: () => {},
691
+ },
692
+ passTurn: {
693
+ enumerator: () => [{}],
694
+ condition: () => true,
695
+ reducer: () => {},
696
+ },
697
+ },
698
+ };
699
+
700
+ const engine = new RuleEngine(gameDefinition, players);
701
+ const playerId = createPlayerId("p1");
702
+
703
+ const moves = engine.enumerateMoves(playerId, { validOnly: true });
704
+ const attackMoves = moves.filter((m) => m.moveId === "attack");
705
+
706
+ // Should have 2 attackers * 1 target = 2 attack combinations
707
+ expect(attackMoves.length).toBe(2);
708
+
709
+ // Check all combinations are present
710
+ const combinations = attackMoves.map((m) => ({
711
+ attacker: (m.params as AttackParams).attackerId,
712
+ target: (m.params as AttackParams).targetId,
713
+ }));
714
+
715
+ expect(combinations).toContainEqual({
716
+ attacker: "attacker1",
717
+ target: "target1",
718
+ });
719
+ expect(combinations).toContainEqual({
720
+ attacker: "attacker2",
721
+ target: "target1",
722
+ });
723
+ });
724
+ });
725
+ });