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