@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.
Files changed (100) hide show
  1. package/README.md +160 -0
  2. package/package.json +45 -0
  3. package/src/__tests__/integration/move-enumeration.test.ts +256 -0
  4. package/src/__tests__/rules/section-01-concepts.test.ts +426 -0
  5. package/src/__tests__/rules/section-03-gameplay.test.ts +298 -0
  6. package/src/__tests__/rules/section-04-turn-structure.test.ts +708 -0
  7. package/src/__tests__/rules/section-05-cards.test.ts +158 -0
  8. package/src/__tests__/rules/section-06-card-types.test.ts +342 -0
  9. package/src/__tests__/rules/section-07-abilities.test.ts +333 -0
  10. package/src/__tests__/rules/section-08-zones.test.ts +231 -0
  11. package/src/__tests__/rules/section-09-damage.test.ts +148 -0
  12. package/src/__tests__/rules/section-10-keywords.test.ts +469 -0
  13. package/src/__tests__/spec-01-foundation-types.test.ts +534 -0
  14. package/src/__tests__/spec-02-zones-card-states.test.ts +295 -0
  15. package/src/card-utils.ts +302 -0
  16. package/src/cards/README.md +296 -0
  17. package/src/cards/abilities/index.ts +175 -0
  18. package/src/cards/index.ts +10 -0
  19. package/src/deck-validation.ts +175 -0
  20. package/src/engine/lorcana-engine.ts +625 -0
  21. package/src/game-definition/__tests__/core-zone-integration.test.ts +553 -0
  22. package/src/game-definition/__tests__/zone-operations.test.ts +362 -0
  23. package/src/game-definition/__tests__/zones.test.ts +176 -0
  24. package/src/game-definition/definition.ts +45 -0
  25. package/src/game-definition/flow/turn-flow.ts +216 -0
  26. package/src/game-definition/index.ts +31 -0
  27. package/src/game-definition/moves/abilities/activate-ability.ts +51 -0
  28. package/src/game-definition/moves/core/__tests__/move-parameter-enumeration.test.ts +316 -0
  29. package/src/game-definition/moves/core/challenge.test.ts +545 -0
  30. package/src/game-definition/moves/core/challenge.ts +81 -0
  31. package/src/game-definition/moves/core/play-card.ts +83 -0
  32. package/src/game-definition/moves/core/quest.test.ts +448 -0
  33. package/src/game-definition/moves/core/quest.ts +49 -0
  34. package/src/game-definition/moves/debug/manual-exert.ts +36 -0
  35. package/src/game-definition/moves/effects/resolve-bag.ts +35 -0
  36. package/src/game-definition/moves/effects/resolve-effect.ts +34 -0
  37. package/src/game-definition/moves/index.ts +85 -0
  38. package/src/game-definition/moves/locations/move-character-to-location.ts +42 -0
  39. package/src/game-definition/moves/resources/put-card-into-inkwell.test.ts +462 -0
  40. package/src/game-definition/moves/resources/put-card-into-inkwell.ts +51 -0
  41. package/src/game-definition/moves/setup/alter-hand.test.ts +395 -0
  42. package/src/game-definition/moves/setup/alter-hand.ts +210 -0
  43. package/src/game-definition/moves/setup/choose-first-player.test.ts +450 -0
  44. package/src/game-definition/moves/setup/choose-first-player.ts +105 -0
  45. package/src/game-definition/moves/setup/draw-cards.ts +37 -0
  46. package/src/game-definition/moves/songs/sing-together.ts +47 -0
  47. package/src/game-definition/moves/songs/sing.ts +56 -0
  48. package/src/game-definition/moves/standard/concede.test.ts +189 -0
  49. package/src/game-definition/moves/standard/concede.ts +72 -0
  50. package/src/game-definition/moves/standard/pass-turn.ts +49 -0
  51. package/src/game-definition/setup/game-setup.ts +19 -0
  52. package/src/game-definition/trackers/tracker-config.ts +23 -0
  53. package/src/game-definition/win-conditions/lore-victory.ts +26 -0
  54. package/src/game-definition/zone-operations.ts +405 -0
  55. package/src/game-definition/zones/zone-configs.ts +59 -0
  56. package/src/game-definition/zones.ts +283 -0
  57. package/src/index.ts +189 -0
  58. package/src/operations/index.ts +7 -0
  59. package/src/operations/lorcana-operations.ts +288 -0
  60. package/src/queries/README.md +56 -0
  61. package/src/resolvers/__tests__/condition-resolver.test.ts +301 -0
  62. package/src/resolvers/condition-registry.ts +70 -0
  63. package/src/resolvers/condition-resolver.ts +85 -0
  64. package/src/resolvers/conditions/basic.ts +81 -0
  65. package/src/resolvers/conditions/card-state.ts +12 -0
  66. package/src/resolvers/conditions/comparison.ts +102 -0
  67. package/src/resolvers/conditions/existence.ts +219 -0
  68. package/src/resolvers/conditions/history.ts +68 -0
  69. package/src/resolvers/conditions/index.ts +15 -0
  70. package/src/resolvers/conditions/logical.ts +55 -0
  71. package/src/resolvers/conditions/resolution.ts +41 -0
  72. package/src/resolvers/conditions/revealed.ts +42 -0
  73. package/src/resolvers/conditions/zone.ts +84 -0
  74. package/src/setup.test.ts +18 -0
  75. package/src/targeting/__tests__/filter-resolver.test.ts +294 -0
  76. package/src/targeting/__tests__/real-cards-targeting.test.ts +303 -0
  77. package/src/targeting/__tests__/targeting-dsl.test.ts +386 -0
  78. package/src/targeting/enum-expansion.ts +387 -0
  79. package/src/targeting/filter-registry.ts +322 -0
  80. package/src/targeting/filter-resolver.ts +145 -0
  81. package/src/targeting/index.ts +91 -0
  82. package/src/targeting/lorcana-target-dsl.ts +495 -0
  83. package/src/targeting/targeting-ui.ts +407 -0
  84. package/src/testing/index.ts +14 -0
  85. package/src/testing/lorcana-test-engine.ts +813 -0
  86. package/src/types/README.md +303 -0
  87. package/src/types/__tests__/lorcana-state.test.ts +168 -0
  88. package/src/types/__tests__/move-enumeration.test.ts +179 -0
  89. package/src/types/branded-types.ts +106 -0
  90. package/src/types/game-state.ts +184 -0
  91. package/src/types/index.ts +87 -0
  92. package/src/types/keywords.ts +187 -0
  93. package/src/types/lorcana-state.ts +260 -0
  94. package/src/types/move-enumeration.ts +126 -0
  95. package/src/types/move-params.ts +216 -0
  96. package/src/validators/index.ts +7 -0
  97. package/src/validators/move-validators.ts +374 -0
  98. package/src/zones/card-state.ts +234 -0
  99. package/src/zones/index.ts +42 -0
  100. package/src/zones/zone-config.ts +150 -0
