@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,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Targeting UI Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for UI integration with the targeting DSL.
|
|
5
|
+
* Converts DSL specifications to human-readable descriptions and
|
|
6
|
+
* UI hints for target selection interfaces.
|
|
7
|
+
*
|
|
8
|
+
* @module targeting/targeting-ui
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TargetingUIHint } from "@drmxrcy/tcg-core";
|
|
12
|
+
import {
|
|
13
|
+
expandCharacterTarget,
|
|
14
|
+
expandItemTarget,
|
|
15
|
+
expandLocationTarget,
|
|
16
|
+
isCharacterEnum,
|
|
17
|
+
isItemEnum,
|
|
18
|
+
isLocationEnum,
|
|
19
|
+
} from "./enum-expansion";
|
|
20
|
+
import type {
|
|
21
|
+
LorcanaCardTarget,
|
|
22
|
+
LorcanaCharacterTarget,
|
|
23
|
+
LorcanaFilter,
|
|
24
|
+
LorcanaItemTarget,
|
|
25
|
+
LorcanaLocationTarget,
|
|
26
|
+
} from "./lorcana-target-dsl";
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Description Generation
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a human-readable description of a target specification
|
|
34
|
+
*
|
|
35
|
+
* @param target - Target DSL or enum
|
|
36
|
+
* @returns Human-readable description
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* generateTargetDescription("CHOSEN_OPPOSING_CHARACTER")
|
|
41
|
+
* // => "an opposing character"
|
|
42
|
+
*
|
|
43
|
+
* generateTargetDescription({
|
|
44
|
+
* selector: "all",
|
|
45
|
+
* owner: "opponent",
|
|
46
|
+
* cardType: "character",
|
|
47
|
+
* filters: [{ type: "damaged" }]
|
|
48
|
+
* })
|
|
49
|
+
* // => "all opposing damaged characters"
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function generateTargetDescription(
|
|
53
|
+
target: LorcanaCharacterTarget | LorcanaItemTarget | LorcanaLocationTarget,
|
|
54
|
+
): string {
|
|
55
|
+
// Expand enum to DSL if needed
|
|
56
|
+
let dsl: LorcanaCardTarget;
|
|
57
|
+
if (typeof target === "string") {
|
|
58
|
+
if (isCharacterEnum(target as LorcanaCharacterTarget)) {
|
|
59
|
+
dsl = expandCharacterTarget(target as LorcanaCharacterTarget);
|
|
60
|
+
} else if (isItemEnum(target as LorcanaItemTarget)) {
|
|
61
|
+
dsl = expandItemTarget(target as LorcanaItemTarget);
|
|
62
|
+
} else if (isLocationEnum(target as LorcanaLocationTarget)) {
|
|
63
|
+
dsl = expandLocationTarget(target as LorcanaLocationTarget);
|
|
64
|
+
} else {
|
|
65
|
+
return target; // Fallback to raw string
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
dsl = target;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return generateDSLDescription(dsl);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Generate description from a DSL object
|
|
76
|
+
*/
|
|
77
|
+
function generateDSLDescription(target: LorcanaCardTarget): string {
|
|
78
|
+
const parts: string[] = [];
|
|
79
|
+
|
|
80
|
+
// Handle self-reference
|
|
81
|
+
if (target.selector === "self") {
|
|
82
|
+
return `this ${target.cardType || "card"}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Count/Selector
|
|
86
|
+
if (target.selector === "all" || target.selector === "each") {
|
|
87
|
+
parts.push("all");
|
|
88
|
+
} else if (target.selector === "chosen") {
|
|
89
|
+
const count = getCountValue(target.count);
|
|
90
|
+
if (count === 1) {
|
|
91
|
+
// Defer article choice until we know the next word
|
|
92
|
+
parts.push("__ARTICLE__");
|
|
93
|
+
} else if (typeof count === "number") {
|
|
94
|
+
parts.push(String(count));
|
|
95
|
+
} else if (count && typeof count === "object" && "upTo" in count) {
|
|
96
|
+
parts.push(`up to ${count.upTo}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Ownership
|
|
101
|
+
if (target.owner === "opponent") {
|
|
102
|
+
parts.push("opposing");
|
|
103
|
+
} else if (target.owner === "you") {
|
|
104
|
+
parts.push("your");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Filter-based adjectives (state filters come before card type)
|
|
108
|
+
if (target.filters) {
|
|
109
|
+
for (const filter of target.filters) {
|
|
110
|
+
const adjective = getFilterAdjective(filter);
|
|
111
|
+
if (adjective) {
|
|
112
|
+
parts.push(adjective);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Card type (pluralized if multiple)
|
|
118
|
+
const cardType = target.cardType || "card";
|
|
119
|
+
const isPlural =
|
|
120
|
+
target.selector === "all" ||
|
|
121
|
+
target.selector === "each" ||
|
|
122
|
+
(typeof target.count === "number" && target.count > 1);
|
|
123
|
+
parts.push(isPlural ? pluralize(cardType) : cardType);
|
|
124
|
+
|
|
125
|
+
// Numeric filter suffixes
|
|
126
|
+
if (target.filters) {
|
|
127
|
+
for (const filter of target.filters) {
|
|
128
|
+
const suffix = getFilterSuffix(filter);
|
|
129
|
+
if (suffix) {
|
|
130
|
+
parts.push(suffix);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Replace article placeholder with correct article based on following word
|
|
136
|
+
const result = parts.join(" ");
|
|
137
|
+
return result.replace(/__ARTICLE__\s+(\w)/, (_, nextChar) => {
|
|
138
|
+
const vowels = ["a", "e", "i", "o", "u"];
|
|
139
|
+
return vowels.includes(nextChar.toLowerCase())
|
|
140
|
+
? `an ${nextChar}`
|
|
141
|
+
: `a ${nextChar}`;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get count value from TargetCount
|
|
147
|
+
*/
|
|
148
|
+
function getCountValue(
|
|
149
|
+
count: LorcanaCardTarget["count"],
|
|
150
|
+
): number | { upTo: number } | "all" | undefined {
|
|
151
|
+
if (count === undefined) return 1;
|
|
152
|
+
if (count === "all") return "all";
|
|
153
|
+
if (typeof count === "number") return count;
|
|
154
|
+
if ("exactly" in count) return count.exactly;
|
|
155
|
+
if ("upTo" in count) return { upTo: count.upTo };
|
|
156
|
+
if ("atLeast" in count) return count.atLeast;
|
|
157
|
+
if ("between" in count) return count.between[0];
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Exhaustive check helper - fails at compile time if a case is not handled
|
|
163
|
+
*/
|
|
164
|
+
function assertNever(x: never): never {
|
|
165
|
+
throw new Error(`Unhandled filter type: ${JSON.stringify(x)}`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get adjective form of a filter (for placement before card type)
|
|
170
|
+
*/
|
|
171
|
+
function getFilterAdjective(filter: LorcanaFilter): string | undefined {
|
|
172
|
+
switch (filter.type) {
|
|
173
|
+
case "damaged":
|
|
174
|
+
return "damaged";
|
|
175
|
+
case "undamaged":
|
|
176
|
+
return "undamaged";
|
|
177
|
+
case "exerted":
|
|
178
|
+
return "exerted";
|
|
179
|
+
case "ready":
|
|
180
|
+
return "ready";
|
|
181
|
+
case "dry":
|
|
182
|
+
return "fresh";
|
|
183
|
+
case "inkable":
|
|
184
|
+
return filter.value ? "inkable" : "non-inkable";
|
|
185
|
+
// Filters that don't produce adjectives (handled in getFilterSuffix)
|
|
186
|
+
case "has-keyword":
|
|
187
|
+
case "has-classification":
|
|
188
|
+
case "cost":
|
|
189
|
+
case "strength":
|
|
190
|
+
case "willpower":
|
|
191
|
+
case "lore-value":
|
|
192
|
+
case "name":
|
|
193
|
+
case "at-location":
|
|
194
|
+
case "move-cost":
|
|
195
|
+
case "and":
|
|
196
|
+
case "or":
|
|
197
|
+
case "card-type":
|
|
198
|
+
case "not":
|
|
199
|
+
return undefined;
|
|
200
|
+
default:
|
|
201
|
+
// Exhaustive check - will fail to compile if a new filter type is added
|
|
202
|
+
return assertNever(filter);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Get suffix form of a filter (for placement after card type)
|
|
208
|
+
*/
|
|
209
|
+
function getFilterSuffix(filter: LorcanaFilter): string | undefined {
|
|
210
|
+
switch (filter.type) {
|
|
211
|
+
case "has-keyword":
|
|
212
|
+
return `with ${filter.keyword}`;
|
|
213
|
+
case "has-classification":
|
|
214
|
+
return `with ${filter.classification} classification`;
|
|
215
|
+
case "cost":
|
|
216
|
+
return `with cost ${getComparisonSymbol(filter.comparison)} ${filter.value}`;
|
|
217
|
+
case "strength":
|
|
218
|
+
return `with strength ${getComparisonSymbol(filter.comparison)} ${filter.value}`;
|
|
219
|
+
case "willpower":
|
|
220
|
+
return `with willpower ${getComparisonSymbol(filter.comparison)} ${filter.value}`;
|
|
221
|
+
case "lore-value":
|
|
222
|
+
return `with lore ${getComparisonSymbol(filter.comparison)} ${filter.value}`;
|
|
223
|
+
case "at-location":
|
|
224
|
+
return filter.location ? `at ${filter.location}` : "at a location";
|
|
225
|
+
case "move-cost":
|
|
226
|
+
return `with move cost ${getComparisonSymbol(filter.comparison)} ${filter.value}`;
|
|
227
|
+
case "name":
|
|
228
|
+
if ("equals" in filter) return `named ${filter.equals}`;
|
|
229
|
+
if ("contains" in filter) return `with "${filter.contains}" in name`;
|
|
230
|
+
return undefined;
|
|
231
|
+
// State filters handled in getFilterAdjective
|
|
232
|
+
case "damaged":
|
|
233
|
+
case "undamaged":
|
|
234
|
+
case "exerted":
|
|
235
|
+
case "ready":
|
|
236
|
+
case "dry":
|
|
237
|
+
case "inkable":
|
|
238
|
+
return undefined;
|
|
239
|
+
// Composite filters - recursively process
|
|
240
|
+
case "and":
|
|
241
|
+
case "or":
|
|
242
|
+
case "card-type":
|
|
243
|
+
case "not":
|
|
244
|
+
return undefined;
|
|
245
|
+
default:
|
|
246
|
+
// Exhaustive check - will fail to compile if a new filter type is added
|
|
247
|
+
return assertNever(filter);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get comparison symbol for display
|
|
253
|
+
*/
|
|
254
|
+
function getComparisonSymbol(
|
|
255
|
+
comparison: "eq" | "ne" | "gt" | "gte" | "lt" | "lte",
|
|
256
|
+
): string {
|
|
257
|
+
switch (comparison) {
|
|
258
|
+
case "eq":
|
|
259
|
+
return "=";
|
|
260
|
+
case "ne":
|
|
261
|
+
return "!=";
|
|
262
|
+
case "gt":
|
|
263
|
+
return ">";
|
|
264
|
+
case "gte":
|
|
265
|
+
return ">=";
|
|
266
|
+
case "lt":
|
|
267
|
+
return "<";
|
|
268
|
+
case "lte":
|
|
269
|
+
return "<=";
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Pluralize a card type name
|
|
275
|
+
*/
|
|
276
|
+
function pluralize(word: string): string {
|
|
277
|
+
if (word.endsWith("y")) {
|
|
278
|
+
return `${word.slice(0, -1)}ies`;
|
|
279
|
+
}
|
|
280
|
+
if (word.endsWith("s") || word.endsWith("x") || word.endsWith("ch")) {
|
|
281
|
+
return `${word}es`;
|
|
282
|
+
}
|
|
283
|
+
return `${word}s`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// UI Hint Generation
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Generate UI hints for target selection
|
|
292
|
+
*
|
|
293
|
+
* @param target - Target DSL or enum
|
|
294
|
+
* @returns UI hints for rendering selection interface
|
|
295
|
+
*/
|
|
296
|
+
export function getTargetUIHints(
|
|
297
|
+
target: LorcanaCharacterTarget | LorcanaItemTarget | LorcanaLocationTarget,
|
|
298
|
+
): LorcanaTargetUIHint {
|
|
299
|
+
// Expand enum to DSL if needed
|
|
300
|
+
let dsl: LorcanaCardTarget;
|
|
301
|
+
if (typeof target === "string") {
|
|
302
|
+
if (isCharacterEnum(target as LorcanaCharacterTarget)) {
|
|
303
|
+
dsl = expandCharacterTarget(target as LorcanaCharacterTarget);
|
|
304
|
+
} else if (isItemEnum(target as LorcanaItemTarget)) {
|
|
305
|
+
dsl = expandItemTarget(target as LorcanaItemTarget);
|
|
306
|
+
} else if (isLocationEnum(target as LorcanaLocationTarget)) {
|
|
307
|
+
dsl = expandLocationTarget(target as LorcanaLocationTarget);
|
|
308
|
+
} else {
|
|
309
|
+
// Fallback for unknown enum
|
|
310
|
+
return {
|
|
311
|
+
selectionType: "single",
|
|
312
|
+
minSelections: 1,
|
|
313
|
+
maxSelections: 1,
|
|
314
|
+
prompt: "Choose a target",
|
|
315
|
+
optional: false,
|
|
316
|
+
highlightZones: ["play"],
|
|
317
|
+
cardType: undefined,
|
|
318
|
+
ownerFilter: "any",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
} else {
|
|
322
|
+
dsl = target;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return generateUIHintsFromDSL(dsl);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Extended UI hints with Lorcana-specific info
|
|
330
|
+
*/
|
|
331
|
+
export interface LorcanaTargetUIHint extends TargetingUIHint {
|
|
332
|
+
/** Card type to filter by */
|
|
333
|
+
cardType: string | undefined;
|
|
334
|
+
/** Owner filter for highlighting */
|
|
335
|
+
ownerFilter: "you" | "opponent" | "any";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Generate UI hints from DSL object
|
|
340
|
+
*/
|
|
341
|
+
function generateUIHintsFromDSL(
|
|
342
|
+
target: LorcanaCardTarget,
|
|
343
|
+
): LorcanaTargetUIHint {
|
|
344
|
+
const description = generateDSLDescription(target);
|
|
345
|
+
|
|
346
|
+
// Determine selection type
|
|
347
|
+
let selectionType: LorcanaTargetUIHint["selectionType"];
|
|
348
|
+
if (target.selector === "self") {
|
|
349
|
+
selectionType = "none";
|
|
350
|
+
} else if (target.selector === "all" || target.selector === "each") {
|
|
351
|
+
selectionType = "automatic";
|
|
352
|
+
} else if (target.selector === "random" || target.selector === "any") {
|
|
353
|
+
selectionType = "automatic";
|
|
354
|
+
} else {
|
|
355
|
+
// "chosen" - check count for single vs multiple
|
|
356
|
+
const count = target.count;
|
|
357
|
+
if (count === "all") {
|
|
358
|
+
selectionType = "multiple";
|
|
359
|
+
} else if (typeof count === "number") {
|
|
360
|
+
selectionType = count > 1 ? "multiple" : "single";
|
|
361
|
+
} else if (count && "upTo" in count) {
|
|
362
|
+
selectionType = count.upTo > 1 ? "multiple" : "single";
|
|
363
|
+
} else if (count && "between" in count) {
|
|
364
|
+
selectionType = count.between[1] > 1 ? "multiple" : "single";
|
|
365
|
+
} else {
|
|
366
|
+
selectionType = "single";
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Calculate min/max
|
|
371
|
+
let minSelections = 1;
|
|
372
|
+
let maxSelections: number | "unlimited" = 1;
|
|
373
|
+
|
|
374
|
+
if (target.count === undefined) {
|
|
375
|
+
minSelections = 1;
|
|
376
|
+
maxSelections = 1;
|
|
377
|
+
} else if (target.count === "all") {
|
|
378
|
+
minSelections = 0;
|
|
379
|
+
maxSelections = "unlimited";
|
|
380
|
+
} else if (typeof target.count === "number") {
|
|
381
|
+
minSelections = target.count;
|
|
382
|
+
maxSelections = target.count;
|
|
383
|
+
} else if ("exactly" in target.count) {
|
|
384
|
+
minSelections = target.count.exactly;
|
|
385
|
+
maxSelections = target.count.exactly;
|
|
386
|
+
} else if ("upTo" in target.count) {
|
|
387
|
+
minSelections = 0;
|
|
388
|
+
maxSelections = target.count.upTo;
|
|
389
|
+
} else if ("atLeast" in target.count) {
|
|
390
|
+
minSelections = target.count.atLeast;
|
|
391
|
+
maxSelections = "unlimited";
|
|
392
|
+
} else if ("between" in target.count) {
|
|
393
|
+
minSelections = target.count.between[0];
|
|
394
|
+
maxSelections = target.count.between[1];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
selectionType,
|
|
399
|
+
minSelections,
|
|
400
|
+
maxSelections,
|
|
401
|
+
prompt: `Choose ${description}`,
|
|
402
|
+
optional: minSelections === 0,
|
|
403
|
+
highlightZones: target.zones || ["play"],
|
|
404
|
+
cardType: target.cardType,
|
|
405
|
+
ownerFilter: target.owner || "any",
|
|
406
|
+
};
|
|
407
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lorcana Testing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Exports test engine and helpers for writing Lorcana tests
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
LorcanaTestEngine,
|
|
9
|
+
PLAYER_ONE,
|
|
10
|
+
PLAYER_TWO,
|
|
11
|
+
TestCardModel,
|
|
12
|
+
type TestEngineOptions,
|
|
13
|
+
type TestInitialState,
|
|
14
|
+
} from "./lorcana-test-engine";
|