@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,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
|
+
});
|