@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,390 @@
|
|
|
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 { getCardPower } from "./computed-properties";
|
|
7
|
+
import type { Modifier } from "./modifiers";
|
|
8
|
+
|
|
9
|
+
type GameStateWithCards = {
|
|
10
|
+
// biome-ignore lint/suspicious/noExplicitAny: any required for test covariance
|
|
11
|
+
cards: Record<
|
|
12
|
+
string,
|
|
13
|
+
CardInstance<{ tapped: boolean; modifiers: Modifier<any>[] }>
|
|
14
|
+
>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe("Conditional Modifiers", () => {
|
|
18
|
+
describe("while-condition duration", () => {
|
|
19
|
+
it("should apply modifier when condition is true", () => {
|
|
20
|
+
const cardId = createCardId("card-1");
|
|
21
|
+
|
|
22
|
+
const definition: CardDefinition = {
|
|
23
|
+
id: "creature",
|
|
24
|
+
name: "Creature",
|
|
25
|
+
type: "creature",
|
|
26
|
+
basePower: 2,
|
|
27
|
+
abilities: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const registry = createCardRegistry([definition]);
|
|
31
|
+
|
|
32
|
+
const card: CardInstance<{
|
|
33
|
+
tapped: boolean;
|
|
34
|
+
modifiers: Modifier<GameStateWithCards>[];
|
|
35
|
+
}> = {
|
|
36
|
+
id: cardId,
|
|
37
|
+
definitionId: "creature",
|
|
38
|
+
owner: createPlayerId("player-1"),
|
|
39
|
+
controller: createPlayerId("player-1"),
|
|
40
|
+
zone: createZoneId("play"),
|
|
41
|
+
tapped: true, // Card is tapped
|
|
42
|
+
flipped: false,
|
|
43
|
+
revealed: false,
|
|
44
|
+
phased: false,
|
|
45
|
+
modifiers: [
|
|
46
|
+
{
|
|
47
|
+
id: "mod-1",
|
|
48
|
+
type: "stat",
|
|
49
|
+
property: "power",
|
|
50
|
+
value: 2,
|
|
51
|
+
duration: "while-condition",
|
|
52
|
+
condition: (state: GameStateWithCards) => {
|
|
53
|
+
const c = state.cards[String(cardId)];
|
|
54
|
+
return c?.tapped === true; // Only apply if tapped
|
|
55
|
+
},
|
|
56
|
+
source: createCardId("source-1"),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const state: GameStateWithCards = {
|
|
62
|
+
cards: {
|
|
63
|
+
[String(cardId)]: card,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const power = getCardPower(card, state, registry);
|
|
68
|
+
|
|
69
|
+
expect(power).toBe(4); // 2 base + 2 from conditional modifier
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should not apply modifier when condition is false", () => {
|
|
73
|
+
const cardId = createCardId("card-1");
|
|
74
|
+
|
|
75
|
+
const definition: CardDefinition = {
|
|
76
|
+
id: "creature",
|
|
77
|
+
name: "Creature",
|
|
78
|
+
type: "creature",
|
|
79
|
+
basePower: 2,
|
|
80
|
+
abilities: [],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const registry = createCardRegistry([definition]);
|
|
84
|
+
|
|
85
|
+
const card: CardInstance<{
|
|
86
|
+
tapped: boolean;
|
|
87
|
+
modifiers: Modifier<GameStateWithCards>[];
|
|
88
|
+
}> = {
|
|
89
|
+
id: cardId,
|
|
90
|
+
definitionId: "creature",
|
|
91
|
+
owner: createPlayerId("player-1"),
|
|
92
|
+
controller: createPlayerId("player-1"),
|
|
93
|
+
zone: createZoneId("play"),
|
|
94
|
+
tapped: false, // Card is NOT tapped
|
|
95
|
+
flipped: false,
|
|
96
|
+
revealed: false,
|
|
97
|
+
phased: false,
|
|
98
|
+
modifiers: [
|
|
99
|
+
{
|
|
100
|
+
id: "mod-1",
|
|
101
|
+
type: "stat",
|
|
102
|
+
property: "power",
|
|
103
|
+
value: 2,
|
|
104
|
+
duration: "while-condition",
|
|
105
|
+
condition: (state: GameStateWithCards) => {
|
|
106
|
+
const c = state.cards[String(cardId)];
|
|
107
|
+
return c?.tapped === true; // Only apply if tapped
|
|
108
|
+
},
|
|
109
|
+
source: createCardId("source-1"),
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const state: GameStateWithCards = {
|
|
115
|
+
cards: {
|
|
116
|
+
[String(cardId)]: card,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const power = getCardPower(card, state, registry);
|
|
121
|
+
|
|
122
|
+
expect(power).toBe(2); // 2 base only, modifier not applied
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should dynamically re-evaluate condition when state changes", () => {
|
|
126
|
+
const cardId = createCardId("card-1");
|
|
127
|
+
|
|
128
|
+
const definition: CardDefinition = {
|
|
129
|
+
id: "creature",
|
|
130
|
+
name: "Creature",
|
|
131
|
+
type: "creature",
|
|
132
|
+
basePower: 3,
|
|
133
|
+
abilities: [],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const registry = createCardRegistry([definition]);
|
|
137
|
+
|
|
138
|
+
const card: CardInstance<{
|
|
139
|
+
tapped: boolean;
|
|
140
|
+
modifiers: Modifier<GameStateWithCards>[];
|
|
141
|
+
}> = {
|
|
142
|
+
id: cardId,
|
|
143
|
+
definitionId: "creature",
|
|
144
|
+
owner: createPlayerId("player-1"),
|
|
145
|
+
controller: createPlayerId("player-1"),
|
|
146
|
+
zone: createZoneId("play"),
|
|
147
|
+
tapped: false,
|
|
148
|
+
flipped: false,
|
|
149
|
+
revealed: false,
|
|
150
|
+
phased: false,
|
|
151
|
+
modifiers: [
|
|
152
|
+
{
|
|
153
|
+
id: "mod-1",
|
|
154
|
+
type: "stat",
|
|
155
|
+
property: "power",
|
|
156
|
+
value: 2,
|
|
157
|
+
duration: "while-condition",
|
|
158
|
+
condition: (state: GameStateWithCards) => {
|
|
159
|
+
const c = state.cards[String(cardId)];
|
|
160
|
+
return c?.tapped === true;
|
|
161
|
+
},
|
|
162
|
+
source: createCardId("source-1"),
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Initial state: card is untapped
|
|
168
|
+
const state1: GameStateWithCards = {
|
|
169
|
+
cards: {
|
|
170
|
+
[String(cardId)]: card,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const power1 = getCardPower(card, state1, registry);
|
|
175
|
+
expect(power1).toBe(3); // Base power only
|
|
176
|
+
|
|
177
|
+
// State changes: card becomes tapped
|
|
178
|
+
const tappedCard = { ...card, tapped: true };
|
|
179
|
+
const state2: GameStateWithCards = {
|
|
180
|
+
cards: {
|
|
181
|
+
[String(cardId)]: tappedCard,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const power2 = getCardPower(tappedCard, state2, registry);
|
|
186
|
+
expect(power2).toBe(5); // Base + conditional modifier
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("should handle multiple conditional modifiers independently", () => {
|
|
190
|
+
const cardId = createCardId("card-1");
|
|
191
|
+
|
|
192
|
+
const definition: CardDefinition = {
|
|
193
|
+
id: "creature",
|
|
194
|
+
name: "Creature",
|
|
195
|
+
type: "creature",
|
|
196
|
+
basePower: 1,
|
|
197
|
+
abilities: [],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const registry = createCardRegistry([definition]);
|
|
201
|
+
|
|
202
|
+
const card: CardInstance<{
|
|
203
|
+
tapped: boolean;
|
|
204
|
+
modifiers: Modifier<GameStateWithCards>[];
|
|
205
|
+
}> = {
|
|
206
|
+
id: cardId,
|
|
207
|
+
definitionId: "creature",
|
|
208
|
+
owner: createPlayerId("player-1"),
|
|
209
|
+
controller: createPlayerId("player-1"),
|
|
210
|
+
zone: createZoneId("play"),
|
|
211
|
+
tapped: true,
|
|
212
|
+
flipped: false,
|
|
213
|
+
revealed: false,
|
|
214
|
+
phased: false,
|
|
215
|
+
modifiers: [
|
|
216
|
+
{
|
|
217
|
+
id: "mod-1",
|
|
218
|
+
type: "stat",
|
|
219
|
+
property: "power",
|
|
220
|
+
value: 2,
|
|
221
|
+
duration: "while-condition",
|
|
222
|
+
condition: (state: GameStateWithCards) => {
|
|
223
|
+
const c = state.cards[String(cardId)];
|
|
224
|
+
return c?.tapped === true; // Applies when tapped
|
|
225
|
+
},
|
|
226
|
+
source: createCardId("source-1"),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "mod-2",
|
|
230
|
+
type: "stat",
|
|
231
|
+
property: "power",
|
|
232
|
+
value: 3,
|
|
233
|
+
duration: "while-condition",
|
|
234
|
+
condition: (state: GameStateWithCards) => {
|
|
235
|
+
const c = state.cards[String(cardId)];
|
|
236
|
+
return c?.tapped === false; // Applies when NOT tapped
|
|
237
|
+
},
|
|
238
|
+
source: createCardId("source-2"),
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const state: GameStateWithCards = {
|
|
244
|
+
cards: {
|
|
245
|
+
[String(cardId)]: card,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const power = getCardPower(card, state, registry);
|
|
250
|
+
|
|
251
|
+
// Card is tapped, so first modifier applies (+2), second doesn't
|
|
252
|
+
expect(power).toBe(3); // 1 base + 2 from first modifier
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should handle complex condition logic", () => {
|
|
256
|
+
const cardId = createCardId("card-1");
|
|
257
|
+
const otherCardId = createCardId("card-2");
|
|
258
|
+
|
|
259
|
+
const definition: CardDefinition = {
|
|
260
|
+
id: "creature",
|
|
261
|
+
name: "Creature",
|
|
262
|
+
type: "creature",
|
|
263
|
+
basePower: 2,
|
|
264
|
+
abilities: [],
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const registry = createCardRegistry([definition]);
|
|
268
|
+
|
|
269
|
+
const card: CardInstance<{
|
|
270
|
+
tapped: boolean;
|
|
271
|
+
modifiers: Modifier<GameStateWithCards>[];
|
|
272
|
+
}> = {
|
|
273
|
+
id: cardId,
|
|
274
|
+
definitionId: "creature",
|
|
275
|
+
owner: createPlayerId("player-1"),
|
|
276
|
+
controller: createPlayerId("player-1"),
|
|
277
|
+
zone: createZoneId("play"),
|
|
278
|
+
tapped: false,
|
|
279
|
+
flipped: false,
|
|
280
|
+
revealed: false,
|
|
281
|
+
phased: false,
|
|
282
|
+
modifiers: [
|
|
283
|
+
{
|
|
284
|
+
id: "mod-1",
|
|
285
|
+
type: "stat",
|
|
286
|
+
property: "power",
|
|
287
|
+
value: 3,
|
|
288
|
+
duration: "while-condition",
|
|
289
|
+
condition: (state: GameStateWithCards) => {
|
|
290
|
+
// Complex condition: +3 power if there's another tapped card
|
|
291
|
+
const otherCard = state.cards[String(otherCardId)];
|
|
292
|
+
return otherCard?.tapped === true;
|
|
293
|
+
},
|
|
294
|
+
source: createCardId("source-1"),
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const otherCard: CardInstance<{
|
|
300
|
+
tapped: boolean;
|
|
301
|
+
modifiers: Modifier[];
|
|
302
|
+
}> = {
|
|
303
|
+
id: otherCardId,
|
|
304
|
+
definitionId: "creature",
|
|
305
|
+
owner: createPlayerId("player-1"),
|
|
306
|
+
controller: createPlayerId("player-1"),
|
|
307
|
+
zone: createZoneId("play"),
|
|
308
|
+
tapped: true, // This card is tapped
|
|
309
|
+
flipped: false,
|
|
310
|
+
revealed: false,
|
|
311
|
+
phased: false,
|
|
312
|
+
modifiers: [],
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const state: GameStateWithCards = {
|
|
316
|
+
cards: {
|
|
317
|
+
[String(cardId)]: card,
|
|
318
|
+
[String(otherCardId)]: otherCard,
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const power = getCardPower(card, state, registry);
|
|
323
|
+
|
|
324
|
+
expect(power).toBe(5); // 2 base + 3 from conditional (other card is tapped)
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("should mix conditional and unconditional modifiers correctly", () => {
|
|
328
|
+
const cardId = createCardId("card-1");
|
|
329
|
+
|
|
330
|
+
const definition: CardDefinition = {
|
|
331
|
+
id: "creature",
|
|
332
|
+
name: "Creature",
|
|
333
|
+
type: "creature",
|
|
334
|
+
basePower: 1,
|
|
335
|
+
abilities: [],
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const registry = createCardRegistry([definition]);
|
|
339
|
+
|
|
340
|
+
const card: CardInstance<{
|
|
341
|
+
tapped: boolean;
|
|
342
|
+
modifiers: Modifier<GameStateWithCards>[];
|
|
343
|
+
}> = {
|
|
344
|
+
id: cardId,
|
|
345
|
+
definitionId: "creature",
|
|
346
|
+
owner: createPlayerId("player-1"),
|
|
347
|
+
controller: createPlayerId("player-1"),
|
|
348
|
+
zone: createZoneId("play"),
|
|
349
|
+
tapped: false,
|
|
350
|
+
flipped: false,
|
|
351
|
+
revealed: false,
|
|
352
|
+
phased: false,
|
|
353
|
+
modifiers: [
|
|
354
|
+
{
|
|
355
|
+
id: "mod-1",
|
|
356
|
+
type: "stat",
|
|
357
|
+
property: "power",
|
|
358
|
+
value: 2,
|
|
359
|
+
duration: "permanent",
|
|
360
|
+
source: createCardId("source-1"),
|
|
361
|
+
// No condition - always applies
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
id: "mod-2",
|
|
365
|
+
type: "stat",
|
|
366
|
+
property: "power",
|
|
367
|
+
value: 3,
|
|
368
|
+
duration: "while-condition",
|
|
369
|
+
condition: (state: GameStateWithCards) => {
|
|
370
|
+
const c = state.cards[String(cardId)];
|
|
371
|
+
return c?.tapped === true; // Only applies if tapped
|
|
372
|
+
},
|
|
373
|
+
source: createCardId("source-2"),
|
|
374
|
+
},
|
|
375
|
+
],
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const state: GameStateWithCards = {
|
|
379
|
+
cards: {
|
|
380
|
+
[String(cardId)]: card,
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const power = getCardPower(card, state, registry);
|
|
385
|
+
|
|
386
|
+
// Card is NOT tapped: 1 base + 2 permanent modifier (conditional doesn't apply)
|
|
387
|
+
expect(power).toBe(3);
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createCardId } from "../types";
|
|
3
|
+
import type { Modifier } from "./modifiers";
|
|
4
|
+
|
|
5
|
+
describe("Modifier System", () => {
|
|
6
|
+
describe("Modifier Type", () => {
|
|
7
|
+
it("should define stat modifier with all required fields", () => {
|
|
8
|
+
const modifier: Modifier = {
|
|
9
|
+
id: "mod-1",
|
|
10
|
+
type: "stat",
|
|
11
|
+
property: "power",
|
|
12
|
+
value: 2,
|
|
13
|
+
duration: "permanent",
|
|
14
|
+
source: createCardId("card-1"),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
expect(modifier.id).toBe("mod-1");
|
|
18
|
+
expect(modifier.type).toBe("stat");
|
|
19
|
+
expect(modifier.property).toBe("power");
|
|
20
|
+
expect(modifier.value).toBe(2);
|
|
21
|
+
expect(modifier.duration).toBe("permanent");
|
|
22
|
+
expect(modifier.source).toBeDefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should support stat modifier type", () => {
|
|
26
|
+
const modifier: Modifier = {
|
|
27
|
+
id: "mod-1",
|
|
28
|
+
type: "stat",
|
|
29
|
+
property: "power",
|
|
30
|
+
value: 3,
|
|
31
|
+
duration: "permanent",
|
|
32
|
+
source: createCardId("card-1"),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
expect(modifier.type).toBe("stat");
|
|
36
|
+
expect(typeof modifier.value).toBe("number");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should support ability modifier type", () => {
|
|
40
|
+
const modifier: Modifier = {
|
|
41
|
+
id: "mod-2",
|
|
42
|
+
type: "ability",
|
|
43
|
+
property: "flying",
|
|
44
|
+
value: true,
|
|
45
|
+
duration: "until-end-of-turn",
|
|
46
|
+
source: createCardId("card-1"),
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
expect(modifier.type).toBe("ability");
|
|
50
|
+
expect(typeof modifier.value).toBe("boolean");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should support type modifier type", () => {
|
|
54
|
+
const modifier: Modifier = {
|
|
55
|
+
id: "mod-3",
|
|
56
|
+
type: "type",
|
|
57
|
+
property: "creature-type",
|
|
58
|
+
value: "zombie",
|
|
59
|
+
duration: "permanent",
|
|
60
|
+
source: createCardId("card-1"),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
expect(modifier.type).toBe("type");
|
|
64
|
+
expect(typeof modifier.value).toBe("string");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should support keyword modifier type", () => {
|
|
68
|
+
const modifier: Modifier = {
|
|
69
|
+
id: "mod-4",
|
|
70
|
+
type: "keyword",
|
|
71
|
+
property: "haste",
|
|
72
|
+
value: true,
|
|
73
|
+
duration: "until-end-of-turn",
|
|
74
|
+
source: createCardId("card-1"),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
expect(modifier.type).toBe("keyword");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should support permanent duration", () => {
|
|
81
|
+
const modifier: Modifier = {
|
|
82
|
+
id: "mod-1",
|
|
83
|
+
type: "stat",
|
|
84
|
+
property: "power",
|
|
85
|
+
value: 1,
|
|
86
|
+
duration: "permanent",
|
|
87
|
+
source: createCardId("card-1"),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
expect(modifier.duration).toBe("permanent");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should support until-end-of-turn duration", () => {
|
|
94
|
+
const modifier: Modifier = {
|
|
95
|
+
id: "mod-2",
|
|
96
|
+
type: "stat",
|
|
97
|
+
property: "power",
|
|
98
|
+
value: 2,
|
|
99
|
+
duration: "until-end-of-turn",
|
|
100
|
+
source: createCardId("card-1"),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
expect(modifier.duration).toBe("until-end-of-turn");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should support while-condition duration", () => {
|
|
107
|
+
const modifier: Modifier = {
|
|
108
|
+
id: "mod-3",
|
|
109
|
+
type: "stat",
|
|
110
|
+
property: "power",
|
|
111
|
+
value: 3,
|
|
112
|
+
duration: "while-condition",
|
|
113
|
+
source: createCardId("card-1"),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
expect(modifier.duration).toBe("while-condition");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should support optional condition function", () => {
|
|
120
|
+
const modifier: Modifier = {
|
|
121
|
+
id: "mod-1",
|
|
122
|
+
type: "stat",
|
|
123
|
+
property: "power",
|
|
124
|
+
value: 2,
|
|
125
|
+
duration: "while-condition",
|
|
126
|
+
condition: () => true,
|
|
127
|
+
source: createCardId("card-1"),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
expect(modifier.condition).toBeDefined();
|
|
131
|
+
expect(typeof modifier.condition).toBe("function");
|
|
132
|
+
expect(modifier.condition?.({} as never)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should support optional layer for complex interactions", () => {
|
|
136
|
+
const modifier: Modifier = {
|
|
137
|
+
id: "mod-1",
|
|
138
|
+
type: "stat",
|
|
139
|
+
property: "power",
|
|
140
|
+
value: 2,
|
|
141
|
+
duration: "permanent",
|
|
142
|
+
source: createCardId("card-1"),
|
|
143
|
+
layer: 7,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
expect(modifier.layer).toBe(7);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should work without optional fields", () => {
|
|
150
|
+
const modifier: Modifier = {
|
|
151
|
+
id: "mod-1",
|
|
152
|
+
type: "stat",
|
|
153
|
+
property: "power",
|
|
154
|
+
value: 2,
|
|
155
|
+
duration: "permanent",
|
|
156
|
+
source: createCardId("card-1"),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
expect(modifier.condition).toBeUndefined();
|
|
160
|
+
expect(modifier.layer).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("Modifier Values", () => {
|
|
165
|
+
it("should support number values", () => {
|
|
166
|
+
const modifier: Modifier = {
|
|
167
|
+
id: "mod-1",
|
|
168
|
+
type: "stat",
|
|
169
|
+
property: "power",
|
|
170
|
+
value: 5,
|
|
171
|
+
duration: "permanent",
|
|
172
|
+
source: createCardId("card-1"),
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
expect(typeof modifier.value).toBe("number");
|
|
176
|
+
expect(modifier.value).toBe(5);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should support negative number values", () => {
|
|
180
|
+
const modifier: Modifier = {
|
|
181
|
+
id: "mod-2",
|
|
182
|
+
type: "stat",
|
|
183
|
+
property: "power",
|
|
184
|
+
value: -2,
|
|
185
|
+
duration: "permanent",
|
|
186
|
+
source: createCardId("card-1"),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
expect(modifier.value).toBe(-2);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should support string values", () => {
|
|
193
|
+
const modifier: Modifier = {
|
|
194
|
+
id: "mod-3",
|
|
195
|
+
type: "type",
|
|
196
|
+
property: "creature-type",
|
|
197
|
+
value: "dragon",
|
|
198
|
+
duration: "permanent",
|
|
199
|
+
source: createCardId("card-1"),
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
expect(typeof modifier.value).toBe("string");
|
|
203
|
+
expect(modifier.value).toBe("dragon");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should support boolean values", () => {
|
|
207
|
+
const modifier: Modifier = {
|
|
208
|
+
id: "mod-4",
|
|
209
|
+
type: "ability",
|
|
210
|
+
property: "flying",
|
|
211
|
+
value: true,
|
|
212
|
+
duration: "permanent",
|
|
213
|
+
source: createCardId("card-1"),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
expect(typeof modifier.value).toBe("boolean");
|
|
217
|
+
expect(modifier.value).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("Modifier Source Tracking", () => {
|
|
222
|
+
it("should track source card", () => {
|
|
223
|
+
const sourceCard = createCardId("source-1");
|
|
224
|
+
const modifier: Modifier = {
|
|
225
|
+
id: "mod-1",
|
|
226
|
+
type: "stat",
|
|
227
|
+
property: "power",
|
|
228
|
+
value: 2,
|
|
229
|
+
duration: "permanent",
|
|
230
|
+
source: sourceCard,
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
expect(modifier.source).toBe(sourceCard);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should enforce CardId type for source", () => {
|
|
237
|
+
const sourceCard = createCardId("source-1");
|
|
238
|
+
const modifier: Modifier = {
|
|
239
|
+
id: "mod-1",
|
|
240
|
+
type: "stat",
|
|
241
|
+
property: "power",
|
|
242
|
+
value: 2,
|
|
243
|
+
duration: "permanent",
|
|
244
|
+
source: sourceCard,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const _typeCheck: typeof sourceCard = modifier.source;
|
|
248
|
+
expect(modifier.source).toBe(sourceCard);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("Modifier Condition", () => {
|
|
253
|
+
it("should accept condition function that returns boolean", () => {
|
|
254
|
+
const modifier: Modifier = {
|
|
255
|
+
id: "mod-1",
|
|
256
|
+
type: "stat",
|
|
257
|
+
property: "power",
|
|
258
|
+
value: 2,
|
|
259
|
+
duration: "while-condition",
|
|
260
|
+
condition: (state) => {
|
|
261
|
+
return state !== null;
|
|
262
|
+
},
|
|
263
|
+
source: createCardId("card-1"),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
expect(modifier.condition).toBeDefined();
|
|
267
|
+
expect(modifier.condition?.({} as never)).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("should support complex condition logic", () => {
|
|
271
|
+
type GameState = { cardTapped: boolean };
|
|
272
|
+
const modifier: Modifier<GameState> = {
|
|
273
|
+
id: "mod-1",
|
|
274
|
+
type: "stat",
|
|
275
|
+
property: "power",
|
|
276
|
+
value: 2,
|
|
277
|
+
duration: "while-condition",
|
|
278
|
+
condition: (state) => state.cardTapped === true,
|
|
279
|
+
source: createCardId("card-1"),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
expect(modifier.condition?.({ cardTapped: true })).toBe(true);
|
|
283
|
+
expect(modifier.condition?.({ cardTapped: false })).toBe(false);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
});
|