@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,468 @@
1
+ import type { Logger } from "../logging";
2
+ import type { CardId, PlayerId } from "../types";
3
+ import type { InternalState } from "../types/state";
4
+ import type { CardOperations } from "./card-operations";
5
+ import type { CounterOperations } from "./counter-operations";
6
+ import type { GameOperations } from "./game-operations";
7
+ import type { ZoneOperations } from "./zone-operations";
8
+
9
+ /**
10
+ * Create a ZoneOperations implementation backed by InternalState
11
+ *
12
+ * @param state - Internal state to operate on (will be mutated)
13
+ * @param logger - Optional logger for TRACE-level logging
14
+ * @returns ZoneOperations implementation
15
+ */
16
+ export const createZoneOperations = <TCardDef, TCardMeta>(
17
+ state: InternalState<TCardDef, TCardMeta>,
18
+ logger?: Logger,
19
+ ): ZoneOperations => {
20
+ const zoneOps: ZoneOperations = {
21
+ moveCard: ({ cardId, targetZoneId, position = "bottom" }) => {
22
+ logger?.trace("Moving card", { cardId, targetZoneId, position });
23
+ // Find current zone and remove card
24
+ let sourceZoneId: string | undefined;
25
+ for (const zoneId in state.zones) {
26
+ const zone = state.zones[zoneId];
27
+ if (!zone) continue;
28
+ const index = zone.cardIds.indexOf(cardId);
29
+ if (index !== -1) {
30
+ zone.cardIds.splice(index, 1);
31
+ sourceZoneId = zoneId;
32
+
33
+ // Update positions in source zone if ordered
34
+ if (zone.config.ordered) {
35
+ for (let i = index; i < zone.cardIds.length; i++) {
36
+ const cid = zone.cardIds[i];
37
+ if (!cid) continue;
38
+ if (state.cards[cid]) {
39
+ state.cards[cid].position = i;
40
+ }
41
+ }
42
+ }
43
+ break;
44
+ }
45
+ }
46
+
47
+ // Add to target zone
48
+ const targetZone = state.zones[targetZoneId as string];
49
+ if (!targetZone) {
50
+ throw new Error(`Target zone ${targetZoneId} does not exist`);
51
+ }
52
+
53
+ let targetPosition: number | undefined;
54
+
55
+ if (position === "top") {
56
+ targetZone.cardIds.unshift(cardId);
57
+ targetPosition = 0;
58
+
59
+ // Update positions of other cards in ordered zones
60
+ if (targetZone.config.ordered) {
61
+ for (let i = 1; i < targetZone.cardIds.length; i++) {
62
+ const cid = targetZone.cardIds[i] as string;
63
+ if (state.cards[cid]) {
64
+ state.cards[cid].position = i;
65
+ }
66
+ }
67
+ }
68
+ } else if (position === "bottom") {
69
+ targetZone.cardIds.push(cardId);
70
+ targetPosition = targetZone.config.ordered
71
+ ? targetZone.cardIds.length - 1
72
+ : undefined;
73
+ } else {
74
+ // Numeric position
75
+ const idx = position as number;
76
+ targetZone.cardIds.splice(idx, 0, cardId);
77
+ targetPosition = targetZone.config.ordered ? idx : undefined;
78
+
79
+ // Update positions of cards after insertion point
80
+ if (targetZone.config.ordered) {
81
+ for (let i = idx + 1; i < targetZone.cardIds.length; i++) {
82
+ const cid = targetZone.cardIds[i] as string;
83
+ if (state.cards[cid]) {
84
+ state.cards[cid].position = i;
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ // Update card's zone and position
91
+ const card = state.cards[cardId as string];
92
+ if (card) {
93
+ card.zone = targetZoneId;
94
+ card.position = targetPosition;
95
+ }
96
+ },
97
+
98
+ getCardsInZone: (zoneId, ownerId?) => {
99
+ const zone = state.zones[zoneId as string];
100
+ if (!zone) {
101
+ return [];
102
+ }
103
+
104
+ let cards = zone.cardIds;
105
+
106
+ // Filter by owner if specified
107
+ if (ownerId !== undefined) {
108
+ cards = cards.filter((cardId) => {
109
+ const card = state.cards[cardId as string];
110
+ return card && card.owner === ownerId;
111
+ }) as CardId[];
112
+ }
113
+
114
+ // Return a copy to prevent external mutation
115
+ return [...cards];
116
+ },
117
+
118
+ shuffleZone: (zoneId, ownerId?) => {
119
+ const zone = state.zones[zoneId as string];
120
+ if (!zone) {
121
+ return;
122
+ }
123
+
124
+ // Simple Fisher-Yates shuffle
125
+ // Note: In production, this should use a seeded RNG for determinism
126
+ const cards = [...zone.cardIds];
127
+ for (let i = cards.length - 1; i > 0; i--) {
128
+ const j = Math.floor(Math.random() * (i + 1));
129
+ const temp = cards[i];
130
+ const swapCard = cards[j];
131
+ if (temp && swapCard) {
132
+ cards[i] = swapCard;
133
+ cards[j] = temp;
134
+ }
135
+ }
136
+
137
+ zone.cardIds = cards;
138
+
139
+ // Update positions if ordered
140
+ if (zone.config.ordered) {
141
+ for (let i = 0; i < cards.length; i++) {
142
+ const cardId = cards[i] as string;
143
+ if (state.cards[cardId]) {
144
+ state.cards[cardId].position = i;
145
+ }
146
+ }
147
+ }
148
+ },
149
+
150
+ getCardZone: (cardId) => {
151
+ const card = state.cards[cardId as string];
152
+ return card?.zone;
153
+ },
154
+
155
+ drawCards: ({ from, to, count, playerId }) => {
156
+ const sourceCards = zoneOps.getCardsInZone(from, playerId);
157
+ const drawnCards: CardId[] = [];
158
+
159
+ for (let i = 0; i < count && i < sourceCards.length; i++) {
160
+ const cardId = sourceCards[i];
161
+ if (cardId) {
162
+ zoneOps.moveCard({
163
+ cardId,
164
+ targetZoneId: to,
165
+ position: "bottom",
166
+ });
167
+ drawnCards.push(cardId);
168
+ }
169
+ }
170
+
171
+ return drawnCards;
172
+ },
173
+
174
+ mulligan: ({ hand, deck, drawCount, playerId }) => {
175
+ const handCards = zoneOps.getCardsInZone(hand, playerId);
176
+
177
+ // Move all hand cards back to deck
178
+ for (const cardId of handCards) {
179
+ zoneOps.moveCard({
180
+ cardId,
181
+ targetZoneId: deck,
182
+ position: "bottom",
183
+ });
184
+ }
185
+
186
+ // Shuffle deck
187
+ zoneOps.shuffleZone(deck, playerId);
188
+
189
+ // Draw new hand
190
+ zoneOps.drawCards({ from: deck, to: hand, count: drawCount, playerId });
191
+ },
192
+
193
+ bulkMove: ({ from, to, count, playerId, position = "bottom" }) => {
194
+ const sourceCards = zoneOps.getCardsInZone(from, playerId);
195
+ const movedCards: CardId[] = [];
196
+
197
+ for (let i = 0; i < count && i < sourceCards.length; i++) {
198
+ const cardId = sourceCards[i];
199
+ if (cardId) {
200
+ zoneOps.moveCard({
201
+ cardId,
202
+ targetZoneId: to,
203
+ position,
204
+ });
205
+ movedCards.push(cardId);
206
+ }
207
+ }
208
+
209
+ return movedCards;
210
+ },
211
+
212
+ createDeck: ({ zoneId, playerId, cardCount, shuffle = false }) => {
213
+ const createdCards: CardId[] = [];
214
+
215
+ // Create card instances
216
+ for (let i = 0; i < cardCount; i++) {
217
+ const cardId = `${playerId}-${zoneId}-${i}` as CardId;
218
+ createdCards.push(cardId);
219
+
220
+ // Add card to internal state
221
+ state.cards[cardId as string] = {
222
+ definitionId: "placeholder", // Games can customize this
223
+ owner: playerId,
224
+ controller: playerId, // Initially controller equals owner
225
+ zone: zoneId,
226
+ position: i,
227
+ };
228
+
229
+ // Add to zone
230
+ const zone = state.zones[zoneId as string];
231
+ if (zone) {
232
+ zone.cardIds.push(cardId);
233
+ }
234
+ }
235
+
236
+ // Shuffle if requested
237
+ if (shuffle) {
238
+ zoneOps.shuffleZone(zoneId, playerId);
239
+ }
240
+
241
+ return createdCards;
242
+ },
243
+ };
244
+
245
+ return zoneOps;
246
+ };
247
+
248
+ /**
249
+ * Create a CardOperations implementation backed by InternalState
250
+ *
251
+ * @param state - Internal state to operate on (will be mutated)
252
+ * @param logger - Optional logger for TRACE-level logging
253
+ * @returns CardOperations implementation
254
+ */
255
+ export const createCardOperations = <TCardDef, TCardMeta>(
256
+ state: InternalState<TCardDef, TCardMeta>,
257
+ logger?: Logger,
258
+ ): CardOperations<TCardMeta> => {
259
+ return {
260
+ getCardMeta: (cardId) => {
261
+ logger?.trace("Getting card meta", { cardId });
262
+ return (state.cardMetas[cardId as string] || {}) as Partial<TCardMeta>;
263
+ },
264
+
265
+ updateCardMeta: (cardId, meta) => {
266
+ logger?.trace("Updating card meta", { cardId, updates: meta });
267
+ const existing = state.cardMetas[cardId as string];
268
+ if (existing) {
269
+ Object.assign(existing, meta);
270
+ } else {
271
+ state.cardMetas[cardId as string] = meta as TCardMeta;
272
+ }
273
+ },
274
+
275
+ setCardMeta: (cardId, meta) => {
276
+ logger?.trace("Setting card meta", { cardId });
277
+ state.cardMetas[cardId as string] = meta;
278
+ },
279
+
280
+ getCardOwner: (cardId) => {
281
+ const card = state.cards[cardId as string];
282
+ return card?.owner;
283
+ },
284
+
285
+ queryCards: (predicate) => {
286
+ const results: CardId[] = [];
287
+ for (const cardId in state.cardMetas) {
288
+ const meta = state.cardMetas[cardId];
289
+ if (predicate(cardId as CardId, meta as Partial<TCardMeta>)) {
290
+ results.push(cardId as CardId);
291
+ }
292
+ }
293
+ return results;
294
+ },
295
+ };
296
+ };
297
+
298
+ /**
299
+ * Create a GameOperations implementation backed by InternalState
300
+ *
301
+ * @param state - Internal state to operate on (will be mutated)
302
+ * @param logger - Optional logger for TRACE-level logging
303
+ * @returns GameOperations implementation
304
+ */
305
+ export const createGameOperations = <TCardDef, TCardMeta>(
306
+ state: InternalState<TCardDef, TCardMeta>,
307
+ logger?: Logger,
308
+ ): GameOperations => {
309
+ return {
310
+ setOTP: (playerId: PlayerId) => {
311
+ logger?.trace("Setting OTP", { playerId });
312
+ state.otp = playerId;
313
+ },
314
+
315
+ getOTP: () => {
316
+ return state.otp;
317
+ },
318
+
319
+ setChoosingFirstPlayer: (playerId: PlayerId) => {
320
+ state.choosingFirstPlayer = playerId;
321
+ },
322
+
323
+ getChoosingFirstPlayer: () => {
324
+ return state.choosingFirstPlayer;
325
+ },
326
+
327
+ setPendingMulligan: (playerIds: PlayerId[]) => {
328
+ state.pendingMulligan = playerIds;
329
+ },
330
+
331
+ getPendingMulligan: () => {
332
+ // Return copy to prevent external mutation
333
+ return state.pendingMulligan ? [...state.pendingMulligan] : [];
334
+ },
335
+
336
+ addPendingMulligan: (playerId: PlayerId) => {
337
+ if (!state.pendingMulligan) {
338
+ state.pendingMulligan = [playerId];
339
+ } else if (!state.pendingMulligan.includes(playerId)) {
340
+ state.pendingMulligan.push(playerId);
341
+ }
342
+ },
343
+
344
+ removePendingMulligan: (playerId: PlayerId) => {
345
+ if (!state.pendingMulligan) {
346
+ return;
347
+ }
348
+ const index = state.pendingMulligan.indexOf(playerId);
349
+ if (index !== -1) {
350
+ state.pendingMulligan.splice(index, 1);
351
+ }
352
+ },
353
+ };
354
+ };
355
+
356
+ /**
357
+ * Internal counter state stored in cardMetas
358
+ * Uses a reserved key to avoid conflicts with game-specific metadata
359
+ */
360
+ interface CounterState {
361
+ __counters?: Record<string, number>;
362
+ __flags?: Record<string, boolean>;
363
+ }
364
+
365
+ /**
366
+ * Create a CounterOperations implementation backed by InternalState
367
+ *
368
+ * Counters and flags are stored in cardMetas using reserved keys (__counters, __flags)
369
+ * to avoid conflicts with game-specific metadata.
370
+ *
371
+ * @param state - Internal state to operate on (will be mutated)
372
+ * @param logger - Optional logger for TRACE-level logging
373
+ * @returns CounterOperations implementation
374
+ */
375
+ export const createCounterOperations = <TCardDef, TCardMeta>(
376
+ state: InternalState<TCardDef, TCardMeta>,
377
+ logger?: Logger,
378
+ ): CounterOperations => {
379
+ const getCounterState = (cardId: CardId): CounterState => {
380
+ const meta = state.cardMetas[cardId as string];
381
+ if (!meta) {
382
+ state.cardMetas[cardId as string] = {} as TCardMeta;
383
+ }
384
+ return state.cardMetas[cardId as string] as unknown as CounterState;
385
+ };
386
+
387
+ return {
388
+ setFlag: (cardId, flag, value) => {
389
+ logger?.trace("Setting flag", { cardId, flag, value });
390
+ const counterState = getCounterState(cardId);
391
+ if (!counterState.__flags) {
392
+ counterState.__flags = {};
393
+ }
394
+ counterState.__flags[flag] = value;
395
+ },
396
+
397
+ getFlag: (cardId, flag) => {
398
+ const meta = state.cardMetas[cardId as string] as unknown as CounterState;
399
+ return meta?.__flags?.[flag] ?? false;
400
+ },
401
+
402
+ addCounter: (cardId, type, amount) => {
403
+ logger?.trace("Adding counter", { cardId, type, amount });
404
+ if (amount <= 0) return;
405
+ const counterState = getCounterState(cardId);
406
+ if (!counterState.__counters) {
407
+ counterState.__counters = {};
408
+ }
409
+ counterState.__counters[type] =
410
+ (counterState.__counters[type] ?? 0) + amount;
411
+ },
412
+
413
+ removeCounter: (cardId, type, amount) => {
414
+ logger?.trace("Removing counter", { cardId, type, amount });
415
+ if (amount <= 0) return;
416
+ const counterState = getCounterState(cardId);
417
+ if (!counterState.__counters) return;
418
+ const current = counterState.__counters[type] ?? 0;
419
+ counterState.__counters[type] = Math.max(0, current - amount);
420
+ },
421
+
422
+ getCounter: (cardId, type) => {
423
+ const meta = state.cardMetas[cardId as string] as unknown as CounterState;
424
+ return meta?.__counters?.[type] ?? 0;
425
+ },
426
+
427
+ clearCounter: (cardId, type) => {
428
+ logger?.trace("Clearing counter", { cardId, type });
429
+ const meta = state.cardMetas[cardId as string] as unknown as CounterState;
430
+ if (meta?.__counters) {
431
+ delete meta.__counters[type];
432
+ }
433
+ },
434
+
435
+ clearAllCounters: (cardId) => {
436
+ logger?.trace("Clearing all counters", { cardId });
437
+ const meta = state.cardMetas[cardId as string] as unknown as CounterState;
438
+ if (meta) {
439
+ delete meta.__counters;
440
+ delete meta.__flags;
441
+ }
442
+ },
443
+
444
+ getCardsWithFlag: (flag, value) => {
445
+ const results: CardId[] = [];
446
+ for (const cardId in state.cardMetas) {
447
+ const meta = state.cardMetas[cardId] as unknown as CounterState;
448
+ const flagValue = meta?.__flags?.[flag] ?? false;
449
+ if (flagValue === value) {
450
+ results.push(cardId as CardId);
451
+ }
452
+ }
453
+ return results;
454
+ },
455
+
456
+ getCardsWithCounter: (type, minValue = 1) => {
457
+ const results: CardId[] = [];
458
+ for (const cardId in state.cardMetas) {
459
+ const meta = state.cardMetas[cardId] as unknown as CounterState;
460
+ const counterValue = meta?.__counters?.[type] ?? 0;
461
+ if (counterValue >= minValue) {
462
+ results.push(cardId as CardId);
463
+ }
464
+ }
465
+ return results;
466
+ },
467
+ };
468
+ };