@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.
- package/README.md +882 -0
- package/package.json +58 -0
- package/src/__tests__/alpha-clash-engine-definition.test.ts +319 -0
- package/src/__tests__/createMockAlphaClashGame.ts +462 -0
- package/src/__tests__/createMockGrandArchiveGame.ts +373 -0
- package/src/__tests__/createMockGundamGame.ts +379 -0
- package/src/__tests__/createMockLorcanaGame.ts +328 -0
- package/src/__tests__/createMockOnePieceGame.ts +429 -0
- package/src/__tests__/createMockRiftboundGame.ts +462 -0
- package/src/__tests__/grand-archive-engine-definition.test.ts +118 -0
- package/src/__tests__/gundam-engine-definition.test.ts +110 -0
- package/src/__tests__/integration-complete-game.test.ts +508 -0
- package/src/__tests__/integration-network-sync.test.ts +469 -0
- package/src/__tests__/lorcana-engine-definition.test.ts +100 -0
- package/src/__tests__/move-enumeration.test.ts +725 -0
- package/src/__tests__/multiplayer-engine.test.ts +555 -0
- package/src/__tests__/one-piece-engine-definition.test.ts +114 -0
- package/src/__tests__/riftbound-engine-definition.test.ts +124 -0
- package/src/actions/action-definition.test.ts +201 -0
- package/src/actions/action-definition.ts +122 -0
- package/src/actions/action-timing.test.ts +490 -0
- package/src/actions/action-timing.ts +257 -0
- package/src/cards/card-definition.test.ts +268 -0
- package/src/cards/card-definition.ts +27 -0
- package/src/cards/card-instance.test.ts +422 -0
- package/src/cards/card-instance.ts +49 -0
- package/src/cards/computed-properties.test.ts +530 -0
- package/src/cards/computed-properties.ts +84 -0
- package/src/cards/conditional-modifiers.test.ts +390 -0
- package/src/cards/modifiers.test.ts +286 -0
- package/src/cards/modifiers.ts +51 -0
- package/src/engine/MULTIPLAYER.md +425 -0
- package/src/engine/__tests__/rule-engine-flow.test.ts +348 -0
- package/src/engine/__tests__/rule-engine-history.test.ts +535 -0
- package/src/engine/__tests__/rule-engine-moves.test.ts +488 -0
- package/src/engine/__tests__/rule-engine.test.ts +366 -0
- package/src/engine/index.ts +14 -0
- package/src/engine/multiplayer-engine.example.ts +571 -0
- package/src/engine/multiplayer-engine.ts +409 -0
- package/src/engine/rule-engine.test.ts +286 -0
- package/src/engine/rule-engine.ts +1539 -0
- package/src/engine/tracker-system.ts +172 -0
- package/src/examples/__tests__/coin-flip-game.test.ts +641 -0
- package/src/filtering/card-filter.test.ts +230 -0
- package/src/filtering/card-filter.ts +91 -0
- package/src/filtering/card-query.test.ts +901 -0
- package/src/filtering/card-query.ts +273 -0
- package/src/filtering/filter-matching.test.ts +944 -0
- package/src/filtering/filter-matching.ts +315 -0
- package/src/flow/SERIALIZATION.md +428 -0
- package/src/flow/__tests__/flow-definition.test.ts +427 -0
- package/src/flow/__tests__/flow-manager.test.ts +756 -0
- package/src/flow/__tests__/flow-serialization.test.ts +565 -0
- package/src/flow/flow-definition.ts +453 -0
- package/src/flow/flow-manager.ts +1044 -0
- package/src/flow/index.ts +35 -0
- package/src/game-definition/__tests__/game-definition-validation.test.ts +359 -0
- package/src/game-definition/__tests__/game-definition.test.ts +291 -0
- package/src/game-definition/__tests__/move-definitions.test.ts +328 -0
- package/src/game-definition/game-definition.ts +261 -0
- package/src/game-definition/index.ts +28 -0
- package/src/game-definition/move-definitions.ts +188 -0
- package/src/game-definition/validation.ts +183 -0
- package/src/history/history-manager.test.ts +497 -0
- package/src/history/history-manager.ts +312 -0
- package/src/history/history-operations.ts +122 -0
- package/src/history/index.ts +9 -0
- package/src/history/types.ts +255 -0
- package/src/index.ts +32 -0
- package/src/logging/index.ts +27 -0
- package/src/logging/log-formatter.ts +187 -0
- package/src/logging/logger.ts +276 -0
- package/src/logging/types.ts +148 -0
- package/src/moves/create-move.test.ts +331 -0
- package/src/moves/create-move.ts +64 -0
- package/src/moves/move-enumeration.ts +228 -0
- package/src/moves/move-executor.test.ts +431 -0
- package/src/moves/move-executor.ts +195 -0
- package/src/moves/move-system.test.ts +380 -0
- package/src/moves/move-system.ts +463 -0
- package/src/moves/standard-moves.ts +231 -0
- package/src/operations/card-operations.test.ts +236 -0
- package/src/operations/card-operations.ts +116 -0
- package/src/operations/card-registry-impl.test.ts +251 -0
- package/src/operations/card-registry-impl.ts +70 -0
- package/src/operations/card-registry.test.ts +234 -0
- package/src/operations/card-registry.ts +106 -0
- package/src/operations/counter-operations.ts +152 -0
- package/src/operations/game-operations.test.ts +280 -0
- package/src/operations/game-operations.ts +140 -0
- package/src/operations/index.ts +24 -0
- package/src/operations/operations-impl.test.ts +354 -0
- package/src/operations/operations-impl.ts +468 -0
- package/src/operations/zone-operations.test.ts +295 -0
- package/src/operations/zone-operations.ts +223 -0
- package/src/rng/seeded-rng.test.ts +339 -0
- package/src/rng/seeded-rng.ts +123 -0
- package/src/targeting/index.ts +48 -0
- package/src/targeting/target-definition.test.ts +273 -0
- package/src/targeting/target-definition.ts +37 -0
- package/src/targeting/target-dsl.ts +279 -0
- package/src/targeting/target-resolver.ts +486 -0
- package/src/targeting/target-validation.test.ts +994 -0
- package/src/targeting/target-validation.ts +286 -0
- package/src/telemetry/events.ts +202 -0
- package/src/telemetry/index.ts +21 -0
- package/src/telemetry/telemetry-manager.ts +127 -0
- package/src/telemetry/types.ts +68 -0
- package/src/testing/__tests__/testing-utilities-integration.test.ts +161 -0
- package/src/testing/index.ts +88 -0
- package/src/testing/test-assertions.test.ts +341 -0
- package/src/testing/test-assertions.ts +256 -0
- package/src/testing/test-card-factory.test.ts +228 -0
- package/src/testing/test-card-factory.ts +111 -0
- package/src/testing/test-context-factory.ts +187 -0
- package/src/testing/test-end-assertions.test.ts +262 -0
- package/src/testing/test-end-assertions.ts +95 -0
- package/src/testing/test-engine-builder.test.ts +389 -0
- package/src/testing/test-engine-builder.ts +46 -0
- package/src/testing/test-flow-assertions.test.ts +284 -0
- package/src/testing/test-flow-assertions.ts +115 -0
- package/src/testing/test-player-builder.test.ts +132 -0
- package/src/testing/test-player-builder.ts +46 -0
- package/src/testing/test-replay-assertions.test.ts +356 -0
- package/src/testing/test-replay-assertions.ts +164 -0
- package/src/testing/test-rng-helpers.test.ts +260 -0
- package/src/testing/test-rng-helpers.ts +190 -0
- package/src/testing/test-state-builder.test.ts +373 -0
- package/src/testing/test-state-builder.ts +99 -0
- package/src/testing/test-zone-factory.test.ts +295 -0
- package/src/testing/test-zone-factory.ts +224 -0
- package/src/types/branded-utils.ts +54 -0
- package/src/types/branded.test.ts +175 -0
- package/src/types/branded.ts +33 -0
- package/src/types/index.ts +8 -0
- package/src/types/state.test.ts +198 -0
- package/src/types/state.ts +154 -0
- package/src/validation/card-type-guards.test.ts +242 -0
- package/src/validation/card-type-guards.ts +179 -0
- package/src/validation/index.ts +40 -0
- package/src/validation/schema-builders.test.ts +403 -0
- package/src/validation/schema-builders.ts +345 -0
- package/src/validation/type-guard-builder.test.ts +216 -0
- package/src/validation/type-guard-builder.ts +109 -0
- package/src/validation/validator-builder.test.ts +375 -0
- package/src/validation/validator-builder.ts +273 -0
- package/src/zones/index.ts +28 -0
- package/src/zones/zone-factory.test.ts +183 -0
- package/src/zones/zone-factory.ts +44 -0
- package/src/zones/zone-operations.test.ts +800 -0
- package/src/zones/zone-operations.ts +306 -0
- package/src/zones/zone-state-helpers.test.ts +337 -0
- package/src/zones/zone-state-helpers.ts +128 -0
- package/src/zones/zone-visibility.test.ts +156 -0
- package/src/zones/zone-visibility.ts +36 -0
- package/src/zones/zone.test.ts +186 -0
- 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
|
+
}
|