@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,257 @@
1
+ import type { CardDefinition } from "../cards/card-definition";
2
+ import type { CardInstance } from "../cards/card-instance";
3
+ import type { CardRegistry } from "../operations/card-registry";
4
+ import {
5
+ type TargetContext,
6
+ validateTargetSelection,
7
+ } from "../targeting/target-validation";
8
+ import type {
9
+ ActionDefinition,
10
+ ActionInstance,
11
+ ActionValidationResult,
12
+ } from "./action-definition";
13
+
14
+ /**
15
+ * Game State Context for Timing Validation
16
+ *
17
+ * This is the minimal state information needed to validate action timing.
18
+ * Games using core-engine will have their full state here, but we only
19
+ * require the flow control properties.
20
+ */
21
+ export type TimingContext = {
22
+ /** Current segment in the game flow */
23
+ currentSegment?: string | null;
24
+
25
+ /** Current phase within the segment */
26
+ currentPhase?: string | null;
27
+
28
+ /** Current step within the phase */
29
+ currentStep?: string | null;
30
+ };
31
+
32
+ /**
33
+ * Validate Action Timing
34
+ *
35
+ * Checks if an action can be performed based on current game flow state.
36
+ * This validates segments/phases/steps and custom timing predicates.
37
+ *
38
+ * Does NOT validate:
39
+ * - Costs (handled by core-engine's getConstraints)
40
+ * - Game-specific rules (handled by core-engine's getConstraints)
41
+ * - Complex state conditions (handled by core-engine's getConstraints)
42
+ *
43
+ * @param action - The action definition
44
+ * @param timingContext - Current game flow state (segment/phase/step)
45
+ * @param gameState - Full game state for custom timing predicates
46
+ * @returns True if timing is valid
47
+ */
48
+ export function validateActionTiming<TGameState extends TimingContext>(
49
+ action: ActionDefinition<TGameState>,
50
+ timingContext: TimingContext,
51
+ gameState?: TGameState,
52
+ ): boolean {
53
+ const timing = action.timing;
54
+
55
+ // No timing restrictions means action is always valid (timing-wise)
56
+ if (!timing) {
57
+ return true;
58
+ }
59
+
60
+ // Check segment restrictions
61
+ if (timing.segments && timing.segments.length > 0) {
62
+ if (
63
+ !(
64
+ timingContext.currentSegment &&
65
+ timing.segments.includes(timingContext.currentSegment)
66
+ )
67
+ ) {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ // Check phase restrictions
73
+ if (timing.phases && timing.phases.length > 0) {
74
+ if (
75
+ !(
76
+ timingContext.currentPhase &&
77
+ timing.phases.includes(timingContext.currentPhase)
78
+ )
79
+ ) {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ // Check step restrictions
85
+ if (timing.steps && timing.steps.length > 0) {
86
+ if (
87
+ !(
88
+ timingContext.currentStep &&
89
+ timing.steps.includes(timingContext.currentStep)
90
+ )
91
+ ) {
92
+ return false;
93
+ }
94
+ }
95
+
96
+ // Check custom timing predicate
97
+ if (timing.custom && gameState) {
98
+ return timing.custom(gameState);
99
+ }
100
+
101
+ return true;
102
+ }
103
+
104
+ /**
105
+ * Validate Action Instance
106
+ *
107
+ * Validates both timing and target selection for an action instance.
108
+ * This bridges @drmxrcy/tcg-core's validation with core-engine's execution.
109
+ *
110
+ * @param instance - The action instance to validate
111
+ * @param definition - The action definition
112
+ * @param timingContext - Current game flow state
113
+ * @param state - Full game state with card information
114
+ * @param registry - Card definition registry for target validation
115
+ * @returns Validation result
116
+ */
117
+ export function validateAction<
118
+ TCustomState = unknown,
119
+ TGameState extends TimingContext & {
120
+ cards: Record<string, CardInstance<TCustomState>>;
121
+ } = {
122
+ cards: Record<string, CardInstance<TCustomState>>;
123
+ } & TimingContext,
124
+ >(
125
+ instance: ActionInstance,
126
+ definition: ActionDefinition<TGameState>,
127
+ timingContext: TimingContext,
128
+ state: TGameState,
129
+ registry: CardRegistry<CardDefinition>,
130
+ ): ActionValidationResult {
131
+ // Validate timing
132
+ const timingValid = validateActionTiming(definition, timingContext, state);
133
+ if (!timingValid) {
134
+ return {
135
+ valid: false,
136
+ error: "Action cannot be performed at this time",
137
+ reason: "timing",
138
+ };
139
+ }
140
+
141
+ // Validate targets if action requires them
142
+ if (definition.targets && definition.targets.length > 0) {
143
+ if (!instance.targets || instance.targets.length === 0) {
144
+ return {
145
+ valid: false,
146
+ error: "Action requires targets but none were provided",
147
+ reason: "targets",
148
+ invalidTargets: [],
149
+ };
150
+ }
151
+
152
+ // Validate each target group
153
+ for (let i = 0; i < definition.targets.length; i++) {
154
+ const targetDef = definition.targets[i];
155
+ const selectedTargets = instance.targets[i] || [];
156
+
157
+ // Convert target IDs to CardInstances
158
+ const targetCards = selectedTargets
159
+ .map((targetId) => state.cards[targetId])
160
+ .filter((card) => card !== undefined);
161
+
162
+ if (targetCards.length !== selectedTargets.length) {
163
+ return {
164
+ valid: false,
165
+ error: `Some target cards at index ${i} do not exist in game state`,
166
+ reason: "targets",
167
+ invalidTargets: [i],
168
+ };
169
+ }
170
+
171
+ // Create minimal target context for validation
172
+ // Note: We don't have a source card concept in actions, so we use the player
173
+ const context: Omit<TargetContext<TCustomState>, "previousTargets"> = {
174
+ sourceCard: {
175
+ id: "" as any,
176
+ owner: instance.playerId,
177
+ controller: instance.playerId,
178
+ } as any,
179
+ controller: instance.playerId,
180
+ };
181
+
182
+ // Validate target selection using @drmxrcy/tcg-core's targeting system
183
+ if (!targetDef) {
184
+ return {
185
+ valid: false,
186
+ error: `Target definition at index ${i} is undefined`,
187
+ reason: "targets",
188
+ invalidTargets: [i],
189
+ };
190
+ }
191
+
192
+ const validationResult = validateTargetSelection(
193
+ targetCards,
194
+ targetDef,
195
+ state,
196
+ registry,
197
+ context,
198
+ );
199
+
200
+ if (!validationResult.valid) {
201
+ return {
202
+ valid: false,
203
+ error: `Invalid targets at index ${i}: ${validationResult.error}`,
204
+ reason: "targets",
205
+ invalidTargets: [i],
206
+ };
207
+ }
208
+ }
209
+ }
210
+
211
+ return { valid: true };
212
+ }
213
+
214
+ /**
215
+ * Get Available Actions
216
+ *
217
+ * Filters a list of action definitions to only those that are valid
218
+ * for the current timing context.
219
+ *
220
+ * This is useful for UI to show only valid actions, or for AI to
221
+ * enumerate possible actions.
222
+ *
223
+ * @param actions - All possible action definitions
224
+ * @param timingContext - Current game flow state
225
+ * @param gameState - Full game state for custom timing predicates
226
+ * @returns Array of actions that can be performed now (timing-wise)
227
+ */
228
+ export function getAvailableActions<TGameState extends TimingContext>(
229
+ actions: ActionDefinition<TGameState>[],
230
+ timingContext: TimingContext,
231
+ gameState?: TGameState,
232
+ ): ActionDefinition<TGameState>[] {
233
+ return actions.filter((action) =>
234
+ validateActionTiming(action, timingContext, gameState),
235
+ );
236
+ }
237
+
238
+ /**
239
+ * Check if any action is available
240
+ *
241
+ * Quick check to see if the player has any valid actions.
242
+ * More efficient than getAvailableActions when you only need a boolean.
243
+ *
244
+ * @param actions - All possible action definitions
245
+ * @param timingContext - Current game flow state
246
+ * @param gameState - Full game state for custom timing predicates
247
+ * @returns True if at least one action can be performed
248
+ */
249
+ export function hasAvailableActions<TGameState extends TimingContext>(
250
+ actions: ActionDefinition<TGameState>[],
251
+ timingContext: TimingContext,
252
+ gameState?: TGameState,
253
+ ): boolean {
254
+ return actions.some((action) =>
255
+ validateActionTiming(action, timingContext, gameState),
256
+ );
257
+ }
@@ -0,0 +1,268 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createCardRegistry } from "../operations/card-registry-impl";
3
+ import type { CardDefinition } from "./card-definition";
4
+
5
+ describe("Card Definition", () => {
6
+ describe("CardDefinition Type", () => {
7
+ it("should define static card data with all required fields", () => {
8
+ const definition: CardDefinition = {
9
+ id: "fire-bolt",
10
+ name: "Fire Bolt",
11
+ type: "instant",
12
+ };
13
+
14
+ expect(definition.id).toBe("fire-bolt");
15
+ expect(definition.name).toBe("Fire Bolt");
16
+ expect(definition.type).toBe("instant");
17
+ });
18
+
19
+ it("should support optional basePower field", () => {
20
+ const definition: CardDefinition = {
21
+ id: "grizzly-bears",
22
+ name: "Grizzly Bears",
23
+ type: "creature",
24
+ basePower: 2,
25
+ };
26
+
27
+ expect(definition.basePower).toBe(2);
28
+ });
29
+
30
+ it("should support optional baseToughness field", () => {
31
+ const definition: CardDefinition = {
32
+ id: "grizzly-bears",
33
+ name: "Grizzly Bears",
34
+ type: "creature",
35
+ baseToughness: 2,
36
+ };
37
+
38
+ expect(definition.baseToughness).toBe(2);
39
+ });
40
+
41
+ it("should support optional baseCost field", () => {
42
+ const definition: CardDefinition = {
43
+ id: "fire-bolt",
44
+ name: "Fire Bolt",
45
+ type: "instant",
46
+ baseCost: 1,
47
+ };
48
+
49
+ expect(definition.baseCost).toBe(1);
50
+ });
51
+
52
+ it("should support abilities array", () => {
53
+ const definition: CardDefinition = {
54
+ id: "serra-angel",
55
+ name: "Serra Angel",
56
+ type: "creature",
57
+ abilities: ["flying", "vigilance"],
58
+ };
59
+
60
+ expect(definition.abilities).toHaveLength(2);
61
+ expect(definition.abilities).toContain("flying");
62
+ expect(definition.abilities).toContain("vigilance");
63
+ });
64
+
65
+ it("should work with empty abilities array", () => {
66
+ const definition: CardDefinition = {
67
+ id: "vanilla-creature",
68
+ name: "Vanilla Creature",
69
+ type: "creature",
70
+ abilities: [],
71
+ };
72
+
73
+ expect(definition.abilities).toHaveLength(0);
74
+ });
75
+
76
+ it("should support all fields together", () => {
77
+ const definition: CardDefinition = {
78
+ id: "lightning-dragon",
79
+ name: "Lightning Dragon",
80
+ type: "creature",
81
+ basePower: 4,
82
+ baseToughness: 4,
83
+ baseCost: 4,
84
+ abilities: ["flying", "haste"],
85
+ };
86
+
87
+ expect(definition.id).toBe("lightning-dragon");
88
+ expect(definition.name).toBe("Lightning Dragon");
89
+ expect(definition.type).toBe("creature");
90
+ expect(definition.basePower).toBe(4);
91
+ expect(definition.baseToughness).toBe(4);
92
+ expect(definition.baseCost).toBe(4);
93
+ expect(definition.abilities).toHaveLength(2);
94
+ });
95
+ });
96
+
97
+ describe("Card Registry", () => {
98
+ it("should create empty registry", () => {
99
+ const registry = createCardRegistry<CardDefinition>([]);
100
+ expect(registry).toBeDefined();
101
+ expect(registry.getCardCount()).toBe(0);
102
+ });
103
+
104
+ it("should register single card definition", () => {
105
+ const definition: CardDefinition = {
106
+ id: "fire-bolt",
107
+ name: "Fire Bolt",
108
+ type: "instant",
109
+ baseCost: 1,
110
+ abilities: [],
111
+ };
112
+
113
+ const registry = createCardRegistry([definition]);
114
+ const retrieved = registry.getCard("fire-bolt");
115
+
116
+ expect(retrieved).toBeDefined();
117
+ expect(retrieved?.id).toBe("fire-bolt");
118
+ expect(retrieved?.name).toBe("Fire Bolt");
119
+ });
120
+
121
+ it("should register multiple card definitions", () => {
122
+ const definitions: CardDefinition[] = [
123
+ {
124
+ id: "fire-bolt",
125
+ name: "Fire Bolt",
126
+ type: "instant",
127
+ baseCost: 1,
128
+ abilities: [],
129
+ },
130
+ {
131
+ id: "grizzly-bears",
132
+ name: "Grizzly Bears",
133
+ type: "creature",
134
+ basePower: 2,
135
+ baseToughness: 2,
136
+ baseCost: 2,
137
+ abilities: [],
138
+ },
139
+ ];
140
+
141
+ const registry = createCardRegistry(definitions);
142
+
143
+ const fireBolt = registry.getCard("fire-bolt");
144
+ const bears = registry.getCard("grizzly-bears");
145
+
146
+ expect(fireBolt?.name).toBe("Fire Bolt");
147
+ expect(bears?.name).toBe("Grizzly Bears");
148
+ });
149
+
150
+ it("should return undefined for non-existent definition", () => {
151
+ const registry = createCardRegistry<CardDefinition>([]);
152
+ const retrieved = registry.getCard("non-existent");
153
+
154
+ expect(retrieved).toBeUndefined();
155
+ });
156
+
157
+ it("should overwrite duplicate definitions with last one", () => {
158
+ const definitions: CardDefinition[] = [
159
+ {
160
+ id: "fire-bolt",
161
+ name: "Fire Bolt V1",
162
+ type: "instant",
163
+ baseCost: 1,
164
+ abilities: [],
165
+ },
166
+ {
167
+ id: "fire-bolt",
168
+ name: "Fire Bolt V2",
169
+ type: "instant",
170
+ baseCost: 2,
171
+ abilities: [],
172
+ },
173
+ ];
174
+
175
+ const registry = createCardRegistry(definitions);
176
+ const retrieved = registry.getCard("fire-bolt");
177
+
178
+ expect(retrieved?.name).toBe("Fire Bolt V2");
179
+ expect(retrieved?.baseCost).toBe(2);
180
+ });
181
+ });
182
+
183
+ describe("CardRegistry.getCard", () => {
184
+ it("should retrieve definition by id", () => {
185
+ const definition: CardDefinition = {
186
+ id: "test-card",
187
+ name: "Test Card",
188
+ type: "creature",
189
+ abilities: [],
190
+ };
191
+
192
+ const registry = createCardRegistry([definition]);
193
+ const retrieved = registry.getCard("test-card");
194
+
195
+ expect(retrieved).toEqual(definition);
196
+ });
197
+
198
+ it("should be case-sensitive for ids", () => {
199
+ const definition: CardDefinition = {
200
+ id: "TestCard",
201
+ name: "Test Card",
202
+ type: "creature",
203
+ abilities: [],
204
+ };
205
+
206
+ const registry = createCardRegistry([definition]);
207
+
208
+ expect(registry.getCard("TestCard")).toBeDefined();
209
+ expect(registry.getCard("testcard")).toBeUndefined();
210
+ });
211
+
212
+ it("should handle definitions with zero values", () => {
213
+ const definition: CardDefinition = {
214
+ id: "zero-cost",
215
+ name: "Zero Cost Card",
216
+ type: "instant",
217
+ baseCost: 0,
218
+ abilities: [],
219
+ };
220
+
221
+ const registry = createCardRegistry([definition]);
222
+ const retrieved = registry.getCard("zero-cost");
223
+
224
+ expect(retrieved?.baseCost).toBe(0);
225
+ });
226
+ });
227
+
228
+ describe("CardRegistry additional methods", () => {
229
+ it("should check if card exists with hasCard", () => {
230
+ const registry = createCardRegistry([
231
+ { id: "card1", name: "Card 1", type: "creature" },
232
+ ]);
233
+
234
+ expect(registry.hasCard("card1")).toBe(true);
235
+ expect(registry.hasCard("nonexistent")).toBe(false);
236
+ });
237
+
238
+ it("should get all cards with getAllCards", () => {
239
+ const registry = createCardRegistry([
240
+ { id: "card1", name: "Card 1", type: "creature" },
241
+ { id: "card2", name: "Card 2", type: "instant" },
242
+ ]);
243
+
244
+ const allCards = registry.getAllCards();
245
+ expect(allCards).toHaveLength(2);
246
+ });
247
+
248
+ it("should query cards with predicate", () => {
249
+ const registry = createCardRegistry([
250
+ { id: "card1", name: "Card 1", type: "creature" },
251
+ { id: "card2", name: "Card 2", type: "instant" },
252
+ { id: "card3", name: "Card 3", type: "creature" },
253
+ ]);
254
+
255
+ const creatures = registry.queryCards((card) => card.type === "creature");
256
+ expect(creatures).toHaveLength(2);
257
+ });
258
+
259
+ it("should return card count", () => {
260
+ const registry = createCardRegistry([
261
+ { id: "card1", name: "Card 1", type: "creature" },
262
+ { id: "card2", name: "Card 2", type: "instant" },
263
+ ]);
264
+
265
+ expect(registry.getCardCount()).toBe(2);
266
+ });
267
+ });
268
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Card Definition - Static, immutable data for a card type
3
+ * This represents the "blueprint" of a card, not an instance in play.
4
+ * All instances of the same card share the same definition.
5
+ */
6
+ export type CardDefinition = {
7
+ /** Unique identifier for this card definition */
8
+ id: string;
9
+
10
+ /** Display name of the card */
11
+ name: string;
12
+
13
+ /** Card type (e.g., 'creature', 'instant', 'sorcery', 'enchantment') */
14
+ type: string;
15
+
16
+ /** Base power value (for creatures) */
17
+ basePower?: number;
18
+
19
+ /** Base toughness/health value (for creatures) */
20
+ baseToughness?: number;
21
+
22
+ /** Base mana/resource cost to play the card */
23
+ baseCost?: number;
24
+
25
+ /** Static abilities this card has */
26
+ abilities?: string[];
27
+ };