@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,530 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createCardRegistry } from "../operations/card-registry-impl";
|
|
3
|
+
import { createCardId, createPlayerId, createZoneId } from "../types";
|
|
4
|
+
import type { CardDefinition } from "./card-definition";
|
|
5
|
+
import type { CardInstance } from "./card-instance";
|
|
6
|
+
import {
|
|
7
|
+
getCardCost,
|
|
8
|
+
getCardPower,
|
|
9
|
+
getCardToughness,
|
|
10
|
+
} from "./computed-properties";
|
|
11
|
+
import type { Modifier } from "./modifiers";
|
|
12
|
+
|
|
13
|
+
type GameStateWithModifiers = {
|
|
14
|
+
cards: Record<string, CardInstance<{ modifiers: Modifier[] }>>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe("Computed Properties", () => {
|
|
18
|
+
describe("getCardPower", () => {
|
|
19
|
+
it("should return base power from definition", () => {
|
|
20
|
+
const definition: CardDefinition = {
|
|
21
|
+
id: "grizzly-bears",
|
|
22
|
+
name: "Grizzly Bears",
|
|
23
|
+
type: "creature",
|
|
24
|
+
basePower: 2,
|
|
25
|
+
baseToughness: 2,
|
|
26
|
+
abilities: [],
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const registry = createCardRegistry([definition]);
|
|
30
|
+
|
|
31
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
32
|
+
id: createCardId("card-1"),
|
|
33
|
+
definitionId: "grizzly-bears",
|
|
34
|
+
owner: createPlayerId("player-1"),
|
|
35
|
+
controller: createPlayerId("player-1"),
|
|
36
|
+
zone: createZoneId("play"),
|
|
37
|
+
tapped: false,
|
|
38
|
+
flipped: false,
|
|
39
|
+
revealed: false,
|
|
40
|
+
phased: false,
|
|
41
|
+
modifiers: [],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
45
|
+
const power = getCardPower(card, state, registry);
|
|
46
|
+
|
|
47
|
+
expect(power).toBe(2);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should return 0 if definition has no basePower", () => {
|
|
51
|
+
const definition: CardDefinition = {
|
|
52
|
+
id: "instant-spell",
|
|
53
|
+
name: "Instant Spell",
|
|
54
|
+
type: "instant",
|
|
55
|
+
abilities: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const registry = createCardRegistry([definition]);
|
|
59
|
+
|
|
60
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
61
|
+
id: createCardId("card-1"),
|
|
62
|
+
definitionId: "instant-spell",
|
|
63
|
+
owner: createPlayerId("player-1"),
|
|
64
|
+
controller: createPlayerId("player-1"),
|
|
65
|
+
zone: createZoneId("hand"),
|
|
66
|
+
tapped: false,
|
|
67
|
+
flipped: false,
|
|
68
|
+
revealed: false,
|
|
69
|
+
phased: false,
|
|
70
|
+
modifiers: [],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
74
|
+
const power = getCardPower(card, state, registry);
|
|
75
|
+
|
|
76
|
+
expect(power).toBe(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should add positive power modifiers to base power", () => {
|
|
80
|
+
const definition: CardDefinition = {
|
|
81
|
+
id: "creature",
|
|
82
|
+
name: "Creature",
|
|
83
|
+
type: "creature",
|
|
84
|
+
basePower: 2,
|
|
85
|
+
abilities: [],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const registry = createCardRegistry([definition]);
|
|
89
|
+
|
|
90
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
91
|
+
id: createCardId("card-1"),
|
|
92
|
+
definitionId: "creature",
|
|
93
|
+
owner: createPlayerId("player-1"),
|
|
94
|
+
controller: createPlayerId("player-1"),
|
|
95
|
+
zone: createZoneId("play"),
|
|
96
|
+
tapped: false,
|
|
97
|
+
flipped: false,
|
|
98
|
+
revealed: false,
|
|
99
|
+
phased: false,
|
|
100
|
+
modifiers: [
|
|
101
|
+
{
|
|
102
|
+
id: "mod-1",
|
|
103
|
+
type: "stat",
|
|
104
|
+
property: "power",
|
|
105
|
+
value: 3,
|
|
106
|
+
duration: "permanent",
|
|
107
|
+
source: createCardId("source-1"),
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
113
|
+
const power = getCardPower(card, state, registry);
|
|
114
|
+
|
|
115
|
+
expect(power).toBe(5); // 2 + 3
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should subtract negative power modifiers from base power", () => {
|
|
119
|
+
const definition: CardDefinition = {
|
|
120
|
+
id: "creature",
|
|
121
|
+
name: "Creature",
|
|
122
|
+
type: "creature",
|
|
123
|
+
basePower: 5,
|
|
124
|
+
abilities: [],
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const registry = createCardRegistry([definition]);
|
|
128
|
+
|
|
129
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
130
|
+
id: createCardId("card-1"),
|
|
131
|
+
definitionId: "creature",
|
|
132
|
+
owner: createPlayerId("player-1"),
|
|
133
|
+
controller: createPlayerId("player-1"),
|
|
134
|
+
zone: createZoneId("play"),
|
|
135
|
+
tapped: false,
|
|
136
|
+
flipped: false,
|
|
137
|
+
revealed: false,
|
|
138
|
+
phased: false,
|
|
139
|
+
modifiers: [
|
|
140
|
+
{
|
|
141
|
+
id: "mod-1",
|
|
142
|
+
type: "stat",
|
|
143
|
+
property: "power",
|
|
144
|
+
value: -2,
|
|
145
|
+
duration: "permanent",
|
|
146
|
+
source: createCardId("source-1"),
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
152
|
+
const power = getCardPower(card, state, registry);
|
|
153
|
+
|
|
154
|
+
expect(power).toBe(3); // 5 - 2
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("should sum multiple power modifiers", () => {
|
|
158
|
+
const definition: CardDefinition = {
|
|
159
|
+
id: "creature",
|
|
160
|
+
name: "Creature",
|
|
161
|
+
type: "creature",
|
|
162
|
+
basePower: 1,
|
|
163
|
+
abilities: [],
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const registry = createCardRegistry([definition]);
|
|
167
|
+
|
|
168
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
169
|
+
id: createCardId("card-1"),
|
|
170
|
+
definitionId: "creature",
|
|
171
|
+
owner: createPlayerId("player-1"),
|
|
172
|
+
controller: createPlayerId("player-1"),
|
|
173
|
+
zone: createZoneId("play"),
|
|
174
|
+
tapped: false,
|
|
175
|
+
flipped: false,
|
|
176
|
+
revealed: false,
|
|
177
|
+
phased: false,
|
|
178
|
+
modifiers: [
|
|
179
|
+
{
|
|
180
|
+
id: "mod-1",
|
|
181
|
+
type: "stat",
|
|
182
|
+
property: "power",
|
|
183
|
+
value: 2,
|
|
184
|
+
duration: "permanent",
|
|
185
|
+
source: createCardId("source-1"),
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: "mod-2",
|
|
189
|
+
type: "stat",
|
|
190
|
+
property: "power",
|
|
191
|
+
value: 3,
|
|
192
|
+
duration: "permanent",
|
|
193
|
+
source: createCardId("source-2"),
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
199
|
+
const power = getCardPower(card, state, registry);
|
|
200
|
+
|
|
201
|
+
expect(power).toBe(6); // 1 + 2 + 3
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should ignore non-power modifiers", () => {
|
|
205
|
+
const definition: CardDefinition = {
|
|
206
|
+
id: "creature",
|
|
207
|
+
name: "Creature",
|
|
208
|
+
type: "creature",
|
|
209
|
+
basePower: 2,
|
|
210
|
+
abilities: [],
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const registry = createCardRegistry([definition]);
|
|
214
|
+
|
|
215
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
216
|
+
id: createCardId("card-1"),
|
|
217
|
+
definitionId: "creature",
|
|
218
|
+
owner: createPlayerId("player-1"),
|
|
219
|
+
controller: createPlayerId("player-1"),
|
|
220
|
+
zone: createZoneId("play"),
|
|
221
|
+
tapped: false,
|
|
222
|
+
flipped: false,
|
|
223
|
+
revealed: false,
|
|
224
|
+
phased: false,
|
|
225
|
+
modifiers: [
|
|
226
|
+
{
|
|
227
|
+
id: "mod-1",
|
|
228
|
+
type: "stat",
|
|
229
|
+
property: "toughness",
|
|
230
|
+
value: 5,
|
|
231
|
+
duration: "permanent",
|
|
232
|
+
source: createCardId("source-1"),
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
id: "mod-2",
|
|
236
|
+
type: "ability",
|
|
237
|
+
property: "flying",
|
|
238
|
+
value: true,
|
|
239
|
+
duration: "permanent",
|
|
240
|
+
source: createCardId("source-2"),
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
246
|
+
const power = getCardPower(card, state, registry);
|
|
247
|
+
|
|
248
|
+
expect(power).toBe(2); // Base power only
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("getCardToughness", () => {
|
|
253
|
+
it("should return base toughness from definition", () => {
|
|
254
|
+
const definition: CardDefinition = {
|
|
255
|
+
id: "grizzly-bears",
|
|
256
|
+
name: "Grizzly Bears",
|
|
257
|
+
type: "creature",
|
|
258
|
+
basePower: 2,
|
|
259
|
+
baseToughness: 2,
|
|
260
|
+
abilities: [],
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const registry = createCardRegistry([definition]);
|
|
264
|
+
|
|
265
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
266
|
+
id: createCardId("card-1"),
|
|
267
|
+
definitionId: "grizzly-bears",
|
|
268
|
+
owner: createPlayerId("player-1"),
|
|
269
|
+
controller: createPlayerId("player-1"),
|
|
270
|
+
zone: createZoneId("play"),
|
|
271
|
+
tapped: false,
|
|
272
|
+
flipped: false,
|
|
273
|
+
revealed: false,
|
|
274
|
+
phased: false,
|
|
275
|
+
modifiers: [],
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
279
|
+
const toughness = getCardToughness(card, state, registry);
|
|
280
|
+
|
|
281
|
+
expect(toughness).toBe(2);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("should return 0 if definition has no baseToughness", () => {
|
|
285
|
+
const definition: CardDefinition = {
|
|
286
|
+
id: "instant-spell",
|
|
287
|
+
name: "Instant Spell",
|
|
288
|
+
type: "instant",
|
|
289
|
+
abilities: [],
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const registry = createCardRegistry([definition]);
|
|
293
|
+
|
|
294
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
295
|
+
id: createCardId("card-1"),
|
|
296
|
+
definitionId: "instant-spell",
|
|
297
|
+
owner: createPlayerId("player-1"),
|
|
298
|
+
controller: createPlayerId("player-1"),
|
|
299
|
+
zone: createZoneId("hand"),
|
|
300
|
+
tapped: false,
|
|
301
|
+
flipped: false,
|
|
302
|
+
revealed: false,
|
|
303
|
+
phased: false,
|
|
304
|
+
modifiers: [],
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
308
|
+
const toughness = getCardToughness(card, state, registry);
|
|
309
|
+
|
|
310
|
+
expect(toughness).toBe(0);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("should add toughness modifiers to base toughness", () => {
|
|
314
|
+
const definition: CardDefinition = {
|
|
315
|
+
id: "creature",
|
|
316
|
+
name: "Creature",
|
|
317
|
+
type: "creature",
|
|
318
|
+
baseToughness: 3,
|
|
319
|
+
abilities: [],
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const registry = createCardRegistry([definition]);
|
|
323
|
+
|
|
324
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
325
|
+
id: createCardId("card-1"),
|
|
326
|
+
definitionId: "creature",
|
|
327
|
+
owner: createPlayerId("player-1"),
|
|
328
|
+
controller: createPlayerId("player-1"),
|
|
329
|
+
zone: createZoneId("play"),
|
|
330
|
+
tapped: false,
|
|
331
|
+
flipped: false,
|
|
332
|
+
revealed: false,
|
|
333
|
+
phased: false,
|
|
334
|
+
modifiers: [
|
|
335
|
+
{
|
|
336
|
+
id: "mod-1",
|
|
337
|
+
type: "stat",
|
|
338
|
+
property: "toughness",
|
|
339
|
+
value: 2,
|
|
340
|
+
duration: "permanent",
|
|
341
|
+
source: createCardId("source-1"),
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
347
|
+
const toughness = getCardToughness(card, state, registry);
|
|
348
|
+
|
|
349
|
+
expect(toughness).toBe(5); // 3 + 2
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("getCardCost", () => {
|
|
354
|
+
it("should return base cost from definition", () => {
|
|
355
|
+
const definition: CardDefinition = {
|
|
356
|
+
id: "fire-bolt",
|
|
357
|
+
name: "Fire Bolt",
|
|
358
|
+
type: "instant",
|
|
359
|
+
baseCost: 1,
|
|
360
|
+
abilities: [],
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const registry = createCardRegistry([definition]);
|
|
364
|
+
|
|
365
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
366
|
+
id: createCardId("card-1"),
|
|
367
|
+
definitionId: "fire-bolt",
|
|
368
|
+
owner: createPlayerId("player-1"),
|
|
369
|
+
controller: createPlayerId("player-1"),
|
|
370
|
+
zone: createZoneId("hand"),
|
|
371
|
+
tapped: false,
|
|
372
|
+
flipped: false,
|
|
373
|
+
revealed: false,
|
|
374
|
+
phased: false,
|
|
375
|
+
modifiers: [],
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
379
|
+
const cost = getCardCost(card, state, registry);
|
|
380
|
+
|
|
381
|
+
expect(cost).toBe(1);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it("should return 0 if definition has no baseCost", () => {
|
|
385
|
+
const definition: CardDefinition = {
|
|
386
|
+
id: "free-spell",
|
|
387
|
+
name: "Free Spell",
|
|
388
|
+
type: "instant",
|
|
389
|
+
abilities: [],
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const registry = createCardRegistry([definition]);
|
|
393
|
+
|
|
394
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
395
|
+
id: createCardId("card-1"),
|
|
396
|
+
definitionId: "free-spell",
|
|
397
|
+
owner: createPlayerId("player-1"),
|
|
398
|
+
controller: createPlayerId("player-1"),
|
|
399
|
+
zone: createZoneId("hand"),
|
|
400
|
+
tapped: false,
|
|
401
|
+
flipped: false,
|
|
402
|
+
revealed: false,
|
|
403
|
+
phased: false,
|
|
404
|
+
modifiers: [],
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
408
|
+
const cost = getCardCost(card, state, registry);
|
|
409
|
+
|
|
410
|
+
expect(cost).toBe(0);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("should apply cost reduction modifiers", () => {
|
|
414
|
+
const definition: CardDefinition = {
|
|
415
|
+
id: "spell",
|
|
416
|
+
name: "Spell",
|
|
417
|
+
type: "instant",
|
|
418
|
+
baseCost: 5,
|
|
419
|
+
abilities: [],
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const registry = createCardRegistry([definition]);
|
|
423
|
+
|
|
424
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
425
|
+
id: createCardId("card-1"),
|
|
426
|
+
definitionId: "spell",
|
|
427
|
+
owner: createPlayerId("player-1"),
|
|
428
|
+
controller: createPlayerId("player-1"),
|
|
429
|
+
zone: createZoneId("hand"),
|
|
430
|
+
tapped: false,
|
|
431
|
+
flipped: false,
|
|
432
|
+
revealed: false,
|
|
433
|
+
phased: false,
|
|
434
|
+
modifiers: [
|
|
435
|
+
{
|
|
436
|
+
id: "mod-1",
|
|
437
|
+
type: "stat",
|
|
438
|
+
property: "cost",
|
|
439
|
+
value: -2,
|
|
440
|
+
duration: "permanent",
|
|
441
|
+
source: createCardId("source-1"),
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
447
|
+
const cost = getCardCost(card, state, registry);
|
|
448
|
+
|
|
449
|
+
expect(cost).toBe(3); // 5 - 2
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("should not allow cost to go below zero", () => {
|
|
453
|
+
const definition: CardDefinition = {
|
|
454
|
+
id: "spell",
|
|
455
|
+
name: "Spell",
|
|
456
|
+
type: "instant",
|
|
457
|
+
baseCost: 2,
|
|
458
|
+
abilities: [],
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const registry = createCardRegistry([definition]);
|
|
462
|
+
|
|
463
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
464
|
+
id: createCardId("card-1"),
|
|
465
|
+
definitionId: "spell",
|
|
466
|
+
owner: createPlayerId("player-1"),
|
|
467
|
+
controller: createPlayerId("player-1"),
|
|
468
|
+
zone: createZoneId("hand"),
|
|
469
|
+
tapped: false,
|
|
470
|
+
flipped: false,
|
|
471
|
+
revealed: false,
|
|
472
|
+
phased: false,
|
|
473
|
+
modifiers: [
|
|
474
|
+
{
|
|
475
|
+
id: "mod-1",
|
|
476
|
+
type: "stat",
|
|
477
|
+
property: "cost",
|
|
478
|
+
value: -5,
|
|
479
|
+
duration: "permanent",
|
|
480
|
+
source: createCardId("source-1"),
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
486
|
+
const cost = getCardCost(card, state, registry);
|
|
487
|
+
|
|
488
|
+
expect(cost).toBe(0); // Can't go negative
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("should apply cost increase modifiers", () => {
|
|
492
|
+
const definition: CardDefinition = {
|
|
493
|
+
id: "spell",
|
|
494
|
+
name: "Spell",
|
|
495
|
+
type: "instant",
|
|
496
|
+
baseCost: 3,
|
|
497
|
+
abilities: [],
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const registry = createCardRegistry([definition]);
|
|
501
|
+
|
|
502
|
+
const card: CardInstance<{ modifiers: Modifier[] }> = {
|
|
503
|
+
id: createCardId("card-1"),
|
|
504
|
+
definitionId: "spell",
|
|
505
|
+
owner: createPlayerId("player-1"),
|
|
506
|
+
controller: createPlayerId("player-1"),
|
|
507
|
+
zone: createZoneId("hand"),
|
|
508
|
+
tapped: false,
|
|
509
|
+
flipped: false,
|
|
510
|
+
revealed: false,
|
|
511
|
+
phased: false,
|
|
512
|
+
modifiers: [
|
|
513
|
+
{
|
|
514
|
+
id: "mod-1",
|
|
515
|
+
type: "stat",
|
|
516
|
+
property: "cost",
|
|
517
|
+
value: 2,
|
|
518
|
+
duration: "permanent",
|
|
519
|
+
source: createCardId("source-1"),
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const state: GameStateWithModifiers = { cards: {} };
|
|
525
|
+
const cost = getCardCost(card, state, registry);
|
|
526
|
+
|
|
527
|
+
expect(cost).toBe(5); // 3 + 2
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { CardRegistry } from "../operations/card-registry";
|
|
2
|
+
import type { CardDefinition } from "./card-definition";
|
|
3
|
+
import type { CardInstance } from "./card-instance";
|
|
4
|
+
import type { Modifier } from "./modifiers";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Gets the computed power of a card (base power + modifiers)
|
|
8
|
+
* Pure function - same inputs always produce same output
|
|
9
|
+
*
|
|
10
|
+
* @param card - Card instance with modifiers
|
|
11
|
+
* @param state - Game state (for conditional modifiers)
|
|
12
|
+
* @param registry - Card definition registry
|
|
13
|
+
* @returns Computed power value
|
|
14
|
+
*/
|
|
15
|
+
export function getCardPower<TGameState = unknown>(
|
|
16
|
+
card: CardInstance<{ modifiers: Modifier<TGameState>[] }>,
|
|
17
|
+
state: TGameState,
|
|
18
|
+
registry: CardRegistry<CardDefinition>,
|
|
19
|
+
): number {
|
|
20
|
+
const definition = registry.getCard(card.definitionId);
|
|
21
|
+
const basePower = definition?.basePower ?? 0;
|
|
22
|
+
|
|
23
|
+
// Sum all power modifiers
|
|
24
|
+
const modifierBonus = card.modifiers
|
|
25
|
+
.filter((m) => m.type === "stat" && m.property === "power")
|
|
26
|
+
.filter((m) => !m.condition || m.condition(state)) // check conditions
|
|
27
|
+
.reduce((sum, m) => sum + (m.value as number), 0);
|
|
28
|
+
|
|
29
|
+
return basePower + modifierBonus;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Gets the computed toughness of a card (base toughness + modifiers)
|
|
34
|
+
* Pure function - same inputs always produce same output
|
|
35
|
+
*
|
|
36
|
+
* @param card - Card instance with modifiers
|
|
37
|
+
* @param state - Game state (for conditional modifiers)
|
|
38
|
+
* @param registry - Card definition registry
|
|
39
|
+
* @returns Computed toughness value
|
|
40
|
+
*/
|
|
41
|
+
export function getCardToughness<TGameState = unknown>(
|
|
42
|
+
card: CardInstance<{ modifiers: Modifier<TGameState>[] }>,
|
|
43
|
+
state: TGameState,
|
|
44
|
+
registry: CardRegistry<CardDefinition>,
|
|
45
|
+
): number {
|
|
46
|
+
const definition = registry.getCard(card.definitionId);
|
|
47
|
+
const baseToughness = definition?.baseToughness ?? 0;
|
|
48
|
+
|
|
49
|
+
// Sum all toughness modifiers
|
|
50
|
+
const modifierBonus = card.modifiers
|
|
51
|
+
.filter((m) => m.type === "stat" && m.property === "toughness")
|
|
52
|
+
.filter((m) => !m.condition || m.condition(state)) // check conditions
|
|
53
|
+
.reduce((sum, m) => sum + (m.value as number), 0);
|
|
54
|
+
|
|
55
|
+
return baseToughness + modifierBonus;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Gets the computed cost of a card (base cost + modifiers)
|
|
60
|
+
* Pure function - same inputs always produce same output
|
|
61
|
+
* Cost cannot go below zero
|
|
62
|
+
*
|
|
63
|
+
* @param card - Card instance with modifiers
|
|
64
|
+
* @param state - Game state (for conditional modifiers)
|
|
65
|
+
* @param registry - Card definition registry
|
|
66
|
+
* @returns Computed cost value (minimum 0)
|
|
67
|
+
*/
|
|
68
|
+
export function getCardCost<TGameState = unknown>(
|
|
69
|
+
card: CardInstance<{ modifiers: Modifier<TGameState>[] }>,
|
|
70
|
+
state: TGameState,
|
|
71
|
+
registry: CardRegistry<CardDefinition>,
|
|
72
|
+
): number {
|
|
73
|
+
const definition = registry.getCard(card.definitionId);
|
|
74
|
+
const baseCost = definition?.baseCost ?? 0;
|
|
75
|
+
|
|
76
|
+
// Sum all cost modifiers (can be negative for cost reduction)
|
|
77
|
+
const costModification = card.modifiers
|
|
78
|
+
.filter((m) => m.type === "stat" && m.property === "cost")
|
|
79
|
+
.filter((m) => !m.condition || m.condition(state)) // check conditions
|
|
80
|
+
.reduce((sum, m) => sum + (m.value as number), 0);
|
|
81
|
+
|
|
82
|
+
// Cost cannot go below zero
|
|
83
|
+
return Math.max(0, baseCost + costModification);
|
|
84
|
+
}
|