@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,189 @@
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: Concede", () => {
10
+ let testEngine: LorcanaTestEngine;
11
+
12
+ beforeEach(() => {
13
+ testEngine = new LorcanaTestEngine(
14
+ { hand: 7, deck: 10 },
15
+ { hand: 7, deck: 10 },
16
+ { skipPreGame: true },
17
+ );
18
+ });
19
+
20
+ afterEach(() => {
21
+ testEngine.dispose();
22
+ });
23
+
24
+ // ========== Basic Behavior Tests ==========
25
+
26
+ describe("Basic Concede Behavior", () => {
27
+ it("should end game when player concedes", () => {
28
+ // Player one concedes
29
+ testEngine.changeActivePlayer(PLAYER_ONE);
30
+ const result = testEngine.engine.executeMove("concede", {
31
+ playerId: createPlayerId(PLAYER_ONE),
32
+ params: {},
33
+ });
34
+
35
+ expect(result.success).toBe(true);
36
+
37
+ // Game should be ended
38
+ expect(testEngine.engine.hasGameEnded()).toBe(true);
39
+
40
+ // Verify game end result
41
+ const gameEndResult = testEngine.engine.getGameEndResult();
42
+ expect(gameEndResult).toBeDefined();
43
+ expect(gameEndResult?.reason).toBe("concede");
44
+ });
45
+
46
+ it("should include concede metadata with concedeBy field", () => {
47
+ // Player two concedes
48
+ testEngine.changeActivePlayer(PLAYER_TWO);
49
+ const result = testEngine.engine.executeMove("concede", {
50
+ playerId: createPlayerId(PLAYER_TWO),
51
+ params: {},
52
+ });
53
+
54
+ expect(result.success).toBe(true);
55
+
56
+ // Verify metadata includes concedeBy
57
+ const gameEndResult = testEngine.engine.getGameEndResult();
58
+ expect(gameEndResult).toBeDefined();
59
+ expect(gameEndResult?.metadata?.concedeBy).toBe(PLAYER_TWO);
60
+ });
61
+
62
+ it("should set opponent as winner when player concedes", () => {
63
+ // Player one concedes
64
+ testEngine.changeActivePlayer(PLAYER_ONE);
65
+ const result = testEngine.engine.executeMove("concede", {
66
+ playerId: createPlayerId(PLAYER_ONE),
67
+ params: {},
68
+ });
69
+
70
+ expect(result.success).toBe(true);
71
+
72
+ // Winner should be player two (the opponent)
73
+ const gameEndResult = testEngine.engine.getGameEndResult();
74
+ expect(gameEndResult).toBeDefined();
75
+ expect(gameEndResult?.winner).toBe(PLAYER_TWO);
76
+ });
77
+
78
+ it("should set correct winner when different player concedes", () => {
79
+ // Player two concedes
80
+ testEngine.changeActivePlayer(PLAYER_TWO);
81
+ const result = testEngine.engine.executeMove("concede", {
82
+ playerId: createPlayerId(PLAYER_TWO),
83
+ params: {},
84
+ });
85
+
86
+ expect(result.success).toBe(true);
87
+
88
+ // Winner should be player one (the opponent)
89
+ const gameEndResult = testEngine.engine.getGameEndResult();
90
+ expect(gameEndResult).toBeDefined();
91
+ expect(gameEndResult?.winner).toBe(PLAYER_ONE);
92
+ });
93
+ });
94
+
95
+ // ========== Game End Verification ==========
96
+
97
+ describe("Game End State", () => {
98
+ it("should prevent further moves after concede", () => {
99
+ // Player one concedes
100
+ testEngine.changeActivePlayer(PLAYER_ONE);
101
+ const concedeResult = testEngine.engine.executeMove("concede", {
102
+ playerId: createPlayerId(PLAYER_ONE),
103
+ params: {},
104
+ });
105
+
106
+ // Verify concede succeeded
107
+ expect(concedeResult.success).toBe(true);
108
+ expect(testEngine.engine.hasGameEnded()).toBe(true);
109
+
110
+ // Try to execute another move
111
+ testEngine.changeActivePlayer(PLAYER_TWO);
112
+ const result = testEngine.engine.executeMove("passTurn", {
113
+ playerId: createPlayerId(PLAYER_TWO),
114
+ params: {},
115
+ });
116
+
117
+ expect(result.success).toBe(false);
118
+ if (!result.success) {
119
+ expect(result.errorCode).toBe("GAME_ENDED");
120
+ expect(result.error).toContain("already ended");
121
+ }
122
+ });
123
+
124
+ it("should allow concede at any time (during any phase)", () => {
125
+ // Concede during main phase (default)
126
+ testEngine.changeActivePlayer(PLAYER_ONE);
127
+ const result = testEngine.engine.executeMove("concede", {
128
+ playerId: createPlayerId(PLAYER_ONE),
129
+ params: {},
130
+ });
131
+
132
+ expect(result.success).toBe(true);
133
+ });
134
+ });
135
+
136
+ // ========== Edge Cases ==========
137
+
138
+ describe("Edge Cases", () => {
139
+ it("should not allow concede when game already ended", () => {
140
+ // First player concedes
141
+ testEngine.changeActivePlayer(PLAYER_ONE);
142
+ testEngine.engine.executeMove("concede", {
143
+ playerId: createPlayerId(PLAYER_ONE),
144
+ params: {},
145
+ });
146
+
147
+ // Try to concede again
148
+ testEngine.changeActivePlayer(PLAYER_TWO);
149
+ const result = testEngine.engine.executeMove("concede", {
150
+ playerId: createPlayerId(PLAYER_TWO),
151
+ params: {},
152
+ });
153
+
154
+ expect(result.success).toBe(false);
155
+ if (!result.success) {
156
+ expect(result.errorCode).toBe("GAME_ENDED");
157
+ }
158
+ });
159
+
160
+ it("should handle concede when player has no cards", () => {
161
+ // Setup: Create a game where player one has no cards in any zone
162
+ const emptyTestEngine = new LorcanaTestEngine(
163
+ { hand: 0, deck: 0 }, // Player one has no cards
164
+ { hand: 7, deck: 10 }, // Player two has cards
165
+ { skipPreGame: true },
166
+ );
167
+
168
+ // Player one (with no cards) concedes
169
+ emptyTestEngine.changeActivePlayer(PLAYER_ONE);
170
+ const result = emptyTestEngine.engine.executeMove("concede", {
171
+ playerId: createPlayerId(PLAYER_ONE),
172
+ params: {},
173
+ });
174
+
175
+ expect(result.success).toBe(true);
176
+
177
+ // Game should be ended
178
+ expect(emptyTestEngine.engine.hasGameEnded()).toBe(true);
179
+
180
+ // Winner should be player two (even though player one had no cards)
181
+ const gameEndResult = emptyTestEngine.engine.getGameEndResult();
182
+ expect(gameEndResult).toBeDefined();
183
+ expect(gameEndResult?.winner).toBe(PLAYER_TWO);
184
+ expect(gameEndResult?.reason).toBe("concede");
185
+
186
+ emptyTestEngine.dispose();
187
+ });
188
+ });
189
+ });
@@ -0,0 +1,72 @@
1
+ import { createMove, type PlayerId, type ZoneId } from "@drmxrcy/tcg-core";
2
+ import type {
3
+ LorcanaCardMeta,
4
+ LorcanaGameState,
5
+ LorcanaMoveParams,
6
+ } from "../../../types";
7
+ import { lorcanaZones } from "../../zones/zone-configs";
8
+
9
+ /**
10
+ * Concede
11
+ *
12
+ * Rule 1.9.1.2: Player can concede at any time
13
+ *
14
+ * Effects:
15
+ * - Current player loses immediately
16
+ * - Game ends
17
+ * - Other player(s) win
18
+ *
19
+ * The engine handles game end logic automatically.
20
+ */
21
+ export const concede = createMove<
22
+ LorcanaGameState,
23
+ LorcanaMoveParams,
24
+ "concede",
25
+ LorcanaCardMeta
26
+ >({
27
+ condition: (_state, context) => {
28
+ // Cannot concede during setup phases
29
+ const phase = context.flow?.currentPhase;
30
+ if (phase === "chooseFirstPlayer" || phase === "mulligan") {
31
+ return false;
32
+ }
33
+ return true;
34
+ },
35
+ reducer: (draft, context) => {
36
+ // Get all players from the game state
37
+ const allPlayers = Object.keys(draft.external.loreScores) as PlayerId[];
38
+
39
+ // Determine winner: the opponent who is NOT conceding
40
+ // Try to find active players by checking zones
41
+ const uniquePlayerIds = new Set<PlayerId>();
42
+
43
+ // Get all zone IDs dynamically from zone configuration
44
+ const zoneIds = Object.keys(lorcanaZones) as ZoneId[];
45
+
46
+ for (const zoneId of zoneIds) {
47
+ for (const playerId of allPlayers) {
48
+ try {
49
+ const cards = context.zones.getCardsInZone(zoneId, playerId);
50
+ if (cards.length > 0) {
51
+ uniquePlayerIds.add(playerId);
52
+ }
53
+ } catch {
54
+ // Zone might not exist for this player or other errors
55
+ // Continue processing other zones/players
56
+ }
57
+ }
58
+ }
59
+
60
+ // Find the opponent (player who is not conceding)
61
+ const playerIds = Array.from(uniquePlayerIds);
62
+ const winner = playerIds.find((id) => id !== context.playerId);
63
+
64
+ // Signal game end via context
65
+ // Note: winner may be undefined if no other players have cards (edge case)
66
+ context.endGame?.({
67
+ winner,
68
+ reason: "concede",
69
+ metadata: { concedeBy: context.playerId },
70
+ });
71
+ },
72
+ });
@@ -0,0 +1,49 @@
1
+ import { createMove } from "@drmxrcy/tcg-core";
2
+ import type {
3
+ LorcanaCardMeta,
4
+ LorcanaGameState,
5
+ LorcanaMoveParams,
6
+ } from "../../../types";
7
+
8
+ /**
9
+ * Pass Turn
10
+ *
11
+ * Rule 4.1.2: Player completes their turn
12
+ *
13
+ * Effects:
14
+ * - End current phase
15
+ * - Pass turn to next player
16
+ * - Reset turn-based trackers
17
+ * - Ready all cards (in beginning phase)
18
+ *
19
+ * The engine handles all turn transition logic automatically.
20
+ */
21
+ export const passTurn = createMove<
22
+ LorcanaGameState,
23
+ LorcanaMoveParams,
24
+ "passTurn",
25
+ LorcanaCardMeta
26
+ >({
27
+ condition: (state, context) => {
28
+ // Cannot pass turn during setup phases
29
+ const phase = context.flow?.currentPhase;
30
+ if (phase === "chooseFirstPlayer" || phase === "mulligan") {
31
+ return false;
32
+ }
33
+
34
+ // Can only pass turn if it's your turn
35
+ if (
36
+ context.flow?.currentPlayer &&
37
+ context.flow.currentPlayer !== context.playerId
38
+ ) {
39
+ return false;
40
+ }
41
+
42
+ return true;
43
+ },
44
+ reducer: (_draft, context) => {
45
+ // End the current phase
46
+ // Flow will automatically transition: main → end → next turn's beginning
47
+ context.flow?.endPhase();
48
+ },
49
+ });
@@ -0,0 +1,19 @@
1
+ import type { PlayerId } from "@drmxrcy/tcg-core";
2
+ import { createInitialLorcanaState, type LorcanaGameState } from "../../types";
3
+
4
+ /**
5
+ * Game Setup Function
6
+ *
7
+ * Initializes the Lorcana game state.
8
+ *
9
+ * @param players - List of players in the game
10
+ * @returns Initial Lorcana game state
11
+ */
12
+ export function setupLorcanaGame(
13
+ players: Array<{ id: string }>,
14
+ ): LorcanaGameState {
15
+ const playerIds = players.map((p) => p.id as PlayerId);
16
+ // Default to first player starting if not specified
17
+ // In a real game, this would be determined by coin flip or similar
18
+ return createInitialLorcanaState(playerIds[0], playerIds[1], playerIds[0]);
19
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Tracker Configuration
3
+ *
4
+ * Defines boolean flags that track actions taken during gameplay.
5
+ * These trackers are automatically reset based on their configuration:
6
+ *
7
+ * - hasInked: Player can only put one card into inkwell per turn
8
+ * - quested:{cardId}: Each character can only quest once per turn
9
+ *
10
+ * The engine automatically resets perTurn trackers at the end of each turn.
11
+ */
12
+ export const trackerConfig = {
13
+ /**
14
+ * Actions that reset at the end of each turn
15
+ * Supports wildcards - "quested:*" matches all "quested:cardId" trackers
16
+ */
17
+ perTurn: ["hasInked", "quested:*"],
18
+
19
+ /**
20
+ * Track actions separately for each player
21
+ */
22
+ perPlayer: true,
23
+ };
@@ -0,0 +1,26 @@
1
+ import type { LorcanaGameState } from "../../types";
2
+
3
+ /**
4
+ * Lore Victory Win Condition
5
+ *
6
+ * Rule 1.9.1.1: First player to reach 20 lore wins the game
7
+ *
8
+ * @param state - Current game state
9
+ * @returns Win condition result or undefined if game continues
10
+ */
11
+ export function checkLoreVictory(
12
+ state: LorcanaGameState,
13
+ ):
14
+ | { winner: string; reason: string; metadata: { finalLore: number } }
15
+ | undefined {
16
+ for (const [playerId, lore] of Object.entries(state.external.loreScores)) {
17
+ if (lore >= 20) {
18
+ return {
19
+ winner: playerId,
20
+ reason: "lore_victory",
21
+ metadata: { finalLore: lore },
22
+ };
23
+ }
24
+ }
25
+ return undefined;
26
+ }