@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,362 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { createCardId, createPlayerId } from "@drmxrcy/tcg-core";
3
+ import {
4
+ addCardToZone,
5
+ createZoneState,
6
+ getCardsInZone,
7
+ isCardInZone,
8
+ moveCardBetweenZones,
9
+ removeCardFromZone,
10
+ type ZoneState,
11
+ } from "../zone-operations";
12
+
13
+ /**
14
+ * Task 1.5, 1.6: Tests for Zone Transition Operations
15
+ *
16
+ * Validates helper functions for managing cards in zones:
17
+ * - Adding cards to zones
18
+ * - Removing cards from zones
19
+ * - Moving cards between zones
20
+ * - Querying cards in zones
21
+ *
22
+ * References:
23
+ * - Rule 8.1 (Zones are separate)
24
+ * - Rule 8.4.4 (Leaving play triggers abilities)
25
+ * - Rule 8.1.5 (Cards entering private zones lose all info)
26
+ */
27
+
28
+ // Test helper to create test players
29
+ function createTestPlayers(...names: string[]) {
30
+ return names.map((name) => createPlayerId(name));
31
+ }
32
+
33
+ // Test helper to create test cards
34
+ function createTestCards(...names: string[]) {
35
+ return names.map((name) => createCardId(name));
36
+ }
37
+
38
+ describe("Zone Operations", () => {
39
+ describe("createZoneState", () => {
40
+ it("should create empty zone state for all players", () => {
41
+ const [player1, player2] = createTestPlayers("player1", "player2");
42
+
43
+ const zoneState = createZoneState([player1, player2]);
44
+
45
+ expect(zoneState[player1]).toEqual([]);
46
+ expect(zoneState[player2]).toEqual([]);
47
+ });
48
+ });
49
+
50
+ describe("addCardToZone", () => {
51
+ it("should add card to player's zone", () => {
52
+ const [player1, player2] = createTestPlayers("player1", "player2");
53
+ const [card1] = createTestCards("card-1");
54
+
55
+ const zoneState: ZoneState = {
56
+ [player1]: [],
57
+ [player2]: [],
58
+ };
59
+
60
+ addCardToZone(zoneState, player1, card1);
61
+
62
+ expect(zoneState[player1]).toEqual([card1]);
63
+ expect(zoneState[player2]).toEqual([]);
64
+ });
65
+
66
+ it("should add multiple cards maintaining order", () => {
67
+ const [player1] = createTestPlayers("player1");
68
+ const [card1, card2, card3] = createTestCards(
69
+ "card-1",
70
+ "card-2",
71
+ "card-3",
72
+ );
73
+
74
+ const zoneState: ZoneState = {
75
+ [player1]: [],
76
+ };
77
+
78
+ addCardToZone(zoneState, player1, card1);
79
+ addCardToZone(zoneState, player1, card2);
80
+ addCardToZone(zoneState, player1, card3);
81
+
82
+ expect(zoneState[player1]).toEqual([card1, card2, card3]);
83
+ });
84
+ });
85
+
86
+ describe("removeCardFromZone", () => {
87
+ it("should remove card from player's zone", () => {
88
+ const [player1] = createTestPlayers("player1");
89
+ const [card1, card2] = createTestCards("card-1", "card-2");
90
+
91
+ const zoneState: ZoneState = {
92
+ [player1]: [card1, card2],
93
+ };
94
+
95
+ removeCardFromZone(zoneState, player1, card1);
96
+
97
+ expect(zoneState[player1]).toEqual([card2]);
98
+ });
99
+
100
+ it("should maintain order when removing from middle", () => {
101
+ const [player1] = createTestPlayers("player1");
102
+ const [card1, card2, card3] = createTestCards(
103
+ "card-1",
104
+ "card-2",
105
+ "card-3",
106
+ );
107
+
108
+ const zoneState: ZoneState = {
109
+ [player1]: [card1, card2, card3],
110
+ };
111
+
112
+ removeCardFromZone(zoneState, player1, card2);
113
+
114
+ expect(zoneState[player1]).toEqual([card1, card3]);
115
+ });
116
+
117
+ it("should do nothing if card not in zone", () => {
118
+ const [player1] = createTestPlayers("player1");
119
+ const [card1, card2] = createTestCards("card-1", "card-2");
120
+
121
+ const zoneState: ZoneState = {
122
+ [player1]: [card1],
123
+ };
124
+
125
+ removeCardFromZone(zoneState, player1, card2);
126
+
127
+ expect(zoneState[player1]).toEqual([card1]);
128
+ });
129
+ });
130
+
131
+ describe("moveCardBetweenZones", () => {
132
+ it("should move card from one zone to another", () => {
133
+ const [player1] = createTestPlayers("player1");
134
+ const [card1] = createTestCards("card-1");
135
+
136
+ const handZone: ZoneState = {
137
+ [player1]: [card1],
138
+ };
139
+
140
+ const playZone: ZoneState = {
141
+ [player1]: [],
142
+ };
143
+
144
+ moveCardBetweenZones(handZone, playZone, player1, card1);
145
+
146
+ expect(handZone[player1]).toEqual([]);
147
+ expect(playZone[player1]).toEqual([card1]);
148
+ });
149
+
150
+ it("should handle moving between zones of different players", () => {
151
+ const [player1, player2] = createTestPlayers("player1", "player2");
152
+ const [card1] = createTestCards("card-1");
153
+
154
+ const player1Hand: ZoneState = {
155
+ [player1]: [card1],
156
+ };
157
+
158
+ const player2Hand: ZoneState = {
159
+ [player2]: [],
160
+ };
161
+
162
+ // This would be unusual but tests the operation works
163
+ removeCardFromZone(player1Hand, player1, card1);
164
+ addCardToZone(player2Hand, player2, card1);
165
+
166
+ expect(player1Hand[player1]).toEqual([]);
167
+ expect(player2Hand[player2]).toEqual([card1]);
168
+ });
169
+
170
+ it("should maintain card order in destination zone", () => {
171
+ const [player1] = createTestPlayers("player1");
172
+ const [card1, card2, card3] = createTestCards(
173
+ "card-1",
174
+ "card-2",
175
+ "card-3",
176
+ );
177
+
178
+ const sourceZone: ZoneState = {
179
+ [player1]: [card2],
180
+ };
181
+
182
+ const destZone: ZoneState = {
183
+ [player1]: [card1, card3],
184
+ };
185
+
186
+ moveCardBetweenZones(sourceZone, destZone, player1, card2);
187
+
188
+ expect(sourceZone[player1]).toEqual([]);
189
+ expect(destZone[player1]).toEqual([card1, card3, card2]);
190
+ });
191
+ });
192
+
193
+ describe("isCardInZone", () => {
194
+ it("should return true if card is in player's zone", () => {
195
+ const [player1] = createTestPlayers("player1");
196
+ const [card1] = createTestCards("card-1");
197
+
198
+ const zoneState: ZoneState = {
199
+ [player1]: [card1],
200
+ };
201
+
202
+ expect(isCardInZone(zoneState, player1, card1)).toBe(true);
203
+ });
204
+
205
+ it("should return false if card is not in player's zone", () => {
206
+ const [player1] = createTestPlayers("player1");
207
+ const [card1, card2] = createTestCards("card-1", "card-2");
208
+
209
+ const zoneState: ZoneState = {
210
+ [player1]: [card1],
211
+ };
212
+
213
+ expect(isCardInZone(zoneState, player1, card2)).toBe(false);
214
+ });
215
+
216
+ it("should return false if player has no zone", () => {
217
+ const [player1, player2] = createTestPlayers("player1", "player2");
218
+ const [card1] = createTestCards("card-1");
219
+
220
+ const zoneState: ZoneState = {
221
+ [player1]: [card1],
222
+ };
223
+
224
+ expect(isCardInZone(zoneState, player2, card1)).toBe(false);
225
+ });
226
+ });
227
+
228
+ describe("getCardsInZone", () => {
229
+ it("should return all cards in player's zone", () => {
230
+ const [player1] = createTestPlayers("player1");
231
+ const [card1, card2, card3] = createTestCards(
232
+ "card-1",
233
+ "card-2",
234
+ "card-3",
235
+ );
236
+
237
+ const zoneState: ZoneState = {
238
+ [player1]: [card1, card2, card3],
239
+ };
240
+
241
+ const cards = getCardsInZone(zoneState, player1);
242
+
243
+ expect(cards).toEqual([card1, card2, card3]);
244
+ });
245
+
246
+ it("should return empty array if player has no cards", () => {
247
+ const [player1] = createTestPlayers("player1");
248
+
249
+ const zoneState: ZoneState = {
250
+ [player1]: [],
251
+ };
252
+
253
+ const cards = getCardsInZone(zoneState, player1);
254
+
255
+ expect(cards).toEqual([]);
256
+ });
257
+
258
+ it("should return empty array if player has no zone", () => {
259
+ const [player1] = createTestPlayers("player1");
260
+
261
+ const zoneState: ZoneState = {};
262
+
263
+ const cards = getCardsInZone(zoneState, player1);
264
+
265
+ expect(cards).toEqual([]);
266
+ });
267
+
268
+ it("should return copy of array (not mutate original)", () => {
269
+ const [player1] = createTestPlayers("player1");
270
+ const [card1] = createTestCards("card-1");
271
+
272
+ const zoneState: ZoneState = {
273
+ [player1]: [card1],
274
+ };
275
+
276
+ const cards = getCardsInZone(zoneState, player1);
277
+ cards.push(createCardId("card-2"));
278
+
279
+ // Original should be unchanged
280
+ expect(zoneState[player1]).toEqual([card1]);
281
+ });
282
+ });
283
+
284
+ describe("Zone Transition Scenarios", () => {
285
+ it("should handle draw card scenario (deck -> hand)", () => {
286
+ const [player1] = createTestPlayers("player1");
287
+ const [topCard, card2, card3] = createTestCards(
288
+ "top-card",
289
+ "card-2",
290
+ "card-3",
291
+ );
292
+
293
+ const deckZone: ZoneState = {
294
+ [player1]: [topCard, card2, card3],
295
+ };
296
+
297
+ const handZone: ZoneState = {
298
+ [player1]: [],
299
+ };
300
+
301
+ // Draw top card
302
+ moveCardBetweenZones(deckZone, handZone, player1, topCard);
303
+
304
+ expect(deckZone[player1]).toHaveLength(2);
305
+ expect(handZone[player1]).toEqual([topCard]);
306
+ });
307
+
308
+ it("should handle play card scenario (hand -> play)", () => {
309
+ const [player1] = createTestPlayers("player1");
310
+ const [card] = createTestCards("character-1");
311
+
312
+ const handZone: ZoneState = {
313
+ [player1]: [card],
314
+ };
315
+
316
+ const playZone: ZoneState = {
317
+ [player1]: [],
318
+ };
319
+
320
+ moveCardBetweenZones(handZone, playZone, player1, card);
321
+
322
+ expect(handZone[player1]).toEqual([]);
323
+ expect(playZone[player1]).toEqual([card]);
324
+ });
325
+
326
+ it("should handle banish scenario (play -> discard)", () => {
327
+ const [player1] = createTestPlayers("player1");
328
+ const [character] = createTestCards("character-1");
329
+
330
+ const playZone: ZoneState = {
331
+ [player1]: [character],
332
+ };
333
+
334
+ const discardZone: ZoneState = {
335
+ [player1]: [],
336
+ };
337
+
338
+ moveCardBetweenZones(playZone, discardZone, player1, character);
339
+
340
+ expect(playZone[player1]).toEqual([]);
341
+ expect(discardZone[player1]).toEqual([character]);
342
+ });
343
+
344
+ it("should handle ink card scenario (hand -> inkwell)", () => {
345
+ const [player1] = createTestPlayers("player1");
346
+ const [inkableCard] = createTestCards("inkable-1");
347
+
348
+ const handZone: ZoneState = {
349
+ [player1]: [inkableCard],
350
+ };
351
+
352
+ const inkwellZone: ZoneState = {
353
+ [player1]: [],
354
+ };
355
+
356
+ moveCardBetweenZones(handZone, inkwellZone, player1, inkableCard);
357
+
358
+ expect(handZone[player1]).toEqual([]);
359
+ expect(inkwellZone[player1]).toEqual([inkableCard]);
360
+ });
361
+ });
362
+ });
@@ -0,0 +1,176 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { type LorcanaZoneId, lorcanaZones } from "../zones";
3
+
4
+ /**
5
+ * Task 1.3: Tests for Zone Configurations
6
+ *
7
+ * Validates zone configurations match Lorcana Comprehensive Rules Section 8:
8
+ * - Deck (Rule 8.2): Private, ordered, facedown
9
+ * - Hand (Rule 8.3): Private, can rearrange
10
+ * - Play (Rule 8.4): Public, all can see
11
+ * - Discard (Rule 8.6): Public, ordered, faceup
12
+ * - Inkwell (Rule 8.5): Private, facedown
13
+ *
14
+ * References:
15
+ * - Rule 8.1.2 (Public zones)
16
+ * - Rule 8.1.3 (Private zones)
17
+ * - Rule 8.2.2 (Deck is private and ordered)
18
+ * - Rule 8.3.2 (Hand is private)
19
+ * - Rule 8.4.3 (Play is public)
20
+ * - Rule 8.5.3 (Inkwell is private)
21
+ * - Rule 8.6.3 (Discard is public and ordered)
22
+ */
23
+
24
+ describe("Lorcana Zone Configurations", () => {
25
+ it("should have all 6 required zones", () => {
26
+ const zoneIds = Object.keys(lorcanaZones);
27
+
28
+ expect(zoneIds).toContain("deck");
29
+ expect(zoneIds).toContain("hand");
30
+ expect(zoneIds).toContain("play");
31
+ expect(zoneIds).toContain("discard");
32
+ expect(zoneIds).toContain("inkwell");
33
+ expect(zoneIds).toContain("limbo");
34
+ expect(zoneIds).toHaveLength(6);
35
+ });
36
+
37
+ describe("Deck Zone", () => {
38
+ it("should be private visibility (Rule 8.2.2)", () => {
39
+ expect(lorcanaZones.deck.visibility).toBe("owner");
40
+ });
41
+
42
+ it("should be ordered (Rule 8.2.2)", () => {
43
+ expect(lorcanaZones.deck.ordered).toBe(true);
44
+ });
45
+
46
+ it("should be facedown (Rule 8.2.2)", () => {
47
+ expect(lorcanaZones.deck.facedown).toBe(true);
48
+ });
49
+ });
50
+
51
+ describe("Hand Zone", () => {
52
+ it("should be private visibility (Rule 8.3.2)", () => {
53
+ expect(lorcanaZones.hand.visibility).toBe("owner");
54
+ });
55
+
56
+ it("should be unordered - can rearrange (Rule 8.3.2)", () => {
57
+ expect(lorcanaZones.hand.ordered).toBe(false);
58
+ });
59
+
60
+ it("should not be facedown (cards are visible to owner)", () => {
61
+ expect(lorcanaZones.hand.facedown).toBe(false);
62
+ });
63
+ });
64
+
65
+ describe("Play Zone", () => {
66
+ it("should be public visibility (Rule 8.4.3)", () => {
67
+ expect(lorcanaZones.play.visibility).toBe("all");
68
+ });
69
+
70
+ it("should be unordered (no specific arrangement required)", () => {
71
+ expect(lorcanaZones.play.ordered).toBe(false);
72
+ });
73
+
74
+ it("should not be facedown (cards are visible)", () => {
75
+ expect(lorcanaZones.play.facedown).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe("Discard Zone", () => {
80
+ it("should be public visibility (Rule 8.6.3)", () => {
81
+ expect(lorcanaZones.discard.visibility).toBe("all");
82
+ });
83
+
84
+ it("should be ordered (Rule 8.6.3)", () => {
85
+ expect(lorcanaZones.discard.ordered).toBe(true);
86
+ });
87
+
88
+ it("should not be facedown (cards are visible)", () => {
89
+ expect(lorcanaZones.discard.facedown).toBe(false);
90
+ });
91
+ });
92
+
93
+ describe("Inkwell Zone", () => {
94
+ it("should be private visibility (Rule 8.5.3)", () => {
95
+ expect(lorcanaZones.inkwell.visibility).toBe("owner");
96
+ });
97
+
98
+ it("should be unordered (can arrange as convenient) (Rule 8.5.4)", () => {
99
+ expect(lorcanaZones.inkwell.ordered).toBe(false);
100
+ });
101
+
102
+ it("should be facedown (Rule 8.5.2, 8.5.3)", () => {
103
+ expect(lorcanaZones.inkwell.facedown).toBe(true);
104
+ });
105
+ });
106
+
107
+ describe("Limbo Zone", () => {
108
+ it("should be private visibility (phased out cards not directly interactable)", () => {
109
+ expect(lorcanaZones.limbo.visibility).toBe("owner");
110
+ });
111
+
112
+ it("should be ordered (maintain stacking sequence for shifts)", () => {
113
+ expect(lorcanaZones.limbo.ordered).toBe(true);
114
+ });
115
+
116
+ it("should not be facedown (for tracking purposes)", () => {
117
+ expect(lorcanaZones.limbo.facedown).toBe(false);
118
+ });
119
+ });
120
+
121
+ describe("Zone Properties Validation", () => {
122
+ it("should have exactly 4 private zones", () => {
123
+ const privateZones = Object.values(lorcanaZones).filter(
124
+ (zone) => zone.visibility === "owner",
125
+ );
126
+ expect(privateZones).toHaveLength(4); // deck, hand, inkwell, limbo
127
+ });
128
+
129
+ it("should have exactly 2 public zones", () => {
130
+ const publicZones = Object.values(lorcanaZones).filter(
131
+ (zone) => zone.visibility === "all",
132
+ );
133
+ expect(publicZones).toHaveLength(2); // play, discard
134
+ });
135
+
136
+ it("should have exactly 2 facedown zones", () => {
137
+ const facedownZones = Object.values(lorcanaZones).filter(
138
+ (zone) => zone.facedown === true,
139
+ );
140
+ expect(facedownZones).toHaveLength(2); // deck, inkwell
141
+ });
142
+
143
+ it("should have exactly 3 ordered zones", () => {
144
+ const orderedZones = Object.values(lorcanaZones).filter(
145
+ (zone) => zone.ordered === true,
146
+ );
147
+ expect(orderedZones).toHaveLength(3); // deck, discard, limbo
148
+ });
149
+ });
150
+
151
+ describe("Zone Type Safety", () => {
152
+ it("should type-check valid zone IDs", () => {
153
+ const validIds: LorcanaZoneId[] = [
154
+ "deck",
155
+ "hand",
156
+ "play",
157
+ "discard",
158
+ "inkwell",
159
+ "limbo",
160
+ ];
161
+
162
+ for (const id of validIds) {
163
+ expect(lorcanaZones[id]).toBeDefined();
164
+ }
165
+ });
166
+
167
+ it("should have correct zone ID type", () => {
168
+ // This test validates TypeScript type checking at compile time
169
+ const zoneId: LorcanaZoneId = "deck";
170
+ expect(zoneId).toBe("deck");
171
+
172
+ // TypeScript should prevent this:
173
+ // const invalid: LorcanaZoneId = "invalid"; // Type error
174
+ });
175
+ });
176
+ });
@@ -0,0 +1,45 @@
1
+ import type { GameDefinition } from "@drmxrcy/tcg-core";
2
+ import type {
3
+ LorcanaCardMeta,
4
+ LorcanaGameState,
5
+ LorcanaMoveParams,
6
+ } from "../types";
7
+ import { lorcanaFlow } from "./flow/turn-flow";
8
+ import { lorcanaMoves } from "./moves";
9
+ import { setupLorcanaGame } from "./setup/game-setup";
10
+ import { trackerConfig } from "./trackers/tracker-config";
11
+ import { checkLoreVictory } from "./win-conditions/lore-victory";
12
+ // Import modular components
13
+ import { lorcanaZones } from "./zones/zone-configs";
14
+
15
+ /**
16
+ * Complete Lorcana Game Definition
17
+ *
18
+ * Modular architecture with single responsibility:
19
+ * - zones/: Zone configurations
20
+ * - flow/: Turn and phase flow
21
+ * - moves/: All game moves organized by category
22
+ * - trackers/: Action tracking configuration
23
+ * - setup/: Game initialization
24
+ * - win-conditions/: Victory conditions
25
+ *
26
+ * Benefits:
27
+ * - Easy to navigate and maintain
28
+ * - Test components in isolation
29
+ * - Clear separation of concerns
30
+ * - Scalable for future additions
31
+ */
32
+ export const lorcanaGameDefinition: GameDefinition<
33
+ LorcanaGameState,
34
+ LorcanaMoveParams,
35
+ unknown, // Card definitions (to be added)
36
+ LorcanaCardMeta
37
+ > = {
38
+ name: "Disney Lorcana TCG",
39
+ zones: lorcanaZones,
40
+ flow: lorcanaFlow,
41
+ moves: lorcanaMoves,
42
+ trackers: trackerConfig,
43
+ setup: setupLorcanaGame,
44
+ endIf: checkLoreVictory,
45
+ };