@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,625 @@
1
+ import { type PlayerId, RuleEngine } from "@drmxrcy/tcg-core";
2
+ import type {
3
+ LorcanaCardMeta,
4
+ LorcanaGameState,
5
+ LorcanaMoveParams,
6
+ } from "../types";
7
+ import type {
8
+ AvailableMoveInfo,
9
+ MoveParameterOptions,
10
+ MoveValidationError,
11
+ } from "../types/move-enumeration";
12
+
13
+ /**
14
+ * Lorcana Engine
15
+ *
16
+ * Extended RuleEngine with move enumeration capabilities for AI agents
17
+ * and UI components.
18
+ *
19
+ * Provides APIs to:
20
+ * - Discover available moves at any game state
21
+ * - Enumerate valid parameters for moves
22
+ * - Get detailed move information with metadata
23
+ * - Understand why moves cannot be executed
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const engine = new LorcanaEngine(gameDefinition);
28
+ *
29
+ * // Get available moves for a player
30
+ * const moves = engine.getAvailableMoves(playerId);
31
+ *
32
+ * // Enumerate parameters for a specific move
33
+ * const params = engine.enumerateMoveParameters("playCard", playerId);
34
+ *
35
+ * // Get detailed info about available moves
36
+ * const detailedMoves = engine.getAvailableMovesDetailed(playerId);
37
+ *
38
+ * // Understand why a move is invalid
39
+ * const error = engine.whyCannotExecuteMove("playCard", { cardId: "card1" });
40
+ * ```
41
+ */
42
+ export class LorcanaEngine extends RuleEngine<
43
+ LorcanaGameState,
44
+ LorcanaMoveParams,
45
+ any,
46
+ LorcanaCardMeta
47
+ > {
48
+ /**
49
+ * Get all available moves for a player
50
+ *
51
+ * Returns move IDs that:
52
+ * 1. Pass their condition checks
53
+ * 2. Are appropriate for current phase
54
+ * 3. Can be executed by the given player
55
+ *
56
+ * @param playerId - Player to check moves for
57
+ * @returns Array of available move IDs
58
+ *
59
+ * @example
60
+ * ```typescript
61
+ * const moves = engine.getAvailableMoves("player_one");
62
+ * // => ["chooseWhoGoesFirstMove"]
63
+ * ```
64
+ */
65
+ getAvailableMoves(playerId: PlayerId): string[] {
66
+ const validMoves: string[] = [];
67
+
68
+ // Check each registered move
69
+ for (const moveId of Object.keys(this.gameDefinition.moves)) {
70
+ // Special case: For moves that require parameters to be valid,
71
+ // try to enumerate and check if we have any valid combinations
72
+ if (this.moveRequiresParameters(moveId)) {
73
+ const params = this.enumerateMoveParameters(
74
+ moveId as keyof LorcanaMoveParams,
75
+ playerId,
76
+ );
77
+
78
+ if (params !== null && params.validCombinations.length > 0) {
79
+ validMoves.push(moveId);
80
+ }
81
+ } else {
82
+ // For moves that work with empty params or are parameterless,
83
+ // use simple condition check
84
+ const canExecute = this.canExecuteMove(moveId, {
85
+ playerId,
86
+ params: {},
87
+ });
88
+
89
+ if (canExecute) {
90
+ validMoves.push(moveId);
91
+ }
92
+ }
93
+ }
94
+
95
+ return validMoves;
96
+ }
97
+
98
+ /**
99
+ * Check if a move requires parameters to be valid
100
+ *
101
+ * Some moves like chooseWhoGoesFirstMove must have parameters to pass validation.
102
+ * Others like passTurn and concede work with empty parameters.
103
+ *
104
+ * @param moveId - Move ID to check
105
+ * @returns True if move requires parameters
106
+ * @private
107
+ */
108
+ private moveRequiresParameters(moveId: string): boolean {
109
+ // Moves that MUST have parameters to be valid
110
+ const requiresParams = new Set([
111
+ "chooseWhoGoesFirstMove",
112
+ "alterHand", // Has enumerator and requires cardsToMulligan parameter
113
+ // Note: Other moves like playCard, quest, challenge, putACardIntoTheInkwell
114
+ // are NOT in this list because their enumeration returns null (not yet implemented).
115
+ // They will be checked with empty params and conditions will handle validation.
116
+ ]);
117
+
118
+ return requiresParams.has(moveId);
119
+ }
120
+
121
+ /**
122
+ * Get detailed information about available moves
123
+ *
124
+ * Includes move metadata, display information, and parameter hints.
125
+ * Useful for UI to show rich move information to players.
126
+ *
127
+ * @param playerId - Player to check moves for
128
+ * @returns Array of move information objects
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const moves = engine.getAvailableMovesDetailed("player_one");
133
+ * // => [
134
+ * // {
135
+ * // moveId: "chooseWhoGoesFirstMove",
136
+ * // displayName: "Choose First Player",
137
+ * // description: "Select which player goes first",
138
+ * // paramSchema: { required: [...] }
139
+ * // }
140
+ * // ]
141
+ * ```
142
+ */
143
+ getAvailableMovesDetailed(playerId: PlayerId): AvailableMoveInfo[] {
144
+ // Get available move IDs
145
+ const moveIds = this.getAvailableMoves(playerId);
146
+
147
+ // Enrich each move with detailed metadata
148
+ return moveIds.map((moveId) => this.getMoveInfo(moveId));
149
+ }
150
+
151
+ /**
152
+ * Enumerate valid parameters for a specific move
153
+ *
154
+ * Returns all valid parameter combinations for the given move and player.
155
+ * Returns null if the move is not available for the player.
156
+ *
157
+ * @param moveId - Move to enumerate parameters for
158
+ * @param playerId - Player attempting the move
159
+ * @returns Valid parameter combinations or null if move not available
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * const params = engine.enumerateMoveParameters(
164
+ * "chooseWhoGoesFirstMove",
165
+ * "player_one"
166
+ * );
167
+ * // => {
168
+ * // validCombinations: [
169
+ * // { playerId: "player_one" },
170
+ * // { playerId: "player_two" }
171
+ * // ],
172
+ * // parameterInfo: {
173
+ * // playerId: {
174
+ * // type: "playerId",
175
+ * // description: "Player who will go first",
176
+ * // validValues: ["player_one", "player_two"]
177
+ * // }
178
+ * // }
179
+ * // }
180
+ * ```
181
+ */
182
+ enumerateMoveParameters(
183
+ moveId: string,
184
+ playerId: PlayerId,
185
+ ): MoveParameterOptions | null {
186
+ // Switch statement with exhaustive check for each move type
187
+ switch (moveId) {
188
+ case "chooseWhoGoesFirstMove":
189
+ return this.enumerateChooseFirstPlayerParams(playerId);
190
+
191
+ case "playCard":
192
+ return this.enumeratePlayCardParams(playerId);
193
+
194
+ case "quest":
195
+ return this.enumerateQuestParams(playerId);
196
+
197
+ case "challenge":
198
+ return this.enumerateChallengeParams(playerId);
199
+
200
+ case "alterHand":
201
+ return this.enumerateAlterHandParams(playerId);
202
+
203
+ case "putACardIntoTheInkwell":
204
+ return this.enumerateInkwellParams(playerId);
205
+
206
+ default:
207
+ // For moves not yet implemented or parameterless moves, return null
208
+ return null;
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Get detailed explanation of why a move cannot be executed
214
+ *
215
+ * Executes move validation and returns structured error information
216
+ * with context and suggestions. Returns null if the move is valid.
217
+ *
218
+ * **Implementation Note**: This method uses `executeMove()` internally,
219
+ * which is safe because `@drmxrcy/tcg-core`'s RuleEngine uses Immer for immutable
220
+ * state management. Failed move executions are automatically rolled back
221
+ * and do not modify game state.
222
+ *
223
+ * @param moveId - Move to check
224
+ * @param params - Parameters to use for the move
225
+ * @returns Error information or null if move is valid
226
+ *
227
+ * @example
228
+ * ```typescript
229
+ * const error = engine.whyCannotExecuteMove(
230
+ * "chooseWhoGoesFirstMove",
231
+ * { playerId: "player_one", params: { playerId: "invalid" } }
232
+ * );
233
+ * // => {
234
+ * // moveId: "chooseWhoGoesFirstMove",
235
+ * // errorCode: "INVALID_PLAYER_ID",
236
+ * // reason: "Invalid player ID: invalid",
237
+ * // context: { playerId: "invalid", validPlayers: [...] },
238
+ * // suggestions: ["Choose a valid player ID"]
239
+ * // }
240
+ * ```
241
+ */
242
+ whyCannotExecuteMove(
243
+ moveId: string,
244
+ params: any,
245
+ ): MoveValidationError | null {
246
+ // Attempt to execute the move to get detailed error information
247
+ // Safe: Failed executions are rolled back by Immer (no side effects)
248
+ const result = this.executeMove(moveId, params);
249
+
250
+ // If move succeeded, no error
251
+ if (result.success) {
252
+ return null;
253
+ }
254
+
255
+ // Parse error result and generate helpful error object
256
+ return {
257
+ moveId,
258
+ errorCode: result.errorCode || "UNKNOWN_ERROR",
259
+ reason: result.error || "Move cannot be executed",
260
+ context: result.errorContext,
261
+ suggestions: this.generateSuggestions(
262
+ moveId,
263
+ result.errorCode,
264
+ result.errorContext,
265
+ ),
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Generate helpful suggestions based on error context
271
+ *
272
+ * @param moveId - Move that failed
273
+ * @param errorCode - Error code from move execution
274
+ * @param errorContext - Additional error context
275
+ * @returns Array of helpful suggestions
276
+ * @private
277
+ */
278
+ private generateSuggestions(
279
+ moveId: string,
280
+ errorCode?: string,
281
+ errorContext?: Record<string, any>,
282
+ ): string[] {
283
+ const suggestions: string[] = [];
284
+
285
+ switch (errorCode) {
286
+ case "NOT_CHOOSING_PLAYER":
287
+ if (errorContext?.choosingPlayer) {
288
+ suggestions.push(
289
+ `Wait for ${errorContext.choosingPlayer} to choose the first player`,
290
+ );
291
+ }
292
+ break;
293
+
294
+ case "INVALID_PLAYER_ID":
295
+ if (errorContext?.validPlayers) {
296
+ suggestions.push(
297
+ `Choose one of the valid players: ${errorContext.validPlayers.join(", ")}`,
298
+ );
299
+ }
300
+ break;
301
+
302
+ case "WRONG_PHASE":
303
+ if (errorContext?.requiredPhase) {
304
+ suggestions.push(
305
+ `Wait until ${errorContext.requiredPhase} phase to use this move`,
306
+ );
307
+ }
308
+ break;
309
+
310
+ case "FIRST_PLAYER_ALREADY_CHOSEN":
311
+ suggestions.push("The first player has already been selected");
312
+ break;
313
+
314
+ case "INSUFFICIENT_INK":
315
+ if (
316
+ errorContext?.required !== undefined &&
317
+ errorContext?.available !== undefined
318
+ ) {
319
+ const needed = errorContext.required - errorContext.available;
320
+ suggestions.push(`Add ${needed} more cards to your inkwell`);
321
+ }
322
+ break;
323
+
324
+ case "NOT_YOUR_TURN":
325
+ suggestions.push("Wait for your turn");
326
+ break;
327
+
328
+ case "CONDITION_FAILED":
329
+ suggestions.push(
330
+ `The conditions for ${moveId} are not met at this time`,
331
+ );
332
+ break;
333
+
334
+ default:
335
+ // Generic suggestion if no specific one available
336
+ if (errorCode) {
337
+ suggestions.push(`Check the requirements for ${moveId}`);
338
+ }
339
+ break;
340
+ }
341
+
342
+ return suggestions;
343
+ }
344
+
345
+ // ========== Private Helper Methods ==========
346
+
347
+ /**
348
+ * Get detailed information about a specific move
349
+ *
350
+ * @param moveId - Move to get information for
351
+ * @returns Move information with metadata
352
+ * @private
353
+ */
354
+ private getMoveInfo(moveId: string): AvailableMoveInfo {
355
+ // Move metadata mapping
356
+ // This provides display names, descriptions, and parameter schemas for moves
357
+ switch (moveId) {
358
+ case "chooseWhoGoesFirstMove":
359
+ return {
360
+ moveId,
361
+ displayName: "Choose First Player",
362
+ description: "Select which player will take the first turn",
363
+ icon: "dice",
364
+ paramSchema: {
365
+ required: [
366
+ {
367
+ name: "playerId",
368
+ type: "playerId",
369
+ description: "Player to go first",
370
+ },
371
+ ],
372
+ },
373
+ };
374
+
375
+ case "alterHand":
376
+ return {
377
+ moveId,
378
+ displayName: "Mulligan",
379
+ description:
380
+ "Choose cards to put on bottom of deck and draw new ones",
381
+ icon: "hand",
382
+ paramSchema: {
383
+ required: [
384
+ {
385
+ name: "playerId",
386
+ type: "playerId",
387
+ description: "Player mulliganing",
388
+ },
389
+ {
390
+ name: "cardsToMulligan",
391
+ type: "object",
392
+ description: "Cards to put on bottom of deck",
393
+ },
394
+ ],
395
+ },
396
+ };
397
+
398
+ case "passTurn":
399
+ return {
400
+ moveId,
401
+ displayName: "Pass Turn",
402
+ description: "End your turn and pass priority to the next player",
403
+ icon: "forward",
404
+ };
405
+
406
+ // Default fallback for moves without explicit metadata
407
+ default:
408
+ return {
409
+ moveId,
410
+ displayName: moveId
411
+ .replace(/([A-Z])/g, " $1")
412
+ .replace(/^./, (str) => str.toUpperCase())
413
+ .trim(),
414
+ description: `Execute ${moveId} move`,
415
+ };
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Enumerate parameters for chooseWhoGoesFirstMove
421
+ *
422
+ * @param playerId - Player attempting the move
423
+ * @returns Valid parameter combinations or null if move not available
424
+ * @private
425
+ */
426
+ private enumerateChooseFirstPlayerParams(
427
+ playerId: PlayerId,
428
+ ): MoveParameterOptions | null {
429
+ // Get all valid player IDs from game state
430
+ const state = this.getState();
431
+ const validPlayers = Object.keys(state.external.loreScores) as PlayerId[];
432
+
433
+ // For each valid player choice, check if the move can be executed
434
+ const validCombinations: Array<{ playerId: PlayerId }> = [];
435
+ for (const targetPlayerId of validPlayers) {
436
+ const canExecute = this.canExecuteMove("chooseWhoGoesFirstMove", {
437
+ playerId,
438
+ params: { playerId: targetPlayerId },
439
+ });
440
+
441
+ if (canExecute) {
442
+ validCombinations.push({ playerId: targetPlayerId });
443
+ }
444
+ }
445
+
446
+ // If no valid combinations, move is not available
447
+ if (validCombinations.length === 0) {
448
+ return null;
449
+ }
450
+
451
+ // Return valid player choices
452
+ return {
453
+ validCombinations,
454
+ parameterInfo: {
455
+ playerId: {
456
+ type: "playerId",
457
+ description: "Player who will go first",
458
+ validValues: validPlayers,
459
+ },
460
+ },
461
+ };
462
+ }
463
+
464
+ /**
465
+ * Enumerate parameters for playCard
466
+ *
467
+ * @param playerId - Player attempting the move
468
+ * @returns Valid parameter combinations or null if move not available
469
+ * @private
470
+ */
471
+ private enumeratePlayCardParams(
472
+ _playerId: PlayerId,
473
+ ): MoveParameterOptions | null {
474
+ // TODO: Implement full enumeration with access to internal zone state
475
+ // Current limitation: Cannot access RuleEngine's internal zone state directly
476
+ //
477
+ // Full implementation requires:
478
+ // 1. Access to hand zone cards (currently via internal state)
479
+ // 2. Card registry access for ink cost filtering
480
+ // 3. Available ink calculation from game state
481
+ //
482
+ // Temporary workaround: Return null to indicate no enumeration available
483
+ // This moves parameter validation to execution time via whyCannotExecuteMove()
484
+ return null;
485
+ }
486
+
487
+ /**
488
+ * Enumerate parameters for quest
489
+ *
490
+ * @param playerId - Player attempting the move
491
+ * @returns Valid parameter combinations or null if move not available
492
+ * @private
493
+ */
494
+ private enumerateQuestParams(
495
+ _playerId: PlayerId,
496
+ ): MoveParameterOptions | null {
497
+ // TODO: Implement full enumeration with access to internal zone and card state
498
+ // Current limitation: Cannot access RuleEngine's internal zone state directly
499
+ //
500
+ // Full implementation requires:
501
+ // 1. Access to play zone cards
502
+ // 2. Card metadata access for ready status (not exerted, not drying)
503
+ // 3. Card type filtering (only characters can quest)
504
+ // 4. Lore value calculation (from card registry)
505
+ //
506
+ // Temporary workaround: Return null to indicate no enumeration available
507
+ // This moves parameter validation to execution time via whyCannotExecuteMove()
508
+ return null;
509
+ }
510
+
511
+ /**
512
+ * Enumerate parameters for challenge
513
+ *
514
+ * @param playerId - Player attempting the move
515
+ * @returns Valid parameter combinations or null if move not available
516
+ * @private
517
+ */
518
+ private enumerateChallengeParams(
519
+ _playerId: PlayerId,
520
+ ): MoveParameterOptions | null {
521
+ // TODO: Implement full enumeration with access to internal zone and card state
522
+ // Current limitation: Cannot access RuleEngine's internal zone state directly
523
+ //
524
+ // Full implementation requires:
525
+ // 1. Access to play zone cards for both player and opponents
526
+ // 2. Card metadata for ready status and character type
527
+ // 3. Valid attacker-defender pair generation (N x M combinations)
528
+ // 4. Evasive and Bodyguard ability filtering
529
+ //
530
+ // Temporary workaround: Return null to indicate no enumeration available
531
+ // This moves parameter validation to execution time via whyCannotExecuteMove()
532
+ return null;
533
+ }
534
+
535
+ /**
536
+ * Enumerate parameters for alterHand (mulligan)
537
+ *
538
+ * @param playerId - Player attempting the move
539
+ * @returns Valid parameter combinations or null if move not available
540
+ * @private
541
+ */
542
+ private enumerateAlterHandParams(
543
+ playerId: PlayerId,
544
+ ): MoveParameterOptions | null {
545
+ // Check if move is available (validates phase, pending mulligan status, etc.)
546
+ const canExecute = this.canExecuteMove("alterHand", {
547
+ playerId,
548
+ params: { playerId, cardsToMulligan: [] }, // Empty array = keep all cards
549
+ });
550
+
551
+ if (!canExecute) {
552
+ return null;
553
+ }
554
+
555
+ // Get cards in hand to enumerate mulligan options
556
+ // Access internal state to get hand cards (testing backdoor similar to LorcanaTestEngine)
557
+ const internalState = (this as any).internalState;
558
+ if (!internalState) {
559
+ // Fallback to simple keep-all option if we can't access internal state
560
+ return {
561
+ validCombinations: [
562
+ {
563
+ playerId,
564
+ cardsToMulligan: [],
565
+ },
566
+ ],
567
+ parameterInfo: {},
568
+ };
569
+ }
570
+
571
+ const handCards =
572
+ internalState.zones?.hand?.cardIds.filter((cardId: string) => {
573
+ const card = internalState.cards?.[cardId];
574
+ return card && String(card.owner) === String(playerId);
575
+ }) || [];
576
+
577
+ // Generate combinations: keep all (empty array) and mulligan all (all cards)
578
+ // For efficiency, we only generate these two extreme options
579
+ // Full power-set enumeration (2^n combinations) would be too expensive for large hands
580
+ const validCombinations = [
581
+ {
582
+ playerId,
583
+ cardsToMulligan: [], // Keep all cards
584
+ },
585
+ {
586
+ playerId,
587
+ cardsToMulligan: handCards, // Mulligan all cards
588
+ },
589
+ ];
590
+
591
+ return {
592
+ validCombinations,
593
+ parameterInfo: {
594
+ cardsToMulligan: {
595
+ type: "cardId",
596
+ description: "Cards to mulligan (put on bottom of deck)",
597
+ validValues: handCards,
598
+ },
599
+ },
600
+ };
601
+ }
602
+
603
+ /**
604
+ * Enumerate parameters for putACardIntoTheInkwell
605
+ *
606
+ * @param playerId - Player attempting the move
607
+ * @returns Valid parameter combinations or null if move not available
608
+ * @private
609
+ */
610
+ private enumerateInkwellParams(
611
+ _playerId: PlayerId,
612
+ ): MoveParameterOptions | null {
613
+ // TODO: Implement full enumeration with access to internal zone state
614
+ // Current limitation: Cannot access RuleEngine's internal zone state directly
615
+ //
616
+ // Full implementation requires:
617
+ // 1. Access to hand zone cards
618
+ // 2. Check if player has already inked this turn (turnMetadata)
619
+ // 3. Optional: Card registry access to prefer inkable cards
620
+ //
621
+ // Temporary workaround: Return null to indicate no enumeration available
622
+ // This moves parameter validation to execution time via whyCannotExecuteMove()
623
+ return null;
624
+ }
625
+ }