@drmxrcy/tcg-core 0.0.0-202602060542
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 +882 -0
- package/package.json +58 -0
- package/src/__tests__/alpha-clash-engine-definition.test.ts +319 -0
- package/src/__tests__/createMockAlphaClashGame.ts +462 -0
- package/src/__tests__/createMockGrandArchiveGame.ts +373 -0
- package/src/__tests__/createMockGundamGame.ts +379 -0
- package/src/__tests__/createMockLorcanaGame.ts +328 -0
- package/src/__tests__/createMockOnePieceGame.ts +429 -0
- package/src/__tests__/createMockRiftboundGame.ts +462 -0
- package/src/__tests__/grand-archive-engine-definition.test.ts +118 -0
- package/src/__tests__/gundam-engine-definition.test.ts +110 -0
- package/src/__tests__/integration-complete-game.test.ts +508 -0
- package/src/__tests__/integration-network-sync.test.ts +469 -0
- package/src/__tests__/lorcana-engine-definition.test.ts +100 -0
- package/src/__tests__/move-enumeration.test.ts +725 -0
- package/src/__tests__/multiplayer-engine.test.ts +555 -0
- package/src/__tests__/one-piece-engine-definition.test.ts +114 -0
- package/src/__tests__/riftbound-engine-definition.test.ts +124 -0
- package/src/actions/action-definition.test.ts +201 -0
- package/src/actions/action-definition.ts +122 -0
- package/src/actions/action-timing.test.ts +490 -0
- package/src/actions/action-timing.ts +257 -0
- package/src/cards/card-definition.test.ts +268 -0
- package/src/cards/card-definition.ts +27 -0
- package/src/cards/card-instance.test.ts +422 -0
- package/src/cards/card-instance.ts +49 -0
- package/src/cards/computed-properties.test.ts +530 -0
- package/src/cards/computed-properties.ts +84 -0
- package/src/cards/conditional-modifiers.test.ts +390 -0
- package/src/cards/modifiers.test.ts +286 -0
- package/src/cards/modifiers.ts +51 -0
- package/src/engine/MULTIPLAYER.md +425 -0
- package/src/engine/__tests__/rule-engine-flow.test.ts +348 -0
- package/src/engine/__tests__/rule-engine-history.test.ts +535 -0
- package/src/engine/__tests__/rule-engine-moves.test.ts +488 -0
- package/src/engine/__tests__/rule-engine.test.ts +366 -0
- package/src/engine/index.ts +14 -0
- package/src/engine/multiplayer-engine.example.ts +571 -0
- package/src/engine/multiplayer-engine.ts +409 -0
- package/src/engine/rule-engine.test.ts +286 -0
- package/src/engine/rule-engine.ts +1539 -0
- package/src/engine/tracker-system.ts +172 -0
- package/src/examples/__tests__/coin-flip-game.test.ts +641 -0
- package/src/filtering/card-filter.test.ts +230 -0
- package/src/filtering/card-filter.ts +91 -0
- package/src/filtering/card-query.test.ts +901 -0
- package/src/filtering/card-query.ts +273 -0
- package/src/filtering/filter-matching.test.ts +944 -0
- package/src/filtering/filter-matching.ts +315 -0
- package/src/flow/SERIALIZATION.md +428 -0
- package/src/flow/__tests__/flow-definition.test.ts +427 -0
- package/src/flow/__tests__/flow-manager.test.ts +756 -0
- package/src/flow/__tests__/flow-serialization.test.ts +565 -0
- package/src/flow/flow-definition.ts +453 -0
- package/src/flow/flow-manager.ts +1044 -0
- package/src/flow/index.ts +35 -0
- package/src/game-definition/__tests__/game-definition-validation.test.ts +359 -0
- package/src/game-definition/__tests__/game-definition.test.ts +291 -0
- package/src/game-definition/__tests__/move-definitions.test.ts +328 -0
- package/src/game-definition/game-definition.ts +261 -0
- package/src/game-definition/index.ts +28 -0
- package/src/game-definition/move-definitions.ts +188 -0
- package/src/game-definition/validation.ts +183 -0
- package/src/history/history-manager.test.ts +497 -0
- package/src/history/history-manager.ts +312 -0
- package/src/history/history-operations.ts +122 -0
- package/src/history/index.ts +9 -0
- package/src/history/types.ts +255 -0
- package/src/index.ts +32 -0
- package/src/logging/index.ts +27 -0
- package/src/logging/log-formatter.ts +187 -0
- package/src/logging/logger.ts +276 -0
- package/src/logging/types.ts +148 -0
- package/src/moves/create-move.test.ts +331 -0
- package/src/moves/create-move.ts +64 -0
- package/src/moves/move-enumeration.ts +228 -0
- package/src/moves/move-executor.test.ts +431 -0
- package/src/moves/move-executor.ts +195 -0
- package/src/moves/move-system.test.ts +380 -0
- package/src/moves/move-system.ts +463 -0
- package/src/moves/standard-moves.ts +231 -0
- package/src/operations/card-operations.test.ts +236 -0
- package/src/operations/card-operations.ts +116 -0
- package/src/operations/card-registry-impl.test.ts +251 -0
- package/src/operations/card-registry-impl.ts +70 -0
- package/src/operations/card-registry.test.ts +234 -0
- package/src/operations/card-registry.ts +106 -0
- package/src/operations/counter-operations.ts +152 -0
- package/src/operations/game-operations.test.ts +280 -0
- package/src/operations/game-operations.ts +140 -0
- package/src/operations/index.ts +24 -0
- package/src/operations/operations-impl.test.ts +354 -0
- package/src/operations/operations-impl.ts +468 -0
- package/src/operations/zone-operations.test.ts +295 -0
- package/src/operations/zone-operations.ts +223 -0
- package/src/rng/seeded-rng.test.ts +339 -0
- package/src/rng/seeded-rng.ts +123 -0
- package/src/targeting/index.ts +48 -0
- package/src/targeting/target-definition.test.ts +273 -0
- package/src/targeting/target-definition.ts +37 -0
- package/src/targeting/target-dsl.ts +279 -0
- package/src/targeting/target-resolver.ts +486 -0
- package/src/targeting/target-validation.test.ts +994 -0
- package/src/targeting/target-validation.ts +286 -0
- package/src/telemetry/events.ts +202 -0
- package/src/telemetry/index.ts +21 -0
- package/src/telemetry/telemetry-manager.ts +127 -0
- package/src/telemetry/types.ts +68 -0
- package/src/testing/__tests__/testing-utilities-integration.test.ts +161 -0
- package/src/testing/index.ts +88 -0
- package/src/testing/test-assertions.test.ts +341 -0
- package/src/testing/test-assertions.ts +256 -0
- package/src/testing/test-card-factory.test.ts +228 -0
- package/src/testing/test-card-factory.ts +111 -0
- package/src/testing/test-context-factory.ts +187 -0
- package/src/testing/test-end-assertions.test.ts +262 -0
- package/src/testing/test-end-assertions.ts +95 -0
- package/src/testing/test-engine-builder.test.ts +389 -0
- package/src/testing/test-engine-builder.ts +46 -0
- package/src/testing/test-flow-assertions.test.ts +284 -0
- package/src/testing/test-flow-assertions.ts +115 -0
- package/src/testing/test-player-builder.test.ts +132 -0
- package/src/testing/test-player-builder.ts +46 -0
- package/src/testing/test-replay-assertions.test.ts +356 -0
- package/src/testing/test-replay-assertions.ts +164 -0
- package/src/testing/test-rng-helpers.test.ts +260 -0
- package/src/testing/test-rng-helpers.ts +190 -0
- package/src/testing/test-state-builder.test.ts +373 -0
- package/src/testing/test-state-builder.ts +99 -0
- package/src/testing/test-zone-factory.test.ts +295 -0
- package/src/testing/test-zone-factory.ts +224 -0
- package/src/types/branded-utils.ts +54 -0
- package/src/types/branded.test.ts +175 -0
- package/src/types/branded.ts +33 -0
- package/src/types/index.ts +8 -0
- package/src/types/state.test.ts +198 -0
- package/src/types/state.ts +154 -0
- package/src/validation/card-type-guards.test.ts +242 -0
- package/src/validation/card-type-guards.ts +179 -0
- package/src/validation/index.ts +40 -0
- package/src/validation/schema-builders.test.ts +403 -0
- package/src/validation/schema-builders.ts +345 -0
- package/src/validation/type-guard-builder.test.ts +216 -0
- package/src/validation/type-guard-builder.ts +109 -0
- package/src/validation/validator-builder.test.ts +375 -0
- package/src/validation/validator-builder.ts +273 -0
- package/src/zones/index.ts +28 -0
- package/src/zones/zone-factory.test.ts +183 -0
- package/src/zones/zone-factory.ts +44 -0
- package/src/zones/zone-operations.test.ts +800 -0
- package/src/zones/zone-operations.ts +306 -0
- package/src/zones/zone-state-helpers.test.ts +337 -0
- package/src/zones/zone-state-helpers.ts +128 -0
- package/src/zones/zone-visibility.test.ts +156 -0
- package/src/zones/zone-visibility.ts +36 -0
- package/src/zones/zone.test.ts +186 -0
- package/src/zones/zone.ts +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
# @drmxrcy/tcg-core
|
|
2
|
+
|
|
3
|
+
> Production-ready game engine for trading card games and turn-based strategy games
|
|
4
|
+
|
|
5
|
+
**@drmxrcy/tcg-core** is a declarative, type-safe game engine built with **Immer** for immutable state management and **delta synchronization** for multiplayer games. It provides a complete framework for building complex card games with deterministic gameplay, network synchronization, and time-travel debugging.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **🎮 Declarative Game Definition** - Define your game rules declaratively with TypeScript
|
|
10
|
+
- **🔄 Immutable State Management** - Powered by Immer for structural sharing and performance
|
|
11
|
+
- **🌐 Network Synchronization** - Delta patches enable server-authoritative multiplayer
|
|
12
|
+
- **🎲 Deterministic RNG** - Seeded random number generation for replay and testing
|
|
13
|
+
- **⏮️ Time-Travel Debugging** - Complete undo/redo with history replay
|
|
14
|
+
- **👁️ Player Views** - Automatic information hiding for multiplayer games
|
|
15
|
+
- **📊 Flow Orchestration** - Optional turn/phase/segment management
|
|
16
|
+
- **🎯 Type Safety** - Full TypeScript support with branded types
|
|
17
|
+
- **🧪 Test-Driven** - 95%+ test coverage with real engine instances
|
|
18
|
+
- **🏗️ Zone Management** - Comprehensive zone operations for card locations
|
|
19
|
+
- **🔍 Testing Utilities** - Complete TDD toolkit with assertions and factories
|
|
20
|
+
- **🛠️ Card Tooling** - Reusable infrastructure for parsers, generators, and validators
|
|
21
|
+
- **✅ Validation System** - Type guards and runtime validators for data integrity
|
|
22
|
+
- **📝 Logging System** - Structured logging with configurable verbosity levels
|
|
23
|
+
- **📡 Telemetry System** - Event-based telemetry for analytics and debugging
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun add @drmxrcy/tcg-core
|
|
31
|
+
# or
|
|
32
|
+
npm install @drmxrcy/tcg-core
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Create Your First Game
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { RuleEngine, createPlayerId } from "@drmxrcy/tcg-core";
|
|
39
|
+
import type { GameDefinition } from "@drmxrcy/tcg-core";
|
|
40
|
+
|
|
41
|
+
// 1. Define your game state
|
|
42
|
+
type CoinFlipState = {
|
|
43
|
+
players: Array<{
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
score: number;
|
|
47
|
+
}>;
|
|
48
|
+
currentPlayerIndex: number;
|
|
49
|
+
turnNumber: number;
|
|
50
|
+
phase: "flip" | "ended";
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// 2. Define your moves
|
|
54
|
+
type CoinFlipMoves = {
|
|
55
|
+
flipCoin: Record<string, never>;
|
|
56
|
+
endTurn: Record<string, never>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// 3. Create game definition
|
|
60
|
+
const gameDefinition: GameDefinition<CoinFlipState, CoinFlipMoves> = {
|
|
61
|
+
name: "Coin Flip",
|
|
62
|
+
setup: (players) => ({
|
|
63
|
+
players: players.map((p) => ({
|
|
64
|
+
id: p.id,
|
|
65
|
+
name: p.name || "Player",
|
|
66
|
+
score: 0,
|
|
67
|
+
})),
|
|
68
|
+
currentPlayerIndex: 0,
|
|
69
|
+
turnNumber: 1,
|
|
70
|
+
phase: "flip",
|
|
71
|
+
}),
|
|
72
|
+
moves: {
|
|
73
|
+
flipCoin: {
|
|
74
|
+
reducer: (draft, context) => {
|
|
75
|
+
// Use deterministic RNG
|
|
76
|
+
const isHeads = context.rng?.flipCoin() ?? Math.random() >= 0.5;
|
|
77
|
+
|
|
78
|
+
if (isHeads) {
|
|
79
|
+
const player = draft.players[draft.currentPlayerIndex];
|
|
80
|
+
if (player) {
|
|
81
|
+
player.score += 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
endTurn: {
|
|
87
|
+
reducer: (draft) => {
|
|
88
|
+
draft.currentPlayerIndex =
|
|
89
|
+
(draft.currentPlayerIndex + 1) % draft.players.length;
|
|
90
|
+
draft.turnNumber += 1;
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
endIf: (state) => {
|
|
95
|
+
const winner = state.players.find((p) => p.score >= 3);
|
|
96
|
+
return winner
|
|
97
|
+
? { winner: winner.id, reason: "Reached 3 points" }
|
|
98
|
+
: undefined;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// 4. Create engine and play
|
|
103
|
+
const players = [
|
|
104
|
+
{ id: createPlayerId("p1"), name: "Alice" },
|
|
105
|
+
{ id: createPlayerId("p2"), name: "Bob" },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const engine = new RuleEngine(gameDefinition, players, {
|
|
109
|
+
seed: "game-123", // Deterministic seed
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Execute moves
|
|
113
|
+
const result = engine.executeMove("flipCoin", {
|
|
114
|
+
playerId: createPlayerId("p1"),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (result.success) {
|
|
118
|
+
console.log("Move successful!");
|
|
119
|
+
|
|
120
|
+
// Check if game ended
|
|
121
|
+
const gameEnd = engine.checkGameEnd();
|
|
122
|
+
if (gameEnd) {
|
|
123
|
+
console.log(`Winner: ${gameEnd.winner}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Core Concepts
|
|
129
|
+
|
|
130
|
+
### 1. GameDefinition
|
|
131
|
+
|
|
132
|
+
The heart of your game. Defines setup, moves, flow, and end conditions.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
const gameDefinition: GameDefinition<TState, TMoves> = {
|
|
136
|
+
name: "My Game",
|
|
137
|
+
|
|
138
|
+
// Setup: Create initial state
|
|
139
|
+
setup: (players) => ({
|
|
140
|
+
players: players.map(p => ({ id: p.id, hand: [], deck: [] })),
|
|
141
|
+
turn: 1,
|
|
142
|
+
}),
|
|
143
|
+
|
|
144
|
+
// Moves: Define all possible actions
|
|
145
|
+
moves: {
|
|
146
|
+
drawCard: {
|
|
147
|
+
condition: (state, context) => {
|
|
148
|
+
// Optional: check if move is legal
|
|
149
|
+
return state.currentPlayer === context.playerId;
|
|
150
|
+
},
|
|
151
|
+
reducer: (draft, context) => {
|
|
152
|
+
// Modify state using Immer draft
|
|
153
|
+
const player = draft.players.find(p => p.id === context.playerId);
|
|
154
|
+
const card = draft.deck.pop();
|
|
155
|
+
if (card) player.hand.push(card);
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
// Optional: Game end condition
|
|
161
|
+
endIf: (state) => {
|
|
162
|
+
const winner = state.players.find(p => p.score >= 10);
|
|
163
|
+
return winner ? { winner: winner.id, reason: "Score limit" } : undefined;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// Optional: Filter state for each player
|
|
167
|
+
playerView: (state, playerId) => ({
|
|
168
|
+
...state,
|
|
169
|
+
players: state.players.map(p => ({
|
|
170
|
+
...p,
|
|
171
|
+
hand: p.id === playerId ? p.hand : [], // Hide opponent hands
|
|
172
|
+
})),
|
|
173
|
+
}),
|
|
174
|
+
};
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 2. RuleEngine
|
|
178
|
+
|
|
179
|
+
The game engine that executes moves, manages state, and provides game services.
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// Create engine
|
|
183
|
+
const engine = new RuleEngine(gameDefinition, players, {
|
|
184
|
+
seed: "deterministic-seed", // Optional: for deterministic gameplay
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Execute moves
|
|
188
|
+
const result = engine.executeMove("playCard", {
|
|
189
|
+
playerId: createPlayerId("p1"),
|
|
190
|
+
data: { cardId: "card-123" },
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Get current state
|
|
194
|
+
const state = engine.getState();
|
|
195
|
+
|
|
196
|
+
// Get player-specific view
|
|
197
|
+
const playerView = engine.getPlayerView(createPlayerId("p1"));
|
|
198
|
+
|
|
199
|
+
// Time travel
|
|
200
|
+
engine.undo(); // Undo last move
|
|
201
|
+
engine.redo(); // Redo undone move
|
|
202
|
+
|
|
203
|
+
// Replay from history
|
|
204
|
+
const finalState = engine.replay();
|
|
205
|
+
|
|
206
|
+
// Network sync
|
|
207
|
+
const patches = engine.getPatches(); // Get all patches
|
|
208
|
+
engine.applyPatches(patches); // Apply patches from server
|
|
209
|
+
|
|
210
|
+
// Check game end
|
|
211
|
+
const gameEnd = engine.checkGameEnd();
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 3. Move System
|
|
215
|
+
|
|
216
|
+
Moves are the only way to modify game state. Each move has:
|
|
217
|
+
|
|
218
|
+
- **Condition** (optional): Determines if move is legal
|
|
219
|
+
- **Reducer**: Updates state using Immer draft
|
|
220
|
+
- **Context**: Provides playerId, data, targets, and RNG
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const moves: GameMoveDefinitions<GameState, GameMoves> = {
|
|
224
|
+
playCard: {
|
|
225
|
+
// Optional condition
|
|
226
|
+
condition: (state, context) => {
|
|
227
|
+
const player = state.players.find(p => p.id === context.playerId);
|
|
228
|
+
return player?.hand.includes(context.data?.cardId as string) ?? false;
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// Required reducer
|
|
232
|
+
reducer: (draft, context) => {
|
|
233
|
+
const player = draft.players.find(p => p.id === context.playerId);
|
|
234
|
+
const cardId = context.data?.cardId as string;
|
|
235
|
+
|
|
236
|
+
// Remove from hand
|
|
237
|
+
const index = player.hand.indexOf(cardId);
|
|
238
|
+
player.hand.splice(index, 1);
|
|
239
|
+
|
|
240
|
+
// Add to field
|
|
241
|
+
draft.field.push(cardId);
|
|
242
|
+
|
|
243
|
+
// Use RNG for random effects
|
|
244
|
+
if (context.rng?.flipCoin()) {
|
|
245
|
+
player.life += 1;
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 4. Flow Management (Optional)
|
|
253
|
+
|
|
254
|
+
For games with structured turns/phases/segments:
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
const flow: FlowDefinition<GameState> = {
|
|
258
|
+
turn: {
|
|
259
|
+
onBegin: (context) => {
|
|
260
|
+
context.state.phase = "draw";
|
|
261
|
+
},
|
|
262
|
+
phases: {
|
|
263
|
+
draw: { order: 0, next: "main" },
|
|
264
|
+
main: { order: 1, next: "end" },
|
|
265
|
+
end: {
|
|
266
|
+
order: 2,
|
|
267
|
+
next: undefined, // End of turn
|
|
268
|
+
onEnd: (context) => {
|
|
269
|
+
// Next player's turn
|
|
270
|
+
context.state.currentPlayer =
|
|
271
|
+
(context.state.currentPlayer + 1) % context.state.players.length;
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Multiplayer Pattern
|
|
280
|
+
|
|
281
|
+
**@drmxrcy/tcg-core** is designed for server-authoritative multiplayer:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// SERVER
|
|
285
|
+
const serverEngine = new RuleEngine(gameDefinition, players, {
|
|
286
|
+
seed: "server-seed",
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Client sends move
|
|
290
|
+
socket.on("move", (moveId, context) => {
|
|
291
|
+
const result = serverEngine.executeMove(moveId, context);
|
|
292
|
+
|
|
293
|
+
if (result.success) {
|
|
294
|
+
// Broadcast patches to all clients
|
|
295
|
+
io.emit("patches", result.patches);
|
|
296
|
+
} else {
|
|
297
|
+
// Send error to client
|
|
298
|
+
socket.emit("error", result.error);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// CLIENTS
|
|
303
|
+
const clientEngine = new RuleEngine(gameDefinition, players, {
|
|
304
|
+
seed: "client-seed",
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Apply patches from server
|
|
308
|
+
socket.on("patches", (patches) => {
|
|
309
|
+
clientEngine.applyPatches(patches);
|
|
310
|
+
updateUI(clientEngine.getState());
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Deterministic Replay
|
|
315
|
+
|
|
316
|
+
Games are fully deterministic when using seeded RNG:
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
// Game 1
|
|
320
|
+
const engine1 = new RuleEngine(gameDefinition, players, { seed: "test" });
|
|
321
|
+
engine1.executeMove("drawCard", { playerId: "p1" });
|
|
322
|
+
const state1 = engine1.getState();
|
|
323
|
+
|
|
324
|
+
// Game 2 - Same seed = Same outcome
|
|
325
|
+
const engine2 = new RuleEngine(gameDefinition, players, { seed: "test" });
|
|
326
|
+
engine2.executeMove("drawCard", { playerId: "p1" });
|
|
327
|
+
const state2 = engine2.getState();
|
|
328
|
+
|
|
329
|
+
// States are identical
|
|
330
|
+
console.log(state1 === state2); // true
|
|
331
|
+
|
|
332
|
+
// Replay from history
|
|
333
|
+
const replayedState = engine1.replay();
|
|
334
|
+
console.log(replayedState === state1); // true
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Advanced Features
|
|
338
|
+
|
|
339
|
+
### Branded Types
|
|
340
|
+
|
|
341
|
+
Safe type wrappers prevent ID mixups:
|
|
342
|
+
|
|
343
|
+
```typescript
|
|
344
|
+
import { createPlayerId, createCardId, createZoneId } from "@drmxrcy/tcg-core";
|
|
345
|
+
|
|
346
|
+
const playerId = createPlayerId("p1"); // PlayerId type
|
|
347
|
+
const cardId = createCardId("c1"); // CardId type
|
|
348
|
+
const zoneId = createZoneId("hand"); // ZoneId type
|
|
349
|
+
|
|
350
|
+
// Type error: can't pass CardId where PlayerId expected ✅
|
|
351
|
+
engine.executeMove("draw", { playerId: cardId }); // ❌ Type error
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Zone Management
|
|
355
|
+
|
|
356
|
+
Built-in zone system for card locations:
|
|
357
|
+
|
|
358
|
+
```typescript
|
|
359
|
+
import { createZone, moveCard } from "@drmxrcy/tcg-core";
|
|
360
|
+
|
|
361
|
+
const deck = createZone({
|
|
362
|
+
id: createZoneId("deck"),
|
|
363
|
+
visibility: "secret", // Hidden from all players
|
|
364
|
+
ordered: true,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const hand = createZone({
|
|
368
|
+
id: createZoneId("hand"),
|
|
369
|
+
visibility: "owner", // Visible to owner only
|
|
370
|
+
maxSize: 7,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
// Move cards between zones
|
|
374
|
+
moveCard(state, cardId, sourcezone, destZone);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Card Filtering DSL
|
|
378
|
+
|
|
379
|
+
Query cards with a fluent API:
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
import { selectCards } from "@drmxrcy/tcg-core";
|
|
383
|
+
|
|
384
|
+
// Find all creatures with power >= 3
|
|
385
|
+
const creatures = selectCards(state, {
|
|
386
|
+
and: [
|
|
387
|
+
{ type: "creature" },
|
|
388
|
+
{ power: { gte: 3 } },
|
|
389
|
+
{ zone: createZoneId("field") },
|
|
390
|
+
],
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Fluent builder
|
|
394
|
+
const cards = new CardQuery(state)
|
|
395
|
+
.inZone(createZoneId("hand"))
|
|
396
|
+
.withType("spell")
|
|
397
|
+
.withCost({ lte: 3 })
|
|
398
|
+
.build();
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Targeting System
|
|
402
|
+
|
|
403
|
+
Define complex targeting requirements:
|
|
404
|
+
|
|
405
|
+
```typescript
|
|
406
|
+
const targetDefinition: TargetDefinition = {
|
|
407
|
+
min: 1,
|
|
408
|
+
max: 1,
|
|
409
|
+
filter: {
|
|
410
|
+
type: "creature",
|
|
411
|
+
zone: createZoneId("field"),
|
|
412
|
+
},
|
|
413
|
+
restrictions: ["not-self", "not-controller"],
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// Validate targets
|
|
417
|
+
const isValid = validateTargetSelection(
|
|
418
|
+
state,
|
|
419
|
+
targetDefinition,
|
|
420
|
+
selectedTargets,
|
|
421
|
+
);
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
## Logging & Telemetry
|
|
425
|
+
|
|
426
|
+
Production-grade logging and telemetry for debugging, transparency, and analytics.
|
|
427
|
+
|
|
428
|
+
### Logging System
|
|
429
|
+
|
|
430
|
+
Structured logging with zero-overhead SILENT mode:
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
import { RuleEngine, LogLevel } from '@drmxrcy/tcg-core';
|
|
434
|
+
|
|
435
|
+
const engine = new RuleEngine(gameDefinition, players, {
|
|
436
|
+
seed: 'game-123',
|
|
437
|
+
logger: {
|
|
438
|
+
level: 'DEVELOPER', // SILENT, NORMAL_PLAYER, ADVANCED_PLAYER, DEVELOPER
|
|
439
|
+
pretty: true // Human-readable output
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Access logger for custom logging
|
|
444
|
+
const logger = engine.getLogger();
|
|
445
|
+
logger.info('Custom event', { eventId: 'custom-123', data: { value: 42 } });
|
|
446
|
+
|
|
447
|
+
// Create child loggers for subsystems
|
|
448
|
+
const aiLogger = logger.child('ai');
|
|
449
|
+
aiLogger.debug('Evaluating move options', { count: 12 });
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Verbosity Levels:**
|
|
453
|
+
- `SILENT` (0): No logging - zero overhead
|
|
454
|
+
- `NORMAL_PLAYER` (INFO): Basic game events
|
|
455
|
+
- `ADVANCED_PLAYER` (DEBUG): Detailed game mechanics
|
|
456
|
+
- `DEVELOPER` (TRACE): Full internal details
|
|
457
|
+
|
|
458
|
+
**Learn more:** [Logging Guide](./docs/LOGGING.md)
|
|
459
|
+
|
|
460
|
+
### Telemetry System
|
|
461
|
+
|
|
462
|
+
Event-based telemetry for tracking player actions and engine events:
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
const engine = new RuleEngine(gameDefinition, players, {
|
|
466
|
+
seed: 'game-123',
|
|
467
|
+
telemetry: {
|
|
468
|
+
enabled: true,
|
|
469
|
+
hooks: {
|
|
470
|
+
onPlayerAction: (event) => {
|
|
471
|
+
analytics.track('game.move', {
|
|
472
|
+
moveId: event.moveId,
|
|
473
|
+
playerId: event.playerId,
|
|
474
|
+
duration: event.duration
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
onStateChange: (event) => {
|
|
478
|
+
database.savePatches(event.patches);
|
|
479
|
+
},
|
|
480
|
+
onEngineError: (event) => {
|
|
481
|
+
errorReporter.captureException(event.error, event.context);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// EventEmitter style
|
|
488
|
+
const telemetry = engine.getTelemetry();
|
|
489
|
+
telemetry.on('playerAction', (event) => {
|
|
490
|
+
console.log(`Move: ${event.moveId}, Result: ${event.result}`);
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Event Types:**
|
|
495
|
+
- `PlayerActionEvent`: Move execution tracking
|
|
496
|
+
- `StateChangeEvent`: State mutations with patches
|
|
497
|
+
- `RuleEvaluationEvent`: Condition checks
|
|
498
|
+
- `FlowTransitionEvent`: Phase/turn/segment changes
|
|
499
|
+
- `EngineErrorEvent`: Error tracking
|
|
500
|
+
- `PerformanceEvent`: Performance metrics
|
|
501
|
+
|
|
502
|
+
**Learn more:** [Telemetry Guide](./docs/TELEMETRY.md)
|
|
503
|
+
|
|
504
|
+
## Testing Utilities
|
|
505
|
+
|
|
506
|
+
Comprehensive testing utilities for TDD workflow:
|
|
507
|
+
|
|
508
|
+
```typescript
|
|
509
|
+
import {
|
|
510
|
+
createTestEngine,
|
|
511
|
+
expectMoveSuccess,
|
|
512
|
+
expectStateProperty,
|
|
513
|
+
createTestCard,
|
|
514
|
+
withSeed,
|
|
515
|
+
} from '@drmxrcy/tcg-core/testing';
|
|
516
|
+
|
|
517
|
+
// Create test engine with deterministic seed
|
|
518
|
+
const engine = createTestEngine(gameDefinition, players, { seed: 'test' });
|
|
519
|
+
|
|
520
|
+
// Test move execution
|
|
521
|
+
expectMoveSuccess(engine, 'playCard', {
|
|
522
|
+
playerId: 'p1',
|
|
523
|
+
data: { cardId: 'card-123' }
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Verify state changes
|
|
527
|
+
expectStateProperty(engine, 'players[0].hand.length', 6);
|
|
528
|
+
expectStateProperty(engine, 'field.length', 1);
|
|
529
|
+
|
|
530
|
+
// Test with deterministic RNG
|
|
531
|
+
const shuffled = withSeed('test-seed', (rng) => {
|
|
532
|
+
return rng.shuffle([1, 2, 3, 4, 5]);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
// Create test data
|
|
536
|
+
const card = createTestCard({ type: 'creature', basePower: 3 });
|
|
537
|
+
const deck = createTestDeck(['card1', 'card2', 'card3'], 'player1');
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**Learn more:** [Testing Utilities Guide](./docs/guides/testing-utilities.md)
|
|
541
|
+
|
|
542
|
+
## Card Tooling
|
|
543
|
+
|
|
544
|
+
Build card management pipelines with reusable infrastructure:
|
|
545
|
+
|
|
546
|
+
```typescript
|
|
547
|
+
import {
|
|
548
|
+
CardParser,
|
|
549
|
+
CardGenerator,
|
|
550
|
+
FileWriter,
|
|
551
|
+
formatTypeScript,
|
|
552
|
+
generateVariableName,
|
|
553
|
+
} from '@drmxrcy/tcg-core/tooling';
|
|
554
|
+
|
|
555
|
+
// Extend CardParser for game-specific parsing
|
|
556
|
+
class MyCardParser extends CardParser<string, MyCard> {
|
|
557
|
+
protected doParse(text: string): ParserResult<MyCard> {
|
|
558
|
+
// Parse logic here
|
|
559
|
+
return { success: true, data: card, warnings: [] };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Extend CardGenerator for code generation
|
|
564
|
+
class MyCardGenerator extends CardGenerator<MyCard> {
|
|
565
|
+
protected generateContent(card: MyCard): string {
|
|
566
|
+
return `export const ${generateVariableName(card.name)} = ${JSON.stringify(card)};`;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
protected generateFileName(card: MyCard): string {
|
|
570
|
+
return `${card.name.toLowerCase()}.ts`;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Use file utilities
|
|
575
|
+
const writer = new FileWriter('./cards');
|
|
576
|
+
const formatted = await formatTypeScript(code);
|
|
577
|
+
await writer.write('card.ts', formatted);
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Learn more:** [Card Tooling Guide](./docs/guides/card-tooling.md)
|
|
581
|
+
|
|
582
|
+
## Validation Utilities
|
|
583
|
+
|
|
584
|
+
Type-safe runtime validation with type guards and validators:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
import {
|
|
588
|
+
createTypeGuard,
|
|
589
|
+
isCardOfType,
|
|
590
|
+
ValidatorBuilder,
|
|
591
|
+
combineTypeGuards,
|
|
592
|
+
} from '@drmxrcy/tcg-core/validation';
|
|
593
|
+
|
|
594
|
+
// Type guards for filtering
|
|
595
|
+
const isCreature = isCardOfType('creature');
|
|
596
|
+
const creatures = cards.filter(isCreature);
|
|
597
|
+
|
|
598
|
+
// Complex filtering
|
|
599
|
+
const isRareLegendary = combineTypeGuards([
|
|
600
|
+
isCardOfType('creature'),
|
|
601
|
+
isCardWithField('rarity', 'rare'),
|
|
602
|
+
isCardWithField('legendary', true),
|
|
603
|
+
]);
|
|
604
|
+
|
|
605
|
+
// Runtime validation
|
|
606
|
+
const validator = new ValidatorBuilder<CardData>()
|
|
607
|
+
.required('name', 'Name is required')
|
|
608
|
+
.type('cost', 'number', 'Cost must be a number')
|
|
609
|
+
.min('cost', 0, 'Cost must be non-negative')
|
|
610
|
+
.max('cost', 10, 'Cost cannot exceed 10')
|
|
611
|
+
.custom('power', (power) => power > 0, 'Power must be positive')
|
|
612
|
+
.build();
|
|
613
|
+
|
|
614
|
+
const result = validator.validate(cardData);
|
|
615
|
+
if (!result.success) {
|
|
616
|
+
console.error('Validation errors:', result.errors);
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
**Learn more:** [Validation Guide](./docs/guides/validation.md)
|
|
621
|
+
|
|
622
|
+
## Comprehensive Zone Operations
|
|
623
|
+
|
|
624
|
+
Complete zone management utilities:
|
|
625
|
+
|
|
626
|
+
```typescript
|
|
627
|
+
import {
|
|
628
|
+
createZone,
|
|
629
|
+
addCard,
|
|
630
|
+
removeCard,
|
|
631
|
+
moveCard,
|
|
632
|
+
draw,
|
|
633
|
+
shuffle,
|
|
634
|
+
mill,
|
|
635
|
+
search,
|
|
636
|
+
peek,
|
|
637
|
+
findCardInZones,
|
|
638
|
+
createPlayerZones,
|
|
639
|
+
moveCardInState,
|
|
640
|
+
} from '@drmxrcy/tcg-core';
|
|
641
|
+
|
|
642
|
+
// Basic operations
|
|
643
|
+
let deck = createZone(config, [card1, card2, card3]);
|
|
644
|
+
deck = addCardToTop(deck, card4);
|
|
645
|
+
deck = shuffle(deck, 'game-seed-123');
|
|
646
|
+
|
|
647
|
+
// Draw cards
|
|
648
|
+
const { fromZone, toZone, drawnCards } = draw(deck, hand, 3);
|
|
649
|
+
|
|
650
|
+
// Search zones
|
|
651
|
+
const creatures = search(zone, (cardId) => {
|
|
652
|
+
const card = getCard(cardId);
|
|
653
|
+
return card.type === 'creature';
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// State helpers
|
|
657
|
+
const hands = createPlayerZones(playerIds, () => createZone(handConfig));
|
|
658
|
+
const newState = moveCardInState(state, 'hand', 'graveyard', cardId);
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
**Learn more:** [Zone Operations Guide](./docs/guides/zone-operations.md)
|
|
662
|
+
|
|
663
|
+
## API Reference
|
|
664
|
+
|
|
665
|
+
### RuleEngine
|
|
666
|
+
|
|
667
|
+
```typescript
|
|
668
|
+
class RuleEngine<TState, TMoves> {
|
|
669
|
+
constructor(
|
|
670
|
+
gameDefinition: GameDefinition<TState, TMoves>,
|
|
671
|
+
players: Player[],
|
|
672
|
+
options?: { seed?: string }
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
// State access
|
|
676
|
+
getState(): TState;
|
|
677
|
+
getPlayerView(playerId: PlayerId): TState;
|
|
678
|
+
|
|
679
|
+
// Move execution
|
|
680
|
+
executeMove(moveId: string, context: MoveContext): MoveExecutionResult;
|
|
681
|
+
canExecuteMove(moveId: string, context: MoveContext): boolean;
|
|
682
|
+
getValidMoves(playerId: PlayerId): string[];
|
|
683
|
+
|
|
684
|
+
// History
|
|
685
|
+
getHistory(): readonly HistoryEntry[];
|
|
686
|
+
undo(): boolean;
|
|
687
|
+
redo(): boolean;
|
|
688
|
+
replay(upToIndex?: number): TState;
|
|
689
|
+
|
|
690
|
+
// Network sync
|
|
691
|
+
getPatches(sinceIndex?: number): Patch[];
|
|
692
|
+
applyPatches(patches: Patch[]): void;
|
|
693
|
+
|
|
694
|
+
// Game services
|
|
695
|
+
getRNG(): SeededRNG;
|
|
696
|
+
getFlowManager(): FlowManager<TState> | undefined;
|
|
697
|
+
checkGameEnd(): GameEndResult | undefined;
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### GameDefinition
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
type GameDefinition<TState, TMoves> = {
|
|
705
|
+
name: string;
|
|
706
|
+
setup: (players: Player[]) => TState;
|
|
707
|
+
moves: GameMoveDefinitions<TState, TMoves>;
|
|
708
|
+
flow?: FlowDefinition<TState>;
|
|
709
|
+
endIf?: (state: TState) => GameEndResult | undefined;
|
|
710
|
+
playerView?: (state: TState, playerId: PlayerId) => TState;
|
|
711
|
+
};
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### MoveContext
|
|
715
|
+
|
|
716
|
+
```typescript
|
|
717
|
+
type MoveContext = {
|
|
718
|
+
playerId: PlayerId;
|
|
719
|
+
sourceCardId?: CardId;
|
|
720
|
+
targets?: string[][];
|
|
721
|
+
data?: Record<string, unknown>;
|
|
722
|
+
timestamp?: number;
|
|
723
|
+
rng?: SeededRNG;
|
|
724
|
+
};
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
## Examples
|
|
728
|
+
|
|
729
|
+
See the `examples/` directory for complete game implementations:
|
|
730
|
+
|
|
731
|
+
- **Coin Flip Game** - Simple example validating the framework
|
|
732
|
+
- **Rock Paper Scissors** - Turn-based game with flow management
|
|
733
|
+
- **Card Battle** - Full card game with zones, targeting, and combat
|
|
734
|
+
|
|
735
|
+
## Testing
|
|
736
|
+
|
|
737
|
+
@drmxrcy/tcg-core is built with Test-Driven Development:
|
|
738
|
+
|
|
739
|
+
```bash
|
|
740
|
+
# Run all tests
|
|
741
|
+
bun test
|
|
742
|
+
|
|
743
|
+
# Run specific test
|
|
744
|
+
bun test src/engine/__tests__/rule-engine.test.ts
|
|
745
|
+
|
|
746
|
+
# Watch mode
|
|
747
|
+
bun test --watch
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
## Architecture
|
|
751
|
+
|
|
752
|
+
```
|
|
753
|
+
@drmxrcy/tcg-core
|
|
754
|
+
├── engine/ # RuleEngine - Main orchestration
|
|
755
|
+
├── game-definition/ # GameDefinition types and validation
|
|
756
|
+
├── moves/ # Move system and execution
|
|
757
|
+
├── flow/ # Turn/phase/segment management
|
|
758
|
+
├── zones/ # Zone management system
|
|
759
|
+
├── cards/ # Card instances and modifiers
|
|
760
|
+
├── filtering/ # Card query DSL
|
|
761
|
+
├── targeting/ # Targeting system
|
|
762
|
+
├── rng/ # Seeded random number generation
|
|
763
|
+
├── logging/ # Structured logging system
|
|
764
|
+
├── telemetry/ # Event-based telemetry
|
|
765
|
+
├── delta-sync/ # Patch utilities
|
|
766
|
+
├── testing/ # Testing utilities (@drmxrcy/tcg-core/testing)
|
|
767
|
+
├── tooling/ # Card tooling infrastructure (@drmxrcy/tcg-core/tooling)
|
|
768
|
+
├── validation/ # Type guards and validators (@drmxrcy/tcg-core/validation)
|
|
769
|
+
└── types/ # Branded types and utilities
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
## Documentation
|
|
773
|
+
|
|
774
|
+
### Guides
|
|
775
|
+
|
|
776
|
+
- **[Logging Guide](./docs/LOGGING.md)** - Structured logging system
|
|
777
|
+
- **[Telemetry Guide](./docs/TELEMETRY.md)** - Event-based telemetry
|
|
778
|
+
- **[Zone Operations Guide](./docs/guides/zone-operations.md)** - Comprehensive zone management utilities
|
|
779
|
+
- **[Testing Utilities Guide](./docs/guides/testing-utilities.md)** - TDD workflow and test patterns
|
|
780
|
+
- **[Card Tooling Guide](./docs/guides/card-tooling.md)** - Building card management pipelines
|
|
781
|
+
- **[Validation Guide](./docs/guides/validation.md)** - Type guards and runtime validation
|
|
782
|
+
|
|
783
|
+
### Examples
|
|
784
|
+
|
|
785
|
+
- **[Zone Management Examples](./docs/examples/zone-management.ts)** - Runnable zone operation examples
|
|
786
|
+
- **[Test Patterns Examples](./docs/examples/test-patterns.ts)** - Complete testing examples
|
|
787
|
+
- **[Card Parser Extension](./docs/examples/card-parser-extension.ts)** - Extending CardParser
|
|
788
|
+
- **[Custom Validator](./docs/examples/custom-validator.ts)** - Building validators
|
|
789
|
+
|
|
790
|
+
## Performance
|
|
791
|
+
|
|
792
|
+
- **Immutable Updates**: Immer provides structural sharing for efficient updates
|
|
793
|
+
- **Delta Sync**: Only transmit state changes, not entire state
|
|
794
|
+
- **Deterministic**: Seeded RNG enables caching and replay
|
|
795
|
+
- **Type Safety**: Zero runtime overhead for branded types
|
|
796
|
+
|
|
797
|
+
## Move Enumeration
|
|
798
|
+
|
|
799
|
+
Discover all available moves with their valid parameters for AI agents and UI components:
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
// Get all valid moves with parameters
|
|
803
|
+
const moves = engine.enumerateMoves(playerId, {
|
|
804
|
+
validOnly: true,
|
|
805
|
+
includeMetadata: true
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
for (const move of moves) {
|
|
809
|
+
console.log(`Move: ${move.metadata?.displayName}`);
|
|
810
|
+
console.log(` Params:`, move.params);
|
|
811
|
+
|
|
812
|
+
// Execute the move
|
|
813
|
+
if (move.isValid) {
|
|
814
|
+
engine.executeMove(move.moveId, {
|
|
815
|
+
playerId: move.playerId,
|
|
816
|
+
params: move.params
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**Define enumerators in move definitions:**
|
|
823
|
+
|
|
824
|
+
```typescript
|
|
825
|
+
const playCardMove: MoveDefinition<GameState, PlayCardParams> = {
|
|
826
|
+
id: 'play-card',
|
|
827
|
+
name: 'Play Card',
|
|
828
|
+
|
|
829
|
+
// Enumerate all cards in hand
|
|
830
|
+
enumerator: (state, context) => {
|
|
831
|
+
const handCards = context.zones.getCardsInZone('hand', context.playerId);
|
|
832
|
+
return handCards.map(cardId => ({ cardId }));
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
condition: (state, context) => {
|
|
836
|
+
// Validate the move
|
|
837
|
+
return isCardPlayable(state, context.params.cardId);
|
|
838
|
+
},
|
|
839
|
+
|
|
840
|
+
reducer: (draft, context) => {
|
|
841
|
+
// Execute the move
|
|
842
|
+
playCard(draft, context.params.cardId);
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
**Perfect for:**
|
|
848
|
+
- 🤖 AI agents that need to explore all possible moves
|
|
849
|
+
- 🎮 UI components building dynamic action menus
|
|
850
|
+
- 📊 Game analysis and move tree exploration
|
|
851
|
+
- 🔍 Debugging and testing game states
|
|
852
|
+
|
|
853
|
+
**Learn more:** [Move Enumeration Guide](./docs/guides/move-enumeration.md)
|
|
854
|
+
|
|
855
|
+
## Roadmap
|
|
856
|
+
|
|
857
|
+
- [x] Move enumeration system for AI and UI
|
|
858
|
+
- [ ] WebSocket transport layer
|
|
859
|
+
- [ ] React hooks for UI integration
|
|
860
|
+
- [ ] Vue composables
|
|
861
|
+
- [ ] Matchmaking service
|
|
862
|
+
- [ ] Tournament system
|
|
863
|
+
- [ ] Spectator mode
|
|
864
|
+
- [ ] Replay viewer
|
|
865
|
+
|
|
866
|
+
## Contributing
|
|
867
|
+
|
|
868
|
+
We welcome contributions! See [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines.
|
|
869
|
+
|
|
870
|
+
## License
|
|
871
|
+
|
|
872
|
+
MIT © The Card Goat Team
|
|
873
|
+
|
|
874
|
+
## Related Packages
|
|
875
|
+
|
|
876
|
+
- **@drmxrcy/tcg-lorcana** - Disney Lorcana TCG implementation
|
|
877
|
+
- **@drmxrcy/tcg-server** - Authoritative game server
|
|
878
|
+
- **@drmxrcy/tcg-client** - Client SDK for web/mobile
|
|
879
|
+
|
|
880
|
+
---
|
|
881
|
+
|
|
882
|
+
**Built with ❤️ for trading card game developers**
|