@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,216 @@
1
+ import { createPlayerId, type FlowDefinition } from "@drmxrcy/tcg-core";
2
+ import type { LorcanaCardMeta, LorcanaGameState } from "../../types";
3
+
4
+ /**
5
+ * Lorcana Turn Flow
6
+ *
7
+ * Defines the sequence of game segments and phases:
8
+ *
9
+ * Game Segments:
10
+ * 1. Starting a Game - Choose first player and mulligan
11
+ * 2. Main Game - Normal gameplay with turns
12
+ *
13
+ * Turn Phases (Main Game):
14
+ * 1. Beginning Phase - Start of turn, ready all cards
15
+ * 2. Main Phase - Play cards, quest, challenge
16
+ * 3. End Phase - End of turn cleanup
17
+ *
18
+ * The engine automatically handles phase transitions and turn management.
19
+ */
20
+ export const lorcanaFlow: FlowDefinition<LorcanaGameState, LorcanaCardMeta> = {
21
+ initialGameSegment: "startingAGame",
22
+ gameSegments: {
23
+ /**
24
+ * Starting a Game Segment
25
+ *
26
+ * Rule 3.1: Starting a game
27
+ * - Choose who goes first (Rule 3.1.1)
28
+ * - Mulligan phase (Rule 3.1.6)
29
+ */
30
+ startingAGame: {
31
+ order: 0,
32
+ next: "mainGame",
33
+ turn: {
34
+ initialPhase: "chooseFirstPlayer",
35
+ onBegin: (context) => {
36
+ // Set currentPlayer to choosingFirstPlayer for priority
37
+ // During startingAGame, there is no "turn player" yet
38
+ // but there IS a priority player who can take actions
39
+ const chooser = context.game.getChoosingFirstPlayer();
40
+ if (chooser) {
41
+ context.setCurrentPlayer(String(chooser));
42
+ }
43
+ },
44
+ phases: {
45
+ /**
46
+ * Choose First Player Phase
47
+ *
48
+ * Rule 3.1.1: First player determined randomly
49
+ * In practice, decided by players (rock-paper-scissors, dice roll, etc.)
50
+ *
51
+ * Manual transition: The move itself will call context.flow.endPhase()
52
+ */
53
+ chooseFirstPlayer: {
54
+ order: 1,
55
+ next: "mulligan",
56
+ // Manual transition via move - always return false
57
+ // The move itself calls context.flow.endPhase()
58
+ endIf: (context) => context.game.getOTP() !== undefined,
59
+ onEnd: (context) => {
60
+ // After OTP is chosen, set currentPlayer to OTP for mulligan phase
61
+ const otp = context.game.getOTP();
62
+ if (otp) {
63
+ context.setCurrentPlayer(String(otp));
64
+ }
65
+ },
66
+ },
67
+
68
+ /**
69
+ * Mulligan Phase
70
+ *
71
+ * Rule 3.1.6: Players may mulligan by putting cards
72
+ * on bottom of deck and redrawing
73
+ */
74
+ mulligan: {
75
+ order: 2,
76
+ next: undefined, // Transitions to mainGame segment
77
+ onBegin: (context) => {
78
+ // Priority starts with OTP for mulligan
79
+ // Each player will mulligan in turn order
80
+ const otp = context.game.getOTP();
81
+ if (otp) {
82
+ context.setCurrentPlayer(String(otp));
83
+ }
84
+ },
85
+ // Advance when all players have completed mulligan
86
+ // The move itself will call context.flow.endPhase()
87
+ // So this always returns false to wait for manual transition
88
+ endIf: (context) => {
89
+ if (context.getCurrentPhase() === "mulligan") {
90
+ return context.game.getPendingMulligan().length === 0;
91
+ }
92
+
93
+ return false;
94
+ },
95
+ // When this phase ends, transition to mainGame segment
96
+ onEnd: (context) => {
97
+ context.endGameSegment("startingAGame");
98
+ },
99
+ },
100
+ },
101
+ },
102
+ },
103
+
104
+ /**
105
+ * Main Game Segment
106
+ *
107
+ * Normal gameplay with beginning, main, and end phases.
108
+ */
109
+ mainGame: {
110
+ order: 1,
111
+ // No next segment - game ends when this segment ends
112
+ turn: {
113
+ initialPhase: "beginning",
114
+ onBegin: (context) => {
115
+ // Switch to next player at start of each turn
116
+ // In a 2-player game, alternate between players
117
+ const currentPlayer = context.getCurrentPlayer();
118
+ const otp = context.game.getOTP();
119
+
120
+ if (currentPlayer && otp) {
121
+ // Alternate players (assumes 2-player game)
122
+ // TODO: Support N-player games with proper turn order
123
+ const playerIds = [String(otp)];
124
+ // Get the other player (not OTP)
125
+ // This is a simplification for 2-player games
126
+ // In production, you'd have a player list to iterate through
127
+ const otpStr = String(otp);
128
+
129
+ // For now, just toggle between two players based on turn number
130
+ // If turn is odd, OTP plays; if even, other player plays
131
+ const turnNum = context.getTurnNumber();
132
+ // This assumes OTP is player_one - needs improvement for robustness
133
+ context.setCurrentPlayer(
134
+ turnNum % 2 === 1
135
+ ? otpStr
136
+ : otpStr === "player_one"
137
+ ? "player_two"
138
+ : "player_one",
139
+ );
140
+ } else {
141
+ // First turn - set to OTP
142
+ if (otp) {
143
+ context.setCurrentPlayer(String(otp));
144
+ }
145
+ }
146
+ },
147
+ phases: {
148
+ /**
149
+ * Beginning Phase
150
+ * - Ready all exhausted cards
151
+ * - Draw a card (if not first turn)
152
+ * - Automatically advances to Main phase
153
+ */
154
+ beginning: {
155
+ order: 1,
156
+ next: "main",
157
+ onBegin: (context) => {
158
+ // Ready all cards for the current player
159
+ const currentPlayer = context.getCurrentPlayer();
160
+ if (!currentPlayer) return;
161
+
162
+ // Get all cards owned by current player
163
+ const playZone = context.zones.getCardsInZone(
164
+ "play" as any,
165
+ createPlayerId(currentPlayer),
166
+ );
167
+
168
+ // Ready each card (clear exerted status and summoning sickness)
169
+ for (const cardId of playZone) {
170
+ const meta = context.cards.getCardMeta(cardId);
171
+ if (meta) {
172
+ context.cards.updateCardMeta(cardId, {
173
+ state: "ready",
174
+ isDrying: false, // Clear summoning sickness
175
+ });
176
+ }
177
+ }
178
+
179
+ // TODO: Draw a card (if not first turn)
180
+ // This requires checking if it's turn 1 and drawing from deck
181
+ },
182
+ endIf: () => true, // Auto-advance
183
+ },
184
+
185
+ /**
186
+ * Main Phase
187
+ * - Player can take actions (play cards, quest, challenge)
188
+ * - Player manually ends phase by passing
189
+ */
190
+ main: {
191
+ order: 2,
192
+ next: "end",
193
+ onBegin: (_context) => {
194
+ // No automatic actions at start of main phase
195
+ },
196
+ // No endIf - player must manually pass to end phase
197
+ },
198
+
199
+ /**
200
+ * End Phase
201
+ * - Cleanup effects
202
+ * - Automatically advances to next turn (no next phase defined)
203
+ */
204
+ end: {
205
+ order: 3,
206
+ // No 'next' defined - FlowManager will call transitionToNextTurn()
207
+ onBegin: (_context) => {
208
+ // Cleanup logic could go here
209
+ },
210
+ endIf: () => true, // Auto-advance to next turn
211
+ },
212
+ },
213
+ },
214
+ },
215
+ },
216
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Lorcana Game Definition
3
+ *
4
+ * Public exports for game definition components
5
+ */
6
+
7
+ // Main game definition
8
+ export { lorcanaGameDefinition } from "./definition";
9
+ export { lorcanaFlow } from "./flow/turn-flow";
10
+ export { lorcanaMoves } from "./moves";
11
+ export { setupLorcanaGame } from "./setup/game-setup";
12
+ export { trackerConfig } from "./trackers/tracker-config";
13
+ export { checkLoreVictory } from "./win-conditions/lore-victory";
14
+ // Legacy exports (for backward compatibility)
15
+ export * from "./zone-operations";
16
+ export type {
17
+ LorcanaZoneConfig,
18
+ LorcanaZoneId,
19
+ LorcanaZoneVisibility,
20
+ } from "./zones";
21
+ export {
22
+ getZoneConfig,
23
+ isFacedownZone,
24
+ isLorcanaZoneId,
25
+ isOrderedZone,
26
+ isPrivateZone,
27
+ isPublicZone,
28
+ lorcanaZones as legacyLorcanaZones,
29
+ } from "./zones";
30
+ // Modular components (for testing and advanced use)
31
+ export { lorcanaZones } from "./zones/zone-configs";
@@ -0,0 +1,51 @@
1
+ import { createMove } from "@drmxrcy/tcg-core";
2
+ import type {
3
+ LorcanaCardMeta,
4
+ LorcanaGameState,
5
+ LorcanaMoveParams,
6
+ } from "../../../types";
7
+ import { and, isMainPhase } from "../../../validators";
8
+
9
+ /**
10
+ * Activate Ability
11
+ *
12
+ * Rule 7: Abilities with costs can be activated
13
+ *
14
+ * Process:
15
+ * 1. Look up the ability from card definition using context.registry.getCard()
16
+ * 2. Verify ability requirements are met (via conditions)
17
+ * 3. Pay the cost (exert, discard, etc.) using operations
18
+ * 4. Execute the effect
19
+ *
20
+ * TODO: Full implementation requires:
21
+ * - Ability definition system: Need to define abilities with costs and effects in card definitions
22
+ * - Cost payment system: Extend LorcanaOperations with ability cost payment (exert, discard, ink)
23
+ * - Effect execution system: Framework for executing ability effects with proper timing
24
+ * - Targeting system: Allow players to select targets for abilities
25
+ * - Validation: Ensure ability can be activated (not already used this turn, costs can be paid, etc.)
26
+ *
27
+ * Example usage once implemented:
28
+ * ```
29
+ * const card = context.registry.getCard(cardId);
30
+ * const ability = card.abilities?.find(a => a.id === abilityId);
31
+ * if (ability) {
32
+ * ops.payCost(ability.cost);
33
+ * executeEffect(ability.effect, ability.targets);
34
+ * }
35
+ * ```
36
+ */
37
+ export const activateAbility = createMove<
38
+ LorcanaGameState,
39
+ LorcanaMoveParams,
40
+ "activateAbility",
41
+ LorcanaCardMeta
42
+ >({
43
+ condition: and(isMainPhase()),
44
+ reducer: (_draft, _context) => {
45
+ // TODO: Implement ability activation logic
46
+ // This would require:
47
+ // 1. Looking up the ability from card definition
48
+ // 2. Paying the cost
49
+ // 3. Executing the effect
50
+ },
51
+ });
@@ -0,0 +1,316 @@
1
+ import { beforeEach, describe, expect, it } from "bun:test";
2
+ import { createPlayerId } from "@drmxrcy/tcg-core";
3
+ import {
4
+ LorcanaTestEngine,
5
+ PLAYER_ONE,
6
+ PLAYER_TWO,
7
+ } from "../../../../testing/lorcana-test-engine";
8
+
9
+ describe("Core Move Parameter Enumeration", () => {
10
+ describe("playCard Parameter Enumeration", () => {
11
+ let testEngine: LorcanaTestEngine;
12
+
13
+ beforeEach(() => {
14
+ // Start in main phase with cards in hand
15
+ testEngine = new LorcanaTestEngine(
16
+ { hand: 3, deck: 10 },
17
+ { hand: 3, deck: 10 },
18
+ { skipPreGame: true },
19
+ );
20
+ });
21
+
22
+ it.todo("should enumerate cards in hand as valid playCard targets", () => {
23
+ const params = testEngine.enumerateMoveParameters("playCard", PLAYER_ONE);
24
+
25
+ expect(params).not.toBeNull();
26
+ expect(params?.validCombinations).toBeDefined();
27
+ expect(params?.validCombinations.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ it.todo("should include cardId in parameter info", () => {
31
+ const params = testEngine.enumerateMoveParameters("playCard", PLAYER_ONE);
32
+
33
+ expect(params?.parameterInfo.cardId).toBeDefined();
34
+ expect(params?.parameterInfo.cardId.type).toBe("cardId");
35
+ expect(params?.parameterInfo.cardId.validValues).toBeDefined();
36
+ });
37
+
38
+ it("should return empty when no cards in hand", () => {
39
+ // Remove all cards from hand
40
+ const hand = testEngine.getZone("hand", PLAYER_ONE);
41
+ for (const cardId of hand || []) {
42
+ testEngine.moveCard(cardId, "discard", PLAYER_ONE);
43
+ }
44
+
45
+ const params = testEngine.enumerateMoveParameters("playCard", PLAYER_ONE);
46
+
47
+ if (params) {
48
+ expect(params.validCombinations).toHaveLength(0);
49
+ }
50
+ });
51
+
52
+ it("should return null when move not available", () => {
53
+ // Try during opponent's turn or wrong phase
54
+ const params = testEngine.enumerateMoveParameters(
55
+ "playCard",
56
+ PLAYER_TWO, // Not active player
57
+ );
58
+
59
+ // May return null or empty combinations depending on phase/turn rules
60
+ if (params === null) {
61
+ expect(params).toBeNull();
62
+ } else {
63
+ expect(params.validCombinations).toBeDefined();
64
+ }
65
+ });
66
+ });
67
+
68
+ describe("quest Parameter Enumeration", () => {
69
+ let testEngine: LorcanaTestEngine;
70
+
71
+ beforeEach(() => {
72
+ testEngine = new LorcanaTestEngine(
73
+ { hand: 0, deck: 10 },
74
+ { hand: 0, deck: 10 },
75
+ { skipPreGame: true },
76
+ );
77
+ });
78
+
79
+ it.todo("should enumerate ready characters as valid quest targets", () => {
80
+ const params = testEngine.enumerateMoveParameters("quest", PLAYER_ONE);
81
+
82
+ expect(params).toBeDefined();
83
+ expect(params?.parameterInfo.cardId).toBeDefined();
84
+ expect(params?.parameterInfo.cardId.type).toBe("cardId");
85
+ });
86
+
87
+ it("should exclude exerted characters", () => {
88
+ // This test would require characters in play
89
+ // For now, verify the structure
90
+ const params = testEngine.enumerateMoveParameters("quest", PLAYER_ONE);
91
+
92
+ if (params) {
93
+ expect(params.validCombinations).toBeDefined();
94
+ expect(Array.isArray(params.validCombinations)).toBe(true);
95
+ }
96
+ });
97
+
98
+ it("should return empty when no ready characters", () => {
99
+ const params = testEngine.enumerateMoveParameters("quest", PLAYER_ONE);
100
+
101
+ // With no characters in play, should have no valid combinations
102
+ if (params) {
103
+ expect(params.validCombinations).toHaveLength(0);
104
+ }
105
+ });
106
+ });
107
+
108
+ describe("challenge Parameter Enumeration", () => {
109
+ let testEngine: LorcanaTestEngine;
110
+
111
+ beforeEach(() => {
112
+ testEngine = new LorcanaTestEngine(
113
+ { hand: 0, deck: 10 },
114
+ { hand: 0, deck: 10 },
115
+ { skipPreGame: true },
116
+ );
117
+ });
118
+
119
+ it("should enumerate attacker-defender pairs", () => {
120
+ const params = testEngine.enumerateMoveParameters(
121
+ "challenge",
122
+ PLAYER_ONE,
123
+ );
124
+
125
+ expect(params).toBeDefined();
126
+ if (params) {
127
+ expect(params.parameterInfo.attackerId).toBeDefined();
128
+ expect(params.parameterInfo.defenderId).toBeDefined();
129
+ }
130
+ });
131
+
132
+ it("should have attackerId and defenderId in parameter info", () => {
133
+ const params = testEngine.enumerateMoveParameters(
134
+ "challenge",
135
+ PLAYER_ONE,
136
+ );
137
+
138
+ if (params) {
139
+ expect(params.parameterInfo.attackerId.type).toBe("cardId");
140
+ expect(params.parameterInfo.defenderId.type).toBe("cardId");
141
+ }
142
+ });
143
+
144
+ it("should return empty when no valid attackers or defenders", () => {
145
+ const params = testEngine.enumerateMoveParameters(
146
+ "challenge",
147
+ PLAYER_ONE,
148
+ );
149
+
150
+ // With no characters in play, should have no valid combinations
151
+ if (params) {
152
+ expect(params.validCombinations).toHaveLength(0);
153
+ }
154
+ });
155
+ });
156
+
157
+ describe("alterHand Parameter Enumeration", () => {
158
+ let testEngine: LorcanaTestEngine;
159
+
160
+ beforeEach(() => {
161
+ testEngine = new LorcanaTestEngine(
162
+ { hand: 7, deck: 10 },
163
+ { hand: 7, deck: 10 },
164
+ { skipPreGame: false },
165
+ );
166
+
167
+ // Execute choose first player to get to mulligan phase
168
+ const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
169
+ testEngine.changeActivePlayer(choosingPlayer || PLAYER_ONE);
170
+ testEngine.chooseWhoGoesFirst(PLAYER_ONE);
171
+ });
172
+
173
+ it.todo("should enumerate cards in hand as mulligan options", () => {
174
+ const params = testEngine.enumerateMoveParameters(
175
+ "alterHand",
176
+ PLAYER_ONE,
177
+ );
178
+
179
+ expect(params).not.toBeNull();
180
+ if (params) {
181
+ expect(params.parameterInfo.cardsToMulligan).toBeDefined();
182
+ }
183
+ });
184
+
185
+ it("should allow mulliganing 0 cards (keep all)", () => {
186
+ const params = testEngine.enumerateMoveParameters(
187
+ "alterHand",
188
+ PLAYER_ONE,
189
+ );
190
+
191
+ if (params) {
192
+ // Should include option to mulligan no cards
193
+ const keepAllOption = params.validCombinations.find(
194
+ (c: any) =>
195
+ Array.isArray(c.cardsToMulligan) && c.cardsToMulligan.length === 0,
196
+ );
197
+ expect(keepAllOption).toBeDefined();
198
+ }
199
+ });
200
+
201
+ it("should allow mulliganing all cards", () => {
202
+ const params = testEngine.enumerateMoveParameters(
203
+ "alterHand",
204
+ PLAYER_ONE,
205
+ );
206
+
207
+ if (params) {
208
+ const hand = testEngine.getZone("hand", PLAYER_ONE);
209
+ const handSize = hand?.length || 0;
210
+
211
+ // Should include option to mulligan all cards
212
+ const mulliganAllOption = params.validCombinations.find(
213
+ (c: any) =>
214
+ Array.isArray(c.cardsToMulligan) &&
215
+ c.cardsToMulligan.length === handSize,
216
+ );
217
+ expect(mulliganAllOption).toBeDefined();
218
+ }
219
+ });
220
+ });
221
+
222
+ describe("putACardIntoTheInkwell Parameter Enumeration", () => {
223
+ let testEngine: LorcanaTestEngine;
224
+
225
+ beforeEach(() => {
226
+ testEngine = new LorcanaTestEngine(
227
+ { hand: 5, deck: 10 },
228
+ { hand: 5, deck: 10 },
229
+ { skipPreGame: true },
230
+ );
231
+ });
232
+
233
+ it("should enumerate inkable cards in hand", () => {
234
+ const params = testEngine.enumerateMoveParameters(
235
+ "putACardIntoTheInkwell",
236
+ PLAYER_ONE,
237
+ );
238
+
239
+ expect(params).toBeDefined();
240
+ if (params) {
241
+ expect(params.parameterInfo.cardId).toBeDefined();
242
+ expect(params.parameterInfo.cardId.type).toBe("cardId");
243
+ }
244
+ });
245
+
246
+ it("should only include cards with inkable property", () => {
247
+ const params = testEngine.enumerateMoveParameters(
248
+ "putACardIntoTheInkwell",
249
+ PLAYER_ONE,
250
+ );
251
+
252
+ // All returned cards should be inkable (this would be validated by move condition)
253
+ if (params) {
254
+ expect(Array.isArray(params.validCombinations)).toBe(true);
255
+ }
256
+ });
257
+
258
+ it("should return null if already inked this turn", () => {
259
+ // Execute ink move once using the public helper
260
+ const hand = testEngine.getZone("hand", PLAYER_ONE);
261
+ if (hand && hand.length > 0) {
262
+ testEngine.putCardInInkwell(hand[0]);
263
+ }
264
+
265
+ // Try to enumerate again - should return null (move not available)
266
+ const params = testEngine.enumerateMoveParameters(
267
+ "putACardIntoTheInkwell",
268
+ PLAYER_ONE,
269
+ );
270
+
271
+ // Move should no longer be available after using once per turn
272
+ expect(params).toBeNull();
273
+ });
274
+ });
275
+
276
+ describe("Cross-Move Integration", () => {
277
+ let testEngine: LorcanaTestEngine;
278
+
279
+ beforeEach(() => {
280
+ testEngine = new LorcanaTestEngine(
281
+ { hand: 5, deck: 10 },
282
+ { hand: 5, deck: 10 },
283
+ { skipPreGame: true },
284
+ );
285
+ });
286
+
287
+ it("should enumerate parameters for multiple moves simultaneously", () => {
288
+ const playCardParams = testEngine.enumerateMoveParameters(
289
+ "playCard",
290
+ PLAYER_ONE,
291
+ );
292
+ const inkwellParams = testEngine.enumerateMoveParameters(
293
+ "putACardIntoTheInkwell",
294
+ PLAYER_ONE,
295
+ );
296
+
297
+ // Both should be available during main phase
298
+ expect(playCardParams).toBeDefined();
299
+ expect(inkwellParams).toBeDefined();
300
+ });
301
+
302
+ it("should return consistent results across multiple calls", () => {
303
+ const params1 = testEngine.enumerateMoveParameters(
304
+ "playCard",
305
+ PLAYER_ONE,
306
+ );
307
+ const params2 = testEngine.enumerateMoveParameters(
308
+ "playCard",
309
+ PLAYER_ONE,
310
+ );
311
+
312
+ // Should be identical
313
+ expect(params1).toEqual(params2);
314
+ });
315
+ });
316
+ });