@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,303 @@
|
|
|
1
|
+
# Types
|
|
2
|
+
|
|
3
|
+
This directory contains TypeScript type definitions specific to Lorcana.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
- **`lorcana-state.ts`** - Game state type definitions
|
|
8
|
+
- **`lorcana-moves.ts`** - Move parameter types
|
|
9
|
+
- **`lorcana-cards.ts`** - Card type definitions
|
|
10
|
+
- **`lorcana-abilities.ts`** - Ability type definitions
|
|
11
|
+
- **`branded-types.ts`** - Branded type definitions for type safety
|
|
12
|
+
- **`index.ts`** - Type exports
|
|
13
|
+
|
|
14
|
+
## Purpose
|
|
15
|
+
|
|
16
|
+
This directory provides strong TypeScript types that:
|
|
17
|
+
|
|
18
|
+
1. Extend base types from `@drmxrcy/tcg-core` with Lorcana specifics
|
|
19
|
+
2. Ensure type safety throughout the engine
|
|
20
|
+
3. Enable IDE autocomplete and type checking
|
|
21
|
+
4. Document the data structures through types
|
|
22
|
+
|
|
23
|
+
## State Types
|
|
24
|
+
|
|
25
|
+
### Game State
|
|
26
|
+
|
|
27
|
+
The root state type for Lorcana games:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import type { GameState, PlayerId, CardId } from "@drmxrcy/tcg-core";
|
|
31
|
+
|
|
32
|
+
export type LorcanaState = GameState & {
|
|
33
|
+
lorcana: {
|
|
34
|
+
// Lore tracking (win condition at 20)
|
|
35
|
+
lore: Record<PlayerId, number>;
|
|
36
|
+
|
|
37
|
+
// Ink tracking
|
|
38
|
+
ink: {
|
|
39
|
+
available: Record<PlayerId, number>;
|
|
40
|
+
total: Record<PlayerId, number>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Challenge state during challenge resolution
|
|
44
|
+
challengeState?: {
|
|
45
|
+
attacker: CardId;
|
|
46
|
+
defender?: CardId;
|
|
47
|
+
attackerDamage: number;
|
|
48
|
+
defenderDamage: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Location state
|
|
52
|
+
locations: Record<CardId, {
|
|
53
|
+
characters: CardId[];
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
// Turn metadata
|
|
57
|
+
turnMetadata: {
|
|
58
|
+
cardsPlayedThisTurn: CardId[];
|
|
59
|
+
charactersQuestingThisTurn: CardId[];
|
|
60
|
+
damageDealtThisTurn: Record<CardId, number>;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Zone Types
|
|
67
|
+
|
|
68
|
+
Lorcana-specific zone types:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
export type LorcanaZone =
|
|
72
|
+
| "deck"
|
|
73
|
+
| "hand"
|
|
74
|
+
| "play"
|
|
75
|
+
| "discard"
|
|
76
|
+
| "inkwell";
|
|
77
|
+
|
|
78
|
+
export type ZoneVisibility =
|
|
79
|
+
| "all" // All players can see
|
|
80
|
+
| "owner" // Only owner can see
|
|
81
|
+
| "none"; // No one can see (face-down)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Move Types
|
|
85
|
+
|
|
86
|
+
Type-safe move parameters:
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
export type PlayCardMoveParams = {
|
|
90
|
+
cardId: CardId;
|
|
91
|
+
shift?: {
|
|
92
|
+
targetCardId: CardId; // Card to shift onto
|
|
93
|
+
};
|
|
94
|
+
targets?: TargetSelection[];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type QuestMoveParams = {
|
|
98
|
+
cardId: CardId;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type ChallengeMoveParams = {
|
|
102
|
+
attackerId: CardId;
|
|
103
|
+
defenderId: CardId;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type InkCardMoveParams = {
|
|
107
|
+
cardId: CardId;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type ActivateAbilityMoveParams = {
|
|
111
|
+
cardId: CardId;
|
|
112
|
+
abilityIndex: number;
|
|
113
|
+
targets?: TargetSelection[];
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Card Types
|
|
118
|
+
|
|
119
|
+
Lorcana card type definitions:
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
export type LorcanaColor =
|
|
123
|
+
| "amber"
|
|
124
|
+
| "amethyst"
|
|
125
|
+
| "emerald"
|
|
126
|
+
| "ruby"
|
|
127
|
+
| "sapphire"
|
|
128
|
+
| "steel";
|
|
129
|
+
|
|
130
|
+
export type LorcanaCardType =
|
|
131
|
+
| "character"
|
|
132
|
+
| "action"
|
|
133
|
+
| "item"
|
|
134
|
+
| "location"
|
|
135
|
+
| "song";
|
|
136
|
+
|
|
137
|
+
export type LorcanaRarity =
|
|
138
|
+
| "common"
|
|
139
|
+
| "uncommon"
|
|
140
|
+
| "rare"
|
|
141
|
+
| "super_rare"
|
|
142
|
+
| "legendary"
|
|
143
|
+
| "enchanted";
|
|
144
|
+
|
|
145
|
+
export type LorcanaCard = {
|
|
146
|
+
id: CardId;
|
|
147
|
+
name: string;
|
|
148
|
+
type: LorcanaCardType;
|
|
149
|
+
color: LorcanaColor;
|
|
150
|
+
cost: number;
|
|
151
|
+
inkCost: number;
|
|
152
|
+
inkable: boolean;
|
|
153
|
+
rarity: LorcanaRarity;
|
|
154
|
+
set: string;
|
|
155
|
+
number: number;
|
|
156
|
+
|
|
157
|
+
// Character properties
|
|
158
|
+
strength?: number;
|
|
159
|
+
willpower?: number;
|
|
160
|
+
loreValue?: number;
|
|
161
|
+
classifications?: string[]; // "Hero", "Villain", "Princess", etc.
|
|
162
|
+
|
|
163
|
+
// Abilities
|
|
164
|
+
abilities: LorcanaAbility[];
|
|
165
|
+
|
|
166
|
+
// Text
|
|
167
|
+
text?: string;
|
|
168
|
+
flavorText?: string;
|
|
169
|
+
};
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Ability Types
|
|
173
|
+
|
|
174
|
+
Type-safe ability definitions:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
export type LorcanaAbility =
|
|
178
|
+
| KeywordAbility
|
|
179
|
+
| TriggeredAbility
|
|
180
|
+
| ActivatedAbility
|
|
181
|
+
| StaticAbility;
|
|
182
|
+
|
|
183
|
+
export type KeywordAbility = {
|
|
184
|
+
type: "keyword";
|
|
185
|
+
keyword: LorcanaKeyword;
|
|
186
|
+
value?: number; // For Challenger +N, Resist +N, etc.
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export type LorcanaKeyword =
|
|
190
|
+
| "bodyguard"
|
|
191
|
+
| "challenger"
|
|
192
|
+
| "evasive"
|
|
193
|
+
| "reckless"
|
|
194
|
+
| "resist"
|
|
195
|
+
| "rush"
|
|
196
|
+
| "shift"
|
|
197
|
+
| "singer"
|
|
198
|
+
| "support"
|
|
199
|
+
| "ward";
|
|
200
|
+
|
|
201
|
+
export type TriggeredAbility = {
|
|
202
|
+
type: "triggered";
|
|
203
|
+
trigger: TriggerTiming;
|
|
204
|
+
condition?: AbilityCondition;
|
|
205
|
+
effect: AbilityEffect;
|
|
206
|
+
target?: TargetDefinition;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export type TriggerTiming =
|
|
210
|
+
| "whenPlayed"
|
|
211
|
+
| "wheneverQuests"
|
|
212
|
+
| "wheneverChallenges"
|
|
213
|
+
| "wheneverDamaged"
|
|
214
|
+
| "atStartOfTurn"
|
|
215
|
+
| "atEndOfTurn"
|
|
216
|
+
| "whenLeaves";
|
|
217
|
+
|
|
218
|
+
export type ActivatedAbility = {
|
|
219
|
+
type: "activated";
|
|
220
|
+
cost?: AbilityCost;
|
|
221
|
+
effect: AbilityEffect;
|
|
222
|
+
target?: TargetDefinition;
|
|
223
|
+
usesPerTurn?: number;
|
|
224
|
+
};
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Branded Types
|
|
228
|
+
|
|
229
|
+
Type-safe IDs to prevent mixing different ID types:
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// Branded type pattern
|
|
233
|
+
export type Brand<K, T> = K & { __brand: T };
|
|
234
|
+
|
|
235
|
+
// Specific ID types
|
|
236
|
+
export type CardId = Brand<string, "CardId">;
|
|
237
|
+
export type PlayerId = Brand<string, "PlayerId">;
|
|
238
|
+
export type GameId = Brand<string, "GameId">;
|
|
239
|
+
export type AbilityId = Brand<string, "AbilityId">;
|
|
240
|
+
|
|
241
|
+
// Type guards
|
|
242
|
+
export const isCardId = (value: string): value is CardId => {
|
|
243
|
+
return typeof value === "string" && value.length > 0;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Constructor functions
|
|
247
|
+
export const createCardId = (value: string): CardId => {
|
|
248
|
+
return value as CardId;
|
|
249
|
+
};
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Effect Types
|
|
253
|
+
|
|
254
|
+
Type-safe effect definitions:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
export type AbilityEffect =
|
|
258
|
+
| DrawCardsEffect
|
|
259
|
+
| DealDamageEffect
|
|
260
|
+
| GainLoreEffect
|
|
261
|
+
| ExertEffect
|
|
262
|
+
| ReadyEffect
|
|
263
|
+
| ReturnToHandEffect
|
|
264
|
+
| DiscardEffect;
|
|
265
|
+
|
|
266
|
+
export type DrawCardsEffect = {
|
|
267
|
+
type: "drawCards";
|
|
268
|
+
amount: number;
|
|
269
|
+
player?: "controller" | "opponent" | "target";
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export type DealDamageEffect = {
|
|
273
|
+
type: "dealDamage";
|
|
274
|
+
amount: number;
|
|
275
|
+
target: TargetDefinition;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export type GainLoreEffect = {
|
|
279
|
+
type: "gainLore";
|
|
280
|
+
amount: number;
|
|
281
|
+
};
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Type Utilities
|
|
285
|
+
|
|
286
|
+
Helper types for common patterns:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// Make specific properties optional
|
|
290
|
+
export type PartialCard = Partial<LorcanaCard> & Pick<LorcanaCard, "id" | "name">;
|
|
291
|
+
|
|
292
|
+
// Extract character cards only
|
|
293
|
+
export type CharacterCard = LorcanaCard & { type: "character" };
|
|
294
|
+
|
|
295
|
+
// Player-specific data
|
|
296
|
+
export type PlayerState<T> = Record<PlayerId, T>;
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## References
|
|
300
|
+
|
|
301
|
+
- See `@packages/core/src/types/` for base framework types
|
|
302
|
+
- See TypeScript handbook for branded types pattern
|
|
303
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { createCardId, createPlayerId } from "../branded-types";
|
|
3
|
+
import type { LorcanaState } from "../lorcana-state";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Task 1.1: Tests for LorcanaState type structure
|
|
7
|
+
*
|
|
8
|
+
* Validates the complete Lorcana game state type definition:
|
|
9
|
+
* - Lore tracking (Rule 3.1.4 - starts at 0)
|
|
10
|
+
* - Ink management (total and available)
|
|
11
|
+
* - Character states (drying, damage, exerted)
|
|
12
|
+
* - Turn metadata
|
|
13
|
+
* - Challenge state
|
|
14
|
+
*
|
|
15
|
+
* References:
|
|
16
|
+
* - Rule 1.9.1.1 (Win at 20 lore)
|
|
17
|
+
* - Rule 4.2.2.1 (Drying characters)
|
|
18
|
+
* - Rule 4.3.3 (Ink once per turn)
|
|
19
|
+
* - Rule 4.3.6 (Challenge state)
|
|
20
|
+
* - Rule 9 (Damage counters)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// Test helper to create base Lorcana state
|
|
24
|
+
function createBaseLorcanaState(
|
|
25
|
+
players: string[],
|
|
26
|
+
overrides?: Partial<LorcanaState>,
|
|
27
|
+
): LorcanaState {
|
|
28
|
+
const playerIds = players.map((p) => createPlayerId(p));
|
|
29
|
+
const lore: Record<string, number> = {};
|
|
30
|
+
const available: Record<string, number> = {};
|
|
31
|
+
const total: Record<string, number> = {};
|
|
32
|
+
|
|
33
|
+
for (const pid of playerIds) {
|
|
34
|
+
lore[pid] = 0;
|
|
35
|
+
available[pid] = 0;
|
|
36
|
+
total[pid] = 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
players: playerIds,
|
|
41
|
+
currentPlayerIndex: 0,
|
|
42
|
+
turnNumber: 1,
|
|
43
|
+
phase: "beginning",
|
|
44
|
+
lorcana: {
|
|
45
|
+
lore,
|
|
46
|
+
ink: { available, total },
|
|
47
|
+
turnMetadata: {
|
|
48
|
+
cardsPlayedThisTurn: [],
|
|
49
|
+
charactersQuesting: [],
|
|
50
|
+
inkedThisTurn: false,
|
|
51
|
+
},
|
|
52
|
+
characterStates: {},
|
|
53
|
+
permanentStates: {},
|
|
54
|
+
},
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("LorcanaState Type Structure", () => {
|
|
60
|
+
it("should have lore tracking for each player", () => {
|
|
61
|
+
const state = createBaseLorcanaState(["player1", "player2"]);
|
|
62
|
+
const [player1, player2] = state.players;
|
|
63
|
+
|
|
64
|
+
expect(state.lorcana.lore[player1]).toBe(0);
|
|
65
|
+
expect(state.lorcana.lore[player2]).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should track ink separately for each player", () => {
|
|
69
|
+
const state = createBaseLorcanaState(["player1", "player2"]);
|
|
70
|
+
const [player1, player2] = state.players;
|
|
71
|
+
|
|
72
|
+
// Set ink values
|
|
73
|
+
state.lorcana.ink.available[player1] = 3;
|
|
74
|
+
state.lorcana.ink.available[player2] = 2;
|
|
75
|
+
state.lorcana.ink.total[player1] = 5;
|
|
76
|
+
state.lorcana.ink.total[player2] = 4;
|
|
77
|
+
|
|
78
|
+
// Available ink is what can be spent this turn
|
|
79
|
+
expect(state.lorcana.ink.available[player1]).toBe(3);
|
|
80
|
+
// Total ink is maximum capacity
|
|
81
|
+
expect(state.lorcana.ink.total[player1]).toBe(5);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should track character states including drying status", () => {
|
|
85
|
+
const state = createBaseLorcanaState(["player1"], { phase: "main" });
|
|
86
|
+
const cardId = createCardId("card-character-1");
|
|
87
|
+
|
|
88
|
+
state.lorcana.characterStates[cardId] = {
|
|
89
|
+
playedThisTurn: true, // "drying" character (Rule 4.2.2.1)
|
|
90
|
+
damage: 0,
|
|
91
|
+
exerted: false,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const charState = state.lorcana.characterStates[cardId];
|
|
95
|
+
expect(charState.playedThisTurn).toBe(true);
|
|
96
|
+
expect(charState.damage).toBe(0);
|
|
97
|
+
expect(charState.exerted).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("should track damage on characters", () => {
|
|
101
|
+
const state = createBaseLorcanaState(["player1"], { phase: "main" });
|
|
102
|
+
const cardId = createCardId("card-character-1");
|
|
103
|
+
|
|
104
|
+
state.lorcana.characterStates[cardId] = {
|
|
105
|
+
playedThisTurn: false,
|
|
106
|
+
damage: 3, // Has 3 damage counters (Rule 9)
|
|
107
|
+
exerted: true,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
expect(state.lorcana.characterStates[cardId].damage).toBe(3);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should track turn metadata including cards played and characters questing", () => {
|
|
114
|
+
const state = createBaseLorcanaState(["player1"], { phase: "main" });
|
|
115
|
+
const card1 = createCardId("card-1");
|
|
116
|
+
const card2 = createCardId("card-2");
|
|
117
|
+
|
|
118
|
+
state.lorcana.turnMetadata = {
|
|
119
|
+
cardsPlayedThisTurn: [card1],
|
|
120
|
+
charactersQuesting: [card2],
|
|
121
|
+
inkedThisTurn: true, // Already inked this turn (Rule 4.3.3)
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
expect(state.lorcana.turnMetadata.cardsPlayedThisTurn).toContain(card1);
|
|
125
|
+
expect(state.lorcana.turnMetadata.charactersQuesting).toContain(card2);
|
|
126
|
+
expect(state.lorcana.turnMetadata.inkedThisTurn).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should optionally track challenge state during challenges", () => {
|
|
130
|
+
const state = createBaseLorcanaState(["player1"], { phase: "main" });
|
|
131
|
+
const attacker = createCardId("card-attacker");
|
|
132
|
+
const defender = createCardId("card-defender");
|
|
133
|
+
|
|
134
|
+
state.lorcana.challengeState = {
|
|
135
|
+
attacker,
|
|
136
|
+
defender,
|
|
137
|
+
attackerDamage: 5,
|
|
138
|
+
defenderDamage: 3,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
expect(state.lorcana.challengeState).toBeDefined();
|
|
142
|
+
expect(state.lorcana.challengeState?.attacker).toBe(attacker);
|
|
143
|
+
expect(state.lorcana.challengeState?.defender).toBe(defender);
|
|
144
|
+
expect(state.lorcana.challengeState?.attackerDamage).toBe(5);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should track location and item states separately from characters", () => {
|
|
148
|
+
const state = createBaseLorcanaState(["player1"], { phase: "main" });
|
|
149
|
+
const locationId = createCardId("card-location-1");
|
|
150
|
+
|
|
151
|
+
state.lorcana.permanentStates[locationId] = {
|
|
152
|
+
damage: 2, // Locations can take damage
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
expect(state.lorcana.permanentStates[locationId].damage).toBe(2);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should have required base game properties", () => {
|
|
159
|
+
const state = createBaseLorcanaState(["player1", "player2"]);
|
|
160
|
+
const [player1, player2] = state.players;
|
|
161
|
+
|
|
162
|
+
// Verify base game properties exist
|
|
163
|
+
expect(state.players).toEqual([player1, player2]);
|
|
164
|
+
expect(state.currentPlayerIndex).toBe(0);
|
|
165
|
+
expect(state.turnNumber).toBe(1);
|
|
166
|
+
expect(state.phase).toBe("beginning");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { PlayerId } from "@drmxrcy/tcg-core";
|
|
3
|
+
import type {
|
|
4
|
+
AvailableMoveInfo,
|
|
5
|
+
MoveParameterOptions,
|
|
6
|
+
MoveParamSchema,
|
|
7
|
+
MoveValidationError,
|
|
8
|
+
ParameterInfo,
|
|
9
|
+
ParamFieldSchema,
|
|
10
|
+
} from "../move-enumeration";
|
|
11
|
+
|
|
12
|
+
describe("Move Enumeration Types", () => {
|
|
13
|
+
describe("Type Compilation", () => {
|
|
14
|
+
it("should compile AvailableMoveInfo type", () => {
|
|
15
|
+
const moveInfo: AvailableMoveInfo = {
|
|
16
|
+
moveId: "chooseWhoGoesFirstMove",
|
|
17
|
+
displayName: "Choose First Player",
|
|
18
|
+
description: "Select which player goes first",
|
|
19
|
+
icon: "dice",
|
|
20
|
+
paramSchema: {
|
|
21
|
+
required: [
|
|
22
|
+
{
|
|
23
|
+
name: "playerId",
|
|
24
|
+
type: "playerId",
|
|
25
|
+
description: "Player to go first",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
expect(moveInfo.moveId).toBe("chooseWhoGoesFirstMove");
|
|
32
|
+
expect(moveInfo.displayName).toBe("Choose First Player");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should compile AvailableMoveInfo without optional fields", () => {
|
|
36
|
+
const moveInfo: AvailableMoveInfo = {
|
|
37
|
+
moveId: "passTurn",
|
|
38
|
+
displayName: "Pass Turn",
|
|
39
|
+
description: "End your turn",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
expect(moveInfo.moveId).toBe("passTurn");
|
|
43
|
+
expect(moveInfo.icon).toBeUndefined();
|
|
44
|
+
expect(moveInfo.paramSchema).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should compile ParamFieldSchema type", () => {
|
|
48
|
+
const fieldSchema: ParamFieldSchema = {
|
|
49
|
+
name: "cardId",
|
|
50
|
+
type: "cardId",
|
|
51
|
+
description: "Card to play",
|
|
52
|
+
validValues: ["card1", "card2"],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
expect(fieldSchema.name).toBe("cardId");
|
|
56
|
+
expect(fieldSchema.type).toBe("cardId");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should compile ParamFieldSchema with enum values", () => {
|
|
60
|
+
const fieldSchema: ParamFieldSchema = {
|
|
61
|
+
name: "choice",
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Choice to make",
|
|
64
|
+
enumValues: ["option1", "option2"],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
expect(fieldSchema.enumValues).toEqual(["option1", "option2"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("should compile MoveParamSchema type", () => {
|
|
71
|
+
const schema: MoveParamSchema = {
|
|
72
|
+
required: [
|
|
73
|
+
{
|
|
74
|
+
name: "playerId",
|
|
75
|
+
type: "playerId",
|
|
76
|
+
description: "Target player",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
optional: [
|
|
80
|
+
{
|
|
81
|
+
name: "targetId",
|
|
82
|
+
type: "cardId",
|
|
83
|
+
description: "Optional target",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
expect(schema.required).toHaveLength(1);
|
|
89
|
+
expect(schema.optional).toHaveLength(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("should compile MoveParameterOptions type", () => {
|
|
93
|
+
const options: MoveParameterOptions = {
|
|
94
|
+
validCombinations: [
|
|
95
|
+
{ playerId: "player_one" as PlayerId },
|
|
96
|
+
{ playerId: "player_two" as PlayerId },
|
|
97
|
+
],
|
|
98
|
+
parameterInfo: {
|
|
99
|
+
playerId: {
|
|
100
|
+
type: "playerId",
|
|
101
|
+
description: "Player to choose",
|
|
102
|
+
validValues: ["player_one", "player_two"],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
expect(options.validCombinations).toHaveLength(2);
|
|
108
|
+
expect(options.parameterInfo.playerId.type).toBe("playerId");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should compile ParameterInfo type", () => {
|
|
112
|
+
const info: ParameterInfo = {
|
|
113
|
+
type: "number",
|
|
114
|
+
description: "Number of cards to draw",
|
|
115
|
+
validValues: [1, 2, 3],
|
|
116
|
+
min: 1,
|
|
117
|
+
max: 7,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
expect(info.type).toBe("number");
|
|
121
|
+
expect(info.min).toBe(1);
|
|
122
|
+
expect(info.max).toBe(7);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should compile MoveValidationError type", () => {
|
|
126
|
+
const error: MoveValidationError = {
|
|
127
|
+
moveId: "playCard",
|
|
128
|
+
errorCode: "INSUFFICIENT_INK",
|
|
129
|
+
reason: "Not enough ink to play this card",
|
|
130
|
+
context: {
|
|
131
|
+
required: 5,
|
|
132
|
+
available: 3,
|
|
133
|
+
},
|
|
134
|
+
suggestions: ["Add more cards to your inkwell"],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
expect(error.errorCode).toBe("INSUFFICIENT_INK");
|
|
138
|
+
expect(error.suggestions).toHaveLength(1);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("should compile MoveValidationError without optional fields", () => {
|
|
142
|
+
const error: MoveValidationError = {
|
|
143
|
+
moveId: "quest",
|
|
144
|
+
errorCode: "INVALID_TARGET",
|
|
145
|
+
reason: "Character is exhausted",
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
expect(error.context).toBeUndefined();
|
|
149
|
+
expect(error.suggestions).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("Type Safety", () => {
|
|
154
|
+
it("should enforce valid parameter types", () => {
|
|
155
|
+
const paramInfo: ParameterInfo = {
|
|
156
|
+
type: "cardId",
|
|
157
|
+
description: "Card ID",
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Type should be one of the allowed values
|
|
161
|
+
expect(["cardId", "playerId", "number", "boolean", "object"]).toContain(
|
|
162
|
+
paramInfo.type,
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("should allow all valid ParamFieldSchema types", () => {
|
|
167
|
+
const types: Array<ParamFieldSchema["type"]> = [
|
|
168
|
+
"cardId",
|
|
169
|
+
"playerId",
|
|
170
|
+
"number",
|
|
171
|
+
"boolean",
|
|
172
|
+
"object",
|
|
173
|
+
"string",
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
expect(types).toHaveLength(6);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|