@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,488 @@
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 Move Execution Tests
9
+ *
10
+ * Tests for:
11
+ * - Move execution and validation (11.7-11.10)
12
+ * - canExecuteMove checks (11.11-11.12)
13
+ * - Valid move enumeration (11.13-11.14)
14
+ * - Patch generation (11.21-11.24)
15
+ */
16
+
17
+ type TestGameState = {
18
+ players: Array<{ id: string; name: string; score: number; hand: string[] }>;
19
+ currentPlayerIndex: number;
20
+ deck: string[];
21
+ phase: "setup" | "draw" | "play" | "ended";
22
+ turnNumber: number;
23
+ winner?: string;
24
+ };
25
+
26
+ type TestMoves = {
27
+ drawCard: Record<string, never>;
28
+ playCard: { cardId: string };
29
+ nextPhase: Record<string, never>;
30
+ endGame: { winnerId: string };
31
+ };
32
+
33
+ describe("RuleEngine - Move Execution", () => {
34
+ describe("Task 11.7, 11.8, 11.9, 11.10: executeMove", () => {
35
+ it("should execute a valid move successfully", () => {
36
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
37
+ drawCard: {
38
+ reducer: (draft) => {
39
+ const player = draft.players[draft.currentPlayerIndex];
40
+ if (player && draft.deck.length > 0) {
41
+ const card = draft.deck.pop();
42
+ if (card) {
43
+ player.hand.push(card);
44
+ }
45
+ }
46
+ },
47
+ },
48
+ playCard: { reducer: () => {} },
49
+ nextPhase: { reducer: () => {} },
50
+ endGame: { reducer: () => {} },
51
+ };
52
+
53
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
54
+ name: "Test Game",
55
+ setup: (players) => ({
56
+ players: players.map((p) => ({
57
+ id: p.id,
58
+ name: p.name || "Player",
59
+ score: 0,
60
+ hand: [],
61
+ })),
62
+ currentPlayerIndex: 0,
63
+ deck: ["card1", "card2", "card3"],
64
+ phase: "setup",
65
+ turnNumber: 1,
66
+ }),
67
+ moves,
68
+ };
69
+
70
+ const players = [
71
+ { id: createPlayerId("p1"), name: "Alice" },
72
+ { id: createPlayerId("p2"), name: "Bob" },
73
+ ];
74
+
75
+ const engine = new RuleEngine(gameDef, players);
76
+ const result = engine.executeMove("drawCard", {
77
+ playerId: createPlayerId("p1"),
78
+ params: {},
79
+ });
80
+
81
+ expect(result.success).toBe(true);
82
+ if (result.success) {
83
+ expect(result.patches).toBeDefined();
84
+ expect(result.patches.length).toBeGreaterThan(0);
85
+ }
86
+
87
+ const state = engine.getState();
88
+ expect(state.players[0]?.hand).toHaveLength(1);
89
+ expect(state.deck).toHaveLength(2);
90
+ });
91
+
92
+ it("should reject unknown move", () => {
93
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
94
+ drawCard: { reducer: () => {} },
95
+ playCard: { reducer: () => {} },
96
+ nextPhase: { reducer: () => {} },
97
+ endGame: { 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
+ hand: [],
108
+ })),
109
+ currentPlayerIndex: 0,
110
+ deck: [],
111
+ phase: "setup",
112
+ turnNumber: 1,
113
+ }),
114
+ moves,
115
+ };
116
+
117
+ const players = [
118
+ { id: createPlayerId("p1"), name: "Alice" },
119
+ { id: createPlayerId("p2"), name: "Bob" },
120
+ ];
121
+
122
+ const engine = new RuleEngine(gameDef, players);
123
+ const result = engine.executeMove("unknownMove" as any, {
124
+ playerId: createPlayerId("p1"),
125
+ params: {},
126
+ });
127
+
128
+ expect(result.success).toBe(false);
129
+ if (!result.success) {
130
+ expect(result.error).toContain("not found");
131
+ expect(result.errorCode).toBe("MOVE_NOT_FOUND");
132
+ }
133
+ });
134
+
135
+ it("should reject move when condition fails", () => {
136
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
137
+ drawCard: { reducer: () => {} },
138
+ playCard: {
139
+ condition: (state, context) => {
140
+ const player = state.players[state.currentPlayerIndex];
141
+ const cardId = context.params?.cardId as string;
142
+ return player?.hand.includes(cardId) ?? false;
143
+ },
144
+ reducer: (draft, context) => {
145
+ const player = draft.players[draft.currentPlayerIndex];
146
+ const cardId = context.params?.cardId as string;
147
+ if (player && cardId) {
148
+ const index = player.hand.indexOf(cardId);
149
+ if (index >= 0) {
150
+ player.hand.splice(index, 1);
151
+ }
152
+ }
153
+ },
154
+ },
155
+ nextPhase: { reducer: () => {} },
156
+ endGame: { reducer: () => {} },
157
+ };
158
+
159
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
160
+ name: "Test Game",
161
+ setup: (players) => ({
162
+ players: players.map((p) => ({
163
+ id: p.id,
164
+ name: p.name || "Player",
165
+ score: 0,
166
+ hand: [],
167
+ })),
168
+ currentPlayerIndex: 0,
169
+ deck: [],
170
+ phase: "setup",
171
+ turnNumber: 1,
172
+ }),
173
+ moves,
174
+ };
175
+
176
+ const players = [
177
+ { id: createPlayerId("p1"), name: "Alice" },
178
+ { id: createPlayerId("p2"), name: "Bob" },
179
+ ];
180
+
181
+ const engine = new RuleEngine(gameDef, players);
182
+ const result = engine.executeMove("playCard", {
183
+ playerId: createPlayerId("p1"),
184
+ params: { cardId: "card-not-in-hand" },
185
+ });
186
+
187
+ expect(result.success).toBe(false);
188
+ if (!result.success) {
189
+ expect(result.error).toContain("condition not met");
190
+ expect(result.errorCode).toBe("CONDITION_FAILED");
191
+ }
192
+ });
193
+
194
+ it("should capture patches for network sync", () => {
195
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
196
+ drawCard: {
197
+ reducer: (draft) => {
198
+ const player = draft.players[draft.currentPlayerIndex];
199
+ if (player && draft.deck.length > 0) {
200
+ const card = draft.deck.pop();
201
+ if (card) {
202
+ player.hand.push(card);
203
+ }
204
+ }
205
+ },
206
+ },
207
+ playCard: { reducer: () => {} },
208
+ nextPhase: { reducer: () => {} },
209
+ endGame: { reducer: () => {} },
210
+ };
211
+
212
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
213
+ name: "Test Game",
214
+ setup: (players) => ({
215
+ players: players.map((p) => ({
216
+ id: p.id,
217
+ name: p.name || "Player",
218
+ score: 0,
219
+ hand: [],
220
+ })),
221
+ currentPlayerIndex: 0,
222
+ deck: ["card1"],
223
+ phase: "setup",
224
+ turnNumber: 1,
225
+ }),
226
+ moves,
227
+ };
228
+
229
+ const players = [
230
+ { id: createPlayerId("p1"), name: "Alice" },
231
+ { id: createPlayerId("p2"), name: "Bob" },
232
+ ];
233
+
234
+ const engine = new RuleEngine(gameDef, players);
235
+ const result = engine.executeMove("drawCard", {
236
+ playerId: createPlayerId("p1"),
237
+ params: {},
238
+ });
239
+
240
+ expect(result.success).toBe(true);
241
+ if (result.success) {
242
+ // Patches should describe the state changes
243
+ expect(result.patches).toBeDefined();
244
+ expect(result.patches.length).toBeGreaterThan(0);
245
+
246
+ // Inverse patches for undo
247
+ expect(result.inversePatches).toBeDefined();
248
+ expect(result.inversePatches.length).toBeGreaterThan(0);
249
+ }
250
+ });
251
+ });
252
+
253
+ describe("Task 11.11, 11.12: canExecuteMove", () => {
254
+ it("should return true for valid move", () => {
255
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
256
+ drawCard: {
257
+ condition: (state) => state.deck.length > 0,
258
+ reducer: () => {},
259
+ },
260
+ playCard: { reducer: () => {} },
261
+ nextPhase: { reducer: () => {} },
262
+ endGame: { reducer: () => {} },
263
+ };
264
+
265
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
266
+ name: "Test Game",
267
+ setup: (players) => ({
268
+ players: players.map((p) => ({
269
+ id: p.id,
270
+ name: p.name || "Player",
271
+ score: 0,
272
+ hand: [],
273
+ })),
274
+ currentPlayerIndex: 0,
275
+ deck: ["card1", "card2"],
276
+ phase: "setup",
277
+ turnNumber: 1,
278
+ }),
279
+ moves,
280
+ };
281
+
282
+ const players = [
283
+ { id: createPlayerId("p1"), name: "Alice" },
284
+ { id: createPlayerId("p2"), name: "Bob" },
285
+ ];
286
+
287
+ const engine = new RuleEngine(gameDef, players);
288
+ const canDraw = engine.canExecuteMove("drawCard", {
289
+ playerId: createPlayerId("p1"),
290
+ params: {},
291
+ });
292
+
293
+ expect(canDraw).toBe(true);
294
+ });
295
+
296
+ it("should return false when condition fails", () => {
297
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
298
+ drawCard: {
299
+ condition: (state) => state.deck.length > 0,
300
+ reducer: () => {},
301
+ },
302
+ playCard: { reducer: () => {} },
303
+ nextPhase: { reducer: () => {} },
304
+ endGame: { reducer: () => {} },
305
+ };
306
+
307
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
308
+ name: "Test Game",
309
+ setup: (players) => ({
310
+ players: players.map((p) => ({
311
+ id: p.id,
312
+ name: p.name || "Player",
313
+ score: 0,
314
+ hand: [],
315
+ })),
316
+ currentPlayerIndex: 0,
317
+ deck: [], // Empty deck
318
+ phase: "setup",
319
+ turnNumber: 1,
320
+ }),
321
+ moves,
322
+ };
323
+
324
+ const players = [
325
+ { id: createPlayerId("p1"), name: "Alice" },
326
+ { id: createPlayerId("p2"), name: "Bob" },
327
+ ];
328
+
329
+ const engine = new RuleEngine(gameDef, players);
330
+ const canDraw = engine.canExecuteMove("drawCard", {
331
+ playerId: createPlayerId("p1"),
332
+ params: {},
333
+ });
334
+
335
+ expect(canDraw).toBe(false);
336
+ });
337
+
338
+ it("should not mutate state when checking", () => {
339
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
340
+ drawCard: {
341
+ condition: (state) => state.deck.length > 0,
342
+ reducer: (draft) => {
343
+ draft.deck.pop();
344
+ },
345
+ },
346
+ playCard: { reducer: () => {} },
347
+ nextPhase: { reducer: () => {} },
348
+ endGame: { reducer: () => {} },
349
+ };
350
+
351
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
352
+ name: "Test Game",
353
+ setup: (players) => ({
354
+ players: players.map((p) => ({
355
+ id: p.id,
356
+ name: p.name || "Player",
357
+ score: 0,
358
+ hand: [],
359
+ })),
360
+ currentPlayerIndex: 0,
361
+ deck: ["card1", "card2"],
362
+ phase: "setup",
363
+ turnNumber: 1,
364
+ }),
365
+ moves,
366
+ };
367
+
368
+ const players = [
369
+ { id: createPlayerId("p1"), name: "Alice" },
370
+ { id: createPlayerId("p2"), name: "Bob" },
371
+ ];
372
+
373
+ const engine = new RuleEngine(gameDef, players);
374
+ engine.canExecuteMove("drawCard", {
375
+ playerId: createPlayerId("p1"),
376
+ params: {},
377
+ });
378
+
379
+ // State should be unchanged
380
+ const state = engine.getState();
381
+ expect(state.deck).toHaveLength(2);
382
+ });
383
+ });
384
+
385
+ describe("Task 11.13, 11.14: getValidMoves", () => {
386
+ it("should return all valid moves for player", () => {
387
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
388
+ drawCard: {
389
+ condition: (state) => state.deck.length > 0,
390
+ reducer: () => {},
391
+ },
392
+ playCard: {
393
+ condition: (state) => {
394
+ const player = state.players[state.currentPlayerIndex];
395
+ return (player?.hand.length ?? 0) > 0;
396
+ },
397
+ reducer: () => {},
398
+ },
399
+ nextPhase: {
400
+ reducer: () => {},
401
+ },
402
+ endGame: { reducer: () => {} },
403
+ };
404
+
405
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
406
+ name: "Test Game",
407
+ setup: (players) => ({
408
+ players: players.map((p) => ({
409
+ id: p.id,
410
+ name: p.name || "Player",
411
+ score: 0,
412
+ hand: ["card-a"],
413
+ })),
414
+ currentPlayerIndex: 0,
415
+ deck: ["card1"],
416
+ phase: "setup",
417
+ turnNumber: 1,
418
+ }),
419
+ moves,
420
+ };
421
+
422
+ const players = [
423
+ { id: createPlayerId("p1"), name: "Alice" },
424
+ { id: createPlayerId("p2"), name: "Bob" },
425
+ ];
426
+
427
+ const engine = new RuleEngine(gameDef, players);
428
+ const validMoves = engine.getValidMoves(createPlayerId("p1"));
429
+
430
+ // Should include drawCard (deck has cards)
431
+ expect(validMoves).toContain("drawCard");
432
+ // Should include playCard (hand has cards)
433
+ expect(validMoves).toContain("playCard");
434
+ // Should include nextPhase (no condition)
435
+ expect(validMoves).toContain("nextPhase");
436
+ });
437
+
438
+ it("should exclude moves that fail conditions", () => {
439
+ const moves: GameMoveDefinitions<TestGameState, TestMoves> = {
440
+ drawCard: {
441
+ condition: (state) => state.deck.length > 0,
442
+ reducer: () => {},
443
+ },
444
+ playCard: {
445
+ condition: (state) => {
446
+ const player = state.players[state.currentPlayerIndex];
447
+ return (player?.hand.length ?? 0) > 0;
448
+ },
449
+ reducer: () => {},
450
+ },
451
+ nextPhase: { reducer: () => {} },
452
+ endGame: { reducer: () => {} },
453
+ };
454
+
455
+ const gameDef: GameDefinition<TestGameState, TestMoves> = {
456
+ name: "Test Game",
457
+ setup: (players) => ({
458
+ players: players.map((p) => ({
459
+ id: p.id,
460
+ name: p.name || "Player",
461
+ score: 0,
462
+ hand: [], // Empty hand
463
+ })),
464
+ currentPlayerIndex: 0,
465
+ deck: [], // Empty deck
466
+ phase: "setup",
467
+ turnNumber: 1,
468
+ }),
469
+ moves,
470
+ };
471
+
472
+ const players = [
473
+ { id: createPlayerId("p1"), name: "Alice" },
474
+ { id: createPlayerId("p2"), name: "Bob" },
475
+ ];
476
+
477
+ const engine = new RuleEngine(gameDef, players);
478
+ const validMoves = engine.getValidMoves(createPlayerId("p1"));
479
+
480
+ // Should NOT include drawCard (no cards in deck)
481
+ expect(validMoves).not.toContain("drawCard");
482
+ // Should NOT include playCard (no cards in hand)
483
+ expect(validMoves).not.toContain("playCard");
484
+ // Should include nextPhase (no condition)
485
+ expect(validMoves).toContain("nextPhase");
486
+ });
487
+ });
488
+ });