@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,448 @@
1
+ import { afterEach, 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("Move: Quest", () => {
10
+ let testEngine: LorcanaTestEngine;
11
+
12
+ beforeEach(() => {
13
+ testEngine = new LorcanaTestEngine(
14
+ { hand: 5, deck: 10, play: 2, inkwell: 0 }, // Player one with characters in play
15
+ { hand: 5, deck: 10, play: 2, inkwell: 0 }, // Player two with characters in play
16
+ { skipPreGame: false },
17
+ );
18
+
19
+ // Complete pre-game setup to get to main phase
20
+ const ctx = testEngine.getCtx();
21
+ const choosingPlayer = ctx.choosingFirstPlayer;
22
+ testEngine.changeActivePlayer(choosingPlayer || PLAYER_ONE);
23
+ testEngine.chooseWhoGoesFirst(PLAYER_ONE);
24
+
25
+ // Complete mulligans for both players
26
+ testEngine.changeActivePlayer(PLAYER_ONE);
27
+ testEngine.alterHand([]);
28
+ testEngine.changeActivePlayer(PLAYER_TWO);
29
+ testEngine.alterHand([]);
30
+
31
+ // After mulligans, game is in mainGame segment, turn 2, player_two's turn
32
+ // Need to pass turns to get to a stable state and clear summoning sickness
33
+
34
+ // Player two takes their turn (beginning -> main -> end -> next turn)
35
+ testEngine.changeActivePlayer(PLAYER_TWO);
36
+ testEngine.passTurn(); // beginning -> main
37
+ testEngine.passTurn(); // main -> end -> turn 3 beginning -> main (player_one)
38
+
39
+ // Player one takes their turn
40
+ testEngine.changeActivePlayer(PLAYER_ONE);
41
+ testEngine.passTurn(); // main -> end -> turn 4 beginning -> main (player_two)
42
+
43
+ // Back to player two, now characters have been through a turn cycle
44
+ testEngine.changeActivePlayer(PLAYER_TWO);
45
+ testEngine.passTurn(); // main -> end -> turn 5 beginning -> main (player_one)
46
+
47
+ // Now on player_one's turn with characters no longer summoning sick
48
+ testEngine.changeActivePlayer(PLAYER_ONE);
49
+ // Now characters are dry and can quest
50
+ });
51
+
52
+ afterEach(() => {
53
+ testEngine.dispose();
54
+ });
55
+
56
+ // ========== Basic Questing Behavior ==========
57
+
58
+ describe("Basic Questing Behavior", () => {
59
+ it("should successfully quest with a ready character", () => {
60
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
61
+ const character = playZone[0];
62
+ const initialLore = testEngine.getLore(PLAYER_ONE);
63
+
64
+ expect(playZone.length).toBeGreaterThan(0);
65
+ expect(initialLore).toBe(0);
66
+
67
+ // Quest with the character
68
+ testEngine.quest(character);
69
+
70
+ // Verify lore increased
71
+ const newLore = testEngine.getLore(PLAYER_ONE);
72
+ expect(newLore).toBe(initialLore + 1); // Currently hardcoded to 1 lore per quest
73
+ });
74
+
75
+ it("should exert character after questing", () => {
76
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
77
+ const character = playZone[0];
78
+
79
+ // Character should start ready (not exerted)
80
+ const cardMeta = testEngine.getCardMeta(character);
81
+ expect(cardMeta?.state).toBe("ready");
82
+
83
+ // Quest with the character
84
+ testEngine.quest(character);
85
+
86
+ // Character should now be exerted
87
+ const newCardMeta = testEngine.getCardMeta(character);
88
+ expect(newCardMeta?.state).toBe("exerted");
89
+ });
90
+
91
+ it("should mark character as having quested", () => {
92
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
93
+ const character = playZone[0];
94
+
95
+ // Quest with the character
96
+ testEngine.quest(character);
97
+
98
+ // Character should be marked as quested
99
+ const ctx = testEngine.getCtx();
100
+ const hasQuested = ctx.trackers?.check(
101
+ `quested:${character}`,
102
+ createPlayerId(PLAYER_ONE),
103
+ );
104
+ expect(hasQuested).toBe(true);
105
+ });
106
+
107
+ it("should allow multiple different characters to quest in one turn", () => {
108
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
109
+ const char1 = playZone[0];
110
+ const char2 = playZone[1];
111
+
112
+ const initialLore = testEngine.getLore(PLAYER_ONE);
113
+
114
+ // Quest with both characters
115
+ testEngine.quest(char1);
116
+ testEngine.quest(char2);
117
+
118
+ // Lore should increase by 2 (1 per character)
119
+ const newLore = testEngine.getLore(PLAYER_ONE);
120
+ expect(newLore).toBe(initialLore + 2);
121
+ });
122
+ });
123
+
124
+ // ========== Summoning Sickness Validation ==========
125
+
126
+ describe("Summoning Sickness Validation", () => {
127
+ it("should reject questing with drying (just played) characters", () => {
128
+ // Create a fresh engine without passing turns
129
+ const freshEngine = new LorcanaTestEngine(
130
+ { hand: 5, deck: 10, play: 1 },
131
+ { hand: 5, deck: 10 },
132
+ { skipPreGame: false },
133
+ );
134
+
135
+ // Complete setup
136
+ const ctx = freshEngine.getCtx();
137
+ freshEngine.changeActivePlayer(ctx.choosingFirstPlayer || PLAYER_ONE);
138
+ freshEngine.chooseWhoGoesFirst(PLAYER_ONE);
139
+ freshEngine.changeActivePlayer(PLAYER_ONE);
140
+ freshEngine.alterHand([]);
141
+ freshEngine.changeActivePlayer(PLAYER_TWO);
142
+ freshEngine.alterHand([]);
143
+ freshEngine.changeActivePlayer(PLAYER_ONE);
144
+
145
+ // Characters in play are still "drying"
146
+ const playZone = freshEngine.getZone("play", PLAYER_ONE);
147
+ const dryingChar = playZone[0];
148
+
149
+ // Try to quest - should fail
150
+ const result = freshEngine.engine.executeMove("quest", {
151
+ playerId: createPlayerId(PLAYER_ONE),
152
+ params: {
153
+ cardId: dryingChar,
154
+ },
155
+ });
156
+
157
+ expect(result.success).toBe(false);
158
+
159
+ freshEngine.dispose();
160
+ });
161
+
162
+ it("should allow questing after character dries (next turn)", () => {
163
+ // This is the default behavior tested in beforeEach
164
+ // Characters become dry after passing turns
165
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
166
+ const driedChar = playZone[0];
167
+
168
+ // Should successfully quest
169
+ testEngine.quest(driedChar);
170
+
171
+ const lore = testEngine.getLore(PLAYER_ONE);
172
+ expect(lore).toBe(1);
173
+ });
174
+ });
175
+
176
+ // ========== Exerted State Validation ==========
177
+
178
+ describe("Exerted State Validation", () => {
179
+ it("should reject questing with already exerted character", () => {
180
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
181
+ const character = playZone[0];
182
+
183
+ // Quest once (exerts the character)
184
+ testEngine.quest(character);
185
+
186
+ // Try to quest again with exerted character - should fail
187
+ const result = testEngine.engine.executeMove("quest", {
188
+ playerId: createPlayerId(PLAYER_ONE),
189
+ params: {
190
+ cardId: character,
191
+ },
192
+ });
193
+
194
+ expect(result.success).toBe(false);
195
+ });
196
+
197
+ it("should allow questing after character is readied (next turn)", () => {
198
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
199
+ const character = playZone[0];
200
+
201
+ // Quest once
202
+ testEngine.quest(character);
203
+
204
+ // Pass turn and come back (readies characters)
205
+ testEngine.passTurn(); // Pass to player two
206
+ testEngine.passTurn(); // Pass back to player one
207
+
208
+ // Should be able to quest again (character is readied)
209
+ testEngine.quest(character);
210
+
211
+ const lore = testEngine.getLore(PLAYER_ONE);
212
+ expect(lore).toBe(2); // 1 from first quest + 1 from second
213
+ });
214
+ });
215
+
216
+ // ========== Once-Per-Turn Validation ==========
217
+
218
+ describe("Once-Per-Turn Validation", () => {
219
+ it("should reject questing twice with same character in one turn", () => {
220
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
221
+ const character = playZone[0];
222
+
223
+ // Quest once
224
+ testEngine.quest(character);
225
+
226
+ // Try to quest again - should fail due to tracker
227
+ const result = testEngine.engine.executeMove("quest", {
228
+ playerId: createPlayerId(PLAYER_ONE),
229
+ params: {
230
+ cardId: character,
231
+ },
232
+ });
233
+
234
+ expect(result.success).toBe(false);
235
+ });
236
+
237
+ it("should allow questing again after turn passes", () => {
238
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
239
+ const character = playZone[0];
240
+
241
+ // Quest once
242
+ testEngine.quest(character);
243
+
244
+ // Pass turn and come back
245
+ testEngine.passTurn(); // Pass to player two
246
+ testEngine.passTurn(); // Pass back to player one
247
+
248
+ // Should be able to quest again
249
+ testEngine.quest(character);
250
+
251
+ const lore = testEngine.getLore(PLAYER_ONE);
252
+ expect(lore).toBe(2);
253
+ });
254
+ });
255
+
256
+ // ========== Ownership & Location Validation ==========
257
+
258
+ describe("Ownership & Location Validation", () => {
259
+ it("should reject opponent's characters", () => {
260
+ const opponentPlay = testEngine.getZone("play", PLAYER_TWO);
261
+ const opponentChar = opponentPlay[0];
262
+
263
+ const result = testEngine.engine.executeMove("quest", {
264
+ playerId: createPlayerId(PLAYER_ONE),
265
+ params: {
266
+ cardId: opponentChar,
267
+ },
268
+ });
269
+
270
+ expect(result.success).toBe(false);
271
+ });
272
+
273
+ it("should reject characters not in play - character in hand", () => {
274
+ const hand = testEngine.getZone("hand", PLAYER_ONE);
275
+ const charInHand = hand[0];
276
+
277
+ const result = testEngine.engine.executeMove("quest", {
278
+ playerId: createPlayerId(PLAYER_ONE),
279
+ params: {
280
+ cardId: charInHand,
281
+ },
282
+ });
283
+
284
+ expect(result.success).toBe(false);
285
+ });
286
+
287
+ it("should reject characters not in play - character in deck", () => {
288
+ const deck = testEngine.getZone("deck", PLAYER_ONE);
289
+ const charInDeck = deck[0];
290
+
291
+ const result = testEngine.engine.executeMove("quest", {
292
+ playerId: createPlayerId(PLAYER_ONE),
293
+ params: {
294
+ cardId: charInDeck,
295
+ },
296
+ });
297
+
298
+ expect(result.success).toBe(false);
299
+ });
300
+
301
+ it("should reject invalid card IDs", () => {
302
+ const result = testEngine.engine.executeMove("quest", {
303
+ playerId: createPlayerId(PLAYER_ONE),
304
+ params: {
305
+ cardId: "invalid-card-id-12345",
306
+ },
307
+ });
308
+
309
+ expect(result.success).toBe(false);
310
+ });
311
+ });
312
+
313
+ // ========== Lore Accumulation ==========
314
+
315
+ describe("Lore Accumulation", () => {
316
+ it("should accumulate lore correctly over multiple quests", () => {
317
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
318
+
319
+ // Quest with first character
320
+ testEngine.quest(playZone[0]);
321
+ expect(testEngine.getLore(PLAYER_ONE)).toBe(1);
322
+
323
+ // Quest with second character
324
+ testEngine.quest(playZone[1]);
325
+ expect(testEngine.getLore(PLAYER_ONE)).toBe(2);
326
+
327
+ // Pass turn and quest again
328
+ testEngine.passTurn(); // Pass to player two
329
+ testEngine.passTurn(); // Pass back to player one
330
+
331
+ testEngine.quest(playZone[0]);
332
+ expect(testEngine.getLore(PLAYER_ONE)).toBe(3);
333
+ });
334
+
335
+ it("should track lore separately for each player", () => {
336
+ const p1Play = testEngine.getZone("play", PLAYER_ONE);
337
+ const p1Char = p1Play[0];
338
+
339
+ // Player one quests
340
+ testEngine.quest(p1Char);
341
+ expect(testEngine.getLore(PLAYER_ONE)).toBe(1);
342
+ expect(testEngine.getLore(PLAYER_TWO)).toBe(0);
343
+
344
+ // Pass to player two
345
+ testEngine.passTurn();
346
+ testEngine.changeActivePlayer(PLAYER_TWO);
347
+
348
+ const p2Play = testEngine.getZone("play", PLAYER_TWO);
349
+ const p2Char = p2Play[0];
350
+
351
+ // Player two quests
352
+ testEngine.quest(p2Char);
353
+ expect(testEngine.getLore(PLAYER_ONE)).toBe(1);
354
+ expect(testEngine.getLore(PLAYER_TWO)).toBe(1);
355
+ });
356
+
357
+ it("should persist lore across turns", () => {
358
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
359
+ const character = playZone[0];
360
+
361
+ // Quest
362
+ testEngine.quest(character);
363
+ const loreAfterQuest = testEngine.getLore(PLAYER_ONE);
364
+
365
+ // Pass multiple turns
366
+ testEngine.passTurn();
367
+ testEngine.passTurn();
368
+ testEngine.passTurn();
369
+ testEngine.passTurn();
370
+
371
+ // Lore should not decrease
372
+ const loreAfterTurns = testEngine.getLore(PLAYER_ONE);
373
+ expect(loreAfterTurns).toBe(loreAfterQuest);
374
+ });
375
+ });
376
+
377
+ // ========== Win Condition Integration ==========
378
+
379
+ describe("Win Condition Integration", () => {
380
+ it("should continue game when lore is less than 20", () => {
381
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
382
+
383
+ // Quest to gain some lore (but not 20)
384
+ testEngine.quest(playZone[0]);
385
+
386
+ // Game should still be ongoing
387
+ // Game should still be ongoing
388
+ // Note: Win condition check happens at specific points in game flow
389
+ // Just verify lore is less than 20
390
+ expect(testEngine.getLore(PLAYER_ONE)).toBeLessThan(20);
391
+ });
392
+
393
+ it("should end game when player reaches 20 lore", () => {
394
+ // This test verifies the win condition logic exists
395
+ // In a real game, reaching 20 lore would trigger game end
396
+ // For now, we'll just verify we can accumulate toward 20
397
+
398
+ let lore = 0;
399
+ let turns = 0;
400
+ const maxTurns = 100; // Prevent infinite loop
401
+
402
+ // Keep questing until we reach 20 lore or max turns
403
+ while (lore < 20 && turns < maxTurns) {
404
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
405
+
406
+ // Quest with available characters
407
+ for (const char of playZone) {
408
+ if (lore >= 20) break;
409
+
410
+ try {
411
+ testEngine.quest(char);
412
+ lore = testEngine.getLore(PLAYER_ONE);
413
+ } catch {}
414
+ }
415
+
416
+ // Stop if we've reached 20 lore (game ended)
417
+ if (lore >= 20) break;
418
+
419
+ // Pass turns to reset characters
420
+ testEngine.passTurn();
421
+ testEngine.passTurn();
422
+ turns++;
423
+ }
424
+
425
+ // Verify we can reach 20 lore through questing
426
+ expect(lore).toBeGreaterThanOrEqual(20);
427
+ });
428
+ });
429
+
430
+ // ========== Phase Validation ==========
431
+
432
+ describe("Phase Validation", () => {
433
+ it("should only allow questing during main phase", () => {
434
+ const playZone = testEngine.getZone("play", PLAYER_ONE);
435
+ const character = playZone[0];
436
+
437
+ // We're in main phase by default in our tests
438
+ // Quest should succeed
439
+ testEngine.quest(character);
440
+
441
+ const lore = testEngine.getLore(PLAYER_ONE);
442
+ expect(lore).toBe(1);
443
+
444
+ // Note: Testing other phases would require more complex game flow control
445
+ // The move definition already has isMainPhase() check
446
+ });
447
+ });
448
+ });
@@ -0,0 +1,49 @@
1
+ import { createMove } from "@drmxrcy/tcg-core";
2
+ import { useLorcanaOps } from "../../../operations";
3
+ import type {
4
+ LorcanaCardMeta,
5
+ LorcanaGameState,
6
+ LorcanaMoveParams,
7
+ } from "../../../types";
8
+ import { and, canQuest, isMainPhase } from "../../../validators";
9
+
10
+ /**
11
+ * Quest Move
12
+ *
13
+ * Rule 4.3.5: Exert character to gain lore
14
+ *
15
+ * Requirements:
16
+ * - Character is ready (not exerted)
17
+ * - Character is not drying (summoning sickness)
18
+ * - Character has Lore value > 0
19
+ * - Haven't quested with this character this turn
20
+ *
21
+ * Effects:
22
+ * - Exert the character
23
+ * - Add Lore value to player's lore count
24
+ * - Mark character as having quested this turn
25
+ */
26
+ export const quest = createMove<
27
+ LorcanaGameState,
28
+ LorcanaMoveParams,
29
+ "quest",
30
+ LorcanaCardMeta
31
+ >({
32
+ condition: and(isMainPhase(), (state, context) =>
33
+ canQuest(context.params.cardId)(state, context),
34
+ ),
35
+ reducer: (draft, context) => {
36
+ const { cardId } = context.params;
37
+ const ops = useLorcanaOps(context);
38
+
39
+ // Exert the character
40
+ ops.exertCard(cardId);
41
+
42
+ // Add lore (simplified - assume 1 lore per quest)
43
+ // In full implementation, would read Lore value from card definition
44
+ ops.addLore(draft, context.playerId, 1);
45
+
46
+ // Mark as quested
47
+ context.trackers?.mark(`quested:${cardId}`, context.playerId);
48
+ },
49
+ });
@@ -0,0 +1,36 @@
1
+ import { createMove } from "@drmxrcy/tcg-core";
2
+ import { useLorcanaOps } from "../../../operations";
3
+ import type {
4
+ LorcanaCardMeta,
5
+ LorcanaGameState,
6
+ LorcanaMoveParams,
7
+ } from "../../../types";
8
+
9
+ /**
10
+ * Manual Exert (Debug/Testing)
11
+ *
12
+ * Manually exert a card without conditions.
13
+ *
14
+ * WARNING: This bypasses normal game rules and should only be used for:
15
+ * - Testing
16
+ * - Debugging
17
+ * - Development tools
18
+ *
19
+ * Do NOT use in production game logic.
20
+ */
21
+ export const manualExert = createMove<
22
+ LorcanaGameState,
23
+ LorcanaMoveParams,
24
+ "manualExert",
25
+ LorcanaCardMeta
26
+ >({
27
+ condition: (_state, context) => {
28
+ // Not available during chooseFirstPlayer phase
29
+ return context.flow?.currentPhase !== "chooseFirstPlayer";
30
+ },
31
+ reducer: (_draft, context) => {
32
+ const { cardId } = context.params;
33
+ const ops = useLorcanaOps(context);
34
+ ops.exertCard(cardId);
35
+ },
36
+ });
@@ -0,0 +1,35 @@
1
+ import { createMove } from "@drmxrcy/tcg-core";
2
+ import type {
3
+ LorcanaCardMeta,
4
+ LorcanaGameState,
5
+ LorcanaMoveParams,
6
+ } from "../../../types";
7
+
8
+ /**
9
+ * Resolve Bag Effect
10
+ *
11
+ * Rule 8.7: Bag system for triggered effects
12
+ *
13
+ * The "bag" is a queue of triggered effects that need to be resolved.
14
+ * This move removes a resolved effect from the bag.
15
+ *
16
+ * Process:
17
+ * 1. Execute the effect logic
18
+ * 2. Remove the bag entry
19
+ * 3. Allow next effect in bag to be processed
20
+ */
21
+ export const resolveBag = createMove<
22
+ LorcanaGameState,
23
+ LorcanaMoveParams,
24
+ "resolveBag",
25
+ LorcanaCardMeta
26
+ >({
27
+ condition: (state, _context) => {
28
+ // Only available when there are bags to resolve
29
+ return state.external.bag && state.external.bag.length > 0;
30
+ },
31
+ reducer: (draft, context) => {
32
+ const { bagId } = context.params;
33
+ draft.external.bag = draft.external.bag.filter((b) => b.id !== bagId);
34
+ },
35
+ });
@@ -0,0 +1,34 @@
1
+ import { createMove } from "@drmxrcy/tcg-core";
2
+ import type {
3
+ LorcanaCardMeta,
4
+ LorcanaGameState,
5
+ LorcanaMoveParams,
6
+ } from "../../../types";
7
+
8
+ /**
9
+ * Resolve Effect
10
+ *
11
+ * Generic effect resolution move for handling ongoing effects.
12
+ *
13
+ * Process:
14
+ * 1. Execute the effect logic
15
+ * 2. Remove the effect from the effects list
16
+ * 3. Clean up any associated state
17
+ */
18
+ export const resolveEffect = createMove<
19
+ LorcanaGameState,
20
+ LorcanaMoveParams,
21
+ "resolveEffect",
22
+ LorcanaCardMeta
23
+ >({
24
+ condition: (state, _context) => {
25
+ // Only available when there are effects to resolve
26
+ return state.external.effects && state.external.effects.length > 0;
27
+ },
28
+ reducer: (draft, context) => {
29
+ const { effectId } = context.params;
30
+ draft.external.effects = draft.external.effects.filter(
31
+ (e) => e.id !== effectId,
32
+ );
33
+ },
34
+ });
@@ -0,0 +1,85 @@
1
+ import type { GameMoveDefinitions } from "@drmxrcy/tcg-core";
2
+ import type {
3
+ LorcanaCardMeta,
4
+ LorcanaGameState,
5
+ LorcanaMoveParams,
6
+ } from "../../types";
7
+ // Ability moves
8
+ import { activateAbility } from "./abilities/activate-ability";
9
+ import { challenge } from "./core/challenge";
10
+ // Core moves
11
+ import { playCard } from "./core/play-card";
12
+ import { quest } from "./core/quest";
13
+ // Debug moves
14
+ import { manualExert } from "./debug/manual-exert";
15
+ // Effect moves
16
+ import { resolveBag } from "./effects/resolve-bag";
17
+ import { resolveEffect } from "./effects/resolve-effect";
18
+ // Location moves
19
+ import { moveCharacterToLocation } from "./locations/move-character-to-location";
20
+ // Resource moves
21
+ import { putACardIntoTheInkwell } from "./resources/put-card-into-inkwell";
22
+ import { alterHand } from "./setup/alter-hand";
23
+ // Setup moves
24
+ import { chooseWhoGoesFirstMove } from "./setup/choose-first-player";
25
+ import { drawCards } from "./setup/draw-cards";
26
+ // Song moves
27
+ import { sing } from "./songs/sing";
28
+ import { singTogether } from "./songs/sing-together";
29
+ import { concede } from "./standard/concede";
30
+ // Standard moves
31
+ import { passTurn } from "./standard/pass-turn";
32
+
33
+ /**
34
+ * Lorcana Move Definitions
35
+ *
36
+ * All game moves organized by category:
37
+ * - Setup: Game initialization moves
38
+ * - Resources: Ink management
39
+ * - Core: Primary gameplay (play, quest, challenge)
40
+ * - Songs: Singing mechanics
41
+ * - Locations: Location interactions
42
+ * - Abilities: Activated abilities
43
+ * - Effects: Effect resolution
44
+ * - Debug: Testing utilities
45
+ * - Standard: Pass and concede
46
+ */
47
+ export const lorcanaMoves: GameMoveDefinitions<
48
+ LorcanaGameState,
49
+ LorcanaMoveParams,
50
+ LorcanaCardMeta
51
+ > = {
52
+ // ===== Setup Moves =====
53
+ chooseWhoGoesFirstMove,
54
+ alterHand,
55
+ drawCards,
56
+
57
+ // ===== Resource Moves =====
58
+ putACardIntoTheInkwell,
59
+
60
+ // ===== Core Game Moves =====
61
+ playCard,
62
+ quest,
63
+ challenge,
64
+
65
+ // ===== Song Moves =====
66
+ sing,
67
+ singTogether,
68
+
69
+ // ===== Location Moves =====
70
+ moveCharacterToLocation,
71
+
72
+ // ===== Ability Moves =====
73
+ activateAbility,
74
+
75
+ // ===== Effect Resolution =====
76
+ resolveBag,
77
+ resolveEffect,
78
+
79
+ // ===== Manual Actions (Testing/Debug) =====
80
+ manualExert,
81
+
82
+ // ===== Standard Moves =====
83
+ passTurn,
84
+ concede,
85
+ };