@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,530 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createCardRegistry } from "../operations/card-registry-impl";
3
+ import { createCardId, createPlayerId, createZoneId } from "../types";
4
+ import type { CardDefinition } from "./card-definition";
5
+ import type { CardInstance } from "./card-instance";
6
+ import {
7
+ getCardCost,
8
+ getCardPower,
9
+ getCardToughness,
10
+ } from "./computed-properties";
11
+ import type { Modifier } from "./modifiers";
12
+
13
+ type GameStateWithModifiers = {
14
+ cards: Record<string, CardInstance<{ modifiers: Modifier[] }>>;
15
+ };
16
+
17
+ describe("Computed Properties", () => {
18
+ describe("getCardPower", () => {
19
+ it("should return base power from definition", () => {
20
+ const definition: CardDefinition = {
21
+ id: "grizzly-bears",
22
+ name: "Grizzly Bears",
23
+ type: "creature",
24
+ basePower: 2,
25
+ baseToughness: 2,
26
+ abilities: [],
27
+ };
28
+
29
+ const registry = createCardRegistry([definition]);
30
+
31
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
32
+ id: createCardId("card-1"),
33
+ definitionId: "grizzly-bears",
34
+ owner: createPlayerId("player-1"),
35
+ controller: createPlayerId("player-1"),
36
+ zone: createZoneId("play"),
37
+ tapped: false,
38
+ flipped: false,
39
+ revealed: false,
40
+ phased: false,
41
+ modifiers: [],
42
+ };
43
+
44
+ const state: GameStateWithModifiers = { cards: {} };
45
+ const power = getCardPower(card, state, registry);
46
+
47
+ expect(power).toBe(2);
48
+ });
49
+
50
+ it("should return 0 if definition has no basePower", () => {
51
+ const definition: CardDefinition = {
52
+ id: "instant-spell",
53
+ name: "Instant Spell",
54
+ type: "instant",
55
+ abilities: [],
56
+ };
57
+
58
+ const registry = createCardRegistry([definition]);
59
+
60
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
61
+ id: createCardId("card-1"),
62
+ definitionId: "instant-spell",
63
+ owner: createPlayerId("player-1"),
64
+ controller: createPlayerId("player-1"),
65
+ zone: createZoneId("hand"),
66
+ tapped: false,
67
+ flipped: false,
68
+ revealed: false,
69
+ phased: false,
70
+ modifiers: [],
71
+ };
72
+
73
+ const state: GameStateWithModifiers = { cards: {} };
74
+ const power = getCardPower(card, state, registry);
75
+
76
+ expect(power).toBe(0);
77
+ });
78
+
79
+ it("should add positive power modifiers to base power", () => {
80
+ const definition: CardDefinition = {
81
+ id: "creature",
82
+ name: "Creature",
83
+ type: "creature",
84
+ basePower: 2,
85
+ abilities: [],
86
+ };
87
+
88
+ const registry = createCardRegistry([definition]);
89
+
90
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
91
+ id: createCardId("card-1"),
92
+ definitionId: "creature",
93
+ owner: createPlayerId("player-1"),
94
+ controller: createPlayerId("player-1"),
95
+ zone: createZoneId("play"),
96
+ tapped: false,
97
+ flipped: false,
98
+ revealed: false,
99
+ phased: false,
100
+ modifiers: [
101
+ {
102
+ id: "mod-1",
103
+ type: "stat",
104
+ property: "power",
105
+ value: 3,
106
+ duration: "permanent",
107
+ source: createCardId("source-1"),
108
+ },
109
+ ],
110
+ };
111
+
112
+ const state: GameStateWithModifiers = { cards: {} };
113
+ const power = getCardPower(card, state, registry);
114
+
115
+ expect(power).toBe(5); // 2 + 3
116
+ });
117
+
118
+ it("should subtract negative power modifiers from base power", () => {
119
+ const definition: CardDefinition = {
120
+ id: "creature",
121
+ name: "Creature",
122
+ type: "creature",
123
+ basePower: 5,
124
+ abilities: [],
125
+ };
126
+
127
+ const registry = createCardRegistry([definition]);
128
+
129
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
130
+ id: createCardId("card-1"),
131
+ definitionId: "creature",
132
+ owner: createPlayerId("player-1"),
133
+ controller: createPlayerId("player-1"),
134
+ zone: createZoneId("play"),
135
+ tapped: false,
136
+ flipped: false,
137
+ revealed: false,
138
+ phased: false,
139
+ modifiers: [
140
+ {
141
+ id: "mod-1",
142
+ type: "stat",
143
+ property: "power",
144
+ value: -2,
145
+ duration: "permanent",
146
+ source: createCardId("source-1"),
147
+ },
148
+ ],
149
+ };
150
+
151
+ const state: GameStateWithModifiers = { cards: {} };
152
+ const power = getCardPower(card, state, registry);
153
+
154
+ expect(power).toBe(3); // 5 - 2
155
+ });
156
+
157
+ it("should sum multiple power modifiers", () => {
158
+ const definition: CardDefinition = {
159
+ id: "creature",
160
+ name: "Creature",
161
+ type: "creature",
162
+ basePower: 1,
163
+ abilities: [],
164
+ };
165
+
166
+ const registry = createCardRegistry([definition]);
167
+
168
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
169
+ id: createCardId("card-1"),
170
+ definitionId: "creature",
171
+ owner: createPlayerId("player-1"),
172
+ controller: createPlayerId("player-1"),
173
+ zone: createZoneId("play"),
174
+ tapped: false,
175
+ flipped: false,
176
+ revealed: false,
177
+ phased: false,
178
+ modifiers: [
179
+ {
180
+ id: "mod-1",
181
+ type: "stat",
182
+ property: "power",
183
+ value: 2,
184
+ duration: "permanent",
185
+ source: createCardId("source-1"),
186
+ },
187
+ {
188
+ id: "mod-2",
189
+ type: "stat",
190
+ property: "power",
191
+ value: 3,
192
+ duration: "permanent",
193
+ source: createCardId("source-2"),
194
+ },
195
+ ],
196
+ };
197
+
198
+ const state: GameStateWithModifiers = { cards: {} };
199
+ const power = getCardPower(card, state, registry);
200
+
201
+ expect(power).toBe(6); // 1 + 2 + 3
202
+ });
203
+
204
+ it("should ignore non-power modifiers", () => {
205
+ const definition: CardDefinition = {
206
+ id: "creature",
207
+ name: "Creature",
208
+ type: "creature",
209
+ basePower: 2,
210
+ abilities: [],
211
+ };
212
+
213
+ const registry = createCardRegistry([definition]);
214
+
215
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
216
+ id: createCardId("card-1"),
217
+ definitionId: "creature",
218
+ owner: createPlayerId("player-1"),
219
+ controller: createPlayerId("player-1"),
220
+ zone: createZoneId("play"),
221
+ tapped: false,
222
+ flipped: false,
223
+ revealed: false,
224
+ phased: false,
225
+ modifiers: [
226
+ {
227
+ id: "mod-1",
228
+ type: "stat",
229
+ property: "toughness",
230
+ value: 5,
231
+ duration: "permanent",
232
+ source: createCardId("source-1"),
233
+ },
234
+ {
235
+ id: "mod-2",
236
+ type: "ability",
237
+ property: "flying",
238
+ value: true,
239
+ duration: "permanent",
240
+ source: createCardId("source-2"),
241
+ },
242
+ ],
243
+ };
244
+
245
+ const state: GameStateWithModifiers = { cards: {} };
246
+ const power = getCardPower(card, state, registry);
247
+
248
+ expect(power).toBe(2); // Base power only
249
+ });
250
+ });
251
+
252
+ describe("getCardToughness", () => {
253
+ it("should return base toughness from definition", () => {
254
+ const definition: CardDefinition = {
255
+ id: "grizzly-bears",
256
+ name: "Grizzly Bears",
257
+ type: "creature",
258
+ basePower: 2,
259
+ baseToughness: 2,
260
+ abilities: [],
261
+ };
262
+
263
+ const registry = createCardRegistry([definition]);
264
+
265
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
266
+ id: createCardId("card-1"),
267
+ definitionId: "grizzly-bears",
268
+ owner: createPlayerId("player-1"),
269
+ controller: createPlayerId("player-1"),
270
+ zone: createZoneId("play"),
271
+ tapped: false,
272
+ flipped: false,
273
+ revealed: false,
274
+ phased: false,
275
+ modifiers: [],
276
+ };
277
+
278
+ const state: GameStateWithModifiers = { cards: {} };
279
+ const toughness = getCardToughness(card, state, registry);
280
+
281
+ expect(toughness).toBe(2);
282
+ });
283
+
284
+ it("should return 0 if definition has no baseToughness", () => {
285
+ const definition: CardDefinition = {
286
+ id: "instant-spell",
287
+ name: "Instant Spell",
288
+ type: "instant",
289
+ abilities: [],
290
+ };
291
+
292
+ const registry = createCardRegistry([definition]);
293
+
294
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
295
+ id: createCardId("card-1"),
296
+ definitionId: "instant-spell",
297
+ owner: createPlayerId("player-1"),
298
+ controller: createPlayerId("player-1"),
299
+ zone: createZoneId("hand"),
300
+ tapped: false,
301
+ flipped: false,
302
+ revealed: false,
303
+ phased: false,
304
+ modifiers: [],
305
+ };
306
+
307
+ const state: GameStateWithModifiers = { cards: {} };
308
+ const toughness = getCardToughness(card, state, registry);
309
+
310
+ expect(toughness).toBe(0);
311
+ });
312
+
313
+ it("should add toughness modifiers to base toughness", () => {
314
+ const definition: CardDefinition = {
315
+ id: "creature",
316
+ name: "Creature",
317
+ type: "creature",
318
+ baseToughness: 3,
319
+ abilities: [],
320
+ };
321
+
322
+ const registry = createCardRegistry([definition]);
323
+
324
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
325
+ id: createCardId("card-1"),
326
+ definitionId: "creature",
327
+ owner: createPlayerId("player-1"),
328
+ controller: createPlayerId("player-1"),
329
+ zone: createZoneId("play"),
330
+ tapped: false,
331
+ flipped: false,
332
+ revealed: false,
333
+ phased: false,
334
+ modifiers: [
335
+ {
336
+ id: "mod-1",
337
+ type: "stat",
338
+ property: "toughness",
339
+ value: 2,
340
+ duration: "permanent",
341
+ source: createCardId("source-1"),
342
+ },
343
+ ],
344
+ };
345
+
346
+ const state: GameStateWithModifiers = { cards: {} };
347
+ const toughness = getCardToughness(card, state, registry);
348
+
349
+ expect(toughness).toBe(5); // 3 + 2
350
+ });
351
+ });
352
+
353
+ describe("getCardCost", () => {
354
+ it("should return base cost from definition", () => {
355
+ const definition: CardDefinition = {
356
+ id: "fire-bolt",
357
+ name: "Fire Bolt",
358
+ type: "instant",
359
+ baseCost: 1,
360
+ abilities: [],
361
+ };
362
+
363
+ const registry = createCardRegistry([definition]);
364
+
365
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
366
+ id: createCardId("card-1"),
367
+ definitionId: "fire-bolt",
368
+ owner: createPlayerId("player-1"),
369
+ controller: createPlayerId("player-1"),
370
+ zone: createZoneId("hand"),
371
+ tapped: false,
372
+ flipped: false,
373
+ revealed: false,
374
+ phased: false,
375
+ modifiers: [],
376
+ };
377
+
378
+ const state: GameStateWithModifiers = { cards: {} };
379
+ const cost = getCardCost(card, state, registry);
380
+
381
+ expect(cost).toBe(1);
382
+ });
383
+
384
+ it("should return 0 if definition has no baseCost", () => {
385
+ const definition: CardDefinition = {
386
+ id: "free-spell",
387
+ name: "Free Spell",
388
+ type: "instant",
389
+ abilities: [],
390
+ };
391
+
392
+ const registry = createCardRegistry([definition]);
393
+
394
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
395
+ id: createCardId("card-1"),
396
+ definitionId: "free-spell",
397
+ owner: createPlayerId("player-1"),
398
+ controller: createPlayerId("player-1"),
399
+ zone: createZoneId("hand"),
400
+ tapped: false,
401
+ flipped: false,
402
+ revealed: false,
403
+ phased: false,
404
+ modifiers: [],
405
+ };
406
+
407
+ const state: GameStateWithModifiers = { cards: {} };
408
+ const cost = getCardCost(card, state, registry);
409
+
410
+ expect(cost).toBe(0);
411
+ });
412
+
413
+ it("should apply cost reduction modifiers", () => {
414
+ const definition: CardDefinition = {
415
+ id: "spell",
416
+ name: "Spell",
417
+ type: "instant",
418
+ baseCost: 5,
419
+ abilities: [],
420
+ };
421
+
422
+ const registry = createCardRegistry([definition]);
423
+
424
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
425
+ id: createCardId("card-1"),
426
+ definitionId: "spell",
427
+ owner: createPlayerId("player-1"),
428
+ controller: createPlayerId("player-1"),
429
+ zone: createZoneId("hand"),
430
+ tapped: false,
431
+ flipped: false,
432
+ revealed: false,
433
+ phased: false,
434
+ modifiers: [
435
+ {
436
+ id: "mod-1",
437
+ type: "stat",
438
+ property: "cost",
439
+ value: -2,
440
+ duration: "permanent",
441
+ source: createCardId("source-1"),
442
+ },
443
+ ],
444
+ };
445
+
446
+ const state: GameStateWithModifiers = { cards: {} };
447
+ const cost = getCardCost(card, state, registry);
448
+
449
+ expect(cost).toBe(3); // 5 - 2
450
+ });
451
+
452
+ it("should not allow cost to go below zero", () => {
453
+ const definition: CardDefinition = {
454
+ id: "spell",
455
+ name: "Spell",
456
+ type: "instant",
457
+ baseCost: 2,
458
+ abilities: [],
459
+ };
460
+
461
+ const registry = createCardRegistry([definition]);
462
+
463
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
464
+ id: createCardId("card-1"),
465
+ definitionId: "spell",
466
+ owner: createPlayerId("player-1"),
467
+ controller: createPlayerId("player-1"),
468
+ zone: createZoneId("hand"),
469
+ tapped: false,
470
+ flipped: false,
471
+ revealed: false,
472
+ phased: false,
473
+ modifiers: [
474
+ {
475
+ id: "mod-1",
476
+ type: "stat",
477
+ property: "cost",
478
+ value: -5,
479
+ duration: "permanent",
480
+ source: createCardId("source-1"),
481
+ },
482
+ ],
483
+ };
484
+
485
+ const state: GameStateWithModifiers = { cards: {} };
486
+ const cost = getCardCost(card, state, registry);
487
+
488
+ expect(cost).toBe(0); // Can't go negative
489
+ });
490
+
491
+ it("should apply cost increase modifiers", () => {
492
+ const definition: CardDefinition = {
493
+ id: "spell",
494
+ name: "Spell",
495
+ type: "instant",
496
+ baseCost: 3,
497
+ abilities: [],
498
+ };
499
+
500
+ const registry = createCardRegistry([definition]);
501
+
502
+ const card: CardInstance<{ modifiers: Modifier[] }> = {
503
+ id: createCardId("card-1"),
504
+ definitionId: "spell",
505
+ owner: createPlayerId("player-1"),
506
+ controller: createPlayerId("player-1"),
507
+ zone: createZoneId("hand"),
508
+ tapped: false,
509
+ flipped: false,
510
+ revealed: false,
511
+ phased: false,
512
+ modifiers: [
513
+ {
514
+ id: "mod-1",
515
+ type: "stat",
516
+ property: "cost",
517
+ value: 2,
518
+ duration: "permanent",
519
+ source: createCardId("source-1"),
520
+ },
521
+ ],
522
+ };
523
+
524
+ const state: GameStateWithModifiers = { cards: {} };
525
+ const cost = getCardCost(card, state, registry);
526
+
527
+ expect(cost).toBe(5); // 3 + 2
528
+ });
529
+ });
530
+ });
@@ -0,0 +1,84 @@
1
+ import type { CardRegistry } from "../operations/card-registry";
2
+ import type { CardDefinition } from "./card-definition";
3
+ import type { CardInstance } from "./card-instance";
4
+ import type { Modifier } from "./modifiers";
5
+
6
+ /**
7
+ * Gets the computed power of a card (base power + modifiers)
8
+ * Pure function - same inputs always produce same output
9
+ *
10
+ * @param card - Card instance with modifiers
11
+ * @param state - Game state (for conditional modifiers)
12
+ * @param registry - Card definition registry
13
+ * @returns Computed power value
14
+ */
15
+ export function getCardPower<TGameState = unknown>(
16
+ card: CardInstance<{ modifiers: Modifier<TGameState>[] }>,
17
+ state: TGameState,
18
+ registry: CardRegistry<CardDefinition>,
19
+ ): number {
20
+ const definition = registry.getCard(card.definitionId);
21
+ const basePower = definition?.basePower ?? 0;
22
+
23
+ // Sum all power modifiers
24
+ const modifierBonus = card.modifiers
25
+ .filter((m) => m.type === "stat" && m.property === "power")
26
+ .filter((m) => !m.condition || m.condition(state)) // check conditions
27
+ .reduce((sum, m) => sum + (m.value as number), 0);
28
+
29
+ return basePower + modifierBonus;
30
+ }
31
+
32
+ /**
33
+ * Gets the computed toughness of a card (base toughness + modifiers)
34
+ * Pure function - same inputs always produce same output
35
+ *
36
+ * @param card - Card instance with modifiers
37
+ * @param state - Game state (for conditional modifiers)
38
+ * @param registry - Card definition registry
39
+ * @returns Computed toughness value
40
+ */
41
+ export function getCardToughness<TGameState = unknown>(
42
+ card: CardInstance<{ modifiers: Modifier<TGameState>[] }>,
43
+ state: TGameState,
44
+ registry: CardRegistry<CardDefinition>,
45
+ ): number {
46
+ const definition = registry.getCard(card.definitionId);
47
+ const baseToughness = definition?.baseToughness ?? 0;
48
+
49
+ // Sum all toughness modifiers
50
+ const modifierBonus = card.modifiers
51
+ .filter((m) => m.type === "stat" && m.property === "toughness")
52
+ .filter((m) => !m.condition || m.condition(state)) // check conditions
53
+ .reduce((sum, m) => sum + (m.value as number), 0);
54
+
55
+ return baseToughness + modifierBonus;
56
+ }
57
+
58
+ /**
59
+ * Gets the computed cost of a card (base cost + modifiers)
60
+ * Pure function - same inputs always produce same output
61
+ * Cost cannot go below zero
62
+ *
63
+ * @param card - Card instance with modifiers
64
+ * @param state - Game state (for conditional modifiers)
65
+ * @param registry - Card definition registry
66
+ * @returns Computed cost value (minimum 0)
67
+ */
68
+ export function getCardCost<TGameState = unknown>(
69
+ card: CardInstance<{ modifiers: Modifier<TGameState>[] }>,
70
+ state: TGameState,
71
+ registry: CardRegistry<CardDefinition>,
72
+ ): number {
73
+ const definition = registry.getCard(card.definitionId);
74
+ const baseCost = definition?.baseCost ?? 0;
75
+
76
+ // Sum all cost modifiers (can be negative for cost reduction)
77
+ const costModification = card.modifiers
78
+ .filter((m) => m.type === "stat" && m.property === "cost")
79
+ .filter((m) => !m.condition || m.condition(state)) // check conditions
80
+ .reduce((sum, m) => sum + (m.value as number), 0);
81
+
82
+ // Cost cannot go below zero
83
+ return Math.max(0, baseCost + costModification);
84
+ }