@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,901 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { CardDefinition } from "../cards/card-definition";
3
+ import type { CardInstance } from "../cards/card-instance";
4
+ import type { Modifier } from "../cards/modifiers";
5
+ import { createCardRegistry } from "../operations/card-registry-impl";
6
+ import { createCardId, createPlayerId, createZoneId } from "../types";
7
+ import { CardQuery } from "./card-query";
8
+
9
+ type TestGameState = {
10
+ cards: Record<string, CardInstance<{ modifiers: Modifier[] }>>;
11
+ };
12
+
13
+ describe("CardQuery Builder", () => {
14
+ const definitions: CardDefinition[] = [
15
+ {
16
+ id: "bear",
17
+ name: "Grizzly Bears",
18
+ type: "creature",
19
+ basePower: 2,
20
+ baseToughness: 2,
21
+ baseCost: 2,
22
+ abilities: [],
23
+ },
24
+ {
25
+ id: "dragon",
26
+ name: "Shivan Dragon",
27
+ type: "creature",
28
+ basePower: 5,
29
+ baseToughness: 5,
30
+ baseCost: 6,
31
+ abilities: ["flying"],
32
+ },
33
+ {
34
+ id: "bolt",
35
+ name: "Lightning Bolt",
36
+ type: "instant",
37
+ baseCost: 1,
38
+ abilities: [],
39
+ },
40
+ {
41
+ id: "mountain",
42
+ name: "Mountain",
43
+ type: "land",
44
+ abilities: [],
45
+ },
46
+ ];
47
+
48
+ const registry = createCardRegistry(definitions);
49
+
50
+ describe("Basic Filters", () => {
51
+ it("should filter by zone", () => {
52
+ const playZone = createZoneId("play");
53
+ const handZone = createZoneId("hand");
54
+ const player1 = createPlayerId("player-1");
55
+
56
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
57
+ {
58
+ id: createCardId("card-1"),
59
+ definitionId: "bear",
60
+ owner: player1,
61
+ controller: player1,
62
+ zone: playZone,
63
+ tapped: false,
64
+ flipped: false,
65
+ revealed: false,
66
+ phased: false,
67
+ modifiers: [],
68
+ },
69
+ {
70
+ id: createCardId("card-2"),
71
+ definitionId: "bolt",
72
+ owner: player1,
73
+ controller: player1,
74
+ zone: handZone,
75
+ tapped: false,
76
+ flipped: false,
77
+ revealed: false,
78
+ phased: false,
79
+ modifiers: [],
80
+ },
81
+ ];
82
+
83
+ const state: TestGameState = {
84
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
85
+ };
86
+
87
+ const result = CardQuery.create(state, registry)
88
+ .inZone(playZone)
89
+ .execute();
90
+
91
+ expect(result).toHaveLength(1);
92
+ expect(result[0].definitionId).toBe("bear");
93
+ });
94
+
95
+ it("should filter by multiple zones", () => {
96
+ const playZone = createZoneId("play");
97
+ const handZone = createZoneId("hand");
98
+ const deckZone = createZoneId("deck");
99
+ const player1 = createPlayerId("player-1");
100
+
101
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
102
+ {
103
+ id: createCardId("card-1"),
104
+ definitionId: "bear",
105
+ owner: player1,
106
+ controller: player1,
107
+ zone: playZone,
108
+ tapped: false,
109
+ flipped: false,
110
+ revealed: false,
111
+ phased: false,
112
+ modifiers: [],
113
+ },
114
+ {
115
+ id: createCardId("card-2"),
116
+ definitionId: "bolt",
117
+ owner: player1,
118
+ controller: player1,
119
+ zone: handZone,
120
+ tapped: false,
121
+ flipped: false,
122
+ revealed: false,
123
+ phased: false,
124
+ modifiers: [],
125
+ },
126
+ {
127
+ id: createCardId("card-3"),
128
+ definitionId: "mountain",
129
+ owner: player1,
130
+ controller: player1,
131
+ zone: deckZone,
132
+ tapped: false,
133
+ flipped: false,
134
+ revealed: false,
135
+ phased: false,
136
+ modifiers: [],
137
+ },
138
+ ];
139
+
140
+ const state: TestGameState = {
141
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
142
+ };
143
+
144
+ const result = CardQuery.create(state, registry)
145
+ .inZone([playZone, handZone])
146
+ .execute();
147
+
148
+ expect(result).toHaveLength(2);
149
+ });
150
+
151
+ it("should filter by owner", () => {
152
+ const player1 = createPlayerId("player-1");
153
+ const player2 = createPlayerId("player-2");
154
+ const playZone = createZoneId("play");
155
+
156
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
157
+ {
158
+ id: createCardId("card-1"),
159
+ definitionId: "bear",
160
+ owner: player1,
161
+ controller: player1,
162
+ zone: playZone,
163
+ tapped: false,
164
+ flipped: false,
165
+ revealed: false,
166
+ phased: false,
167
+ modifiers: [],
168
+ },
169
+ {
170
+ id: createCardId("card-2"),
171
+ definitionId: "dragon",
172
+ owner: player2,
173
+ controller: player2,
174
+ zone: playZone,
175
+ tapped: false,
176
+ flipped: false,
177
+ revealed: false,
178
+ phased: false,
179
+ modifiers: [],
180
+ },
181
+ ];
182
+
183
+ const state: TestGameState = {
184
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
185
+ };
186
+
187
+ const result = CardQuery.create(state, registry)
188
+ .ownedBy(player1)
189
+ .execute();
190
+
191
+ expect(result).toHaveLength(1);
192
+ expect(result[0].owner).toBe(player1);
193
+ });
194
+
195
+ it("should filter by controller", () => {
196
+ const player1 = createPlayerId("player-1");
197
+ const player2 = createPlayerId("player-2");
198
+ const playZone = createZoneId("play");
199
+
200
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
201
+ {
202
+ id: createCardId("card-1"),
203
+ definitionId: "bear",
204
+ owner: player1,
205
+ controller: player2,
206
+ zone: playZone,
207
+ tapped: false,
208
+ flipped: false,
209
+ revealed: false,
210
+ phased: false,
211
+ modifiers: [],
212
+ },
213
+ ];
214
+
215
+ const state: TestGameState = {
216
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
217
+ };
218
+
219
+ const result = CardQuery.create(state, registry)
220
+ .controlledBy(player2)
221
+ .execute();
222
+
223
+ expect(result).toHaveLength(1);
224
+ });
225
+
226
+ it("should filter by type", () => {
227
+ const player1 = createPlayerId("player-1");
228
+ const playZone = createZoneId("play");
229
+
230
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
231
+ {
232
+ id: createCardId("card-1"),
233
+ definitionId: "bear",
234
+ owner: player1,
235
+ controller: player1,
236
+ zone: playZone,
237
+ tapped: false,
238
+ flipped: false,
239
+ revealed: false,
240
+ phased: false,
241
+ modifiers: [],
242
+ },
243
+ {
244
+ id: createCardId("card-2"),
245
+ definitionId: "bolt",
246
+ owner: player1,
247
+ controller: player1,
248
+ zone: playZone,
249
+ tapped: false,
250
+ flipped: false,
251
+ revealed: false,
252
+ phased: false,
253
+ modifiers: [],
254
+ },
255
+ ];
256
+
257
+ const state: TestGameState = {
258
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
259
+ };
260
+
261
+ const result = CardQuery.create(state, registry)
262
+ .ofType("creature")
263
+ .execute();
264
+
265
+ expect(result).toHaveLength(1);
266
+ expect(result[0].definitionId).toBe("bear");
267
+ });
268
+
269
+ it("should filter by name", () => {
270
+ const player1 = createPlayerId("player-1");
271
+ const playZone = createZoneId("play");
272
+
273
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
274
+ {
275
+ id: createCardId("card-1"),
276
+ definitionId: "bear",
277
+ owner: player1,
278
+ controller: player1,
279
+ zone: playZone,
280
+ tapped: false,
281
+ flipped: false,
282
+ revealed: false,
283
+ phased: false,
284
+ modifiers: [],
285
+ },
286
+ ];
287
+
288
+ const state: TestGameState = {
289
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
290
+ };
291
+
292
+ const result = CardQuery.create(state, registry)
293
+ .withName("Grizzly Bears")
294
+ .execute();
295
+
296
+ expect(result).toHaveLength(1);
297
+ });
298
+
299
+ it("should filter by name pattern", () => {
300
+ const player1 = createPlayerId("player-1");
301
+ const playZone = createZoneId("play");
302
+
303
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
304
+ {
305
+ id: createCardId("card-1"),
306
+ definitionId: "bear",
307
+ owner: player1,
308
+ controller: player1,
309
+ zone: playZone,
310
+ tapped: false,
311
+ flipped: false,
312
+ revealed: false,
313
+ phased: false,
314
+ modifiers: [],
315
+ },
316
+ {
317
+ id: createCardId("card-2"),
318
+ definitionId: "dragon",
319
+ owner: player1,
320
+ controller: player1,
321
+ zone: playZone,
322
+ tapped: false,
323
+ flipped: false,
324
+ revealed: false,
325
+ phased: false,
326
+ modifiers: [],
327
+ },
328
+ ];
329
+
330
+ const state: TestGameState = {
331
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
332
+ };
333
+
334
+ const result = CardQuery.create(state, registry)
335
+ .withNameMatching(/.*Dragon/)
336
+ .execute();
337
+
338
+ expect(result).toHaveLength(1);
339
+ expect(result[0].definitionId).toBe("dragon");
340
+ });
341
+
342
+ it("should filter by tapped state", () => {
343
+ const player1 = createPlayerId("player-1");
344
+ const playZone = createZoneId("play");
345
+
346
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
347
+ {
348
+ id: createCardId("card-1"),
349
+ definitionId: "bear",
350
+ owner: player1,
351
+ controller: player1,
352
+ zone: playZone,
353
+ tapped: true,
354
+ flipped: false,
355
+ revealed: false,
356
+ phased: false,
357
+ modifiers: [],
358
+ },
359
+ {
360
+ id: createCardId("card-2"),
361
+ definitionId: "dragon",
362
+ owner: player1,
363
+ controller: player1,
364
+ zone: playZone,
365
+ tapped: false,
366
+ flipped: false,
367
+ revealed: false,
368
+ phased: false,
369
+ modifiers: [],
370
+ },
371
+ ];
372
+
373
+ const state: TestGameState = {
374
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
375
+ };
376
+
377
+ const result = CardQuery.create(state, registry).tapped().execute();
378
+
379
+ expect(result).toHaveLength(1);
380
+ expect(result[0].tapped).toBe(true);
381
+ });
382
+
383
+ it("should filter by untapped state", () => {
384
+ const player1 = createPlayerId("player-1");
385
+ const playZone = createZoneId("play");
386
+
387
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
388
+ {
389
+ id: createCardId("card-1"),
390
+ definitionId: "bear",
391
+ owner: player1,
392
+ controller: player1,
393
+ zone: playZone,
394
+ tapped: true,
395
+ flipped: false,
396
+ revealed: false,
397
+ phased: false,
398
+ modifiers: [],
399
+ },
400
+ {
401
+ id: createCardId("card-2"),
402
+ definitionId: "dragon",
403
+ owner: player1,
404
+ controller: player1,
405
+ zone: playZone,
406
+ tapped: false,
407
+ flipped: false,
408
+ revealed: false,
409
+ phased: false,
410
+ modifiers: [],
411
+ },
412
+ ];
413
+
414
+ const state: TestGameState = {
415
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
416
+ };
417
+
418
+ const result = CardQuery.create(state, registry).untapped().execute();
419
+
420
+ expect(result).toHaveLength(1);
421
+ expect(result[0].tapped).toBe(false);
422
+ });
423
+ });
424
+
425
+ describe("Property Filters", () => {
426
+ it("should filter by baseCost property", () => {
427
+ const player1 = createPlayerId("player-1");
428
+ const playZone = createZoneId("play");
429
+
430
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
431
+ {
432
+ id: createCardId("card-1"),
433
+ definitionId: "bear",
434
+ owner: player1,
435
+ controller: player1,
436
+ zone: playZone,
437
+ tapped: false,
438
+ flipped: false,
439
+ revealed: false,
440
+ phased: false,
441
+ modifiers: [],
442
+ },
443
+ {
444
+ id: createCardId("card-2"),
445
+ definitionId: "dragon",
446
+ owner: player1,
447
+ controller: player1,
448
+ zone: playZone,
449
+ tapped: false,
450
+ flipped: false,
451
+ revealed: false,
452
+ phased: false,
453
+ modifiers: [],
454
+ },
455
+ ];
456
+
457
+ const state: TestGameState = {
458
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
459
+ };
460
+
461
+ const result = CardQuery.create(state, registry)
462
+ .withProperty("baseCost", { lte: 3 })
463
+ .execute();
464
+
465
+ expect(result).toHaveLength(1);
466
+ expect(result[0].definitionId).toBe("bear");
467
+ });
468
+
469
+ it("should filter by basePower property", () => {
470
+ const player1 = createPlayerId("player-1");
471
+ const playZone = createZoneId("play");
472
+
473
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
474
+ {
475
+ id: createCardId("card-1"),
476
+ definitionId: "bear",
477
+ owner: player1,
478
+ controller: player1,
479
+ zone: playZone,
480
+ tapped: false,
481
+ flipped: false,
482
+ revealed: false,
483
+ phased: false,
484
+ modifiers: [],
485
+ },
486
+ {
487
+ id: createCardId("card-2"),
488
+ definitionId: "dragon",
489
+ owner: player1,
490
+ controller: player1,
491
+ zone: playZone,
492
+ tapped: false,
493
+ flipped: false,
494
+ revealed: false,
495
+ phased: false,
496
+ modifiers: [],
497
+ },
498
+ ];
499
+
500
+ const state: TestGameState = {
501
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
502
+ };
503
+
504
+ const result = CardQuery.create(state, registry)
505
+ .withProperty("basePower", { gte: 4 })
506
+ .execute();
507
+
508
+ expect(result).toHaveLength(1);
509
+ expect(result[0].definitionId).toBe("dragon");
510
+ });
511
+
512
+ it("should filter by baseToughness property", () => {
513
+ const player1 = createPlayerId("player-1");
514
+ const playZone = createZoneId("play");
515
+
516
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
517
+ {
518
+ id: createCardId("card-1"),
519
+ definitionId: "bear",
520
+ owner: player1,
521
+ controller: player1,
522
+ zone: playZone,
523
+ tapped: false,
524
+ flipped: false,
525
+ revealed: false,
526
+ phased: false,
527
+ modifiers: [],
528
+ },
529
+ {
530
+ id: createCardId("card-2"),
531
+ definitionId: "dragon",
532
+ owner: player1,
533
+ controller: player1,
534
+ zone: playZone,
535
+ tapped: false,
536
+ flipped: false,
537
+ revealed: false,
538
+ phased: false,
539
+ modifiers: [],
540
+ },
541
+ ];
542
+
543
+ const state: TestGameState = {
544
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
545
+ };
546
+
547
+ const result = CardQuery.create(state, registry)
548
+ .withProperty("baseToughness", { eq: 2 })
549
+ .execute();
550
+
551
+ expect(result).toHaveLength(1);
552
+ expect(result[0].definitionId).toBe("bear");
553
+ });
554
+
555
+ it("should filter by multiple properties", () => {
556
+ const player1 = createPlayerId("player-1");
557
+ const playZone = createZoneId("play");
558
+
559
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
560
+ {
561
+ id: createCardId("card-1"),
562
+ definitionId: "bear",
563
+ owner: player1,
564
+ controller: player1,
565
+ zone: playZone,
566
+ tapped: false,
567
+ flipped: false,
568
+ revealed: false,
569
+ phased: false,
570
+ modifiers: [],
571
+ },
572
+ {
573
+ id: createCardId("card-2"),
574
+ definitionId: "dragon",
575
+ owner: player1,
576
+ controller: player1,
577
+ zone: playZone,
578
+ tapped: false,
579
+ flipped: false,
580
+ revealed: false,
581
+ phased: false,
582
+ modifiers: [],
583
+ },
584
+ {
585
+ id: createCardId("card-3"),
586
+ definitionId: "bolt",
587
+ owner: player1,
588
+ controller: player1,
589
+ zone: playZone,
590
+ tapped: false,
591
+ flipped: false,
592
+ revealed: false,
593
+ phased: false,
594
+ modifiers: [],
595
+ },
596
+ ];
597
+
598
+ const state: TestGameState = {
599
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
600
+ };
601
+
602
+ // Find creatures with baseCost 2 and basePower 2
603
+ const result = CardQuery.create(state, registry)
604
+ .ofType("creature")
605
+ .withProperty("baseCost", 2)
606
+ .withProperty("basePower", 2)
607
+ .execute();
608
+
609
+ expect(result).toHaveLength(1);
610
+ expect(result[0].definitionId).toBe("bear");
611
+ });
612
+ });
613
+
614
+ describe("Chaining Filters", () => {
615
+ it("should chain multiple filters", () => {
616
+ const player1 = createPlayerId("player-1");
617
+ const playZone = createZoneId("play");
618
+
619
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
620
+ {
621
+ id: createCardId("card-1"),
622
+ definitionId: "bear",
623
+ owner: player1,
624
+ controller: player1,
625
+ zone: playZone,
626
+ tapped: false,
627
+ flipped: false,
628
+ revealed: false,
629
+ phased: false,
630
+ modifiers: [],
631
+ },
632
+ {
633
+ id: createCardId("card-2"),
634
+ definitionId: "dragon",
635
+ owner: player1,
636
+ controller: player1,
637
+ zone: playZone,
638
+ tapped: true,
639
+ flipped: false,
640
+ revealed: false,
641
+ phased: false,
642
+ modifiers: [],
643
+ },
644
+ {
645
+ id: createCardId("card-3"),
646
+ definitionId: "bolt",
647
+ owner: player1,
648
+ controller: player1,
649
+ zone: playZone,
650
+ tapped: false,
651
+ flipped: false,
652
+ revealed: false,
653
+ phased: false,
654
+ modifiers: [],
655
+ },
656
+ ];
657
+
658
+ const state: TestGameState = {
659
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
660
+ };
661
+
662
+ const result = CardQuery.create(state, registry)
663
+ .inZone(playZone)
664
+ .ofType("creature")
665
+ .untapped()
666
+ .execute();
667
+
668
+ expect(result).toHaveLength(1);
669
+ expect(result[0].definitionId).toBe("bear");
670
+ });
671
+ });
672
+
673
+ describe("Custom Predicates", () => {
674
+ it("should support custom where predicate", () => {
675
+ const player1 = createPlayerId("player-1");
676
+ const playZone = createZoneId("play");
677
+
678
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
679
+ {
680
+ id: createCardId("card-1"),
681
+ definitionId: "bear",
682
+ owner: player1,
683
+ controller: player1,
684
+ zone: playZone,
685
+ tapped: false,
686
+ flipped: false,
687
+ revealed: false,
688
+ phased: false,
689
+ modifiers: [],
690
+ },
691
+ {
692
+ id: createCardId("card-2"),
693
+ definitionId: "dragon",
694
+ owner: player1,
695
+ controller: player1,
696
+ zone: playZone,
697
+ tapped: false,
698
+ flipped: false,
699
+ revealed: false,
700
+ phased: false,
701
+ modifiers: [],
702
+ },
703
+ ];
704
+
705
+ const state: TestGameState = {
706
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
707
+ };
708
+
709
+ const result = CardQuery.create(state, registry)
710
+ .where((card, _state) => {
711
+ const definition = registry.getCard(card.definitionId);
712
+ return (
713
+ definition?.baseCost !== undefined && definition.baseCost % 2 === 0
714
+ );
715
+ })
716
+ .execute();
717
+
718
+ expect(result).toHaveLength(2);
719
+ });
720
+ });
721
+
722
+ describe("Query Methods", () => {
723
+ it("should execute query and return cards", () => {
724
+ const player1 = createPlayerId("player-1");
725
+ const playZone = createZoneId("play");
726
+
727
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
728
+ {
729
+ id: createCardId("card-1"),
730
+ definitionId: "bear",
731
+ owner: player1,
732
+ controller: player1,
733
+ zone: playZone,
734
+ tapped: false,
735
+ flipped: false,
736
+ revealed: false,
737
+ phased: false,
738
+ modifiers: [],
739
+ },
740
+ ];
741
+
742
+ const state: TestGameState = {
743
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
744
+ };
745
+
746
+ const result = CardQuery.create(state, registry)
747
+ .inZone(playZone)
748
+ .execute();
749
+
750
+ expect(Array.isArray(result)).toBe(true);
751
+ expect(result).toHaveLength(1);
752
+ });
753
+
754
+ it("should count matching cards", () => {
755
+ const player1 = createPlayerId("player-1");
756
+ const playZone = createZoneId("play");
757
+
758
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
759
+ {
760
+ id: createCardId("card-1"),
761
+ definitionId: "bear",
762
+ owner: player1,
763
+ controller: player1,
764
+ zone: playZone,
765
+ tapped: false,
766
+ flipped: false,
767
+ revealed: false,
768
+ phased: false,
769
+ modifiers: [],
770
+ },
771
+ {
772
+ id: createCardId("card-2"),
773
+ definitionId: "dragon",
774
+ owner: player1,
775
+ controller: player1,
776
+ zone: playZone,
777
+ tapped: false,
778
+ flipped: false,
779
+ revealed: false,
780
+ phased: false,
781
+ modifiers: [],
782
+ },
783
+ ];
784
+
785
+ const state: TestGameState = {
786
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
787
+ };
788
+
789
+ const count = CardQuery.create(state, registry)
790
+ .inZone(playZone)
791
+ .ofType("creature")
792
+ .count();
793
+
794
+ expect(count).toBe(2);
795
+ });
796
+
797
+ it("should check if any card matches", () => {
798
+ const player1 = createPlayerId("player-1");
799
+ const playZone = createZoneId("play");
800
+
801
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
802
+ {
803
+ id: createCardId("card-1"),
804
+ definitionId: "bear",
805
+ owner: player1,
806
+ controller: player1,
807
+ zone: playZone,
808
+ tapped: false,
809
+ flipped: false,
810
+ revealed: false,
811
+ phased: false,
812
+ modifiers: [],
813
+ },
814
+ ];
815
+
816
+ const state: TestGameState = {
817
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
818
+ };
819
+
820
+ const hasCreature = CardQuery.create(state, registry)
821
+ .ofType("creature")
822
+ .any();
823
+
824
+ const hasLand = CardQuery.create(state, registry).ofType("land").any();
825
+
826
+ expect(hasCreature).toBe(true);
827
+ expect(hasLand).toBe(false);
828
+ });
829
+
830
+ it("should get first matching card", () => {
831
+ const player1 = createPlayerId("player-1");
832
+ const playZone = createZoneId("play");
833
+
834
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
835
+ {
836
+ id: createCardId("card-1"),
837
+ definitionId: "bear",
838
+ owner: player1,
839
+ controller: player1,
840
+ zone: playZone,
841
+ tapped: false,
842
+ flipped: false,
843
+ revealed: false,
844
+ phased: false,
845
+ modifiers: [],
846
+ },
847
+ {
848
+ id: createCardId("card-2"),
849
+ definitionId: "dragon",
850
+ owner: player1,
851
+ controller: player1,
852
+ zone: playZone,
853
+ tapped: false,
854
+ flipped: false,
855
+ revealed: false,
856
+ phased: false,
857
+ modifiers: [],
858
+ },
859
+ ];
860
+
861
+ const state: TestGameState = {
862
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
863
+ };
864
+
865
+ const first = CardQuery.create(state, registry)
866
+ .ofType("creature")
867
+ .first();
868
+
869
+ expect(first).toBeDefined();
870
+ expect(first?.definitionId).toBe("bear");
871
+ });
872
+
873
+ it("should return undefined when no card matches for first()", () => {
874
+ const player1 = createPlayerId("player-1");
875
+ const playZone = createZoneId("play");
876
+
877
+ const cards: CardInstance<{ modifiers: Modifier[] }>[] = [
878
+ {
879
+ id: createCardId("card-1"),
880
+ definitionId: "bear",
881
+ owner: player1,
882
+ controller: player1,
883
+ zone: playZone,
884
+ tapped: false,
885
+ flipped: false,
886
+ revealed: false,
887
+ phased: false,
888
+ modifiers: [],
889
+ },
890
+ ];
891
+
892
+ const state: TestGameState = {
893
+ cards: Object.fromEntries(cards.map((c) => [String(c.id), c])),
894
+ };
895
+
896
+ const first = CardQuery.create(state, registry).ofType("land").first();
897
+
898
+ expect(first).toBeUndefined();
899
+ });
900
+ });
901
+ });