@drmxrcy/tcg-lorcana 0.0.0-202602060544
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 +160 -0
- package/package.json +45 -0
- package/src/__tests__/integration/move-enumeration.test.ts +256 -0
- package/src/__tests__/rules/section-01-concepts.test.ts +426 -0
- package/src/__tests__/rules/section-03-gameplay.test.ts +298 -0
- package/src/__tests__/rules/section-04-turn-structure.test.ts +708 -0
- package/src/__tests__/rules/section-05-cards.test.ts +158 -0
- package/src/__tests__/rules/section-06-card-types.test.ts +342 -0
- package/src/__tests__/rules/section-07-abilities.test.ts +333 -0
- package/src/__tests__/rules/section-08-zones.test.ts +231 -0
- package/src/__tests__/rules/section-09-damage.test.ts +148 -0
- package/src/__tests__/rules/section-10-keywords.test.ts +469 -0
- package/src/__tests__/spec-01-foundation-types.test.ts +534 -0
- package/src/__tests__/spec-02-zones-card-states.test.ts +295 -0
- package/src/card-utils.ts +302 -0
- package/src/cards/README.md +296 -0
- package/src/cards/abilities/index.ts +175 -0
- package/src/cards/index.ts +10 -0
- package/src/deck-validation.ts +175 -0
- package/src/engine/lorcana-engine.ts +625 -0
- package/src/game-definition/__tests__/core-zone-integration.test.ts +553 -0
- package/src/game-definition/__tests__/zone-operations.test.ts +362 -0
- package/src/game-definition/__tests__/zones.test.ts +176 -0
- package/src/game-definition/definition.ts +45 -0
- package/src/game-definition/flow/turn-flow.ts +216 -0
- package/src/game-definition/index.ts +31 -0
- package/src/game-definition/moves/abilities/activate-ability.ts +51 -0
- package/src/game-definition/moves/core/__tests__/move-parameter-enumeration.test.ts +316 -0
- package/src/game-definition/moves/core/challenge.test.ts +545 -0
- package/src/game-definition/moves/core/challenge.ts +81 -0
- package/src/game-definition/moves/core/play-card.ts +83 -0
- package/src/game-definition/moves/core/quest.test.ts +448 -0
- package/src/game-definition/moves/core/quest.ts +49 -0
- package/src/game-definition/moves/debug/manual-exert.ts +36 -0
- package/src/game-definition/moves/effects/resolve-bag.ts +35 -0
- package/src/game-definition/moves/effects/resolve-effect.ts +34 -0
- package/src/game-definition/moves/index.ts +85 -0
- package/src/game-definition/moves/locations/move-character-to-location.ts +42 -0
- package/src/game-definition/moves/resources/put-card-into-inkwell.test.ts +462 -0
- package/src/game-definition/moves/resources/put-card-into-inkwell.ts +51 -0
- package/src/game-definition/moves/setup/alter-hand.test.ts +395 -0
- package/src/game-definition/moves/setup/alter-hand.ts +210 -0
- package/src/game-definition/moves/setup/choose-first-player.test.ts +450 -0
- package/src/game-definition/moves/setup/choose-first-player.ts +105 -0
- package/src/game-definition/moves/setup/draw-cards.ts +37 -0
- package/src/game-definition/moves/songs/sing-together.ts +47 -0
- package/src/game-definition/moves/songs/sing.ts +56 -0
- package/src/game-definition/moves/standard/concede.test.ts +189 -0
- package/src/game-definition/moves/standard/concede.ts +72 -0
- package/src/game-definition/moves/standard/pass-turn.ts +49 -0
- package/src/game-definition/setup/game-setup.ts +19 -0
- package/src/game-definition/trackers/tracker-config.ts +23 -0
- package/src/game-definition/win-conditions/lore-victory.ts +26 -0
- package/src/game-definition/zone-operations.ts +405 -0
- package/src/game-definition/zones/zone-configs.ts +59 -0
- package/src/game-definition/zones.ts +283 -0
- package/src/index.ts +189 -0
- package/src/operations/index.ts +7 -0
- package/src/operations/lorcana-operations.ts +288 -0
- package/src/queries/README.md +56 -0
- package/src/resolvers/__tests__/condition-resolver.test.ts +301 -0
- package/src/resolvers/condition-registry.ts +70 -0
- package/src/resolvers/condition-resolver.ts +85 -0
- package/src/resolvers/conditions/basic.ts +81 -0
- package/src/resolvers/conditions/card-state.ts +12 -0
- package/src/resolvers/conditions/comparison.ts +102 -0
- package/src/resolvers/conditions/existence.ts +219 -0
- package/src/resolvers/conditions/history.ts +68 -0
- package/src/resolvers/conditions/index.ts +15 -0
- package/src/resolvers/conditions/logical.ts +55 -0
- package/src/resolvers/conditions/resolution.ts +41 -0
- package/src/resolvers/conditions/revealed.ts +42 -0
- package/src/resolvers/conditions/zone.ts +84 -0
- package/src/setup.test.ts +18 -0
- package/src/targeting/__tests__/filter-resolver.test.ts +294 -0
- package/src/targeting/__tests__/real-cards-targeting.test.ts +303 -0
- package/src/targeting/__tests__/targeting-dsl.test.ts +386 -0
- package/src/targeting/enum-expansion.ts +387 -0
- package/src/targeting/filter-registry.ts +322 -0
- package/src/targeting/filter-resolver.ts +145 -0
- package/src/targeting/index.ts +91 -0
- package/src/targeting/lorcana-target-dsl.ts +495 -0
- package/src/targeting/targeting-ui.ts +407 -0
- package/src/testing/index.ts +14 -0
- package/src/testing/lorcana-test-engine.ts +813 -0
- package/src/types/README.md +303 -0
- package/src/types/__tests__/lorcana-state.test.ts +168 -0
- package/src/types/__tests__/move-enumeration.test.ts +179 -0
- package/src/types/branded-types.ts +106 -0
- package/src/types/game-state.ts +184 -0
- package/src/types/index.ts +87 -0
- package/src/types/keywords.ts +187 -0
- package/src/types/lorcana-state.ts +260 -0
- package/src/types/move-enumeration.ts +126 -0
- package/src/types/move-params.ts +216 -0
- package/src/validators/index.ts +7 -0
- package/src/validators/move-validators.ts +374 -0
- package/src/zones/card-state.ts +234 -0
- package/src/zones/index.ts +42 -0
- package/src/zones/zone-config.ts +150 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enum Expansion - Converts enum shortcuts to full DSL objects
|
|
3
|
+
*
|
|
4
|
+
* This module provides the mapping from string enum shortcuts to
|
|
5
|
+
* their full DSL representations. This enables enums as syntactic
|
|
6
|
+
* sugar while the DSL is the canonical form.
|
|
7
|
+
*
|
|
8
|
+
* @module targeting/enum-expansion
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
CharacterTarget,
|
|
13
|
+
CharacterTargetEnum,
|
|
14
|
+
ItemTarget,
|
|
15
|
+
ItemTargetEnum,
|
|
16
|
+
LocationTarget,
|
|
17
|
+
LocationTargetEnum,
|
|
18
|
+
LorcanaCardTarget,
|
|
19
|
+
LorcanaCharacterTarget,
|
|
20
|
+
LorcanaItemTarget,
|
|
21
|
+
LorcanaLocationTarget,
|
|
22
|
+
} from "./lorcana-target-dsl";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Character Target Expansions
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mapping from character enum shortcuts to full DSL
|
|
30
|
+
*/
|
|
31
|
+
const CHARACTER_ENUM_EXPANSIONS: Record<CharacterTargetEnum, CharacterTarget> =
|
|
32
|
+
{
|
|
33
|
+
// Self-referential
|
|
34
|
+
SELF: {
|
|
35
|
+
selector: "self",
|
|
36
|
+
cardType: "character",
|
|
37
|
+
context: { self: true },
|
|
38
|
+
},
|
|
39
|
+
THIS_CHARACTER: {
|
|
40
|
+
selector: "self",
|
|
41
|
+
cardType: "character",
|
|
42
|
+
context: { self: true },
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Chosen (single target, player choice)
|
|
46
|
+
CHOSEN_CHARACTER: {
|
|
47
|
+
selector: "chosen",
|
|
48
|
+
count: 1,
|
|
49
|
+
owner: "any",
|
|
50
|
+
cardType: "character",
|
|
51
|
+
zones: ["play"],
|
|
52
|
+
},
|
|
53
|
+
CHOSEN_OPPOSING_CHARACTER: {
|
|
54
|
+
selector: "chosen",
|
|
55
|
+
count: 1,
|
|
56
|
+
owner: "opponent",
|
|
57
|
+
cardType: "character",
|
|
58
|
+
zones: ["play"],
|
|
59
|
+
},
|
|
60
|
+
CHOSEN_CHARACTER_OF_YOURS: {
|
|
61
|
+
selector: "chosen",
|
|
62
|
+
count: 1,
|
|
63
|
+
owner: "you",
|
|
64
|
+
cardType: "character",
|
|
65
|
+
zones: ["play"],
|
|
66
|
+
},
|
|
67
|
+
ANOTHER_CHOSEN_CHARACTER: {
|
|
68
|
+
selector: "chosen",
|
|
69
|
+
count: 1,
|
|
70
|
+
owner: "any",
|
|
71
|
+
cardType: "character",
|
|
72
|
+
zones: ["play"],
|
|
73
|
+
excludeSelf: true,
|
|
74
|
+
},
|
|
75
|
+
ANOTHER_CHOSEN_CHARACTER_OF_YOURS: {
|
|
76
|
+
selector: "chosen",
|
|
77
|
+
count: 1,
|
|
78
|
+
owner: "you",
|
|
79
|
+
cardType: "character",
|
|
80
|
+
zones: ["play"],
|
|
81
|
+
excludeSelf: true,
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// All/Each (multiple targets, automatic)
|
|
85
|
+
ALL_CHARACTERS: {
|
|
86
|
+
selector: "all",
|
|
87
|
+
count: "all",
|
|
88
|
+
owner: "any",
|
|
89
|
+
cardType: "character",
|
|
90
|
+
zones: ["play"],
|
|
91
|
+
},
|
|
92
|
+
ALL_OPPOSING_CHARACTERS: {
|
|
93
|
+
selector: "all",
|
|
94
|
+
count: "all",
|
|
95
|
+
owner: "opponent",
|
|
96
|
+
cardType: "character",
|
|
97
|
+
zones: ["play"],
|
|
98
|
+
},
|
|
99
|
+
YOUR_CHARACTERS: {
|
|
100
|
+
selector: "all",
|
|
101
|
+
count: "all",
|
|
102
|
+
owner: "you",
|
|
103
|
+
cardType: "character",
|
|
104
|
+
zones: ["play"],
|
|
105
|
+
},
|
|
106
|
+
YOUR_OTHER_CHARACTERS: {
|
|
107
|
+
selector: "all",
|
|
108
|
+
count: "all",
|
|
109
|
+
owner: "you",
|
|
110
|
+
cardType: "character",
|
|
111
|
+
zones: ["play"],
|
|
112
|
+
excludeSelf: true,
|
|
113
|
+
},
|
|
114
|
+
EACH_CHARACTER: {
|
|
115
|
+
selector: "each",
|
|
116
|
+
count: "all",
|
|
117
|
+
owner: "any",
|
|
118
|
+
cardType: "character",
|
|
119
|
+
zones: ["play"],
|
|
120
|
+
},
|
|
121
|
+
EACH_OPPOSING_CHARACTER: {
|
|
122
|
+
selector: "each",
|
|
123
|
+
count: "all",
|
|
124
|
+
owner: "opponent",
|
|
125
|
+
cardType: "character",
|
|
126
|
+
zones: ["play"],
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
// Damaged variants
|
|
130
|
+
CHOSEN_DAMAGED_CHARACTER: {
|
|
131
|
+
selector: "chosen",
|
|
132
|
+
count: 1,
|
|
133
|
+
owner: "any",
|
|
134
|
+
cardType: "character",
|
|
135
|
+
zones: ["play"],
|
|
136
|
+
filters: [{ type: "damaged" }],
|
|
137
|
+
},
|
|
138
|
+
CHOSEN_OPPOSING_DAMAGED_CHARACTER: {
|
|
139
|
+
selector: "chosen",
|
|
140
|
+
count: 1,
|
|
141
|
+
owner: "opponent",
|
|
142
|
+
cardType: "character",
|
|
143
|
+
zones: ["play"],
|
|
144
|
+
filters: [{ type: "damaged" }],
|
|
145
|
+
},
|
|
146
|
+
ALL_OPPOSING_DAMAGED_CHARACTERS: {
|
|
147
|
+
selector: "all",
|
|
148
|
+
count: "all",
|
|
149
|
+
owner: "opponent",
|
|
150
|
+
cardType: "character",
|
|
151
|
+
zones: ["play"],
|
|
152
|
+
filters: [{ type: "damaged" }],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// Item Target Expansions
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Mapping from item enum shortcuts to full DSL
|
|
162
|
+
*/
|
|
163
|
+
const ITEM_ENUM_EXPANSIONS: Record<ItemTargetEnum, ItemTarget> = {
|
|
164
|
+
CHOSEN_ITEM: {
|
|
165
|
+
selector: "chosen",
|
|
166
|
+
count: 1,
|
|
167
|
+
owner: "any",
|
|
168
|
+
cardType: "item",
|
|
169
|
+
zones: ["play"],
|
|
170
|
+
},
|
|
171
|
+
CHOSEN_OPPOSING_ITEM: {
|
|
172
|
+
selector: "chosen",
|
|
173
|
+
count: 1,
|
|
174
|
+
owner: "opponent",
|
|
175
|
+
cardType: "item",
|
|
176
|
+
zones: ["play"],
|
|
177
|
+
},
|
|
178
|
+
YOUR_ITEMS: {
|
|
179
|
+
selector: "all",
|
|
180
|
+
count: "all",
|
|
181
|
+
owner: "you",
|
|
182
|
+
cardType: "item",
|
|
183
|
+
zones: ["play"],
|
|
184
|
+
},
|
|
185
|
+
ALL_ITEMS: {
|
|
186
|
+
selector: "all",
|
|
187
|
+
count: "all",
|
|
188
|
+
owner: "any",
|
|
189
|
+
cardType: "item",
|
|
190
|
+
zones: ["play"],
|
|
191
|
+
},
|
|
192
|
+
ALL_OPPOSING_ITEMS: {
|
|
193
|
+
selector: "all",
|
|
194
|
+
count: "all",
|
|
195
|
+
owner: "opponent",
|
|
196
|
+
cardType: "item",
|
|
197
|
+
zones: ["play"],
|
|
198
|
+
},
|
|
199
|
+
THIS_ITEM: {
|
|
200
|
+
selector: "self",
|
|
201
|
+
cardType: "item",
|
|
202
|
+
context: { self: true },
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Location Target Expansions
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Mapping from location enum shortcuts to full DSL
|
|
212
|
+
*/
|
|
213
|
+
const LOCATION_ENUM_EXPANSIONS: Record<LocationTargetEnum, LocationTarget> = {
|
|
214
|
+
CHOSEN_LOCATION: {
|
|
215
|
+
selector: "chosen",
|
|
216
|
+
count: 1,
|
|
217
|
+
owner: "any",
|
|
218
|
+
cardType: "location",
|
|
219
|
+
zones: ["play"],
|
|
220
|
+
},
|
|
221
|
+
CHOSEN_OPPOSING_LOCATION: {
|
|
222
|
+
selector: "chosen",
|
|
223
|
+
count: 1,
|
|
224
|
+
owner: "opponent",
|
|
225
|
+
cardType: "location",
|
|
226
|
+
zones: ["play"],
|
|
227
|
+
},
|
|
228
|
+
YOUR_LOCATIONS: {
|
|
229
|
+
selector: "all",
|
|
230
|
+
count: "all",
|
|
231
|
+
owner: "you",
|
|
232
|
+
cardType: "location",
|
|
233
|
+
zones: ["play"],
|
|
234
|
+
},
|
|
235
|
+
ALL_OPPOSING_LOCATIONS: {
|
|
236
|
+
selector: "all",
|
|
237
|
+
count: "all",
|
|
238
|
+
owner: "opponent",
|
|
239
|
+
cardType: "location",
|
|
240
|
+
zones: ["play"],
|
|
241
|
+
},
|
|
242
|
+
THIS_LOCATION: {
|
|
243
|
+
selector: "self",
|
|
244
|
+
cardType: "location",
|
|
245
|
+
context: { self: true },
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Expansion Functions
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Check if a character target is an enum (vs DSL object)
|
|
255
|
+
*/
|
|
256
|
+
export function isCharacterEnum(
|
|
257
|
+
target: LorcanaCharacterTarget,
|
|
258
|
+
): target is CharacterTargetEnum {
|
|
259
|
+
return typeof target === "string" && target in CHARACTER_ENUM_EXPANSIONS;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Expand a character target enum to full DSL
|
|
264
|
+
*
|
|
265
|
+
* @param target - Character target (enum or DSL)
|
|
266
|
+
* @returns Full DSL representation
|
|
267
|
+
*/
|
|
268
|
+
export function expandCharacterTarget(
|
|
269
|
+
target: LorcanaCharacterTarget,
|
|
270
|
+
): CharacterTarget {
|
|
271
|
+
if (isCharacterEnum(target)) {
|
|
272
|
+
const expansion = CHARACTER_ENUM_EXPANSIONS[target];
|
|
273
|
+
if (!expansion) {
|
|
274
|
+
throw new Error(`Unknown character target enum: ${target}`);
|
|
275
|
+
}
|
|
276
|
+
return expansion;
|
|
277
|
+
}
|
|
278
|
+
return target;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Check if an item target is an enum
|
|
283
|
+
*/
|
|
284
|
+
export function isItemEnum(
|
|
285
|
+
target: LorcanaItemTarget,
|
|
286
|
+
): target is ItemTargetEnum {
|
|
287
|
+
return typeof target === "string" && target in ITEM_ENUM_EXPANSIONS;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Expand an item target enum to full DSL
|
|
292
|
+
*/
|
|
293
|
+
export function expandItemTarget(target: LorcanaItemTarget): ItemTarget {
|
|
294
|
+
if (isItemEnum(target)) {
|
|
295
|
+
const expansion = ITEM_ENUM_EXPANSIONS[target];
|
|
296
|
+
if (!expansion) {
|
|
297
|
+
throw new Error(`Unknown item target enum: ${target}`);
|
|
298
|
+
}
|
|
299
|
+
return expansion;
|
|
300
|
+
}
|
|
301
|
+
return target;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Check if a location target is an enum
|
|
306
|
+
*/
|
|
307
|
+
export function isLocationEnum(
|
|
308
|
+
target: LorcanaLocationTarget,
|
|
309
|
+
): target is LocationTargetEnum {
|
|
310
|
+
return typeof target === "string" && target in LOCATION_ENUM_EXPANSIONS;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Expand a location target enum to full DSL
|
|
315
|
+
*/
|
|
316
|
+
export function expandLocationTarget(
|
|
317
|
+
target: LorcanaLocationTarget,
|
|
318
|
+
): LocationTarget {
|
|
319
|
+
if (isLocationEnum(target)) {
|
|
320
|
+
const expansion = LOCATION_ENUM_EXPANSIONS[target];
|
|
321
|
+
if (!expansion) {
|
|
322
|
+
throw new Error(`Unknown location target enum: ${target}`);
|
|
323
|
+
}
|
|
324
|
+
return expansion;
|
|
325
|
+
}
|
|
326
|
+
return target;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Expand any Lorcana target to full DSL
|
|
331
|
+
*
|
|
332
|
+
* Attempts to determine the type and expand accordingly
|
|
333
|
+
*/
|
|
334
|
+
export function expandTarget(
|
|
335
|
+
target: LorcanaCharacterTarget | LorcanaItemTarget | LorcanaLocationTarget,
|
|
336
|
+
): LorcanaCardTarget {
|
|
337
|
+
if (typeof target === "string") {
|
|
338
|
+
// Try character enum first (most common)
|
|
339
|
+
if (target in CHARACTER_ENUM_EXPANSIONS) {
|
|
340
|
+
return CHARACTER_ENUM_EXPANSIONS[target as CharacterTargetEnum];
|
|
341
|
+
}
|
|
342
|
+
// Try item enum
|
|
343
|
+
if (target in ITEM_ENUM_EXPANSIONS) {
|
|
344
|
+
return ITEM_ENUM_EXPANSIONS[target as ItemTargetEnum];
|
|
345
|
+
}
|
|
346
|
+
// Try location enum
|
|
347
|
+
if (target in LOCATION_ENUM_EXPANSIONS) {
|
|
348
|
+
return LOCATION_ENUM_EXPANSIONS[target as LocationTargetEnum];
|
|
349
|
+
}
|
|
350
|
+
throw new Error(`Unknown target enum: ${target}`);
|
|
351
|
+
}
|
|
352
|
+
return target;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ============================================================================
|
|
356
|
+
// All Enum Values (for validation)
|
|
357
|
+
// ============================================================================
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Set of all valid character target enum values
|
|
361
|
+
*/
|
|
362
|
+
export const CHARACTER_TARGET_ENUMS = new Set<string>(
|
|
363
|
+
Object.keys(CHARACTER_ENUM_EXPANSIONS),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Set of all valid item target enum values
|
|
368
|
+
*/
|
|
369
|
+
export const ITEM_TARGET_ENUMS = new Set<string>(
|
|
370
|
+
Object.keys(ITEM_ENUM_EXPANSIONS),
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Set of all valid location target enum values
|
|
375
|
+
*/
|
|
376
|
+
export const LOCATION_TARGET_ENUMS = new Set<string>(
|
|
377
|
+
Object.keys(LOCATION_ENUM_EXPANSIONS),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Set of all valid target enum values
|
|
382
|
+
*/
|
|
383
|
+
export const ALL_TARGET_ENUMS = new Set<string>([
|
|
384
|
+
...CHARACTER_TARGET_ENUMS,
|
|
385
|
+
...ITEM_TARGET_ENUMS,
|
|
386
|
+
...LOCATION_TARGET_ENUMS,
|
|
387
|
+
]);
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import type { CardInstance, CardRegistry } from "@drmxrcy/tcg-core";
|
|
2
|
+
import type { LorcanaCardDefinition } from "@drmxrcy/tcg-lorcana-types";
|
|
3
|
+
import type { LorcanaCardMeta, LorcanaGameState } from "../types/game-state";
|
|
4
|
+
import type { LorcanaContext, LorcanaFilter } from "./lorcana-target-dsl";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Filter evaluation context
|
|
8
|
+
*/
|
|
9
|
+
export interface FilterContext {
|
|
10
|
+
state: LorcanaGameState;
|
|
11
|
+
registry: CardRegistry<LorcanaCardDefinition>;
|
|
12
|
+
context?: LorcanaContext;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Handler for a specific filter type
|
|
17
|
+
*/
|
|
18
|
+
export interface FilterHandler<T extends LorcanaFilter = LorcanaFilter> {
|
|
19
|
+
name: T["type"];
|
|
20
|
+
complexity: number;
|
|
21
|
+
evaluate: (
|
|
22
|
+
filter: T,
|
|
23
|
+
card: CardInstance<LorcanaCardMeta>,
|
|
24
|
+
context: FilterContext,
|
|
25
|
+
) => boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class FilterRegistry {
|
|
29
|
+
private handlers = new Map<string, FilterHandler>();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a filter handler
|
|
33
|
+
*/
|
|
34
|
+
register<T extends LorcanaFilter>(handler: FilterHandler<T>): void {
|
|
35
|
+
this.handlers.set(handler.name, handler as unknown as FilterHandler);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get a handler for a filter type
|
|
40
|
+
*/
|
|
41
|
+
get(type: string): FilterHandler | undefined {
|
|
42
|
+
return this.handlers.get(type);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get all registered handlers
|
|
47
|
+
*/
|
|
48
|
+
getAll(): FilterHandler[] {
|
|
49
|
+
return Array.from(this.handlers.values());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Global registry instance
|
|
54
|
+
export const filterRegistry = new FilterRegistry();
|
|
55
|
+
|
|
56
|
+
export function registerDefaultFilters() {
|
|
57
|
+
// --- State Filters ---
|
|
58
|
+
filterRegistry.register<LorcanaFilter & { type: "damaged" }>({
|
|
59
|
+
name: "damaged",
|
|
60
|
+
complexity: 0,
|
|
61
|
+
evaluate: (_, card) => (card.damage ?? 0) > 0,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
filterRegistry.register<LorcanaFilter & { type: "undamaged" }>({
|
|
65
|
+
name: "undamaged",
|
|
66
|
+
complexity: 0,
|
|
67
|
+
evaluate: (_, card) => (card.damage ?? 0) === 0,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
filterRegistry.register<LorcanaFilter & { type: "exerted" }>({
|
|
71
|
+
name: "exerted",
|
|
72
|
+
complexity: 0,
|
|
73
|
+
evaluate: (_, card) => card.state === "exerted",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
filterRegistry.register<LorcanaFilter & { type: "ready" }>({
|
|
77
|
+
name: "ready",
|
|
78
|
+
complexity: 0,
|
|
79
|
+
evaluate: (_, card) => card.state === "ready",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
filterRegistry.register<LorcanaFilter & { type: "dry" }>({
|
|
83
|
+
name: "dry",
|
|
84
|
+
complexity: 0,
|
|
85
|
+
evaluate: (_, card) => !(card.isDrying ?? false),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
filterRegistry.register<LorcanaFilter & { type: "inkable"; value: boolean }>({
|
|
89
|
+
name: "inkable",
|
|
90
|
+
complexity: 0,
|
|
91
|
+
evaluate: (filter, card, { registry }) => {
|
|
92
|
+
const def = registry.getCard(card.definitionId);
|
|
93
|
+
return def ? def.inkable === filter.value : false;
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// --- Property Filters ---
|
|
98
|
+
filterRegistry.register<
|
|
99
|
+
LorcanaFilter & { type: "has-keyword"; keyword: any }
|
|
100
|
+
>({
|
|
101
|
+
name: "has-keyword",
|
|
102
|
+
complexity: 10,
|
|
103
|
+
evaluate: (filter, card, { registry }) => {
|
|
104
|
+
const def = registry.getCard(card.definitionId);
|
|
105
|
+
return (
|
|
106
|
+
def?.abilities?.some(
|
|
107
|
+
(a: any) => a.type === "keyword" && a.keyword === filter.keyword,
|
|
108
|
+
) ?? false
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
filterRegistry.register<
|
|
114
|
+
LorcanaFilter & { type: "has-classification"; classification: string }
|
|
115
|
+
>({
|
|
116
|
+
name: "has-classification",
|
|
117
|
+
complexity: 10,
|
|
118
|
+
evaluate: (filter, card, { registry }) => {
|
|
119
|
+
const def = registry.getCard(card.definitionId);
|
|
120
|
+
if (!def || def.cardType !== "character") return false;
|
|
121
|
+
return (
|
|
122
|
+
def.classifications?.some(
|
|
123
|
+
(c: string) =>
|
|
124
|
+
c.toLowerCase() === filter.classification.toLowerCase(),
|
|
125
|
+
) ?? false
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
filterRegistry.register<
|
|
131
|
+
LorcanaFilter & { type: "name" } & (
|
|
132
|
+
| { equals: string }
|
|
133
|
+
| { contains: string }
|
|
134
|
+
)
|
|
135
|
+
>({
|
|
136
|
+
name: "name",
|
|
137
|
+
complexity: 10,
|
|
138
|
+
evaluate: (filter, card, { registry }) => {
|
|
139
|
+
const def = registry.getCard(card.definitionId);
|
|
140
|
+
if (!def) return false;
|
|
141
|
+
if ("equals" in filter) {
|
|
142
|
+
return def.name === filter.equals;
|
|
143
|
+
}
|
|
144
|
+
if ("contains" in filter) {
|
|
145
|
+
return def.name.includes(filter.contains);
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
filterRegistry.register<LorcanaFilter & { type: "card-type"; value: any }>({
|
|
152
|
+
name: "card-type",
|
|
153
|
+
complexity: 5,
|
|
154
|
+
evaluate: (filter, card, { registry }) => {
|
|
155
|
+
const def = registry.getCard(card.definitionId);
|
|
156
|
+
return def ? def.cardType === filter.value : false;
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// --- Numeric Filters ---
|
|
161
|
+
const checkComparison = (
|
|
162
|
+
actual: number,
|
|
163
|
+
operator: string,
|
|
164
|
+
target: number,
|
|
165
|
+
) => {
|
|
166
|
+
switch (operator) {
|
|
167
|
+
case "eq":
|
|
168
|
+
return actual === target;
|
|
169
|
+
case "ne":
|
|
170
|
+
return actual !== target;
|
|
171
|
+
case "gt":
|
|
172
|
+
return actual > target;
|
|
173
|
+
case "gte":
|
|
174
|
+
return actual >= target;
|
|
175
|
+
case "lt":
|
|
176
|
+
return actual < target;
|
|
177
|
+
case "lte":
|
|
178
|
+
return actual <= target;
|
|
179
|
+
default:
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const getComparisonValue = (
|
|
185
|
+
value: number | "target",
|
|
186
|
+
// context: FilterContext - unused for now
|
|
187
|
+
// TODO: Implement getTargetValue when context is fully fleshed out
|
|
188
|
+
): number => {
|
|
189
|
+
if (value === "target") return 0; // Placeholder
|
|
190
|
+
return value;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
filterRegistry.register<
|
|
194
|
+
LorcanaFilter & { type: "strength"; comparison: any; value: any }
|
|
195
|
+
>({
|
|
196
|
+
name: "strength",
|
|
197
|
+
complexity: 20,
|
|
198
|
+
evaluate: (filter, card, ctx) => {
|
|
199
|
+
const def = ctx.registry.getCard(card.definitionId);
|
|
200
|
+
if (!def || def.cardType !== "character") return false;
|
|
201
|
+
const targetVal = getComparisonValue(filter.value);
|
|
202
|
+
return checkComparison(
|
|
203
|
+
(def as any).strength ?? 0,
|
|
204
|
+
filter.comparison,
|
|
205
|
+
targetVal,
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
filterRegistry.register<
|
|
211
|
+
LorcanaFilter & { type: "willpower"; comparison: any; value: any }
|
|
212
|
+
>({
|
|
213
|
+
name: "willpower",
|
|
214
|
+
complexity: 20,
|
|
215
|
+
evaluate: (filter, card, ctx) => {
|
|
216
|
+
const def = ctx.registry.getCard(card.definitionId);
|
|
217
|
+
if (!def || def.cardType !== "character") return false;
|
|
218
|
+
const targetVal = getComparisonValue(filter.value);
|
|
219
|
+
return checkComparison(
|
|
220
|
+
(def as any).willpower ?? 0,
|
|
221
|
+
filter.comparison,
|
|
222
|
+
targetVal,
|
|
223
|
+
);
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
filterRegistry.register<
|
|
228
|
+
LorcanaFilter & { type: "cost"; comparison: any; value: any }
|
|
229
|
+
>({
|
|
230
|
+
name: "cost",
|
|
231
|
+
complexity: 20,
|
|
232
|
+
evaluate: (filter, card, ctx) => {
|
|
233
|
+
const def = ctx.registry.getCard(card.definitionId);
|
|
234
|
+
if (!def) return false;
|
|
235
|
+
const targetVal = getComparisonValue(filter.value);
|
|
236
|
+
return checkComparison(def.cost, filter.comparison, targetVal);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
filterRegistry.register<
|
|
241
|
+
LorcanaFilter & { type: "lore-value"; comparison: any; value: any }
|
|
242
|
+
>({
|
|
243
|
+
name: "lore-value",
|
|
244
|
+
complexity: 20,
|
|
245
|
+
evaluate: (filter, card, ctx) => {
|
|
246
|
+
const def = ctx.registry.getCard(card.definitionId);
|
|
247
|
+
if (!def || (def.cardType !== "character" && def.cardType !== "location"))
|
|
248
|
+
return false;
|
|
249
|
+
const targetVal = getComparisonValue(filter.value);
|
|
250
|
+
return checkComparison(
|
|
251
|
+
(def as any).lore ?? 0,
|
|
252
|
+
filter.comparison,
|
|
253
|
+
targetVal,
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
filterRegistry.register<
|
|
259
|
+
LorcanaFilter & { type: "move-cost"; comparison: any; value: number }
|
|
260
|
+
>({
|
|
261
|
+
name: "move-cost",
|
|
262
|
+
complexity: 20,
|
|
263
|
+
evaluate: (filter, card, { registry }) => {
|
|
264
|
+
const def = registry.getCard(card.definitionId);
|
|
265
|
+
if (!def || def.cardType !== "location") return false;
|
|
266
|
+
return checkComparison(
|
|
267
|
+
def.moveCost ?? 0,
|
|
268
|
+
filter.comparison,
|
|
269
|
+
filter.value,
|
|
270
|
+
);
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
filterRegistry.register<LorcanaFilter & { type: "at-location" }>({
|
|
275
|
+
name: "at-location",
|
|
276
|
+
complexity: 30,
|
|
277
|
+
evaluate: (_, card) => {
|
|
278
|
+
return !!card.atLocationId;
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// --- Composite Filters ---
|
|
283
|
+
filterRegistry.register<
|
|
284
|
+
LorcanaFilter & { type: "and"; filters: LorcanaFilter[] }
|
|
285
|
+
>({
|
|
286
|
+
name: "and",
|
|
287
|
+
complexity: 50,
|
|
288
|
+
evaluate: (filter, card, context) => {
|
|
289
|
+
return filter.filters.every((f) => {
|
|
290
|
+
const handler = filterRegistry.get(f.type);
|
|
291
|
+
return handler ? handler.evaluate(f, card, context) : false;
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
filterRegistry.register<
|
|
297
|
+
LorcanaFilter & { type: "or"; filters: LorcanaFilter[] }
|
|
298
|
+
>({
|
|
299
|
+
name: "or",
|
|
300
|
+
complexity: 50,
|
|
301
|
+
evaluate: (filter, card, context) => {
|
|
302
|
+
return filter.filters.some((f) => {
|
|
303
|
+
const handler = filterRegistry.get(f.type);
|
|
304
|
+
return handler ? handler.evaluate(f, card, context) : false;
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
filterRegistry.register<
|
|
310
|
+
LorcanaFilter & { type: "not"; filter: LorcanaFilter }
|
|
311
|
+
>({
|
|
312
|
+
name: "not",
|
|
313
|
+
complexity: 50,
|
|
314
|
+
evaluate: (filter, card, context) => {
|
|
315
|
+
// Safe cast because we validated the type string
|
|
316
|
+
const handler = filterRegistry.get(
|
|
317
|
+
filter.filter.type,
|
|
318
|
+
) as unknown as FilterHandler<LorcanaFilter>;
|
|
319
|
+
return handler ? !handler.evaluate(filter.filter, card, context) : false;
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|