@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,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
|
+
};
|