@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,390 @@
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 { getCardPower } from "./computed-properties";
7
+ import type { Modifier } from "./modifiers";
8
+
9
+ type GameStateWithCards = {
10
+ // biome-ignore lint/suspicious/noExplicitAny: any required for test covariance
11
+ cards: Record<
12
+ string,
13
+ CardInstance<{ tapped: boolean; modifiers: Modifier<any>[] }>
14
+ >;
15
+ };
16
+
17
+ describe("Conditional Modifiers", () => {
18
+ describe("while-condition duration", () => {
19
+ it("should apply modifier when condition is true", () => {
20
+ const cardId = createCardId("card-1");
21
+
22
+ const definition: CardDefinition = {
23
+ id: "creature",
24
+ name: "Creature",
25
+ type: "creature",
26
+ basePower: 2,
27
+ abilities: [],
28
+ };
29
+
30
+ const registry = createCardRegistry([definition]);
31
+
32
+ const card: CardInstance<{
33
+ tapped: boolean;
34
+ modifiers: Modifier<GameStateWithCards>[];
35
+ }> = {
36
+ id: cardId,
37
+ definitionId: "creature",
38
+ owner: createPlayerId("player-1"),
39
+ controller: createPlayerId("player-1"),
40
+ zone: createZoneId("play"),
41
+ tapped: true, // Card is tapped
42
+ flipped: false,
43
+ revealed: false,
44
+ phased: false,
45
+ modifiers: [
46
+ {
47
+ id: "mod-1",
48
+ type: "stat",
49
+ property: "power",
50
+ value: 2,
51
+ duration: "while-condition",
52
+ condition: (state: GameStateWithCards) => {
53
+ const c = state.cards[String(cardId)];
54
+ return c?.tapped === true; // Only apply if tapped
55
+ },
56
+ source: createCardId("source-1"),
57
+ },
58
+ ],
59
+ };
60
+
61
+ const state: GameStateWithCards = {
62
+ cards: {
63
+ [String(cardId)]: card,
64
+ },
65
+ };
66
+
67
+ const power = getCardPower(card, state, registry);
68
+
69
+ expect(power).toBe(4); // 2 base + 2 from conditional modifier
70
+ });
71
+
72
+ it("should not apply modifier when condition is false", () => {
73
+ const cardId = createCardId("card-1");
74
+
75
+ const definition: CardDefinition = {
76
+ id: "creature",
77
+ name: "Creature",
78
+ type: "creature",
79
+ basePower: 2,
80
+ abilities: [],
81
+ };
82
+
83
+ const registry = createCardRegistry([definition]);
84
+
85
+ const card: CardInstance<{
86
+ tapped: boolean;
87
+ modifiers: Modifier<GameStateWithCards>[];
88
+ }> = {
89
+ id: cardId,
90
+ definitionId: "creature",
91
+ owner: createPlayerId("player-1"),
92
+ controller: createPlayerId("player-1"),
93
+ zone: createZoneId("play"),
94
+ tapped: false, // Card is NOT tapped
95
+ flipped: false,
96
+ revealed: false,
97
+ phased: false,
98
+ modifiers: [
99
+ {
100
+ id: "mod-1",
101
+ type: "stat",
102
+ property: "power",
103
+ value: 2,
104
+ duration: "while-condition",
105
+ condition: (state: GameStateWithCards) => {
106
+ const c = state.cards[String(cardId)];
107
+ return c?.tapped === true; // Only apply if tapped
108
+ },
109
+ source: createCardId("source-1"),
110
+ },
111
+ ],
112
+ };
113
+
114
+ const state: GameStateWithCards = {
115
+ cards: {
116
+ [String(cardId)]: card,
117
+ },
118
+ };
119
+
120
+ const power = getCardPower(card, state, registry);
121
+
122
+ expect(power).toBe(2); // 2 base only, modifier not applied
123
+ });
124
+
125
+ it("should dynamically re-evaluate condition when state changes", () => {
126
+ const cardId = createCardId("card-1");
127
+
128
+ const definition: CardDefinition = {
129
+ id: "creature",
130
+ name: "Creature",
131
+ type: "creature",
132
+ basePower: 3,
133
+ abilities: [],
134
+ };
135
+
136
+ const registry = createCardRegistry([definition]);
137
+
138
+ const card: CardInstance<{
139
+ tapped: boolean;
140
+ modifiers: Modifier<GameStateWithCards>[];
141
+ }> = {
142
+ id: cardId,
143
+ definitionId: "creature",
144
+ owner: createPlayerId("player-1"),
145
+ controller: createPlayerId("player-1"),
146
+ zone: createZoneId("play"),
147
+ tapped: false,
148
+ flipped: false,
149
+ revealed: false,
150
+ phased: false,
151
+ modifiers: [
152
+ {
153
+ id: "mod-1",
154
+ type: "stat",
155
+ property: "power",
156
+ value: 2,
157
+ duration: "while-condition",
158
+ condition: (state: GameStateWithCards) => {
159
+ const c = state.cards[String(cardId)];
160
+ return c?.tapped === true;
161
+ },
162
+ source: createCardId("source-1"),
163
+ },
164
+ ],
165
+ };
166
+
167
+ // Initial state: card is untapped
168
+ const state1: GameStateWithCards = {
169
+ cards: {
170
+ [String(cardId)]: card,
171
+ },
172
+ };
173
+
174
+ const power1 = getCardPower(card, state1, registry);
175
+ expect(power1).toBe(3); // Base power only
176
+
177
+ // State changes: card becomes tapped
178
+ const tappedCard = { ...card, tapped: true };
179
+ const state2: GameStateWithCards = {
180
+ cards: {
181
+ [String(cardId)]: tappedCard,
182
+ },
183
+ };
184
+
185
+ const power2 = getCardPower(tappedCard, state2, registry);
186
+ expect(power2).toBe(5); // Base + conditional modifier
187
+ });
188
+
189
+ it("should handle multiple conditional modifiers independently", () => {
190
+ const cardId = createCardId("card-1");
191
+
192
+ const definition: CardDefinition = {
193
+ id: "creature",
194
+ name: "Creature",
195
+ type: "creature",
196
+ basePower: 1,
197
+ abilities: [],
198
+ };
199
+
200
+ const registry = createCardRegistry([definition]);
201
+
202
+ const card: CardInstance<{
203
+ tapped: boolean;
204
+ modifiers: Modifier<GameStateWithCards>[];
205
+ }> = {
206
+ id: cardId,
207
+ definitionId: "creature",
208
+ owner: createPlayerId("player-1"),
209
+ controller: createPlayerId("player-1"),
210
+ zone: createZoneId("play"),
211
+ tapped: true,
212
+ flipped: false,
213
+ revealed: false,
214
+ phased: false,
215
+ modifiers: [
216
+ {
217
+ id: "mod-1",
218
+ type: "stat",
219
+ property: "power",
220
+ value: 2,
221
+ duration: "while-condition",
222
+ condition: (state: GameStateWithCards) => {
223
+ const c = state.cards[String(cardId)];
224
+ return c?.tapped === true; // Applies when tapped
225
+ },
226
+ source: createCardId("source-1"),
227
+ },
228
+ {
229
+ id: "mod-2",
230
+ type: "stat",
231
+ property: "power",
232
+ value: 3,
233
+ duration: "while-condition",
234
+ condition: (state: GameStateWithCards) => {
235
+ const c = state.cards[String(cardId)];
236
+ return c?.tapped === false; // Applies when NOT tapped
237
+ },
238
+ source: createCardId("source-2"),
239
+ },
240
+ ],
241
+ };
242
+
243
+ const state: GameStateWithCards = {
244
+ cards: {
245
+ [String(cardId)]: card,
246
+ },
247
+ };
248
+
249
+ const power = getCardPower(card, state, registry);
250
+
251
+ // Card is tapped, so first modifier applies (+2), second doesn't
252
+ expect(power).toBe(3); // 1 base + 2 from first modifier
253
+ });
254
+
255
+ it("should handle complex condition logic", () => {
256
+ const cardId = createCardId("card-1");
257
+ const otherCardId = createCardId("card-2");
258
+
259
+ const definition: CardDefinition = {
260
+ id: "creature",
261
+ name: "Creature",
262
+ type: "creature",
263
+ basePower: 2,
264
+ abilities: [],
265
+ };
266
+
267
+ const registry = createCardRegistry([definition]);
268
+
269
+ const card: CardInstance<{
270
+ tapped: boolean;
271
+ modifiers: Modifier<GameStateWithCards>[];
272
+ }> = {
273
+ id: cardId,
274
+ definitionId: "creature",
275
+ owner: createPlayerId("player-1"),
276
+ controller: createPlayerId("player-1"),
277
+ zone: createZoneId("play"),
278
+ tapped: false,
279
+ flipped: false,
280
+ revealed: false,
281
+ phased: false,
282
+ modifiers: [
283
+ {
284
+ id: "mod-1",
285
+ type: "stat",
286
+ property: "power",
287
+ value: 3,
288
+ duration: "while-condition",
289
+ condition: (state: GameStateWithCards) => {
290
+ // Complex condition: +3 power if there's another tapped card
291
+ const otherCard = state.cards[String(otherCardId)];
292
+ return otherCard?.tapped === true;
293
+ },
294
+ source: createCardId("source-1"),
295
+ },
296
+ ],
297
+ };
298
+
299
+ const otherCard: CardInstance<{
300
+ tapped: boolean;
301
+ modifiers: Modifier[];
302
+ }> = {
303
+ id: otherCardId,
304
+ definitionId: "creature",
305
+ owner: createPlayerId("player-1"),
306
+ controller: createPlayerId("player-1"),
307
+ zone: createZoneId("play"),
308
+ tapped: true, // This card is tapped
309
+ flipped: false,
310
+ revealed: false,
311
+ phased: false,
312
+ modifiers: [],
313
+ };
314
+
315
+ const state: GameStateWithCards = {
316
+ cards: {
317
+ [String(cardId)]: card,
318
+ [String(otherCardId)]: otherCard,
319
+ },
320
+ };
321
+
322
+ const power = getCardPower(card, state, registry);
323
+
324
+ expect(power).toBe(5); // 2 base + 3 from conditional (other card is tapped)
325
+ });
326
+
327
+ it("should mix conditional and unconditional modifiers correctly", () => {
328
+ const cardId = createCardId("card-1");
329
+
330
+ const definition: CardDefinition = {
331
+ id: "creature",
332
+ name: "Creature",
333
+ type: "creature",
334
+ basePower: 1,
335
+ abilities: [],
336
+ };
337
+
338
+ const registry = createCardRegistry([definition]);
339
+
340
+ const card: CardInstance<{
341
+ tapped: boolean;
342
+ modifiers: Modifier<GameStateWithCards>[];
343
+ }> = {
344
+ id: cardId,
345
+ definitionId: "creature",
346
+ owner: createPlayerId("player-1"),
347
+ controller: createPlayerId("player-1"),
348
+ zone: createZoneId("play"),
349
+ tapped: false,
350
+ flipped: false,
351
+ revealed: false,
352
+ phased: false,
353
+ modifiers: [
354
+ {
355
+ id: "mod-1",
356
+ type: "stat",
357
+ property: "power",
358
+ value: 2,
359
+ duration: "permanent",
360
+ source: createCardId("source-1"),
361
+ // No condition - always applies
362
+ },
363
+ {
364
+ id: "mod-2",
365
+ type: "stat",
366
+ property: "power",
367
+ value: 3,
368
+ duration: "while-condition",
369
+ condition: (state: GameStateWithCards) => {
370
+ const c = state.cards[String(cardId)];
371
+ return c?.tapped === true; // Only applies if tapped
372
+ },
373
+ source: createCardId("source-2"),
374
+ },
375
+ ],
376
+ };
377
+
378
+ const state: GameStateWithCards = {
379
+ cards: {
380
+ [String(cardId)]: card,
381
+ },
382
+ };
383
+
384
+ const power = getCardPower(card, state, registry);
385
+
386
+ // Card is NOT tapped: 1 base + 2 permanent modifier (conditional doesn't apply)
387
+ expect(power).toBe(3);
388
+ });
389
+ });
390
+ });
@@ -0,0 +1,286 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createCardId } from "../types";
3
+ import type { Modifier } from "./modifiers";
4
+
5
+ describe("Modifier System", () => {
6
+ describe("Modifier Type", () => {
7
+ it("should define stat modifier with all required fields", () => {
8
+ const modifier: Modifier = {
9
+ id: "mod-1",
10
+ type: "stat",
11
+ property: "power",
12
+ value: 2,
13
+ duration: "permanent",
14
+ source: createCardId("card-1"),
15
+ };
16
+
17
+ expect(modifier.id).toBe("mod-1");
18
+ expect(modifier.type).toBe("stat");
19
+ expect(modifier.property).toBe("power");
20
+ expect(modifier.value).toBe(2);
21
+ expect(modifier.duration).toBe("permanent");
22
+ expect(modifier.source).toBeDefined();
23
+ });
24
+
25
+ it("should support stat modifier type", () => {
26
+ const modifier: Modifier = {
27
+ id: "mod-1",
28
+ type: "stat",
29
+ property: "power",
30
+ value: 3,
31
+ duration: "permanent",
32
+ source: createCardId("card-1"),
33
+ };
34
+
35
+ expect(modifier.type).toBe("stat");
36
+ expect(typeof modifier.value).toBe("number");
37
+ });
38
+
39
+ it("should support ability modifier type", () => {
40
+ const modifier: Modifier = {
41
+ id: "mod-2",
42
+ type: "ability",
43
+ property: "flying",
44
+ value: true,
45
+ duration: "until-end-of-turn",
46
+ source: createCardId("card-1"),
47
+ };
48
+
49
+ expect(modifier.type).toBe("ability");
50
+ expect(typeof modifier.value).toBe("boolean");
51
+ });
52
+
53
+ it("should support type modifier type", () => {
54
+ const modifier: Modifier = {
55
+ id: "mod-3",
56
+ type: "type",
57
+ property: "creature-type",
58
+ value: "zombie",
59
+ duration: "permanent",
60
+ source: createCardId("card-1"),
61
+ };
62
+
63
+ expect(modifier.type).toBe("type");
64
+ expect(typeof modifier.value).toBe("string");
65
+ });
66
+
67
+ it("should support keyword modifier type", () => {
68
+ const modifier: Modifier = {
69
+ id: "mod-4",
70
+ type: "keyword",
71
+ property: "haste",
72
+ value: true,
73
+ duration: "until-end-of-turn",
74
+ source: createCardId("card-1"),
75
+ };
76
+
77
+ expect(modifier.type).toBe("keyword");
78
+ });
79
+
80
+ it("should support permanent duration", () => {
81
+ const modifier: Modifier = {
82
+ id: "mod-1",
83
+ type: "stat",
84
+ property: "power",
85
+ value: 1,
86
+ duration: "permanent",
87
+ source: createCardId("card-1"),
88
+ };
89
+
90
+ expect(modifier.duration).toBe("permanent");
91
+ });
92
+
93
+ it("should support until-end-of-turn duration", () => {
94
+ const modifier: Modifier = {
95
+ id: "mod-2",
96
+ type: "stat",
97
+ property: "power",
98
+ value: 2,
99
+ duration: "until-end-of-turn",
100
+ source: createCardId("card-1"),
101
+ };
102
+
103
+ expect(modifier.duration).toBe("until-end-of-turn");
104
+ });
105
+
106
+ it("should support while-condition duration", () => {
107
+ const modifier: Modifier = {
108
+ id: "mod-3",
109
+ type: "stat",
110
+ property: "power",
111
+ value: 3,
112
+ duration: "while-condition",
113
+ source: createCardId("card-1"),
114
+ };
115
+
116
+ expect(modifier.duration).toBe("while-condition");
117
+ });
118
+
119
+ it("should support optional condition function", () => {
120
+ const modifier: Modifier = {
121
+ id: "mod-1",
122
+ type: "stat",
123
+ property: "power",
124
+ value: 2,
125
+ duration: "while-condition",
126
+ condition: () => true,
127
+ source: createCardId("card-1"),
128
+ };
129
+
130
+ expect(modifier.condition).toBeDefined();
131
+ expect(typeof modifier.condition).toBe("function");
132
+ expect(modifier.condition?.({} as never)).toBe(true);
133
+ });
134
+
135
+ it("should support optional layer for complex interactions", () => {
136
+ const modifier: Modifier = {
137
+ id: "mod-1",
138
+ type: "stat",
139
+ property: "power",
140
+ value: 2,
141
+ duration: "permanent",
142
+ source: createCardId("card-1"),
143
+ layer: 7,
144
+ };
145
+
146
+ expect(modifier.layer).toBe(7);
147
+ });
148
+
149
+ it("should work without optional fields", () => {
150
+ const modifier: Modifier = {
151
+ id: "mod-1",
152
+ type: "stat",
153
+ property: "power",
154
+ value: 2,
155
+ duration: "permanent",
156
+ source: createCardId("card-1"),
157
+ };
158
+
159
+ expect(modifier.condition).toBeUndefined();
160
+ expect(modifier.layer).toBeUndefined();
161
+ });
162
+ });
163
+
164
+ describe("Modifier Values", () => {
165
+ it("should support number values", () => {
166
+ const modifier: Modifier = {
167
+ id: "mod-1",
168
+ type: "stat",
169
+ property: "power",
170
+ value: 5,
171
+ duration: "permanent",
172
+ source: createCardId("card-1"),
173
+ };
174
+
175
+ expect(typeof modifier.value).toBe("number");
176
+ expect(modifier.value).toBe(5);
177
+ });
178
+
179
+ it("should support negative number values", () => {
180
+ const modifier: Modifier = {
181
+ id: "mod-2",
182
+ type: "stat",
183
+ property: "power",
184
+ value: -2,
185
+ duration: "permanent",
186
+ source: createCardId("card-1"),
187
+ };
188
+
189
+ expect(modifier.value).toBe(-2);
190
+ });
191
+
192
+ it("should support string values", () => {
193
+ const modifier: Modifier = {
194
+ id: "mod-3",
195
+ type: "type",
196
+ property: "creature-type",
197
+ value: "dragon",
198
+ duration: "permanent",
199
+ source: createCardId("card-1"),
200
+ };
201
+
202
+ expect(typeof modifier.value).toBe("string");
203
+ expect(modifier.value).toBe("dragon");
204
+ });
205
+
206
+ it("should support boolean values", () => {
207
+ const modifier: Modifier = {
208
+ id: "mod-4",
209
+ type: "ability",
210
+ property: "flying",
211
+ value: true,
212
+ duration: "permanent",
213
+ source: createCardId("card-1"),
214
+ };
215
+
216
+ expect(typeof modifier.value).toBe("boolean");
217
+ expect(modifier.value).toBe(true);
218
+ });
219
+ });
220
+
221
+ describe("Modifier Source Tracking", () => {
222
+ it("should track source card", () => {
223
+ const sourceCard = createCardId("source-1");
224
+ const modifier: Modifier = {
225
+ id: "mod-1",
226
+ type: "stat",
227
+ property: "power",
228
+ value: 2,
229
+ duration: "permanent",
230
+ source: sourceCard,
231
+ };
232
+
233
+ expect(modifier.source).toBe(sourceCard);
234
+ });
235
+
236
+ it("should enforce CardId type for source", () => {
237
+ const sourceCard = createCardId("source-1");
238
+ const modifier: Modifier = {
239
+ id: "mod-1",
240
+ type: "stat",
241
+ property: "power",
242
+ value: 2,
243
+ duration: "permanent",
244
+ source: sourceCard,
245
+ };
246
+
247
+ const _typeCheck: typeof sourceCard = modifier.source;
248
+ expect(modifier.source).toBe(sourceCard);
249
+ });
250
+ });
251
+
252
+ describe("Modifier Condition", () => {
253
+ it("should accept condition function that returns boolean", () => {
254
+ const modifier: Modifier = {
255
+ id: "mod-1",
256
+ type: "stat",
257
+ property: "power",
258
+ value: 2,
259
+ duration: "while-condition",
260
+ condition: (state) => {
261
+ return state !== null;
262
+ },
263
+ source: createCardId("card-1"),
264
+ };
265
+
266
+ expect(modifier.condition).toBeDefined();
267
+ expect(modifier.condition?.({} as never)).toBe(true);
268
+ });
269
+
270
+ it("should support complex condition logic", () => {
271
+ type GameState = { cardTapped: boolean };
272
+ const modifier: Modifier<GameState> = {
273
+ id: "mod-1",
274
+ type: "stat",
275
+ property: "power",
276
+ value: 2,
277
+ duration: "while-condition",
278
+ condition: (state) => state.cardTapped === true,
279
+ source: createCardId("card-1"),
280
+ };
281
+
282
+ expect(modifier.condition?.({ cardTapped: true })).toBe(true);
283
+ expect(modifier.condition?.({ cardTapped: false })).toBe(false);
284
+ });
285
+ });
286
+ });