@@ -0,0 +1,303 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type {
3
+ CardId,
4
+ CardInstance,
5
+ CardRegistry,
6
+ PlayerId,
7
+ ZoneId,
8
+ } from "@drmxrcy/tcg-core";
9
+ import type { LorcanaCardDefinition } from "@drmxrcy/tcg-lorcana-types";
10
+ import type { LorcanaCardMeta, LorcanaGameState } from "../../types/game-state";
11
+ import { createTargetFiltersPredicate } from "../filter-resolver";
12
+ import type { LorcanaFilter } from "../lorcana-target-dsl";
13
+
14
+ // --- Mock Data Setup ---
15
+
16
+ const mockRegistry = {
17
+ getCard: (id: string) => mockDefinitions[id],
18
+ } as unknown as CardRegistry<LorcanaCardDefinition>;
19
+
20
+ const mockDefinitions: Record<string, LorcanaCardDefinition> = {
21
+ "char-villain": {
22
+ id: "char-villain",
23
+ name: "Maleficent",
24
+ cardType: "character",
25
+ classifications: ["Villain", "Sorcerer"],
26
+ cost: 3,
27
+ strength: 2,
28
+ willpower: 2,
29
+ lore: 1,
30
+ color: "amethyst",
31
+ inkable: true,
32
+ } as any,
33
+ "char-hero": {
34
+ id: "char-hero",
35
+ name: "Aladdin",
36
+ cardType: "character",
37
+ classifications: ["Hero", "Storyborn"],
38
+ cost: 2,
39
+ strength: 2,
40
+ willpower: 2,
41
+ lore: 1,
42
+ color: "ruby",
43
+ inkable: true,
44
+ } as any,
45
+ "char-high-str": {
46
+ id: "char-high-str",
47
+ name: "Maui",
48
+ cardType: "character",
49
+ classifications: ["Hero", "Demigod"],
50
+ cost: 5,
51
+ strength: 6,
52
+ willpower: 5,
53
+ lore: 2,
54
+ color: "ruby",
55
+ inkable: true,
56
+ } as any,
57
+ "item-basic": {
58
+ id: "item-basic",
59
+ name: "Dinglehopper",
60
+ cardType: "item",
61
+ cost: 1,
62
+ color: "emerald",
63
+ inkable: true,
64
+ } as any,
65
+ "loc-basic": {
66
+ id: "loc-basic",
67
+ name: "Forbidden Mountain",
68
+ cardType: "location",
69
+ cost: 2,
70
+ moveCost: 1,
71
+ willpower: 6,
72
+ lore: 1,
73
+ color: "amethyst",
74
+ inkable: true,
75
+ } as any,
76
+ "char-support": {
77
+ id: "char-support",
78
+ name: "Alice",
79
+ cardType: "character",
80
+ abilities: [{ type: "keyword", keyword: "Support" }],
81
+ cost: 3,
82
+ strength: 2,
83
+ willpower: 2,
84
+ lore: 1,
85
+ color: "sapphire",
86
+ inkable: true,
87
+ } as any,
88
+ };
89
+
90
+ const mockState: LorcanaGameState = {
91
+ playerIds: ["player1" as PlayerId, "player2" as PlayerId],
92
+ turnNumber: 1,
93
+ activePlayerId: "player1" as PlayerId,
94
+ cards: {}, // Populated per test
95
+ } as any;
96
+
97
+ const makeCard = (
98
+ id: string,
99
+ defId: string,
100
+ ownerId: string,
101
+ updates: Partial<LorcanaCardMeta> = {},
102
+ ): CardInstance<LorcanaCardMeta> => ({
103
+ id: id as CardId,
104
+ definitionId: defId,
105
+ owner: ownerId as PlayerId,
106
+ controller: ownerId as PlayerId,
107
+ zone: "play" as ZoneId,
108
+ tapped: false,
109
+ flipped: false,
110
+ revealed: true,
111
+ phased: false,
112
+ // Custom State Mixed In
113
+ state: "ready",
114
+ damage: 0,
115
+ isDrying: false,
116
+ ...updates,
117
+ });
118
+
119
+ // --- Tests ---
120
+
121
+ describe("Real Card Targeting Scenarios", () => {
122
+ const p1Villain = makeCard("c1", "char-villain", "player1");
123
+ const p2Hero = makeCard("c2", "char-hero", "player2");
124
+ const p2DamagedHero = makeCard("c3", "char-hero", "player2", { damage: 2 });
125
+ const p1ExertedVillain = makeCard("c4", "char-villain", "player1", {
126
+ state: "exerted",
127
+ });
128
+ const p1HighStr = makeCard("c5", "char-high-str", "player1");
129
+ const p1Item = makeCard("i1", "item-basic", "player1");
130
+ const p2Location = makeCard("l1", "loc-basic", "player2");
131
+
132
+ const cards = [
133
+ p1Villain,
134
+ p2Hero,
135
+ p2DamagedHero,
136
+ p1ExertedVillain,
137
+ p1HighStr,
138
+ p1Item,
139
+ p2Location,
140
+ ];
141
+
142
+ // Helper to filter cards
143
+ const filterCards = (filters: LorcanaFilter[]) => {
144
+ const predicate = createTargetFiltersPredicate(
145
+ filters,
146
+ mockState,
147
+ mockRegistry,
148
+ );
149
+ return cards.filter(predicate);
150
+ };
151
+
152
+ it("should handle 'chosen Villain character'", () => {
153
+ // "Banish chosen Villain character"
154
+ const filters: LorcanaFilter[] = [
155
+ { type: "card-type", value: "character" },
156
+ { type: "has-classification", classification: "Villain" },
157
+ ];
158
+
159
+ const results = filterCards(filters);
160
+ expect(results.map((c) => c.id)).toEqual(
161
+ expect.arrayContaining(["c1", "c4"]),
162
+ );
163
+ expect(results).not.toContain(p2Hero); // Not a Villain
164
+ });
165
+
166
+ it("should handle 'chosen damaged character'", () => {
167
+ // "Banish chosen damaged character"
168
+ const filters: LorcanaFilter[] = [
169
+ { type: "card-type", value: "character" },
170
+ { type: "damaged" },
171
+ ];
172
+
173
+ const results = filterCards(filters);
174
+ expect(results.map((c) => c.id)).toContain("c3" as CardId); // p2DamagedHero
175
+ expect(results).not.toContain(p1Villain); // Not damaged
176
+ });
177
+
178
+ it("should handle 'chosen item'", () => {
179
+ // "Banish chosen item"
180
+ const filters: LorcanaFilter[] = [{ type: "card-type", value: "item" }];
181
+ const results = filterCards(filters);
182
+ expect(results.map((c) => c.id)).toEqual(["i1"] as CardId[]);
183
+ });
184
+
185
+ it("should handle 'chosen location or item'", () => {
186
+ // "Banish chosen location or item"
187
+ const filters: LorcanaFilter[] = [
188
+ {
189
+ type: "or",
190
+ filters: [
191
+ { type: "card-type", value: "location" },
192
+ { type: "card-type", value: "item" },
193
+ ],
194
+ },
195
+ ];
196
+
197
+ const results = filterCards(filters);
198
+ expect(results.map((c) => c.id)).toEqual(
199
+ expect.arrayContaining(["i1", "l1"]),
200
+ );
201
+ expect(results).not.toContain(p1Villain);
202
+ });
203
+
204
+ it("should handle 'chosen character with cost 3 or more'", () => {
205
+ // "characters with cost 3 or more"
206
+ const filters: LorcanaFilter[] = [
207
+ { type: "card-type", value: "character" },
208
+ { type: "cost", comparison: "gte", value: 3 },
209
+ ];
210
+
211
+ const results = filterCards(filters);
212
+ // p1Villain (cost 3), p1HighStr (cost 5)
213
+ expect(results.map((c) => c.id)).toEqual(
214
+ expect.arrayContaining(["c1", "c4", "c5"]),
215
+ );
216
+ expect(results).not.toContain(p2Hero); // Cost 2
217
+ });
218
+
219
+ it("should handle 'chosen character with Strength 3 or less'", () => {
220
+ // "chosen character with 3 strength or less"
221
+ const filters: LorcanaFilter[] = [
222
+ { type: "card-type", value: "character" },
223
+ { type: "strength", comparison: "lte", value: 3 },
224
+ ];
225
+
226
+ const results = filterCards(filters);
227
+ // p1Villain (2), p2Hero (2), p2DamagedHero (2)
228
+ expect(results.map((c) => c.id)).toEqual(
229
+ expect.arrayContaining(["c1", "c2", "c3", "c4"]),
230
+ );
231
+ expect(results).not.toContain(p1HighStr); // Str 6
232
+ });
233
+
234
+ it("should handle 'chosen character named X'", () => {
235
+ // "chosen character named Aladdin"
236
+ const filters: LorcanaFilter[] = [
237
+ { type: "card-type", value: "character" },
238
+ { type: "name", equals: "Aladdin" },
239
+ ];
240
+
241
+ const results = filterCards(filters);
242
+ expect(results.map((c) => c.id)).toEqual(
243
+ expect.arrayContaining(["c2", "c3"]),
244
+ );
245
+ expect(results).not.toContain(p1Villain);
246
+ });
247
+
248
+ it("should handle 'exerted chosen damaged character'", () => {
249
+ // "Exert chosen damaged character" - targeting implies filter
250
+ // Actually command exerts it, but filter selects it.
251
+ // But maybe "chosen exerted character"?
252
+ // Let's test "chosen exerted character"
253
+ const filters: LorcanaFilter[] = [
254
+ { type: "card-type", value: "character" },
255
+ { type: "exerted" },
256
+ ];
257
+ const results = filterCards(filters);
258
+ expect(results.map((c) => c.id)).toEqual(["c4"] as CardId[]);
259
+ });
260
+
261
+ it("should handle composite logic: 'Item OR (Character AND Villain)'", () => {
262
+ // Complex arbitrary test
263
+ const filters: LorcanaFilter[] = [
264
+ {
265
+ type: "or",
266
+ filters: [
267
+ { type: "card-type", value: "item" },
268
+ {
269
+ type: "and",
270
+ filters: [
271
+ { type: "card-type", value: "character" },
272
+ { type: "has-classification", classification: "Villain" },
273
+ ],
274
+ },
275
+ ],
276
+ },
277
+ ];
278
+
279
+ const results = filterCards(filters);
280
+ // Should define p1Item, p1Villain, p1ExertedVillain
281
+ expect(results.map((c) => c.id)).toEqual(
282
+ expect.arrayContaining(["i1", "c1", "c4"]),
283
+ );
284
+ expect(results).not.toContain(p2Hero);
285
+ expect(results).not.toContain(p2Location);
286
+ });
287
+
288
+ it("should handle 'chosen character with Support'", () => {
289
+ // Mock a card with Support
290
+ const p1Support = makeCard("c6", "char-support", "player1");
291
+ // mockDefinitions["char-support"] is already defined in setup
292
+
293
+ cards.push(p1Support);
294
+
295
+ const filters: LorcanaFilter[] = [
296
+ { type: "has-keyword", keyword: "Support" },
297
+ ];
298
+
299
+ const results = filterCards(filters);
300
+ expect(results.map((c) => c.id)).toContain("c6" as CardId);
301
+ expect(results).not.toContain(p1Villain);
302
+ });
303
+ });
@@ -0,0 +1,386 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ ALL_TARGET_ENUMS,
4
+ CHARACTER_TARGET_ENUMS,
5
+ type CharacterTarget,
6
+ type CharacterTargetEnum,
7
+ expandCharacterTarget,
8
+ expandItemTarget,
9
+ expandLocationTarget,
10
+ expandTarget,
11
+ generateTargetDescription,
12
+ getTargetUIHints,
13
+ ITEM_TARGET_ENUMS,
14
+ type ItemTargetEnum,
15
+ isCharacterEnum,
16
+ isDSLTarget,
17
+ LOCATION_TARGET_ENUMS,
18
+ type LocationTargetEnum,
19
+ type LorcanaCardTarget,
20
+ type LorcanaCharacterTarget,
21
+ } from "../index";
22
+
23
+ describe("Targeting DSL", () => {
24
+ describe("Enum Detection", () => {
25
+ it("should detect string enums", () => {
26
+ expect(isCharacterEnum("CHOSEN_CHARACTER")).toBe(true);
27
+ expect(isCharacterEnum("SELF")).toBe(true);
28
+ expect(isCharacterEnum("ALL_OPPOSING_CHARACTERS")).toBe(true);
29
+ });
30
+
31
+ it("should detect DSL objects", () => {
32
+ const dsl: LorcanaCardTarget = {
33
+ selector: "chosen",
34
+ count: 1,
35
+ cardType: "character",
36
+ };
37
+ expect(isDSLTarget(dsl)).toBe(true);
38
+ expect(isDSLTarget("CHOSEN_CHARACTER")).toBe(false);
39
+ });
40
+ });
41
+
42
+ describe("Character Target Expansion", () => {
43
+ it("should expand SELF enum", () => {
44
+ const expanded = expandCharacterTarget("SELF");
45
+ expect(expanded).toMatchObject({
46
+ selector: "self",
47
+ cardType: "character",
48
+ context: { self: true },
49
+ });
50
+ });
51
+
52
+ it("should expand CHOSEN_CHARACTER enum", () => {
53
+ const expanded = expandCharacterTarget("CHOSEN_CHARACTER");
54
+ expect(expanded).toMatchObject({
55
+ selector: "chosen",
56
+ count: 1,
57
+ owner: "any",
58
+ cardType: "character",
59
+ zones: ["play"],
60
+ });
61
+ });
62
+
63
+ it("should expand CHOSEN_OPPOSING_CHARACTER enum", () => {
64
+ const expanded = expandCharacterTarget("CHOSEN_OPPOSING_CHARACTER");
65
+ expect(expanded).toMatchObject({
66
+ selector: "chosen",
67
+ count: 1,
68
+ owner: "opponent",
69
+ cardType: "character",
70
+ zones: ["play"],
71
+ });
72
+ });
73
+
74
+ it("should expand ALL_OPPOSING_CHARACTERS enum", () => {
75
+ const expanded = expandCharacterTarget("ALL_OPPOSING_CHARACTERS");
76
+ expect(expanded).toMatchObject({
77
+ selector: "all",
78
+ count: "all",
79
+ owner: "opponent",
80
+ cardType: "character",
81
+ zones: ["play"],
82
+ });
83
+ });
84
+
85
+ it("should expand CHOSEN_DAMAGED_CHARACTER enum", () => {
86
+ const expanded = expandCharacterTarget("CHOSEN_DAMAGED_CHARACTER");
87
+ expect(expanded).toMatchObject({
88
+ selector: "chosen",
89
+ count: 1,
90
+ owner: "any",
91
+ cardType: "character",
92
+ zones: ["play"],
93
+ filters: [{ type: "damaged" }],
94
+ });
95
+ });
96
+
97
+ it("should expand ANOTHER_CHOSEN_CHARACTER enum", () => {
98
+ const expanded = expandCharacterTarget("ANOTHER_CHOSEN_CHARACTER");
99
+ expect(expanded).toMatchObject({
100
+ selector: "chosen",
101
+ count: 1,
102
+ excludeSelf: true,
103
+ });
104
+ });
105
+
106
+ it("should return DSL object unchanged", () => {
107
+ const dsl: CharacterTarget = {
108
+ selector: "chosen",
109
+ count: 2,
110
+ owner: "opponent",
111
+ cardType: "character",
112
+ filters: [{ type: "exerted" }],
113
+ };
114
+ const expanded = expandCharacterTarget(dsl);
115
+ expect(expanded).toBe(dsl);
116
+ });
117
+
118
+ it("should expand all character enum values", () => {
119
+ for (const enumValue of CHARACTER_TARGET_ENUMS) {
120
+ const expanded = expandCharacterTarget(
121
+ enumValue as CharacterTargetEnum,
122
+ );
123
+ expect(expanded).toBeDefined();
124
+ expect(expanded.cardType).toBe("character");
125
+ }
126
+ });
127
+ });
128
+
129
+ describe("Item Target Expansion", () => {
130
+ it("should expand CHOSEN_ITEM enum", () => {
131
+ const expanded = expandItemTarget("CHOSEN_ITEM");
132
+ expect(expanded).toMatchObject({
133
+ selector: "chosen",
134
+ count: 1,
135
+ owner: "any",
136
+ cardType: "item",
137
+ zones: ["play"],
138
+ });
139
+ });
140
+
141
+ it("should expand THIS_ITEM enum", () => {
142
+ const expanded = expandItemTarget("THIS_ITEM");
143
+ expect(expanded).toMatchObject({
144
+ selector: "self",
145
+ cardType: "item",
146
+ context: { self: true },
147
+ });
148
+ });
149
+
150
+ it("should expand all item enum values", () => {
151
+ for (const enumValue of ITEM_TARGET_ENUMS) {
152
+ const expanded = expandItemTarget(enumValue as ItemTargetEnum);
153
+ expect(expanded).toBeDefined();
154
+ expect(expanded.cardType).toBe("item");
155
+ }
156
+ });
157
+ });
158
+
159
+ describe("Location Target Expansion", () => {
160
+ it("should expand CHOSEN_LOCATION enum", () => {
161
+ const expanded = expandLocationTarget("CHOSEN_LOCATION");
162
+ expect(expanded).toMatchObject({
163
+ selector: "chosen",
164
+ count: 1,
165
+ owner: "any",
166
+ cardType: "location",
167
+ zones: ["play"],
168
+ });
169
+ });
170
+
171
+ it("should expand THIS_LOCATION enum", () => {
172
+ const expanded = expandLocationTarget("THIS_LOCATION");
173
+ expect(expanded).toMatchObject({
174
+ selector: "self",
175
+ cardType: "location",
176
+ context: { self: true },
177
+ });
178
+ });
179
+
180
+ it("should expand all location enum values", () => {
181
+ for (const enumValue of LOCATION_TARGET_ENUMS) {
182
+ const expanded = expandLocationTarget(enumValue as LocationTargetEnum);
183
+ expect(expanded).toBeDefined();
184
+ expect(expanded.cardType).toBe("location");
185
+ }
186
+ });
187
+ });
188
+
189
+ describe("Generic Target Expansion", () => {
190
+ it("should expand any target enum", () => {
191
+ expect(expandTarget("CHOSEN_CHARACTER").cardType).toBe("character");
192
+ expect(expandTarget("CHOSEN_ITEM").cardType).toBe("item");
193
+ expect(expandTarget("CHOSEN_LOCATION").cardType).toBe("location");
194
+ });
195
+
196
+ it("should throw for unknown enum", () => {
197
+ expect(() => expandTarget("UNKNOWN" as any)).toThrow();
198
+ });
199
+ });
200
+
201
+ describe("Enum Registries", () => {
202
+ it("should have all character enums registered", () => {
203
+ expect(CHARACTER_TARGET_ENUMS.has("SELF")).toBe(true);
204
+ expect(CHARACTER_TARGET_ENUMS.has("CHOSEN_CHARACTER")).toBe(true);
205
+ expect(CHARACTER_TARGET_ENUMS.has("ALL_OPPOSING_CHARACTERS")).toBe(true);
206
+ });
207
+
208
+ it("should have all item enums registered", () => {
209
+ expect(ITEM_TARGET_ENUMS.has("CHOSEN_ITEM")).toBe(true);
210
+ expect(ITEM_TARGET_ENUMS.has("YOUR_ITEMS")).toBe(true);
211
+ });
212
+
213
+ it("should have all location enums registered", () => {
214
+ expect(LOCATION_TARGET_ENUMS.has("CHOSEN_LOCATION")).toBe(true);
215
+ expect(LOCATION_TARGET_ENUMS.has("YOUR_LOCATIONS")).toBe(true);
216
+ });
217
+
218
+ it("should have complete ALL_TARGET_ENUMS", () => {
219
+ expect(ALL_TARGET_ENUMS.size).toBe(
220
+ CHARACTER_TARGET_ENUMS.size +
221
+ ITEM_TARGET_ENUMS.size +
222
+ LOCATION_TARGET_ENUMS.size,
223
+ );
224
+ });
225
+ });
226
+ });
227
+
228
+ describe("Target Description Generation", () => {
229
+ it("should describe CHOSEN_CHARACTER", () => {
230
+ const desc = generateTargetDescription("CHOSEN_CHARACTER");
231
+ expect(desc).toBe("a character");
232
+ });
233
+
234
+ it("should describe CHOSEN_OPPOSING_CHARACTER", () => {
235
+ const desc = generateTargetDescription("CHOSEN_OPPOSING_CHARACTER");
236
+ expect(desc).toBe("an opposing character");
237
+ });
238
+
239
+ it("should describe ALL_OPPOSING_CHARACTERS", () => {
240
+ const desc = generateTargetDescription("ALL_OPPOSING_CHARACTERS");
241
+ expect(desc).toBe("all opposing characters");
242
+ });
243
+
244
+ it("should describe CHOSEN_DAMAGED_CHARACTER", () => {
245
+ const desc = generateTargetDescription("CHOSEN_DAMAGED_CHARACTER");
246
+ expect(desc).toBe("a damaged character");
247
+ });
248
+
249
+ it("should describe YOUR_CHARACTERS", () => {
250
+ const desc = generateTargetDescription("YOUR_CHARACTERS");
251
+ expect(desc).toBe("all your characters");
252
+ });
253
+
254
+ it("should describe SELF", () => {
255
+ const desc = generateTargetDescription("SELF");
256
+ expect(desc).toBe("this character");
257
+ });
258
+
259
+ it("should describe complex DSL with filters", () => {
260
+ const dsl: LorcanaCharacterTarget = {
261
+ selector: "chosen",
262
+ count: 2,
263
+ owner: "opponent",
264
+ cardType: "character",
265
+ filters: [
266
+ { type: "damaged" },
267
+ { type: "cost", comparison: "lte", value: 3 },
268
+ ],
269
+ };
270
+ const desc = generateTargetDescription(dsl);
271
+ expect(desc).toContain("opposing");
272
+ expect(desc).toContain("damaged");
273
+ expect(desc).toContain("character");
274
+ expect(desc).toContain("cost");
275
+ });
276
+
277
+ it("should describe DSL with keyword filter", () => {
278
+ const dsl: LorcanaCharacterTarget = {
279
+ selector: "all",
280
+ owner: "you",
281
+ cardType: "character",
282
+ filters: [{ type: "has-keyword", keyword: "Evasive" }],
283
+ };
284
+ const desc = generateTargetDescription(dsl);
285
+ expect(desc).toContain("your");
286
+ expect(desc).toContain("Evasive");
287
+ expect(desc).toContain("characters");
288
+ });
289
+ });
290
+
291
+ describe("Target UI Hints", () => {
292
+ it("should generate single selection hints for CHOSEN_CHARACTER", () => {
293
+ const hints = getTargetUIHints("CHOSEN_CHARACTER");
294
+ expect(hints.selectionType).toBe("single");
295
+ expect(hints.minSelections).toBe(1);
296
+ expect(hints.maxSelections).toBe(1);
297
+ expect(hints.optional).toBe(false);
298
+ });
299
+
300
+ it("should generate automatic hints for ALL_OPPOSING_CHARACTERS", () => {
301
+ const hints = getTargetUIHints("ALL_OPPOSING_CHARACTERS");
302
+ expect(hints.selectionType).toBe("automatic");
303
+ expect(hints.ownerFilter).toBe("opponent");
304
+ });
305
+
306
+ it("should generate no selection for SELF", () => {
307
+ const hints = getTargetUIHints("SELF");
308
+ expect(hints.selectionType).toBe("none");
309
+ });
310
+
311
+ it("should generate multiple selection hints for up-to count", () => {
312
+ const dsl: LorcanaCharacterTarget = {
313
+ selector: "chosen",
314
+ count: { upTo: 3 },
315
+ cardType: "character",
316
+ };
317
+ const hints = getTargetUIHints(dsl);
318
+ expect(hints.selectionType).toBe("multiple");
319
+ expect(hints.minSelections).toBe(0);
320
+ expect(hints.maxSelections).toBe(3);
321
+ expect(hints.optional).toBe(true);
322
+ });
323
+
324
+ it("should include card type in hints", () => {
325
+ const hints = getTargetUIHints("CHOSEN_ITEM");
326
+ expect(hints.cardType).toBe("item");
327
+ });
328
+
329
+ it("should include owner filter in hints", () => {
330
+ const hints = getTargetUIHints("CHOSEN_OPPOSING_CHARACTER");
331
+ expect(hints.ownerFilter).toBe("opponent");
332
+ });
333
+
334
+ it("should include prompt text", () => {
335
+ const hints = getTargetUIHints("CHOSEN_CHARACTER");
336
+ expect(hints.prompt).toContain("Choose");
337
+ });
338
+ });
339
+
340
+ describe("Complex DSL Creation", () => {
341
+ it("should create complex character target", () => {
342
+ const target: LorcanaCardTarget = {
343
+ selector: "chosen",
344
+ count: { upTo: 2 },
345
+ owner: "opponent",
346
+ cardType: "character",
347
+ zones: ["play"],
348
+ filters: [
349
+ { type: "damaged" },
350
+ { type: "strength", comparison: "lte", value: 3 },
351
+ ],
352
+ };
353
+
354
+ expect(target.selector).toBe("chosen");
355
+ expect(target.owner).toBe("opponent");
356
+ expect(target.filters).toHaveLength(2);
357
+ });
358
+
359
+ it("should create target with context reference", () => {
360
+ const target: LorcanaCardTarget = {
361
+ selector: "self",
362
+ cardType: "character",
363
+ context: {
364
+ self: true,
365
+ triggerSource: false,
366
+ },
367
+ };
368
+
369
+ expect(target.context?.self).toBe(true);
370
+ });
371
+
372
+ it("should create target with composite filters", () => {
373
+ const target: LorcanaCardTarget = {
374
+ selector: "all",
375
+ cardType: "character",
376
+ filters: [
377
+ {
378
+ type: "or",
379
+ filters: [{ type: "damaged" }, { type: "exerted" }],
380
+ },
381
+ ],
382
+ };
383
+
384
+ expect(target.filters?.[0].type).toBe("or");
385
+ });
386
+ });