@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,236 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { CardId, PlayerId } from "../types";
3
+ import type { CardOperations } from "./card-operations";
4
+
5
+ describe("CardOperations Interface", () => {
6
+ // Mock card metadata type for testing
7
+ type TestCardMeta = {
8
+ damage?: number;
9
+ exerted?: boolean;
10
+ counters?: number;
11
+ effects?: string[];
12
+ };
13
+
14
+ // Mock implementation for testing the interface structure
15
+ const createMockCardOperations = (): CardOperations<TestCardMeta> => {
16
+ const cardMetas: Record<string, TestCardMeta> = {
17
+ "card-1": { damage: 0, exerted: false },
18
+ "card-2": { damage: 3, exerted: true, counters: 5 },
19
+ };
20
+
21
+ const cardOwners: Record<string, string> = {
22
+ "card-1": "player-1",
23
+ "card-2": "player-2",
24
+ };
25
+
26
+ return {
27
+ getCardMeta: (cardId) => {
28
+ return cardMetas[cardId] || {};
29
+ },
30
+
31
+ updateCardMeta: (cardId, meta) => {
32
+ if (!cardMetas[cardId]) {
33
+ cardMetas[cardId] = {};
34
+ }
35
+ Object.assign(cardMetas[cardId], meta);
36
+ },
37
+
38
+ setCardMeta: (cardId, meta) => {
39
+ cardMetas[cardId] = meta;
40
+ },
41
+
42
+ getCardOwner: (cardId) => {
43
+ return cardOwners[cardId] as unknown as PlayerId | undefined;
44
+ },
45
+
46
+ queryCards: (predicate) => {
47
+ const results: CardId[] = [];
48
+ for (const cardId in cardMetas) {
49
+ if (predicate(cardId as CardId, cardMetas[cardId])) {
50
+ results.push(cardId as CardId);
51
+ }
52
+ }
53
+ return results;
54
+ },
55
+ };
56
+ };
57
+
58
+ describe("getCardMeta", () => {
59
+ it("should return card metadata", () => {
60
+ const ops = createMockCardOperations();
61
+
62
+ const meta = ops.getCardMeta("card-1" as CardId);
63
+
64
+ expect(meta.damage).toBe(0);
65
+ expect(meta.exerted).toBe(false);
66
+ });
67
+
68
+ it("should return empty object for card without metadata", () => {
69
+ const ops = createMockCardOperations();
70
+
71
+ const meta = ops.getCardMeta("nonexistent-card" as CardId);
72
+
73
+ expect(meta).toEqual({});
74
+ });
75
+
76
+ it("should return partial metadata", () => {
77
+ const ops = createMockCardOperations();
78
+
79
+ const meta = ops.getCardMeta("card-2" as CardId);
80
+
81
+ expect(meta.damage).toBe(3);
82
+ expect(meta.exerted).toBe(true);
83
+ expect(meta.counters).toBe(5);
84
+ });
85
+ });
86
+
87
+ describe("updateCardMeta", () => {
88
+ it("should merge new metadata with existing", () => {
89
+ const ops = createMockCardOperations();
90
+
91
+ ops.updateCardMeta("card-1" as CardId, { damage: 2 });
92
+ const meta = ops.getCardMeta("card-1" as CardId);
93
+
94
+ expect(meta.damage).toBe(2);
95
+ expect(meta.exerted).toBe(false); // Preserved
96
+ });
97
+
98
+ it("should add new properties to existing metadata", () => {
99
+ const ops = createMockCardOperations();
100
+
101
+ ops.updateCardMeta("card-1" as CardId, { counters: 3 });
102
+ const meta = ops.getCardMeta("card-1" as CardId);
103
+
104
+ expect(meta.counters).toBe(3);
105
+ expect(meta.damage).toBe(0); // Preserved
106
+ expect(meta.exerted).toBe(false); // Preserved
107
+ });
108
+
109
+ it("should create metadata for card without existing metadata", () => {
110
+ const ops = createMockCardOperations();
111
+
112
+ ops.updateCardMeta("card-3" as CardId, { damage: 5 });
113
+ const meta = ops.getCardMeta("card-3" as CardId);
114
+
115
+ expect(meta.damage).toBe(5);
116
+ });
117
+ });
118
+
119
+ describe("setCardMeta", () => {
120
+ it("should replace existing metadata completely", () => {
121
+ const ops = createMockCardOperations();
122
+
123
+ ops.setCardMeta("card-2" as CardId, { damage: 10 });
124
+ const meta = ops.getCardMeta("card-2" as CardId);
125
+
126
+ expect(meta.damage).toBe(10);
127
+ expect(meta.exerted).toBeUndefined(); // Removed
128
+ expect(meta.counters).toBeUndefined(); // Removed
129
+ });
130
+
131
+ it("should set metadata for new card", () => {
132
+ const ops = createMockCardOperations();
133
+
134
+ ops.setCardMeta("card-3" as CardId, { damage: 7, exerted: true });
135
+ const meta = ops.getCardMeta("card-3" as CardId);
136
+
137
+ expect(meta.damage).toBe(7);
138
+ expect(meta.exerted).toBe(true);
139
+ });
140
+ });
141
+
142
+ describe("getCardOwner", () => {
143
+ it("should return the owner of a card", () => {
144
+ const ops = createMockCardOperations();
145
+
146
+ const owner = ops.getCardOwner("card-1" as CardId);
147
+
148
+ expect(owner).toBe("player-1" as unknown as PlayerId);
149
+ });
150
+
151
+ it("should return undefined for card without owner", () => {
152
+ const ops = createMockCardOperations();
153
+
154
+ const owner = ops.getCardOwner("nonexistent-card" as CardId);
155
+
156
+ expect(owner).toBeUndefined();
157
+ });
158
+ });
159
+
160
+ describe("queryCards", () => {
161
+ it("should find cards matching a predicate", () => {
162
+ const ops = createMockCardOperations();
163
+
164
+ const exertedCards = ops.queryCards(
165
+ (cardId, meta) => meta.exerted === true,
166
+ );
167
+
168
+ expect(exertedCards).toHaveLength(1);
169
+ expect(exertedCards).toContain("card-2");
170
+ });
171
+
172
+ it("should find cards with specific damage", () => {
173
+ const ops = createMockCardOperations();
174
+
175
+ const damagedCards = ops.queryCards(
176
+ (cardId, meta) => (meta.damage ?? 0) > 0,
177
+ );
178
+
179
+ expect(damagedCards).toHaveLength(1);
180
+ expect(damagedCards).toContain("card-2");
181
+ });
182
+
183
+ it("should return empty array when no cards match", () => {
184
+ const ops = createMockCardOperations();
185
+
186
+ const results = ops.queryCards((cardId, meta) => meta.counters === 999);
187
+
188
+ expect(results).toHaveLength(0);
189
+ });
190
+
191
+ it("should support complex predicates", () => {
192
+ const ops = createMockCardOperations();
193
+
194
+ const results = ops.queryCards(
195
+ (cardId, meta) =>
196
+ (meta.damage ?? 0) > 0 &&
197
+ meta.exerted === true &&
198
+ (meta.counters ?? 0) >= 5,
199
+ );
200
+
201
+ expect(results).toHaveLength(1);
202
+ expect(results).toContain("card-2");
203
+ });
204
+ });
205
+
206
+ describe("Type Safety", () => {
207
+ it("should enforce CardId type for card identifiers", () => {
208
+ const ops = createMockCardOperations();
209
+
210
+ // This is a compile-time test
211
+ const meta = ops.getCardMeta("card-1" as unknown as CardId);
212
+
213
+ expect(meta).toBeDefined();
214
+ });
215
+
216
+ it("should enforce PlayerId type for owner", () => {
217
+ const ops = createMockCardOperations();
218
+
219
+ // This is a compile-time test
220
+ const owner = ops.getCardOwner("card-1" as unknown as CardId);
221
+
222
+ expect(owner !== undefined ? typeof owner : "undefined").toBe("string");
223
+ });
224
+
225
+ it("should enforce generic metadata type", () => {
226
+ const ops = createMockCardOperations();
227
+
228
+ // This is a compile-time test - metadata should have TestCardMeta shape
229
+ const meta = ops.getCardMeta("card-1" as unknown as CardId);
230
+
231
+ // These properties should exist on TestCardMeta
232
+ expect(typeof meta.damage).toBe("number");
233
+ expect(typeof meta.exerted).toBe("boolean");
234
+ });
235
+ });
236
+ });
@@ -0,0 +1,116 @@
1
+ import type { CardId, PlayerId } from "../types";
2
+
3
+ /**
4
+ * Card Operations Interface
5
+ *
6
+ * Provides API for managing card metadata without directly mutating internal state.
7
+ * These operations are the only way for moves to interact with card properties.
8
+ *
9
+ * Card metadata includes:
10
+ * - Dynamic properties (damage, counters, effects)
11
+ * - Gained/lost abilities
12
+ * - Temporary modifications
13
+ * - Status effects
14
+ *
15
+ * @template TCardMeta - Game-specific card metadata type
16
+ */
17
+ export interface CardOperations<TCardMeta = any> {
18
+ /**
19
+ * Get card metadata (dynamic properties)
20
+ *
21
+ * @param cardId - ID of the card
22
+ * @returns Card metadata object (may be partial or empty)
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const meta = cards.getCardMeta('card-1');
27
+ * if (meta.damage && meta.damage > 0) {
28
+ * // Card is damaged
29
+ * }
30
+ * ```
31
+ */
32
+ getCardMeta(cardId: CardId): Partial<TCardMeta>;
33
+
34
+ /**
35
+ * Update card metadata (merge with existing)
36
+ *
37
+ * Merges the provided metadata with existing metadata.
38
+ * Use this to modify specific properties without affecting others.
39
+ *
40
+ * @param cardId - ID of the card
41
+ * @param meta - Partial metadata to merge
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // Add 2 damage without affecting other properties
46
+ * const current = cards.getCardMeta('card-1');
47
+ * cards.updateCardMeta('card-1', {
48
+ * damage: (current.damage || 0) + 2
49
+ * });
50
+ * ```
51
+ */
52
+ updateCardMeta(cardId: CardId, meta: Partial<TCardMeta>): void;
53
+
54
+ /**
55
+ * Set card metadata (replace completely)
56
+ *
57
+ * Replaces all existing metadata with the provided metadata.
58
+ * Use this for complete state replacement.
59
+ *
60
+ * @param cardId - ID of the card
61
+ * @param meta - Complete metadata to set
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * // Reset card to pristine state
66
+ * cards.setCardMeta('card-1', {
67
+ * damage: 0,
68
+ * exerted: false,
69
+ * effects: []
70
+ * });
71
+ * ```
72
+ */
73
+ setCardMeta(cardId: CardId, meta: TCardMeta): void;
74
+
75
+ /**
76
+ * Get card owner
77
+ *
78
+ * @param cardId - ID of the card
79
+ * @returns Player ID of the owner, or undefined if card doesn't exist
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * const owner = cards.getCardOwner('card-1');
84
+ * if (owner === context.playerId) {
85
+ * // Player owns this card
86
+ * }
87
+ * ```
88
+ */
89
+ getCardOwner(cardId: CardId): PlayerId | undefined;
90
+
91
+ /**
92
+ * Query cards by metadata criteria
93
+ *
94
+ * Finds all cards matching a predicate function.
95
+ * Useful for complex queries involving multiple metadata properties.
96
+ *
97
+ * @param predicate - Function that returns true for matching cards
98
+ * @returns Array of card IDs that match the predicate
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * // Find all exerted cards with damage
103
+ * const damagedExerted = cards.queryCards((cardId, meta) =>
104
+ * meta.exerted === true && (meta.damage || 0) > 0
105
+ * );
106
+ *
107
+ * // Find all cards with a specific effect
108
+ * const poisoned = cards.queryCards((cardId, meta) =>
109
+ * meta.effects?.includes('poisoned')
110
+ * );
111
+ * ```
112
+ */
113
+ queryCards(
114
+ predicate: (cardId: CardId, meta: Partial<TCardMeta>) => boolean,
115
+ ): CardId[];
116
+ }
@@ -0,0 +1,251 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createCardRegistry } from "./card-registry-impl";
3
+
4
+ describe("CardRegistry Implementation", () => {
5
+ type TestCardDef = {
6
+ id: string;
7
+ name: string;
8
+ cost: number;
9
+ type: "monster" | "spell" | "trap";
10
+ attack?: number;
11
+ defense?: number;
12
+ };
13
+
14
+ const testCards: Record<string, TestCardDef> = {
15
+ "monster-1": {
16
+ id: "monster-1",
17
+ name: "Blue Eyes",
18
+ cost: 8,
19
+ type: "monster",
20
+ attack: 3000,
21
+ defense: 2500,
22
+ },
23
+ "monster-2": {
24
+ id: "monster-2",
25
+ name: "Dark Magician",
26
+ cost: 7,
27
+ type: "monster",
28
+ attack: 2500,
29
+ defense: 2100,
30
+ },
31
+ "spell-1": {
32
+ id: "spell-1",
33
+ name: "Lightning Bolt",
34
+ cost: 1,
35
+ type: "spell",
36
+ },
37
+ "trap-1": {
38
+ id: "trap-1",
39
+ name: "Mirror Force",
40
+ cost: 3,
41
+ type: "trap",
42
+ },
43
+ };
44
+
45
+ describe("createCardRegistry", () => {
46
+ it("should create registry from card definitions", () => {
47
+ const registry = createCardRegistry(testCards);
48
+
49
+ expect(registry.getCardCount()).toBe(4);
50
+ expect(registry.hasCard("monster-1")).toBe(true);
51
+ expect(registry.hasCard("spell-1")).toBe(true);
52
+ });
53
+
54
+ it("should create empty registry with no cards", () => {
55
+ const registry = createCardRegistry<TestCardDef>();
56
+
57
+ expect(registry.getCardCount()).toBe(0);
58
+ expect(registry.getAllCards()).toHaveLength(0);
59
+ });
60
+
61
+ it("should create empty registry with empty object", () => {
62
+ const registry = createCardRegistry<TestCardDef>({});
63
+
64
+ expect(registry.getCardCount()).toBe(0);
65
+ expect(registry.getAllCards()).toHaveLength(0);
66
+ });
67
+ });
68
+
69
+ describe("getCard", () => {
70
+ it("should return card definition by ID", () => {
71
+ const registry = createCardRegistry(testCards);
72
+
73
+ const card = registry.getCard("monster-1");
74
+
75
+ expect(card).toBeDefined();
76
+ expect(card?.name).toBe("Blue Eyes");
77
+ expect(card?.cost).toBe(8);
78
+ expect(card?.attack).toBe(3000);
79
+ });
80
+
81
+ it("should return undefined for nonexistent card", () => {
82
+ const registry = createCardRegistry(testCards);
83
+
84
+ const card = registry.getCard("nonexistent");
85
+
86
+ expect(card).toBeUndefined();
87
+ });
88
+
89
+ it("should handle different card types", () => {
90
+ const registry = createCardRegistry(testCards);
91
+
92
+ const monster = registry.getCard("monster-1");
93
+ const spell = registry.getCard("spell-1");
94
+
95
+ expect(monster?.type).toBe("monster");
96
+ expect(spell?.type).toBe("spell");
97
+ });
98
+ });
99
+
100
+ describe("hasCard", () => {
101
+ it("should return true for existing card", () => {
102
+ const registry = createCardRegistry(testCards);
103
+
104
+ expect(registry.hasCard("monster-1")).toBe(true);
105
+ expect(registry.hasCard("spell-1")).toBe(true);
106
+ expect(registry.hasCard("trap-1")).toBe(true);
107
+ });
108
+
109
+ it("should return false for nonexistent card", () => {
110
+ const registry = createCardRegistry(testCards);
111
+
112
+ expect(registry.hasCard("nonexistent")).toBe(false);
113
+ expect(registry.hasCard("unknown-card")).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe("getAllCards", () => {
118
+ it("should return all card definitions", () => {
119
+ const registry = createCardRegistry(testCards);
120
+
121
+ const allCards = registry.getAllCards();
122
+
123
+ expect(allCards).toHaveLength(4);
124
+ expect(allCards.map((c) => c.id)).toContain("monster-1");
125
+ expect(allCards.map((c) => c.id)).toContain("monster-2");
126
+ expect(allCards.map((c) => c.id)).toContain("spell-1");
127
+ expect(allCards.map((c) => c.id)).toContain("trap-1");
128
+ });
129
+
130
+ it("should return empty array for empty registry", () => {
131
+ const registry = createCardRegistry<TestCardDef>();
132
+
133
+ const allCards = registry.getAllCards();
134
+
135
+ expect(allCards).toHaveLength(0);
136
+ });
137
+
138
+ it("should return new array instance each time", () => {
139
+ const registry = createCardRegistry(testCards);
140
+
141
+ const arr1 = registry.getAllCards();
142
+ const arr2 = registry.getAllCards();
143
+
144
+ expect(arr1).not.toBe(arr2); // Different instances
145
+ expect(arr1).toEqual(arr2); // Same content
146
+ });
147
+ });
148
+
149
+ describe("queryCards", () => {
150
+ it("should find cards by type", () => {
151
+ const registry = createCardRegistry(testCards);
152
+
153
+ const monsters = registry.queryCards((card) => card.type === "monster");
154
+
155
+ expect(monsters).toHaveLength(2);
156
+ expect(monsters.every((c) => c.type === "monster")).toBe(true);
157
+ expect(monsters.map((c) => c.id)).toContain("monster-1");
158
+ expect(monsters.map((c) => c.id)).toContain("monster-2");
159
+ });
160
+
161
+ it("should find cards by cost", () => {
162
+ const registry = createCardRegistry(testCards);
163
+
164
+ const expensive = registry.queryCards((card) => card.cost >= 7);
165
+
166
+ expect(expensive).toHaveLength(2);
167
+ expect(expensive.map((c) => c.id)).toContain("monster-1");
168
+ expect(expensive.map((c) => c.id)).toContain("monster-2");
169
+ });
170
+
171
+ it("should support complex predicates", () => {
172
+ const registry = createCardRegistry(testCards);
173
+
174
+ const strongMonsters = registry.queryCards(
175
+ (card) => card.type === "monster" && (card.attack ?? 0) >= 2500,
176
+ );
177
+
178
+ expect(strongMonsters).toHaveLength(2);
179
+ expect(strongMonsters.every((c) => c.type === "monster")).toBe(true);
180
+ expect(strongMonsters.every((c) => (c.attack ?? 0) >= 2500)).toBe(true);
181
+ });
182
+
183
+ it("should return empty array when no matches", () => {
184
+ const registry = createCardRegistry(testCards);
185
+
186
+ const results = registry.queryCards((card) => card.cost > 100);
187
+
188
+ expect(results).toHaveLength(0);
189
+ });
190
+
191
+ it("should handle predicates on optional properties", () => {
192
+ const registry = createCardRegistry(testCards);
193
+
194
+ const withAttack = registry.queryCards(
195
+ (card) => card.attack !== undefined,
196
+ );
197
+
198
+ expect(withAttack).toHaveLength(2);
199
+ expect(withAttack.every((c) => c.type === "monster")).toBe(true);
200
+ });
201
+ });
202
+
203
+ describe("getCardCount", () => {
204
+ it("should return total number of cards", () => {
205
+ const registry = createCardRegistry(testCards);
206
+
207
+ expect(registry.getCardCount()).toBe(4);
208
+ });
209
+
210
+ it("should return 0 for empty registry", () => {
211
+ const registry = createCardRegistry<TestCardDef>();
212
+
213
+ expect(registry.getCardCount()).toBe(0);
214
+ });
215
+ });
216
+
217
+ describe("Immutability", () => {
218
+ it("should not expose internal card definitions directly", () => {
219
+ const registry = createCardRegistry(testCards);
220
+
221
+ const card1 = registry.getCard("monster-1");
222
+ const card2 = registry.getCard("monster-1");
223
+
224
+ // Should return the same object (not a copy)
225
+ // This is fine since card definitions are static/immutable
226
+ expect(card1).toBeDefined();
227
+ expect(card2).toBeDefined();
228
+ if (card1 && card2) {
229
+ expect(card1).toBe(card2);
230
+ }
231
+ });
232
+
233
+ it("should return new arrays for getAllCards", () => {
234
+ const registry = createCardRegistry(testCards);
235
+
236
+ const all1 = registry.getAllCards();
237
+ const all2 = registry.getAllCards();
238
+
239
+ expect(all1).not.toBe(all2);
240
+ });
241
+
242
+ it("should return new arrays for queryCards", () => {
243
+ const registry = createCardRegistry(testCards);
244
+
245
+ const query1 = registry.queryCards((c) => c.type === "monster");
246
+ const query2 = registry.queryCards((c) => c.type === "monster");
247
+
248
+ expect(query1).not.toBe(query2);
249
+ });
250
+ });
251
+ });
@@ -0,0 +1,70 @@
1
+ import type { CardRegistry } from "./card-registry";
2
+
3
+ /**
4
+ * Creates a CardRegistry implementation from a record or array of card definitions
5
+ *
6
+ * @param cards - Record mapping card definition IDs to card definitions,
7
+ * OR array of definitions with `id` property
8
+ * @returns CardRegistry implementation
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // From record:
13
+ * const cards = {
14
+ * 'pikachu': { id: 'pikachu', name: 'Pikachu', cost: 3 },
15
+ * 'charizard': { id: 'charizard', name: 'Charizard', cost: 8 },
16
+ * };
17
+ * const registry = createCardRegistry(cards);
18
+ *
19
+ * // From array:
20
+ * const cardArray = [
21
+ * { id: 'pikachu', name: 'Pikachu', cost: 3 },
22
+ * { id: 'charizard', name: 'Charizard', cost: 8 },
23
+ * ];
24
+ * const registry = createCardRegistry(cardArray);
25
+ *
26
+ * const pikachu = registry.getCard('pikachu');
27
+ * ```
28
+ */
29
+ export function createCardRegistry<TCardDefinition>(
30
+ cards:
31
+ | Record<string, TCardDefinition>
32
+ | (TCardDefinition & { id: string })[] = {} as Record<
33
+ string,
34
+ TCardDefinition
35
+ >,
36
+ ): CardRegistry<TCardDefinition> {
37
+ // Convert array to record if needed
38
+ const cardsRecord: Record<string, TCardDefinition> = Array.isArray(cards)
39
+ ? cards.reduce(
40
+ (acc, card) => {
41
+ acc[card.id] = card;
42
+ return acc;
43
+ },
44
+ {} as Record<string, TCardDefinition>,
45
+ )
46
+ : cards;
47
+ return {
48
+ getCard(definitionId: string): TCardDefinition | undefined {
49
+ return cardsRecord[definitionId];
50
+ },
51
+
52
+ hasCard(definitionId: string): boolean {
53
+ return definitionId in cardsRecord;
54
+ },
55
+
56
+ getAllCards(): TCardDefinition[] {
57
+ return Object.values(cardsRecord);
58
+ },
59
+
60
+ queryCards(
61
+ predicate: (card: TCardDefinition) => boolean,
62
+ ): TCardDefinition[] {
63
+ return Object.values(cardsRecord).filter(predicate);
64
+ },
65
+
66
+ getCardCount(): number {
67
+ return Object.keys(cardsRecord).length;
68
+ },
69
+ };
70
+ }