@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,374 @@
1
+ /**
2
+ * Composable Move Validators
3
+ *
4
+ * Reusable validation functions for Lorcana moves.
5
+ * Each validator is a pure predicate that can be composed with others.
6
+ *
7
+ * Design principles:
8
+ * - Small, focused validators
9
+ * - Composable via &&, ||, !
10
+ * - Testable in isolation
11
+ * - Type-safe with proper context
12
+ */
13
+
14
+ import type { CardId, MoveContext, PlayerId, ZoneId } from "@drmxrcy/tcg-core";
15
+ import type { LorcanaCardMeta, LorcanaGameState } from "../types";
16
+
17
+ /**
18
+ * Validator function type
19
+ *
20
+ * Matches the condition signature from GameMoveDefinition.
21
+ * Takes state and context, returns boolean indicating validity.
22
+ */
23
+ export type Validator<TParams = unknown> = (
24
+ state: LorcanaGameState,
25
+ context: MoveContext<TParams, LorcanaCardMeta>,
26
+ ) => boolean;
27
+
28
+ /**
29
+ * Parameterized validator type
30
+ *
31
+ * Takes arguments and returns a validator function.
32
+ * Allows validators to be configured per-move.
33
+ */
34
+ export type ParameterizedValidator<
35
+ TArgs extends unknown[],
36
+ TParams = unknown,
37
+ > = (...args: TArgs) => Validator<TParams>;
38
+
39
+ // ===== Phase/Flow Validators =====
40
+
41
+ /**
42
+ * Validate current phase
43
+ *
44
+ * @param expectedPhase - Required phase name
45
+ * @returns Validator that checks if current phase matches
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * condition: isPhase("main")
50
+ * ```
51
+ */
52
+ export const isPhase =
53
+ <TParams = unknown>(expectedPhase: string): Validator<TParams> =>
54
+ (_state, context) => {
55
+ return context.flow?.currentPhase === expectedPhase;
56
+ };
57
+
58
+ /**
59
+ * Validate it's the main phase specifically
60
+ *
61
+ * Most Lorcana actions require main phase.
62
+ */
63
+ export const isMainPhase = <TParams = unknown>(): Validator<TParams> =>
64
+ isPhase<TParams>("main");
65
+
66
+ // ===== Card Location Validators =====
67
+
68
+ /**
69
+ * Validate card is in a specific zone
70
+ *
71
+ * @param cardId - Card to check
72
+ * @param zoneId - Expected zone
73
+ * @returns Validator that checks card location
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * condition: (state, context) =>
78
+ * cardInZone(context.params.cardId, "hand")(context)
79
+ * ```
80
+ */
81
+ export const cardInZone =
82
+ <TParams = unknown>(cardId: CardId, zoneId: ZoneId): Validator<TParams> =>
83
+ (_state, context) => {
84
+ return context.zones.getCardZone(cardId) === zoneId;
85
+ };
86
+
87
+ /**
88
+ * Validate card is in hand
89
+ */
90
+ export const cardInHand = <TParams = unknown>(
91
+ cardId: CardId,
92
+ ): Validator<TParams> => cardInZone(cardId, "hand" as ZoneId);
93
+
94
+ /**
95
+ * Validate card is in play
96
+ */
97
+ export const cardInPlay = <TParams = unknown>(
98
+ cardId: CardId,
99
+ ): Validator<TParams> => cardInZone(cardId, "play" as ZoneId);
100
+
101
+ /**
102
+ * Validate card exists in any zone
103
+ *
104
+ * @param cardId - Card to check
105
+ * @returns Validator that checks if card exists
106
+ */
107
+ export const cardExists =
108
+ <TParams = unknown>(cardId: CardId): Validator<TParams> =>
109
+ (_state, context) => {
110
+ return context.zones.getCardZone(cardId) !== undefined;
111
+ };
112
+
113
+ // ===== Ownership Validators =====
114
+
115
+ /**
116
+ * Validate card is owned by the current player
117
+ *
118
+ * @param cardId - Card to check
119
+ * @returns Validator that checks ownership
120
+ *
121
+ * @example
122
+ * ```typescript
123
+ * condition: (state, context) =>
124
+ * cardOwnedByPlayer(context.params.cardId)(context)
125
+ * ```
126
+ */
127
+ export const cardOwnedByPlayer =
128
+ <TParams = unknown>(cardId: CardId): Validator<TParams> =>
129
+ (_state, context) => {
130
+ return context.cards.getCardOwner(cardId) === context.playerId;
131
+ };
132
+
133
+ /**
134
+ * Validate card is owned by a specific player
135
+ */
136
+ export const cardOwnedBy =
137
+ <TParams = unknown>(cardId: CardId, playerId: PlayerId): Validator<TParams> =>
138
+ (_state, context) => {
139
+ return context.cards.getCardOwner(cardId) === playerId;
140
+ };
141
+
142
+ // ===== Tracker Validators =====
143
+
144
+ /**
145
+ * Validate action has not been used this turn
146
+ *
147
+ * @param actionName - Tracker name to check
148
+ * @param playerId - Optional player ID (defaults to current player)
149
+ * @returns Validator that checks tracker
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * condition: hasNotUsedAction("hasInked")
154
+ * ```
155
+ */
156
+ export const hasNotUsedAction =
157
+ <TParams = unknown>(
158
+ actionName: string,
159
+ playerId?: PlayerId,
160
+ ): Validator<TParams> =>
161
+ (_state, context) => {
162
+ const player = playerId ?? context.playerId;
163
+ return !context.trackers?.check(actionName, player);
164
+ };
165
+
166
+ /**
167
+ * Validate action has been used
168
+ */
169
+ export const hasUsedAction =
170
+ <TParams = unknown>(
171
+ actionName: string,
172
+ playerId?: PlayerId,
173
+ ): Validator<TParams> =>
174
+ (_state, context) => {
175
+ const player = playerId ?? context.playerId;
176
+ return context.trackers?.check(actionName, player) ?? false;
177
+ };
178
+
179
+ // ===== Card State Validators =====
180
+
181
+ /**
182
+ * Validate card is not exerted (ready)
183
+ *
184
+ * @param cardId - Card to check
185
+ * @returns Validator that checks exerted status
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * condition: (state, context) =>
190
+ * cardIsReady(context.params.cardId)(context)
191
+ * ```
192
+ */
193
+ export const cardIsReady =
194
+ <TParams = unknown>(cardId: CardId): Validator<TParams> =>
195
+ (_state, context) => {
196
+ const meta = context.cards.getCardMeta(cardId);
197
+ return meta?.state === "ready";
198
+ };
199
+
200
+ /**
201
+ * Validate card is exerted
202
+ */
203
+ export const cardIsExerted =
204
+ <TParams = unknown>(cardId: CardId): Validator<TParams> =>
205
+ (_state, context) => {
206
+ const meta = context.cards.getCardMeta(cardId);
207
+ return meta?.state === "exerted";
208
+ };
209
+
210
+ /**
211
+ * Validate card is "dry" (not played this turn)
212
+ *
213
+ * Rule 4.2.2.1: Characters are "drying" the turn they're played
214
+ * Rule 6.1.4: Must be dry to quest, challenge, or exert
215
+ *
216
+ * @param cardId - Card to check
217
+ * @returns Validator that checks if card is dry
218
+ */
219
+ export const cardIsDry =
220
+ <TParams = unknown>(cardId: CardId): Validator<TParams> =>
221
+ (_state, context) => {
222
+ const meta = context.cards.getCardMeta(cardId);
223
+ return !meta?.isDrying;
224
+ };
225
+
226
+ /**
227
+ * Validate card was played this turn (is drying)
228
+ */
229
+ export const cardIsDrying =
230
+ <TParams = unknown>(cardId: CardId): Validator<TParams> =>
231
+ (_state, context) => {
232
+ const meta = context.cards.getCardMeta(cardId);
233
+ return meta?.isDrying ?? false;
234
+ };
235
+
236
+ // ===== Composite Validators =====
237
+
238
+ /**
239
+ * Validate card can quest
240
+ *
241
+ * Requirements (Rule 4.3.5, Rule 6.1.4):
242
+ * - Card is in play
243
+ * - Card is owned by current player
244
+ * - Card is ready (not exerted)
245
+ * - Card is dry (not played this turn)
246
+ * - Card hasn't quested this turn
247
+ *
248
+ * @param cardId - Card to check
249
+ * @returns Validator that checks all quest requirements
250
+ *
251
+ * @example
252
+ * ```typescript
253
+ * condition: (state, context) =>
254
+ * canQuest(context.params.cardId)(context)
255
+ * ```
256
+ */
257
+ export const canQuest =
258
+ <TParams = unknown>(cardId: CardId): Validator<TParams> =>
259
+ (state, context) => {
260
+ return (
261
+ cardInPlay(cardId)(state, context) &&
262
+ cardOwnedByPlayer(cardId)(state, context) &&
263
+ cardIsReady(cardId)(state, context) &&
264
+ cardIsDry(cardId)(state, context) &&
265
+ hasNotUsedAction(`quested:${cardId}`)(state, context)
266
+ );
267
+ };
268
+
269
+ /**
270
+ * Validate card can challenge
271
+ *
272
+ * Requirements (Rule 4.3.6, Rule 6.1.4):
273
+ * - Card is in play
274
+ * - Card is owned by current player
275
+ * - Card is ready (not exerted)
276
+ * - Card is dry (not played this turn)
277
+ *
278
+ * @param cardId - Card to check
279
+ * @returns Validator that checks all challenge requirements
280
+ */
281
+ export const canChallenge =
282
+ <TParams = unknown>(cardId: CardId): Validator<TParams> =>
283
+ (state, context) => {
284
+ return (
285
+ cardInPlay(cardId)(state, context) &&
286
+ cardOwnedByPlayer(cardId)(state, context) &&
287
+ cardIsReady(cardId)(state, context) &&
288
+ cardIsDry(cardId)(state, context)
289
+ );
290
+ };
291
+
292
+ /**
293
+ * Validate card can be used to pay costs (e.g., singing, abilities)
294
+ *
295
+ * Requirements (Rule 6.1.4):
296
+ * - Card is in play
297
+ * - Card is owned by current player
298
+ * - Card is ready (not exerted)
299
+ * - Card is dry (not played this turn)
300
+ *
301
+ * @param cardId - Card to check
302
+ * @returns Validator that checks if card can pay costs
303
+ */
304
+ export const canPayCosts =
305
+ <TParams = unknown>(cardId: CardId): Validator<TParams> =>
306
+ (state, context) => {
307
+ return (
308
+ cardInPlay(cardId)(state, context) &&
309
+ cardOwnedByPlayer(cardId)(state, context) &&
310
+ cardIsReady(cardId)(state, context) &&
311
+ cardIsDry(cardId)(state, context)
312
+ );
313
+ };
314
+
315
+ // ===== Logical Combinators =====
316
+
317
+ /**
318
+ * Combine validators with AND logic
319
+ *
320
+ * @param validators - Validators to combine
321
+ * @returns Validator that checks all validators
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * condition: and(
326
+ * isMainPhase(),
327
+ * (context) => cardInHand(context.params.cardId)(context),
328
+ * (context) => hasNotUsedAction("hasInked")()(context)
329
+ * )
330
+ * ```
331
+ */
332
+ export const and =
333
+ <TParams = unknown>(
334
+ ...validators: Validator<TParams>[]
335
+ ): Validator<TParams> =>
336
+ (state, context) => {
337
+ return validators.every((validator) => validator(state, context));
338
+ };
339
+
340
+ /**
341
+ * Combine validators with OR logic
342
+ */
343
+ export const or =
344
+ <TParams = unknown>(
345
+ ...validators: Validator<TParams>[]
346
+ ): Validator<TParams> =>
347
+ (state, context) => {
348
+ return validators.some((validator) => validator(state, context));
349
+ };
350
+
351
+ /**
352
+ * Negate a validator
353
+ */
354
+ export const not =
355
+ <TParams = unknown>(validator: Validator<TParams>): Validator<TParams> =>
356
+ (state, context) => {
357
+ return !validator(state, context);
358
+ };
359
+
360
+ /**
361
+ * Always returns true (no-op validator)
362
+ */
363
+ export const always =
364
+ <TParams = unknown>(): Validator<TParams> =>
365
+ () =>
366
+ true;
367
+
368
+ /**
369
+ * Always returns false (deny-all validator)
370
+ */
371
+ export const never =
372
+ <TParams = unknown>(): Validator<TParams> =>
373
+ () =>
374
+ false;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Card State (Section 5.1)
3
+ *
4
+ * Card states in Lorcana:
5
+ * - Ready/Exerted (5.1.1-5.1.2)
6
+ * - Damaged/Undamaged (5.1.3-5.1.4)
7
+ * - Stack relationships (5.1.5-5.1.7) for Shift
8
+ * - Drying state (summoning sickness)
9
+ */
10
+
11
+ import type { CardId } from "@drmxrcy/tcg-core";
12
+ import type {
13
+ CardReadyState,
14
+ LorcanaCardMeta,
15
+ StackPosition,
16
+ } from "../types/game-state";
17
+
18
+ /**
19
+ * Runtime state for a card instance in play
20
+ * Combines the stored metadata with the instance ID for logical operations
21
+ */
22
+ export type CardInstanceState = LorcanaCardMeta & {
23
+ /** Unique instance ID */
24
+ cardId: CardId;
25
+ };
26
+
27
+ /**
28
+ * Create default card instance state for a newly played card
29
+ */
30
+ export function createCardInstanceState(
31
+ cardId: CardId,
32
+ options?: {
33
+ isDrying?: boolean;
34
+ state?: CardReadyState;
35
+ },
36
+ ): CardInstanceState {
37
+ return {
38
+ cardId,
39
+ state: options?.state ?? "ready",
40
+ damage: 0,
41
+ isDrying: options?.isDrying ?? true, // Characters start drying
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Check if a card is ready
47
+ */
48
+ export function isReady(cardState: CardInstanceState): boolean {
49
+ return cardState.state === "ready";
50
+ }
51
+
52
+ /**
53
+ * Check if a card is exerted
54
+ */
55
+ export function isExerted(cardState: CardInstanceState): boolean {
56
+ return cardState.state === "exerted";
57
+ }
58
+
59
+ /**
60
+ * Check if a card is drying (has summoning sickness)
61
+ */
62
+ export function isDrying(cardState: CardInstanceState): boolean {
63
+ return cardState.isDrying;
64
+ }
65
+
66
+ /**
67
+ * Check if a card is dry (no summoning sickness)
68
+ */
69
+ export function isDry(cardState: CardInstanceState): boolean {
70
+ return !cardState.isDrying;
71
+ }
72
+
73
+ /**
74
+ * Check if a card is damaged (has 1+ damage)
75
+ */
76
+ export function isDamaged(cardState: CardInstanceState): boolean {
77
+ return cardState.damage > 0;
78
+ }
79
+
80
+ /**
81
+ * Get current damage on a card
82
+ */
83
+ export function getDamage(cardState: CardInstanceState): number {
84
+ return cardState.damage;
85
+ }
86
+
87
+ /**
88
+ * Check if card is in a stack (shifted)
89
+ */
90
+ export function isInStack(cardState: CardInstanceState): boolean {
91
+ return cardState.stackPosition !== undefined;
92
+ }
93
+
94
+ /**
95
+ * Check if card is the top of a stack
96
+ */
97
+ export function isTopOfStack(cardState: CardInstanceState): boolean {
98
+ const stack = cardState.stackPosition;
99
+ return stack !== undefined && !stack.isUnder;
100
+ }
101
+
102
+ /**
103
+ * Check if card is under another card in a stack
104
+ */
105
+ export function isUnderCard(cardState: CardInstanceState): boolean {
106
+ const stack = cardState.stackPosition;
107
+ return stack !== undefined && stack.isUnder;
108
+ }
109
+
110
+ /**
111
+ * Exert a card (turn sideways)
112
+ */
113
+ export function exertCard(cardState: CardInstanceState): CardInstanceState {
114
+ return { ...cardState, state: "exerted" };
115
+ }
116
+
117
+ /**
118
+ * Ready a card (turn upright)
119
+ */
120
+ export function readyCard(cardState: CardInstanceState): CardInstanceState {
121
+ return { ...cardState, state: "ready" };
122
+ }
123
+
124
+ /**
125
+ * Add damage to a card
126
+ */
127
+ export function addDamage(
128
+ cardState: CardInstanceState,
129
+ amount: number,
130
+ ): CardInstanceState {
131
+ if (amount < 0) return cardState;
132
+ return { ...cardState, damage: cardState.damage + amount };
133
+ }
134
+
135
+ /**
136
+ * Remove damage from a card
137
+ */
138
+ export function removeDamage(
139
+ cardState: CardInstanceState,
140
+ amount: number,
141
+ ): CardInstanceState {
142
+ if (amount < 0) return cardState;
143
+ return { ...cardState, damage: Math.max(0, cardState.damage - amount) };
144
+ }
145
+
146
+ /**
147
+ * Set damage to a specific value
148
+ */
149
+ export function setDamage(
150
+ cardState: CardInstanceState,
151
+ damage: number,
152
+ ): CardInstanceState {
153
+ return { ...cardState, damage: Math.max(0, damage) };
154
+ }
155
+
156
+ /**
157
+ * Set drying state
158
+ */
159
+ export function setDrying(
160
+ cardState: CardInstanceState,
161
+ isDrying: boolean,
162
+ ): CardInstanceState {
163
+ return { ...cardState, isDrying };
164
+ }
165
+
166
+ /**
167
+ * Clear drying state (character becomes dry)
168
+ */
169
+ export function clearDrying(cardState: CardInstanceState): CardInstanceState {
170
+ return { ...cardState, isDrying: false };
171
+ }
172
+
173
+ /**
174
+ * Set the location a character is at
175
+ */
176
+ export function setAtLocation(
177
+ cardState: CardInstanceState,
178
+ locationId: CardId | undefined,
179
+ ): CardInstanceState {
180
+ return { ...cardState, atLocationId: locationId };
181
+ }
182
+
183
+ /**
184
+ * Create a stack by shifting onto a target
185
+ * The new card goes on top, target goes underneath
186
+ */
187
+ export function createStack(
188
+ topCardState: CardInstanceState,
189
+ underneathCardState: CardInstanceState,
190
+ ): {
191
+ topCard: CardInstanceState;
192
+ underneathCard: CardInstanceState;
193
+ } {
194
+ const topCard: CardInstanceState = {
195
+ ...topCardState,
196
+ // Damage carries over from underneath card (Rule 10.8.x)
197
+ damage: underneathCardState.damage,
198
+ // Shifted card is ready regardless of what's underneath
199
+ state: "ready",
200
+ isDrying: false, // Shifted cards are not drying
201
+ stackPosition: {
202
+ isUnder: false,
203
+ cardsUnderneath: [underneathCardState.cardId],
204
+ },
205
+ };
206
+
207
+ const underneathCard: CardInstanceState = {
208
+ ...underneathCardState,
209
+ stackPosition: {
210
+ isUnder: true,
211
+ topCardId: topCardState.cardId,
212
+ },
213
+ };
214
+
215
+ return { topCard, underneathCard };
216
+ }
217
+
218
+ /**
219
+ * Get all cards in a stack (including top card)
220
+ */
221
+ export function getStackCardIds(cardState: CardInstanceState): CardId[] {
222
+ if (!cardState.stackPosition) {
223
+ return [cardState.cardId];
224
+ }
225
+
226
+ if (cardState.stackPosition.isUnder) {
227
+ // This is an underneath card - return just this card
228
+ // The full stack should be queried from the top card
229
+ return [cardState.cardId];
230
+ }
231
+
232
+ // This is the top card
233
+ return [cardState.cardId, ...(cardState.stackPosition.cardsUnderneath ?? [])];
234
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Zones Module
3
+ *
4
+ * Zone configuration and card state management for Lorcana.
5
+ */
6
+
7
+ // Card state
8
+ export {
9
+ addDamage,
10
+ type CardInstanceState,
11
+ clearDrying,
12
+ createCardInstanceState,
13
+ createStack,
14
+ exertCard,
15
+ getDamage,
16
+ getStackCardIds,
17
+ isDamaged,
18
+ isDry,
19
+ isDrying,
20
+ isExerted,
21
+ isInStack,
22
+ isReady,
23
+ isTopOfStack,
24
+ isUnderCard,
25
+ readyCard,
26
+ removeDamage,
27
+ setAtLocation,
28
+ setDamage,
29
+ setDrying,
30
+ } from "./card-state";
31
+ // Zone configuration
32
+ export {
33
+ areCardsVisibleIn,
34
+ getZoneConfig,
35
+ isLorcanaZoneId,
36
+ isZoneVisibleTo,
37
+ LORCANA_ZONES,
38
+ type LorcanaZoneId,
39
+ ZONE_IDS,
40
+ type ZoneConfig,
41
+ type ZoneVisibility,
42
+ } from "./zone-config";