@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
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# @drmxrcy/tcg-lorcana
|
|
2
|
+
|
|
3
|
+
Disney Lorcana TCG engine built with `@drmxrcy/tcg-core` framework.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package implements the complete Disney Lorcana trading card game using the `@drmxrcy/tcg-core` framework. It serves as both a production-ready Lorcana engine and a reference implementation demonstrating best practices for building TCG engines.
|
|
8
|
+
|
|
9
|
+
## Status
|
|
10
|
+
|
|
11
|
+
🚧 **Work in Progress** - Package structure and configuration complete. Game logic implementation in progress.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
This is a workspace package. Install dependencies from the monorepo root:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Development
|
|
22
|
+
|
|
23
|
+
### Available Scripts
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Type checking
|
|
27
|
+
bun run check-types
|
|
28
|
+
|
|
29
|
+
# Formatting
|
|
30
|
+
bun run format
|
|
31
|
+
|
|
32
|
+
# Linting
|
|
33
|
+
bun run lint
|
|
34
|
+
|
|
35
|
+
# Testing
|
|
36
|
+
bun run test
|
|
37
|
+
bun run test:watch
|
|
38
|
+
bun run test:coverage
|
|
39
|
+
|
|
40
|
+
# Run all checks
|
|
41
|
+
bun run check
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Project Structure
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
src/
|
|
48
|
+
├── game-definition/ # Game definition and configuration
|
|
49
|
+
├── moves/ # Move handlers
|
|
50
|
+
├── cards/ # Card definitions and abilities
|
|
51
|
+
├── types/ # TypeScript type definitions
|
|
52
|
+
├── queries/ # State query functions
|
|
53
|
+
├── rules/ # Rule implementations
|
|
54
|
+
└── index.ts # Main entry point
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Each directory contains a README.md explaining its purpose and usage.
|
|
58
|
+
|
|
59
|
+
## Architecture
|
|
60
|
+
|
|
61
|
+
This package integrates with `@drmxrcy/tcg-core` by:
|
|
62
|
+
|
|
63
|
+
1. **Defining game-specific state** - Extends base `GameState` with Lorcana data
|
|
64
|
+
2. **Registering moves** - Implements move handlers for all player actions
|
|
65
|
+
3. **Configuring zones** - Defines zones (Deck, Hand, Play, Discard, Inkwell)
|
|
66
|
+
4. **Defining flow** - Specifies turn/phase/step structure
|
|
67
|
+
5. **Implementing abilities** - Creates keyword, triggered, and activated abilities
|
|
68
|
+
|
|
69
|
+
## Usage Example
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
import { RuleEngine } from "@drmxrcy/tcg-core";
|
|
73
|
+
import { lorcanaGame } from "@drmxrcy/tcg-lorcana";
|
|
74
|
+
|
|
75
|
+
// Create engine instance
|
|
76
|
+
const engine = new RuleEngine(lorcanaGame, {
|
|
77
|
+
seed: "game-seed-123",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Setup game with player decks
|
|
81
|
+
const initialState = engine.setup({
|
|
82
|
+
players: {
|
|
83
|
+
player1: { deckId: "deck-1" },
|
|
84
|
+
player2: { deckId: "deck-2" },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Execute moves
|
|
89
|
+
const result = engine.executeMove("playCard", {
|
|
90
|
+
playerId: "player1",
|
|
91
|
+
params: { cardId: "card-123" },
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Get current state
|
|
95
|
+
const state = engine.getState();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Dependencies
|
|
99
|
+
|
|
100
|
+
- `@drmxrcy/tcg-core` - Core TCG engine framework
|
|
101
|
+
- All dependencies from `@drmxrcy/tcg-core` (Immer, Zod, nanoid, seedrandom, etc.)
|
|
102
|
+
|
|
103
|
+
## Boundaries
|
|
104
|
+
|
|
105
|
+
This package uses Turborepo boundaries to enforce clean architecture:
|
|
106
|
+
|
|
107
|
+
- ✅ Can depend on: `@drmxrcy/tcg-core`
|
|
108
|
+
- ❌ Cannot depend on: Other game engines, UI packages, networking
|
|
109
|
+
|
|
110
|
+
## Testing
|
|
111
|
+
|
|
112
|
+
All game mechanics are tested through behavior-driven tests:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
describe("Quest Move", () => {
|
|
116
|
+
it("gains lore equal to card's lore value", () => {
|
|
117
|
+
const engine = createTestEngine();
|
|
118
|
+
// Test implementation
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("exerts the character", () => {
|
|
122
|
+
// Test implementation
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("rejects questing with exerted character", () => {
|
|
126
|
+
// Test implementation
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Documentation
|
|
132
|
+
|
|
133
|
+
- **Product Mission**: `.agent-os/packages/lorcana-engine/product/mission.md`
|
|
134
|
+
- **Tech Stack**: `.agent-os/packages/lorcana-engine/product/tech-stack.md`
|
|
135
|
+
- **Roadmap**: `.agent-os/packages/lorcana-engine/product/roadmap.md`
|
|
136
|
+
- **Integration Guide**: `@packages/core/ENGINE_INTEGRATION.md`
|
|
137
|
+
|
|
138
|
+
## Contributing
|
|
139
|
+
|
|
140
|
+
This package follows strict coding standards:
|
|
141
|
+
|
|
142
|
+
- TypeScript strict mode (no `any` types)
|
|
143
|
+
- Test-driven development (TDD mandatory)
|
|
144
|
+
- Behavior-driven testing (no mocking)
|
|
145
|
+
- Immutable state (via Immer)
|
|
146
|
+
- Pure functions preferred
|
|
147
|
+
- Biome for formatting and linting
|
|
148
|
+
|
|
149
|
+
See `@.agent-os/standards/` for detailed guidelines.
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
|
154
|
+
|
|
155
|
+
## References
|
|
156
|
+
|
|
157
|
+
- [Disney Lorcana Official Site](https://www.disneylorcana.com/)
|
|
158
|
+
- [Lorcana Official Rules](https://www.disneylorcana.com/rules)
|
|
159
|
+
- `@drmxrcy/tcg-core` framework documentation
|
|
160
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@drmxrcy/tcg-lorcana",
|
|
3
|
+
"version": "0.0.0-202602060544",
|
|
4
|
+
"description": "Disney Lorcana TCG engine implementation using @drmxrcy/tcg-core framework",
|
|
5
|
+
"private": false,
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src/"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"default": "./src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"./testing": {
|
|
17
|
+
"types": "./src/testing/index.ts",
|
|
18
|
+
"default": "./src/testing/index.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc --noEmit && mkdir -p dist",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"check-types": "tsc --noEmit",
|
|
25
|
+
"format": "bun x @biomejs/biome check --fix --max-diagnostics=none --diagnostic-level=error --linter-enabled=false ./src",
|
|
26
|
+
"lint": "bun x @biomejs/biome lint --fix --max-diagnostics=none --diagnostic-level=error ./src",
|
|
27
|
+
"test": "AGENT=1 bun test --silent",
|
|
28
|
+
"test:watch": "bun test --watch",
|
|
29
|
+
"test:coverage": "bun test --coverage",
|
|
30
|
+
"check": "turbo format lint check-types test"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@drmxrcy/tcg-core": "workspace:*",
|
|
34
|
+
"@drmxrcy/tcg-lorcana-types": "workspace:*",
|
|
35
|
+
"immer": "11.0.1"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "2.3.11",
|
|
39
|
+
"@types/bun": "1.3.4",
|
|
40
|
+
"typescript": "5.9.3"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"typescript": "5.9.3"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
LorcanaTestEngine,
|
|
4
|
+
PLAYER_ONE,
|
|
5
|
+
PLAYER_TWO,
|
|
6
|
+
} from "../../testing/lorcana-test-engine";
|
|
7
|
+
|
|
8
|
+
describe("Move Enumeration Integration", () => {
|
|
9
|
+
describe("Phase Transition Integration", () => {
|
|
10
|
+
let testEngine: LorcanaTestEngine;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
testEngine = new LorcanaTestEngine(
|
|
14
|
+
{ hand: 7, deck: 10 },
|
|
15
|
+
{ hand: 7, deck: 10 },
|
|
16
|
+
{ skipPreGame: false },
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
testEngine.dispose();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it.todo("should enumerate moves correctly across setup → mulligan → main phase", () => {
|
|
25
|
+
// Phase 1: Choose First Player
|
|
26
|
+
expect(testEngine.getGamePhase()).toBe("chooseFirstPlayer");
|
|
27
|
+
|
|
28
|
+
const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
|
|
29
|
+
let availableMoves = testEngine.getAvailableMoves(
|
|
30
|
+
choosingPlayer || PLAYER_ONE,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Should have chooseWhoGoesFirstMove available
|
|
34
|
+
expect(availableMoves).toContain("chooseWhoGoesFirstMove");
|
|
35
|
+
|
|
36
|
+
// Execute choice
|
|
37
|
+
testEngine.changeActivePlayer(choosingPlayer || PLAYER_ONE);
|
|
38
|
+
testEngine.chooseWhoGoesFirst(PLAYER_ONE);
|
|
39
|
+
|
|
40
|
+
// Phase 2: Mulligan
|
|
41
|
+
expect(testEngine.getGamePhase()).toBe("mulligan");
|
|
42
|
+
|
|
43
|
+
// Both players should have alterHand available
|
|
44
|
+
availableMoves = testEngine.getAvailableMoves(PLAYER_ONE);
|
|
45
|
+
expect(availableMoves).toContain("alterHand");
|
|
46
|
+
|
|
47
|
+
availableMoves = testEngine.getAvailableMoves(PLAYER_TWO);
|
|
48
|
+
expect(availableMoves).toContain("alterHand");
|
|
49
|
+
|
|
50
|
+
// Execute mulligans
|
|
51
|
+
testEngine.changeActivePlayer(PLAYER_ONE);
|
|
52
|
+
testEngine.alterHand([]);
|
|
53
|
+
|
|
54
|
+
testEngine.changeActivePlayer(PLAYER_TWO);
|
|
55
|
+
testEngine.alterHand([]);
|
|
56
|
+
|
|
57
|
+
// Phase 3: Main Phase (or next phase in flow)
|
|
58
|
+
// After mulligan, game should transition to next phase
|
|
59
|
+
const finalPhase = testEngine.getGamePhase();
|
|
60
|
+
expect(finalPhase).not.toBe("mulligan");
|
|
61
|
+
expect(finalPhase).not.toBe("chooseFirstPlayer");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should show different available moves for different players", () => {
|
|
65
|
+
const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
|
|
66
|
+
const otherPlayer =
|
|
67
|
+
choosingPlayer === PLAYER_ONE ? PLAYER_TWO : PLAYER_ONE;
|
|
68
|
+
|
|
69
|
+
// Choosing player should see chooseWhoGoesFirstMove
|
|
70
|
+
const chooserMoves = testEngine.getAvailableMoves(
|
|
71
|
+
choosingPlayer || PLAYER_ONE,
|
|
72
|
+
);
|
|
73
|
+
expect(chooserMoves).toContain("chooseWhoGoesFirstMove");
|
|
74
|
+
|
|
75
|
+
// Other player should see no moves
|
|
76
|
+
const otherMoves = testEngine.getAvailableMoves(otherPlayer);
|
|
77
|
+
expect(otherMoves).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should enumerate moves correctly after OTP transitions", () => {
|
|
81
|
+
const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
|
|
82
|
+
|
|
83
|
+
// Before choosing - only chooser has moves
|
|
84
|
+
const chooserMoves = testEngine.getAvailableMoves(
|
|
85
|
+
choosingPlayer || PLAYER_ONE,
|
|
86
|
+
);
|
|
87
|
+
expect(chooserMoves.length).toBeGreaterThan(0);
|
|
88
|
+
|
|
89
|
+
// Choose first player
|
|
90
|
+
testEngine.changeActivePlayer(choosingPlayer || PLAYER_ONE);
|
|
91
|
+
testEngine.chooseWhoGoesFirst(PLAYER_ONE);
|
|
92
|
+
|
|
93
|
+
// After choosing - OTP (player_one) should have mulligan move (has priority)
|
|
94
|
+
const p1Moves = testEngine.getAvailableMoves(PLAYER_ONE);
|
|
95
|
+
const p2Moves = testEngine.getAvailableMoves(PLAYER_TWO);
|
|
96
|
+
|
|
97
|
+
expect(p1Moves).toContain("alterHand");
|
|
98
|
+
// Player two doesn't have priority yet, so no alterHand
|
|
99
|
+
expect(p2Moves).not.toContain("alterHand");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("Multiple Players", () => {
|
|
104
|
+
let testEngine: LorcanaTestEngine;
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
testEngine = new LorcanaTestEngine(
|
|
108
|
+
{ hand: 7, deck: 10 },
|
|
109
|
+
{ hand: 7, deck: 10 },
|
|
110
|
+
{ skipPreGame: false },
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
testEngine.dispose();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should enumerate different moves for each player simultaneously", () => {
|
|
119
|
+
const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
|
|
120
|
+
const otherPlayer =
|
|
121
|
+
choosingPlayer === PLAYER_ONE ? PLAYER_TWO : PLAYER_ONE;
|
|
122
|
+
|
|
123
|
+
// Get moves for both players at the same time
|
|
124
|
+
const chooserMoves = testEngine.getAvailableMoves(
|
|
125
|
+
choosingPlayer || PLAYER_ONE,
|
|
126
|
+
);
|
|
127
|
+
const otherPlayerMoves = testEngine.getAvailableMoves(otherPlayer);
|
|
128
|
+
|
|
129
|
+
// Chooser has moves, other doesn't
|
|
130
|
+
expect(chooserMoves.length).toBeGreaterThan(0);
|
|
131
|
+
expect(otherPlayerMoves.length).toBe(0);
|
|
132
|
+
|
|
133
|
+
// Results should be independent
|
|
134
|
+
expect(chooserMoves).toContain("chooseWhoGoesFirstMove");
|
|
135
|
+
expect(otherPlayerMoves).not.toContain("chooseWhoGoesFirstMove");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should handle rapid enumeration calls without errors", () => {
|
|
139
|
+
const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
|
|
140
|
+
|
|
141
|
+
// Call enumeration multiple times rapidly
|
|
142
|
+
for (let i = 0; i < 10; i++) {
|
|
143
|
+
const moves = testEngine.getAvailableMoves(
|
|
144
|
+
choosingPlayer || PLAYER_ONE,
|
|
145
|
+
);
|
|
146
|
+
expect(moves).toContain("chooseWhoGoesFirstMove");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// State should remain consistent
|
|
150
|
+
expect(testEngine.getGamePhase()).toBe("chooseFirstPlayer");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("Edge Cases", () => {
|
|
155
|
+
let testEngine: LorcanaTestEngine;
|
|
156
|
+
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
testEngine = new LorcanaTestEngine(
|
|
159
|
+
{ hand: 7, deck: 10 },
|
|
160
|
+
{ hand: 7, deck: 10 },
|
|
161
|
+
{ skipPreGame: false },
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
afterEach(() => {
|
|
166
|
+
testEngine.dispose();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should handle enumeration for player with no available moves", () => {
|
|
170
|
+
const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
|
|
171
|
+
const otherPlayer =
|
|
172
|
+
choosingPlayer === PLAYER_ONE ? PLAYER_TWO : PLAYER_ONE;
|
|
173
|
+
|
|
174
|
+
// Other player has no moves during chooseFirstPlayer phase
|
|
175
|
+
const moves = testEngine.getAvailableMoves(otherPlayer);
|
|
176
|
+
|
|
177
|
+
expect(moves).toEqual([]);
|
|
178
|
+
expect(Array.isArray(moves)).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should handle enumeration after move is no longer available", () => {
|
|
182
|
+
const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
|
|
183
|
+
|
|
184
|
+
// Before executing move
|
|
185
|
+
let moves = testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
|
|
186
|
+
expect(moves).toContain("chooseWhoGoesFirstMove");
|
|
187
|
+
|
|
188
|
+
// Execute move
|
|
189
|
+
testEngine.changeActivePlayer(choosingPlayer || PLAYER_ONE);
|
|
190
|
+
testEngine.chooseWhoGoesFirst(PLAYER_ONE);
|
|
191
|
+
|
|
192
|
+
// After executing move - should no longer be available
|
|
193
|
+
moves = testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
|
|
194
|
+
expect(moves).not.toContain("chooseWhoGoesFirstMove");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("should return consistent results for same state", () => {
|
|
198
|
+
const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
|
|
199
|
+
|
|
200
|
+
// Call multiple times without changing state
|
|
201
|
+
const moves1 = testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
|
|
202
|
+
const moves2 = testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
|
|
203
|
+
const moves3 = testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
|
|
204
|
+
|
|
205
|
+
// All results should be identical
|
|
206
|
+
expect(moves1).toEqual(moves2);
|
|
207
|
+
expect(moves2).toEqual(moves3);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should handle invalid player ID gracefully", () => {
|
|
211
|
+
// Try to get moves for a non-existent player
|
|
212
|
+
const moves = testEngine.getAvailableMoves("invalid_player_id");
|
|
213
|
+
|
|
214
|
+
// Should return empty array (no moves available)
|
|
215
|
+
expect(Array.isArray(moves)).toBe(true);
|
|
216
|
+
expect(moves).toEqual([]);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe("Performance", () => {
|
|
221
|
+
let testEngine: LorcanaTestEngine;
|
|
222
|
+
|
|
223
|
+
beforeEach(() => {
|
|
224
|
+
testEngine = new LorcanaTestEngine(
|
|
225
|
+
{ hand: 7, deck: 10 },
|
|
226
|
+
{ hand: 7, deck: 10 },
|
|
227
|
+
{ skipPreGame: false },
|
|
228
|
+
);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
afterEach(() => {
|
|
232
|
+
testEngine.dispose();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should enumerate moves quickly (<100ms for typical state)", () => {
|
|
236
|
+
const choosingPlayer = testEngine.getCtx().choosingFirstPlayer;
|
|
237
|
+
|
|
238
|
+
const startTime = performance.now();
|
|
239
|
+
|
|
240
|
+
// Enumerate moves 100 times
|
|
241
|
+
for (let i = 0; i < 100; i++) {
|
|
242
|
+
testEngine.getAvailableMoves(choosingPlayer || PLAYER_ONE);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const endTime = performance.now();
|
|
246
|
+
const totalTime = endTime - startTime;
|
|
247
|
+
const avgTime = totalTime / 100;
|
|
248
|
+
|
|
249
|
+
// Average time per enumeration should be under 500ms (higher threshold for CI parallel execution)
|
|
250
|
+
expect(avgTime).toBeLessThan(500);
|
|
251
|
+
|
|
252
|
+
// Log for visibility (not an assertion)
|
|
253
|
+
console.log(`Average enumeration time: ${avgTime.toFixed(2)}ms`);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
});
|