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