@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.
- package/README.md +160 -0
- package/package.json +45 -0
- package/src/__tests__/integration/move-enumeration.test.ts +256 -0
- package/src/__tests__/rules/section-01-concepts.test.ts +426 -0
- package/src/__tests__/rules/section-03-gameplay.test.ts +298 -0
- package/src/__tests__/rules/section-04-turn-structure.test.ts +708 -0
- package/src/__tests__/rules/section-05-cards.test.ts +158 -0
- package/src/__tests__/rules/section-06-card-types.test.ts +342 -0
- package/src/__tests__/rules/section-07-abilities.test.ts +333 -0
- package/src/__tests__/rules/section-08-zones.test.ts +231 -0
- package/src/__tests__/rules/section-09-damage.test.ts +148 -0
- package/src/__tests__/rules/section-10-keywords.test.ts +469 -0
- package/src/__tests__/spec-01-foundation-types.test.ts +534 -0
- package/src/__tests__/spec-02-zones-card-states.test.ts +295 -0
- package/src/card-utils.ts +302 -0
- package/src/cards/README.md +296 -0
- package/src/cards/abilities/index.ts +175 -0
- package/src/cards/index.ts +10 -0
- package/src/deck-validation.ts +175 -0
- package/src/engine/lorcana-engine.ts +625 -0
- package/src/game-definition/__tests__/core-zone-integration.test.ts +553 -0
- package/src/game-definition/__tests__/zone-operations.test.ts +362 -0
- package/src/game-definition/__tests__/zones.test.ts +176 -0
- package/src/game-definition/definition.ts +45 -0
- package/src/game-definition/flow/turn-flow.ts +216 -0
- package/src/game-definition/index.ts +31 -0
- package/src/game-definition/moves/abilities/activate-ability.ts +51 -0
- package/src/game-definition/moves/core/__tests__/move-parameter-enumeration.test.ts +316 -0
- package/src/game-definition/moves/core/challenge.test.ts +545 -0
- package/src/game-definition/moves/core/challenge.ts +81 -0
- package/src/game-definition/moves/core/play-card.ts +83 -0
- package/src/game-definition/moves/core/quest.test.ts +448 -0
- package/src/game-definition/moves/core/quest.ts +49 -0
- package/src/game-definition/moves/debug/manual-exert.ts +36 -0
- package/src/game-definition/moves/effects/resolve-bag.ts +35 -0
- package/src/game-definition/moves/effects/resolve-effect.ts +34 -0
- package/src/game-definition/moves/index.ts +85 -0
- package/src/game-definition/moves/locations/move-character-to-location.ts +42 -0
- package/src/game-definition/moves/resources/put-card-into-inkwell.test.ts +462 -0
- package/src/game-definition/moves/resources/put-card-into-inkwell.ts +51 -0
- package/src/game-definition/moves/setup/alter-hand.test.ts +395 -0
- package/src/game-definition/moves/setup/alter-hand.ts +210 -0
- package/src/game-definition/moves/setup/choose-first-player.test.ts +450 -0
- package/src/game-definition/moves/setup/choose-first-player.ts +105 -0
- package/src/game-definition/moves/setup/draw-cards.ts +37 -0
- package/src/game-definition/moves/songs/sing-together.ts +47 -0
- package/src/game-definition/moves/songs/sing.ts +56 -0
- package/src/game-definition/moves/standard/concede.test.ts +189 -0
- package/src/game-definition/moves/standard/concede.ts +72 -0
- package/src/game-definition/moves/standard/pass-turn.ts +49 -0
- package/src/game-definition/setup/game-setup.ts +19 -0
- package/src/game-definition/trackers/tracker-config.ts +23 -0
- package/src/game-definition/win-conditions/lore-victory.ts +26 -0
- package/src/game-definition/zone-operations.ts +405 -0
- package/src/game-definition/zones/zone-configs.ts +59 -0
- package/src/game-definition/zones.ts +283 -0
- package/src/index.ts +189 -0
- package/src/operations/index.ts +7 -0
- package/src/operations/lorcana-operations.ts +288 -0
- package/src/queries/README.md +56 -0
- package/src/resolvers/__tests__/condition-resolver.test.ts +301 -0
- package/src/resolvers/condition-registry.ts +70 -0
- package/src/resolvers/condition-resolver.ts +85 -0
- package/src/resolvers/conditions/basic.ts +81 -0
- package/src/resolvers/conditions/card-state.ts +12 -0
- package/src/resolvers/conditions/comparison.ts +102 -0
- package/src/resolvers/conditions/existence.ts +219 -0
- package/src/resolvers/conditions/history.ts +68 -0
- package/src/resolvers/conditions/index.ts +15 -0
- package/src/resolvers/conditions/logical.ts +55 -0
- package/src/resolvers/conditions/resolution.ts +41 -0
- package/src/resolvers/conditions/revealed.ts +42 -0
- package/src/resolvers/conditions/zone.ts +84 -0
- package/src/setup.test.ts +18 -0
- package/src/targeting/__tests__/filter-resolver.test.ts +294 -0
- package/src/targeting/__tests__/real-cards-targeting.test.ts +303 -0
- package/src/targeting/__tests__/targeting-dsl.test.ts +386 -0
- package/src/targeting/enum-expansion.ts +387 -0
- package/src/targeting/filter-registry.ts +322 -0
- package/src/targeting/filter-resolver.ts +145 -0
- package/src/targeting/index.ts +91 -0
- package/src/targeting/lorcana-target-dsl.ts +495 -0
- package/src/targeting/targeting-ui.ts +407 -0
- package/src/testing/index.ts +14 -0
- package/src/testing/lorcana-test-engine.ts +813 -0
- package/src/types/README.md +303 -0
- package/src/types/__tests__/lorcana-state.test.ts +168 -0
- package/src/types/__tests__/move-enumeration.test.ts +179 -0
- package/src/types/branded-types.ts +106 -0
- package/src/types/game-state.ts +184 -0
- package/src/types/index.ts +87 -0
- package/src/types/keywords.ts +187 -0
- package/src/types/lorcana-state.ts +260 -0
- package/src/types/move-enumeration.ts +126 -0
- package/src/types/move-params.ts +216 -0
- package/src/validators/index.ts +7 -0
- package/src/validators/move-validators.ts +374 -0
- package/src/zones/card-state.ts +234 -0
- package/src/zones/index.ts +42 -0
- 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
|
+
}
|