@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,405 @@
1
+ /**
2
+ * Zone Operations
3
+ *
4
+ * Task 7.3, 7.4: Migrated to use @drmxrcy/tcg-core zone utilities
5
+ *
6
+ * This module provides zone operations for Lorcana. It includes:
7
+ * 1. Flat ZoneState helpers (Record<PlayerId, CardId[]>) - mutable, for simple cases
8
+ * 2. Core Zone re-exports - immutable Zone objects from @drmxrcy/tcg-core (recommended)
9
+ *
10
+ * For new code, prefer using core Zone objects for immutability and advanced features.
11
+ * The flat ZoneState pattern is maintained for backward compatibility.
12
+ *
13
+ * All operations follow Comprehensive Rules Section 8 (Zones)
14
+ */
15
+
16
+ import type { CardId, PlayerId } from "@drmxrcy/tcg-core";
17
+
18
+ // Re-export core zone utilities for direct use (recommended for new code)
19
+ // These work with immutable Zone objects from @drmxrcy/tcg-core
20
+ // Re-export with aliases to avoid conflicts with flat ZoneState helpers
21
+ export {
22
+ addCard,
23
+ addCardToBottom as addCardToBottomZone,
24
+ addCardToTop as addCardToTopZone,
25
+ type CardZoneConfig,
26
+ clearZone as clearZoneImmutable,
27
+ createPlayerZones,
28
+ createZone,
29
+ draw,
30
+ filterZoneByVisibility,
31
+ findCardInZones,
32
+ getBottomCard,
33
+ getCardsInZone as getCardsInZoneImmutable,
34
+ getTopCard as getTopCardFromZone,
35
+ getZoneSize as getZoneSizeImmutable,
36
+ isCardInZone as isCardInZoneImmutable,
37
+ mill,
38
+ moveCard,
39
+ peek,
40
+ removeCard,
41
+ reveal,
42
+ search,
43
+ shuffle,
44
+ type Zone,
45
+ type ZoneVisibility,
46
+ } from "@drmxrcy/tcg-core";
47
+
48
+ /**
49
+ * Zone State
50
+ *
51
+ * Maps each player to their array of cards in a zone.
52
+ * Card order is significant for ordered zones (deck, discard).
53
+ *
54
+ * This is a simplified zone representation. For more advanced features,
55
+ * consider using @drmxrcy/tcg-core's Zone objects directly with zone-factory.
56
+ */
57
+ export type ZoneState = Record<PlayerId, CardId[]>;
58
+
59
+ /**
60
+ * Create empty zone state for players
61
+ *
62
+ * Initializes a zone with empty arrays for each player.
63
+ *
64
+ * @param players - Array of player IDs
65
+ * @returns Zone state with empty arrays for each player
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const players = [createPlayerId("p1"), createPlayerId("p2")];
70
+ * const handZone = createZoneState(players);
71
+ * // { p1: [], p2: [] }
72
+ * ```
73
+ */
74
+ export const createZoneState = (players: PlayerId[]): ZoneState => {
75
+ const result: ZoneState = {};
76
+ for (const player of players) {
77
+ result[player] = [];
78
+ }
79
+ return result;
80
+ };
81
+
82
+ /**
83
+ * Add card to player's zone
84
+ *
85
+ * Adds a card to the end of the player's zone array.
86
+ * For ordered zones (deck, discard), this maintains sequence.
87
+ *
88
+ * Note: This mutates the zone state. For immutable operations,
89
+ * consider using @drmxrcy/tcg-core's addCard with Zone objects.
90
+ *
91
+ * @param zoneState - The zone state to modify
92
+ * @param playerId - The player whose zone to add to
93
+ * @param cardId - The card to add
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * addCardToZone(handZone, playerId, cardId);
98
+ * ```
99
+ */
100
+ export const addCardToZone = (
101
+ zoneState: ZoneState,
102
+ playerId: PlayerId,
103
+ cardId: CardId,
104
+ ): void => {
105
+ if (!zoneState[playerId]) {
106
+ zoneState[playerId] = [];
107
+ }
108
+
109
+ zoneState[playerId].push(cardId);
110
+ };
111
+
112
+ /**
113
+ * Remove card from player's zone
114
+ *
115
+ * Removes the first occurrence of a card from the player's zone.
116
+ * Maintains order for ordered zones.
117
+ *
118
+ * Note: This mutates the zone state. For immutable operations,
119
+ * consider using @drmxrcy/tcg-core's removeCard with Zone objects.
120
+ *
121
+ * @param zoneState - The zone state to modify
122
+ * @param playerId - The player whose zone to remove from
123
+ * @param cardId - The card to remove
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * removeCardFromZone(handZone, playerId, cardId);
128
+ * ```
129
+ */
130
+ export const removeCardFromZone = (
131
+ zoneState: ZoneState,
132
+ playerId: PlayerId,
133
+ cardId: CardId,
134
+ ): void => {
135
+ if (!zoneState[playerId]) {
136
+ return;
137
+ }
138
+
139
+ const index = zoneState[playerId].indexOf(cardId);
140
+ if (index !== -1) {
141
+ zoneState[playerId].splice(index, 1);
142
+ }
143
+ };
144
+
145
+ /**
146
+ * Move card between zones
147
+ *
148
+ * Removes card from source zone and adds to destination zone.
149
+ * This is the standard way to transition cards between zones.
150
+ *
151
+ * Rule 8.1: Zones are separate from one another
152
+ * Rule 8.1.5: Cards entering private zones lose all info
153
+ *
154
+ * Note: This mutates both zone states. For immutable operations,
155
+ * consider using @drmxrcy/tcg-core's moveCard with Zone objects.
156
+ *
157
+ * @param sourceZone - Zone to remove card from
158
+ * @param destZone - Zone to add card to
159
+ * @param playerId - The player whose zones to use
160
+ * @param cardId - The card to move
161
+ *
162
+ * @example
163
+ * ```typescript
164
+ * // Draw card (deck -> hand)
165
+ * moveCardBetweenZones(deckZone, handZone, playerId, topCard);
166
+ *
167
+ * // Play card (hand -> play)
168
+ * moveCardBetweenZones(handZone, playZone, playerId, cardId);
169
+ *
170
+ * // Banish (play -> discard)
171
+ * moveCardBetweenZones(playZone, discardZone, playerId, cardId);
172
+ * ```
173
+ */
174
+ export const moveCardBetweenZones = (
175
+ sourceZone: ZoneState,
176
+ destZone: ZoneState,
177
+ playerId: PlayerId,
178
+ cardId: CardId,
179
+ ): void => {
180
+ removeCardFromZone(sourceZone, playerId, cardId);
181
+ addCardToZone(destZone, playerId, cardId);
182
+ };
183
+
184
+ /**
185
+ * Check if card is in player's zone
186
+ *
187
+ * Uses array.includes which is optimized by JavaScript engines.
188
+ * For checking across multiple zones, see @drmxrcy/tcg-core's findCardInZones.
189
+ *
190
+ * @param zoneState - The zone state to check
191
+ * @param playerId - The player whose zone to check
192
+ * @param cardId - The card to look for
193
+ * @returns True if card is in the player's zone
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * if (isCardInZone(handZone, playerId, cardId)) {
198
+ * // Card is in hand
199
+ * }
200
+ * ```
201
+ */
202
+ export const isCardInZone = (
203
+ zoneState: ZoneState,
204
+ playerId: PlayerId,
205
+ cardId: CardId,
206
+ ): boolean => {
207
+ const playerZone = zoneState[playerId];
208
+ if (!playerZone) {
209
+ return false;
210
+ }
211
+
212
+ return playerZone.includes(cardId);
213
+ };
214
+
215
+ /**
216
+ * Get all cards in player's zone
217
+ *
218
+ * Returns a copy of the card array to prevent external modification.
219
+ * For ordered zones, maintains the card sequence.
220
+ *
221
+ * @param zoneState - The zone state to query
222
+ * @param playerId - The player whose cards to get
223
+ * @returns Array of card IDs (copy)
224
+ *
225
+ * @example
226
+ * ```typescript
227
+ * const handCards = getCardsInZone(handZone, playerId);
228
+ * const deckSize = getCardsInZone(deckZone, playerId).length;
229
+ * ```
230
+ */
231
+ export const getCardsInZone = (
232
+ zoneState: ZoneState,
233
+ playerId: PlayerId,
234
+ ): CardId[] => {
235
+ const playerZone = zoneState[playerId];
236
+ if (!playerZone) {
237
+ return [];
238
+ }
239
+
240
+ // Return copy to prevent external mutation
241
+ return [...playerZone];
242
+ };
243
+
244
+ /**
245
+ * Get number of cards in player's zone
246
+ *
247
+ * Rule 8.1.2, 8.1.3: Players can count cards in any zone
248
+ *
249
+ * @param zoneState - The zone state to query
250
+ * @param playerId - The player whose cards to count
251
+ * @returns Number of cards in zone
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * const handSize = getZoneSize(handZone, playerId);
256
+ * const deckSize = getZoneSize(deckZone, playerId);
257
+ * ```
258
+ */
259
+ export const getZoneSize = (
260
+ zoneState: ZoneState,
261
+ playerId: PlayerId,
262
+ ): number => {
263
+ return getCardsInZone(zoneState, playerId).length;
264
+ };
265
+
266
+ /**
267
+ * Get top card from ordered zone
268
+ *
269
+ * For zones like deck and discard where order matters.
270
+ * Returns undefined if zone is empty.
271
+ *
272
+ * Rule 8.2.3: Draw from top of deck
273
+ *
274
+ * @param zoneState - The zone state to query
275
+ * @param playerId - The player whose zone to check
276
+ * @returns Top card ID or undefined
277
+ *
278
+ * @example
279
+ * ```typescript
280
+ * const topCard = getTopCard(deckZone, playerId);
281
+ * if (topCard) {
282
+ * // Draw the top card
283
+ * moveCardBetweenZones(deckZone, handZone, playerId, topCard);
284
+ * }
285
+ * ```
286
+ */
287
+ export const getTopCard = (
288
+ zoneState: ZoneState,
289
+ playerId: PlayerId,
290
+ ): CardId | undefined => {
291
+ const cards = getCardsInZone(zoneState, playerId);
292
+ return cards[0]; // First card is top
293
+ };
294
+
295
+ /**
296
+ * Clear all cards from player's zone
297
+ *
298
+ * Used for game cleanup or reset scenarios.
299
+ *
300
+ * Note: This mutates the zone state. For immutable operations,
301
+ * consider using @drmxrcy/tcg-core's clearZone with Zone objects.
302
+ *
303
+ * @param zoneState - The zone state to modify
304
+ * @param playerId - The player whose zone to clear
305
+ *
306
+ * @example
307
+ * ```typescript
308
+ * clearZone(handZone, playerId);
309
+ * ```
310
+ */
311
+ export const clearZone = (zoneState: ZoneState, playerId: PlayerId): void => {
312
+ if (zoneState[playerId]) {
313
+ zoneState[playerId] = [];
314
+ }
315
+ };
316
+
317
+ /**
318
+ * Add card to top of ordered zone
319
+ *
320
+ * For zones like deck where adding to top matters.
321
+ *
322
+ * Rule 8.2.4: Cards added to top/bottom in known order
323
+ *
324
+ * Note: This mutates the zone state. For immutable operations,
325
+ * consider using @drmxrcy/tcg-core's addCardToTop with Zone objects.
326
+ *
327
+ * @param zoneState - The zone state to modify
328
+ * @param playerId - The player whose zone to add to
329
+ * @param cardId - The card to add
330
+ *
331
+ * @example
332
+ * ```typescript
333
+ * // Put card on top of deck
334
+ * addCardToTop(deckZone, playerId, cardId);
335
+ * ```
336
+ */
337
+ export const addCardToTop = (
338
+ zoneState: ZoneState,
339
+ playerId: PlayerId,
340
+ cardId: CardId,
341
+ ): void => {
342
+ if (!zoneState[playerId]) {
343
+ zoneState[playerId] = [];
344
+ }
345
+
346
+ zoneState[playerId].unshift(cardId); // Add to front
347
+ };
348
+
349
+ /**
350
+ * Add card to bottom of ordered zone
351
+ *
352
+ * For zones like deck where adding to bottom matters.
353
+ *
354
+ * Rule 8.2.4: Cards added to top/bottom in known order
355
+ * Rule 3.1.6.1: Mulligan cards go to bottom
356
+ *
357
+ * Note: This mutates the zone state. For immutable operations,
358
+ * consider using @drmxrcy/tcg-core's addCardToBottom with Zone objects.
359
+ *
360
+ * @param zoneState - The zone state to modify
361
+ * @param playerId - The player whose zone to add to
362
+ * @param cardId - The card to add
363
+ *
364
+ * @example
365
+ * ```typescript
366
+ * // Put card on bottom of deck (mulligan)
367
+ * addCardToBottom(deckZone, playerId, cardId);
368
+ * ```
369
+ */
370
+ export const addCardToBottom = (
371
+ zoneState: ZoneState,
372
+ playerId: PlayerId,
373
+ cardId: CardId,
374
+ ): void => {
375
+ if (!zoneState[playerId]) {
376
+ zoneState[playerId] = [];
377
+ }
378
+
379
+ zoneState[playerId].push(cardId); // Add to end
380
+ };
381
+
382
+ /**
383
+ * Migration Note
384
+ *
385
+ * The flat ZoneState helpers above (createZoneState, addCardToZone, etc.) are maintained
386
+ * for backward compatibility. They use mutable operations suitable for use with Immer.
387
+ *
388
+ * For new code, prefer the immutable Zone objects from @drmxrcy/tcg-core (re-exported above).
389
+ * Core Zone objects provide:
390
+ * - Immutability by default
391
+ * - Rich zone operations (shuffle, draw, mill, peek, search)
392
+ * - Visibility filtering
393
+ * - Better type safety
394
+ *
395
+ * Example migration:
396
+ * ```typescript
397
+ * // Old (flat ZoneState - mutable)
398
+ * const handZone: ZoneState = { [player1]: [], [player2]: [] };
399
+ * addCardToZone(handZone, player1, cardId);
400
+ *
401
+ * // New (core Zone - immutable)
402
+ * let handZone = createZone({ id: "hand", name: "Hand", visibility: "private", owner: player1 });
403
+ * handZone = addCard(handZone, cardId);
404
+ * ```
405
+ */
@@ -0,0 +1,59 @@
1
+ import type { CardZoneConfig, ZoneId } from "@drmxrcy/tcg-core";
2
+
3
+ /**
4
+ * Lorcana Zone Configurations
5
+ *
6
+ * Defines all zones where cards can exist during the game:
7
+ * - deck: Player's library of cards
8
+ * - hand: Cards available to play
9
+ * - inkwell: Resource zone for paying costs
10
+ * - play: Active cards in play
11
+ * - discard: Graveyard for used/destroyed cards
12
+ */
13
+ export const lorcanaZones: Record<string, CardZoneConfig> = {
14
+ deck: {
15
+ id: "deck" as ZoneId,
16
+ name: "zones.deck",
17
+ visibility: "secret",
18
+ ordered: true,
19
+ owner: undefined,
20
+ faceDown: true,
21
+ maxSize: 60,
22
+ },
23
+ hand: {
24
+ id: "hand" as ZoneId,
25
+ name: "zones.hand",
26
+ visibility: "private",
27
+ ordered: false,
28
+ owner: undefined,
29
+ faceDown: false,
30
+ maxSize: undefined,
31
+ },
32
+ inkwell: {
33
+ id: "inkwell" as ZoneId,
34
+ name: "zones.inkwell",
35
+ visibility: "public",
36
+ ordered: false,
37
+ owner: undefined,
38
+ faceDown: true,
39
+ maxSize: undefined,
40
+ },
41
+ play: {
42
+ id: "play" as ZoneId,
43
+ name: "zones.play",
44
+ visibility: "public",
45
+ ordered: false,
46
+ owner: undefined,
47
+ faceDown: false,
48
+ maxSize: undefined,
49
+ },
50
+ discard: {
51
+ id: "discard" as ZoneId,
52
+ name: "zones.discard",
53
+ visibility: "public",
54
+ ordered: false,
55
+ owner: undefined,
56
+ faceDown: false,
57
+ maxSize: undefined,
58
+ },
59
+ };