@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
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import type { Draft } from "immer";
|
|
2
|
+
import type { HistoryOperations } from "../history/history-operations";
|
|
3
|
+
import type { CardOperations } from "../operations/card-operations";
|
|
4
|
+
import type { CardRegistry } from "../operations/card-registry";
|
|
5
|
+
import type { CounterOperations } from "../operations/counter-operations";
|
|
6
|
+
import type { GameOperations } from "../operations/game-operations";
|
|
7
|
+
import type { ZoneOperations } from "../operations/zone-operations";
|
|
8
|
+
import type { SeededRNG } from "../rng/seeded-rng";
|
|
9
|
+
import type { CardId, PlayerId } from "../types";
|
|
10
|
+
import type { MoveEnumerationContext } from "./move-enumeration";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Helper type to normalize move parameters
|
|
14
|
+
*
|
|
15
|
+
* Converts void/undefined to empty object type for moves without parameters.
|
|
16
|
+
* This ensures consistent typing across all moves.
|
|
17
|
+
*
|
|
18
|
+
* @template T - Raw parameter type from TMoves
|
|
19
|
+
*/
|
|
20
|
+
export type NormalizeParams<T> = T extends void | undefined
|
|
21
|
+
? Record<string, never>
|
|
22
|
+
: T;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Move Context Input
|
|
26
|
+
*
|
|
27
|
+
* The context that callers provide when executing a move via engine.executeMove().
|
|
28
|
+
* This is a subset of MoveContext containing only the fields the caller provides.
|
|
29
|
+
* The engine fills in the remaining fields (rng, zones, cards, etc.).
|
|
30
|
+
*
|
|
31
|
+
* @template TParams - Move-specific parameter type (from TMoves[MoveName])
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* // Execute a move by providing only playerId and params
|
|
36
|
+
* engine.executeMove('playCard', {
|
|
37
|
+
* playerId: 'p1',
|
|
38
|
+
* params: { cardId: 'card-123' }
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export type MoveContextInput<TParams = any> = {
|
|
43
|
+
/** Player performing this move */
|
|
44
|
+
playerId: PlayerId;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Move-specific parameters (fully typed)
|
|
48
|
+
*
|
|
49
|
+
* Type-safe parameters for this specific move.
|
|
50
|
+
* For moves without parameters (passTurn: void), this is an empty object {}.
|
|
51
|
+
*/
|
|
52
|
+
params: TParams;
|
|
53
|
+
|
|
54
|
+
/** Source card for this move (e.g., card being played or ability source) */
|
|
55
|
+
sourceCardId?: CardId;
|
|
56
|
+
|
|
57
|
+
/** Selected targets (array of arrays for multi-target moves) */
|
|
58
|
+
targets?: string[][];
|
|
59
|
+
|
|
60
|
+
/** Timestamp when move was initiated (for deterministic ordering) */
|
|
61
|
+
timestamp?: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Context provided to move reducers and conditions
|
|
66
|
+
*
|
|
67
|
+
* Contains all information needed to execute a move:
|
|
68
|
+
* - Player performing the move
|
|
69
|
+
* - Move-specific parameters (fully typed)
|
|
70
|
+
* - Source card (if applicable)
|
|
71
|
+
* - Selected targets
|
|
72
|
+
* - Timestamp for deterministic ordering
|
|
73
|
+
* - RNG for deterministic randomness
|
|
74
|
+
* - Zone operations API (for framework-managed zones)
|
|
75
|
+
* - Card operations API (for framework-managed card metadata)
|
|
76
|
+
* - Card registry API (for static card definitions)
|
|
77
|
+
*
|
|
78
|
+
* @template TParams - Move-specific parameter type (from TMoves[MoveName])
|
|
79
|
+
* @template TCardMeta - Game-specific card metadata type
|
|
80
|
+
* @template TCardDefinition - Game-specific card definition type
|
|
81
|
+
*/
|
|
82
|
+
export type MoveContext<
|
|
83
|
+
TParams = any,
|
|
84
|
+
TCardMeta = any,
|
|
85
|
+
TCardDefinition = any,
|
|
86
|
+
> = {
|
|
87
|
+
/** Player performing this move */
|
|
88
|
+
playerId: PlayerId;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Move-specific parameters (fully typed)
|
|
92
|
+
*
|
|
93
|
+
* Type-safe parameters for this specific move.
|
|
94
|
+
* For example, playCard receives { cardId: string; alternativeCost?: AlternativeCost }
|
|
95
|
+
*
|
|
96
|
+
* For moves without parameters (passTurn: void), this is an empty object {}.
|
|
97
|
+
*/
|
|
98
|
+
params: TParams;
|
|
99
|
+
|
|
100
|
+
/** Source card for this move (e.g., card being played or ability source) */
|
|
101
|
+
sourceCardId?: CardId;
|
|
102
|
+
|
|
103
|
+
/** Selected targets (array of arrays for multi-target moves) */
|
|
104
|
+
targets?: string[][];
|
|
105
|
+
|
|
106
|
+
/** Timestamp when move was initiated (for deterministic ordering) */
|
|
107
|
+
timestamp?: number;
|
|
108
|
+
|
|
109
|
+
/** Seeded RNG for deterministic randomness (provided by engine) */
|
|
110
|
+
rng: SeededRNG;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Zone operations API (provided by RuleEngine)
|
|
114
|
+
*
|
|
115
|
+
* Provides methods to interact with the framework's zone management:
|
|
116
|
+
* - moveCard: Move cards between zones
|
|
117
|
+
* - getCardsInZone: Query cards in a zone
|
|
118
|
+
* - shuffleZone: Shuffle a zone
|
|
119
|
+
* - getCardZone: Find which zone contains a card
|
|
120
|
+
*
|
|
121
|
+
* This is the ONLY way moves can modify zone state.
|
|
122
|
+
* Always provided by RuleEngine when zones are configured.
|
|
123
|
+
*/
|
|
124
|
+
zones: ZoneOperations;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Card operations API (provided by RuleEngine)
|
|
128
|
+
*
|
|
129
|
+
* Provides methods to interact with the framework's card metadata:
|
|
130
|
+
* - getCardMeta: Get dynamic card properties
|
|
131
|
+
* - updateCardMeta: Merge metadata updates
|
|
132
|
+
* - setCardMeta: Replace metadata completely
|
|
133
|
+
* - getCardOwner: Get card's owner
|
|
134
|
+
* - queryCards: Find cards by predicate
|
|
135
|
+
*
|
|
136
|
+
* This is the ONLY way moves can modify card metadata.
|
|
137
|
+
* Always provided by RuleEngine.
|
|
138
|
+
*/
|
|
139
|
+
cards: CardOperations<TCardMeta>;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Game operations API (provided by RuleEngine)
|
|
143
|
+
*
|
|
144
|
+
* Provides methods to interact with game-level state:
|
|
145
|
+
* - setOTP: Mark player as on the play (goes first)
|
|
146
|
+
* - getOTP: Get the OTP player
|
|
147
|
+
* - setPendingMulligan: Set players pending mulligan
|
|
148
|
+
* - getPendingMulligan: Get players pending mulligan
|
|
149
|
+
* - addPendingMulligan: Add player to mulligan list
|
|
150
|
+
* - removePendingMulligan: Remove player from mulligan list
|
|
151
|
+
*
|
|
152
|
+
* These are universal TCG concepts that apply across all card games.
|
|
153
|
+
* This is the ONLY way moves can modify game-level internal state.
|
|
154
|
+
* Always provided by RuleEngine.
|
|
155
|
+
*/
|
|
156
|
+
game: GameOperations;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* History operations API (provided by RuleEngine)
|
|
160
|
+
*
|
|
161
|
+
* Provides methods to log custom history entries:
|
|
162
|
+
* - log: Add a history entry with custom messages and player-specific visibility
|
|
163
|
+
*
|
|
164
|
+
* Use this to add detailed logging for moves with private information
|
|
165
|
+
* (e.g., card draws, mulligans, hand reveals).
|
|
166
|
+
*
|
|
167
|
+
* Note: The engine automatically creates a base history entry for each move.
|
|
168
|
+
* Use this API to add additional context or player-specific details.
|
|
169
|
+
*
|
|
170
|
+
* Always provided by RuleEngine.
|
|
171
|
+
*/
|
|
172
|
+
history: HistoryOperations;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Card registry API (provided by RuleEngine)
|
|
176
|
+
*
|
|
177
|
+
* Provides read-only access to static card definitions:
|
|
178
|
+
* - getCard: Get a card definition by ID
|
|
179
|
+
* - hasCard: Check if a card definition exists
|
|
180
|
+
* - getAllCards: Get all card definitions
|
|
181
|
+
* - queryCards: Find cards by predicate
|
|
182
|
+
* - getCardCount: Get total number of card definitions
|
|
183
|
+
*
|
|
184
|
+
* Use this to access static card properties (name, cost, abilities, etc).
|
|
185
|
+
* For dynamic card state (damage, tapped, etc), use the cards API.
|
|
186
|
+
*
|
|
187
|
+
* Optional for backward compatibility and testing. In production,
|
|
188
|
+
* this is always provided by RuleEngine when card definitions are configured.
|
|
189
|
+
*/
|
|
190
|
+
registry?: CardRegistry<TCardDefinition>;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Flow state (provided by RuleEngine)
|
|
194
|
+
*
|
|
195
|
+
* Provides access to engine-managed flow state:
|
|
196
|
+
* - currentPhase: Current phase name (from flow definition)
|
|
197
|
+
* - currentSegment: Current segment name within phase (if applicable)
|
|
198
|
+
* - turn: Current turn number (1-indexed)
|
|
199
|
+
* - currentPlayer: Player ID of the active player
|
|
200
|
+
* - isFirstTurn: True if this is turn 1 of the game
|
|
201
|
+
* - endPhase: Trigger phase transition (deferred until after move completes)
|
|
202
|
+
* - endSegment: Trigger segment transition (deferred until after move completes)
|
|
203
|
+
* - endTurn: Trigger turn transition (deferred until after move completes)
|
|
204
|
+
*
|
|
205
|
+
* Games should NOT duplicate this state in their own game state.
|
|
206
|
+
* Access flow information via context.flow instead.
|
|
207
|
+
*
|
|
208
|
+
* Optional for backward compatibility. In production with flow configured,
|
|
209
|
+
* this is always provided by RuleEngine.
|
|
210
|
+
*/
|
|
211
|
+
flow?: {
|
|
212
|
+
currentPhase?: string;
|
|
213
|
+
currentSegment?: string;
|
|
214
|
+
turn: number;
|
|
215
|
+
currentPlayer?: PlayerId;
|
|
216
|
+
isFirstTurn: boolean;
|
|
217
|
+
endPhase: (phaseName?: string) => void;
|
|
218
|
+
endSegment: () => void;
|
|
219
|
+
endTurn: () => void;
|
|
220
|
+
setCurrentPlayer?: (playerId?: PlayerId) => void;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* End the game with a result
|
|
225
|
+
*
|
|
226
|
+
* Call this method to signal game completion. The engine will handle
|
|
227
|
+
* setting the game-ended state and preventing further moves.
|
|
228
|
+
*
|
|
229
|
+
* @param result - Game end result with winner and reason
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```typescript
|
|
233
|
+
* // In a concede move:
|
|
234
|
+
* context.endGame({
|
|
235
|
+
* winner: otherPlayerId,
|
|
236
|
+
* reason: 'concede',
|
|
237
|
+
* metadata: { concedeBy: context.playerId }
|
|
238
|
+
* });
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
endGame?: (result: {
|
|
242
|
+
winner?: PlayerId;
|
|
243
|
+
reason: string;
|
|
244
|
+
metadata?: Record<string, unknown>;
|
|
245
|
+
}) => void;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Tracker operations (provided by RuleEngine)
|
|
249
|
+
*
|
|
250
|
+
* Provides API for boolean flags that auto-reset at turn/phase boundaries.
|
|
251
|
+
* Eliminates boilerplate for "hasDrawnThisTurn", "hasPlayedResourceThisTurn", etc.
|
|
252
|
+
*
|
|
253
|
+
* Operations:
|
|
254
|
+
* - check(name, playerId?): Check if tracker is marked
|
|
255
|
+
* - mark(name, playerId?): Mark tracker as true
|
|
256
|
+
* - unmark(name, playerId?): Mark tracker as false
|
|
257
|
+
*
|
|
258
|
+
* Trackers auto-reset based on game definition config.
|
|
259
|
+
*
|
|
260
|
+
* Optional for backward compatibility. In production with trackers configured,
|
|
261
|
+
* this is always provided by RuleEngine.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* ```typescript
|
|
265
|
+
* // Check if player has drawn this turn
|
|
266
|
+
* if (!context.trackers.check('hasDrawn', context.playerId)) {
|
|
267
|
+
* // Draw a card
|
|
268
|
+
* context.trackers.mark('hasDrawn', context.playerId);
|
|
269
|
+
* }
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
trackers?: {
|
|
273
|
+
check(name: string, playerId?: PlayerId): boolean;
|
|
274
|
+
mark(name: string, playerId?: PlayerId): void;
|
|
275
|
+
unmark(name: string, playerId?: PlayerId): void;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Counter operations API (provided by RuleEngine)
|
|
280
|
+
*
|
|
281
|
+
* Provides methods to manage counters and flags on cards:
|
|
282
|
+
* - setFlag/getFlag: Boolean flags (exhausted, stunned, buffed)
|
|
283
|
+
* - addCounter/removeCounter/getCounter/clearCounter: Numeric counters (damage)
|
|
284
|
+
* - clearAllCounters: Reset all counters on a card
|
|
285
|
+
* - getCardsWithFlag/getCardsWithCounter: Query cards by counter state
|
|
286
|
+
*
|
|
287
|
+
* This is the recommended way to manage card counters/tokens.
|
|
288
|
+
* Always provided by RuleEngine.
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```typescript
|
|
292
|
+
* // Mark card as exhausted
|
|
293
|
+
* context.counters.setFlag(cardId, 'exhausted', true);
|
|
294
|
+
*
|
|
295
|
+
* // Add damage to a card
|
|
296
|
+
* context.counters.addCounter(cardId, 'damage', 3);
|
|
297
|
+
*
|
|
298
|
+
* // Check if card is stunned
|
|
299
|
+
* if (context.counters.getFlag(cardId, 'stunned')) {
|
|
300
|
+
* // Card is stunned
|
|
301
|
+
* }
|
|
302
|
+
* ```
|
|
303
|
+
*/
|
|
304
|
+
counters: CounterOperations;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Move Reducer Function
|
|
309
|
+
*
|
|
310
|
+
* Pure function that updates game state in response to a move.
|
|
311
|
+
* Operates on Immer draft for immutable updates.
|
|
312
|
+
*
|
|
313
|
+
* @template TGameState - Game state type
|
|
314
|
+
* @template TParams - Move-specific parameter type (from TMoves[MoveName])
|
|
315
|
+
* @template TCardMeta - Card metadata type
|
|
316
|
+
* @template TCardDefinition - Card definition type
|
|
317
|
+
*
|
|
318
|
+
* @param draft - Immer draft of game state (mutable proxy)
|
|
319
|
+
* @param context - Move context with player, typed params, targets, etc.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* ```typescript
|
|
323
|
+
* type DrawCardParams = { count: number };
|
|
324
|
+
* const drawCardReducer: MoveReducer<GameState, DrawCardParams> = (draft, context) => {
|
|
325
|
+
* const { count } = context.params; // ✅ Fully typed!
|
|
326
|
+
* const player = draft.players[context.playerId];
|
|
327
|
+
* for (let i = 0; i < count; i++) {
|
|
328
|
+
* const card = draft.deck.pop();
|
|
329
|
+
* if (card) player.hand.push(card);
|
|
330
|
+
* }
|
|
331
|
+
* };
|
|
332
|
+
* ```
|
|
333
|
+
*/
|
|
334
|
+
export type MoveReducer<
|
|
335
|
+
TGameState,
|
|
336
|
+
TParams = any,
|
|
337
|
+
TCardMeta = any,
|
|
338
|
+
TCardDefinition = any,
|
|
339
|
+
> = (
|
|
340
|
+
draft: Draft<TGameState>,
|
|
341
|
+
context: MoveContext<TParams, TCardMeta, TCardDefinition>,
|
|
342
|
+
) => void;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Condition Failure Result
|
|
346
|
+
*
|
|
347
|
+
* Detailed information about why a move condition failed.
|
|
348
|
+
* Returned by conditions to provide meaningful error messages to players.
|
|
349
|
+
*
|
|
350
|
+
* @example
|
|
351
|
+
* ```typescript
|
|
352
|
+
* // In a move condition:
|
|
353
|
+
* if (player.mana < card.cost) {
|
|
354
|
+
* return {
|
|
355
|
+
* reason: `Not enough mana. Required: ${card.cost}, Available: ${player.mana}`,
|
|
356
|
+
* errorCode: "INSUFFICIENT_MANA",
|
|
357
|
+
* context: { required: card.cost, available: player.mana },
|
|
358
|
+
* };
|
|
359
|
+
* }
|
|
360
|
+
* ```
|
|
361
|
+
*/
|
|
362
|
+
export type ConditionFailure = {
|
|
363
|
+
/** Human-readable explanation of why the move failed */
|
|
364
|
+
reason: string;
|
|
365
|
+
/** Machine-readable error code for categorization */
|
|
366
|
+
errorCode: string;
|
|
367
|
+
/** Optional additional context for debugging/logging */
|
|
368
|
+
context?: Record<string, unknown>;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Move Condition Function
|
|
373
|
+
*
|
|
374
|
+
* Pure predicate that determines if a move is legal given current game state.
|
|
375
|
+
* Called BEFORE reducer execution to validate move.
|
|
376
|
+
*
|
|
377
|
+
* @template TGameState - Game state type
|
|
378
|
+
* @template TParams - Move-specific parameter type (from TMoves[MoveName])
|
|
379
|
+
* @template TCardMeta - Card metadata type
|
|
380
|
+
* @template TCardDefinition - Card definition type
|
|
381
|
+
*
|
|
382
|
+
* @param state - Current game state (readonly)
|
|
383
|
+
* @param context - Move context with player, typed params, targets, etc.
|
|
384
|
+
* @returns True if legal, false for generic failure, or ConditionFailure for detailed error
|
|
385
|
+
*
|
|
386
|
+
* @example
|
|
387
|
+
* ```typescript
|
|
388
|
+
* // Simple boolean validation (backward compatible)
|
|
389
|
+
* condition: (state, context) => {
|
|
390
|
+
* return state.players[context.playerId].mana >= 5;
|
|
391
|
+
* }
|
|
392
|
+
*
|
|
393
|
+
* // Detailed failure information (recommended)
|
|
394
|
+
* condition: (state, context) => {
|
|
395
|
+
* const player = state.players[context.playerId];
|
|
396
|
+
* const cost = 5;
|
|
397
|
+
*
|
|
398
|
+
* if (player.mana < cost) {
|
|
399
|
+
* return {
|
|
400
|
+
* reason: `Not enough mana. Required: ${cost}, Available: ${player.mana}`,
|
|
401
|
+
* errorCode: "INSUFFICIENT_MANA",
|
|
402
|
+
* context: { required: cost, available: player.mana },
|
|
403
|
+
* };
|
|
404
|
+
* }
|
|
405
|
+
*
|
|
406
|
+
* return true;
|
|
407
|
+
* }
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
export type MoveCondition<
|
|
411
|
+
TGameState,
|
|
412
|
+
TParams = any,
|
|
413
|
+
TCardMeta = any,
|
|
414
|
+
TCardDefinition = any,
|
|
415
|
+
> = (
|
|
416
|
+
state: TGameState,
|
|
417
|
+
context: MoveContext<TParams, TCardMeta, TCardDefinition>,
|
|
418
|
+
) => boolean | ConditionFailure;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Move Result
|
|
422
|
+
*
|
|
423
|
+
* Result of attempting to execute a move.
|
|
424
|
+
* Either succeeds with new state, or fails with error information.
|
|
425
|
+
*
|
|
426
|
+
* @example
|
|
427
|
+
* ```typescript
|
|
428
|
+
* // Success
|
|
429
|
+
* const result: MoveResult<GameState> = {
|
|
430
|
+
* success: true,
|
|
431
|
+
* state: newGameState
|
|
432
|
+
* };
|
|
433
|
+
*
|
|
434
|
+
* // Failure
|
|
435
|
+
* const result: MoveResult<GameState> = {
|
|
436
|
+
* success: false,
|
|
437
|
+
* error: 'Not enough mana',
|
|
438
|
+
* errorCode: 'INSUFFICIENT_RESOURCES',
|
|
439
|
+
* errorContext: { required: 5, available: 3 }
|
|
440
|
+
* };
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
export type MoveResult<TGameState> =
|
|
444
|
+
| {
|
|
445
|
+
/** Move executed successfully */
|
|
446
|
+
success: true;
|
|
447
|
+
/** New game state after move */
|
|
448
|
+
state: TGameState;
|
|
449
|
+
error?: never;
|
|
450
|
+
errorCode?: never;
|
|
451
|
+
errorContext?: never;
|
|
452
|
+
}
|
|
453
|
+
| {
|
|
454
|
+
/** Move failed validation or execution */
|
|
455
|
+
success: false;
|
|
456
|
+
/** Human-readable error message */
|
|
457
|
+
error: string;
|
|
458
|
+
/** Machine-readable error code */
|
|
459
|
+
errorCode?: string;
|
|
460
|
+
/** Additional error context for debugging */
|
|
461
|
+
errorContext?: Record<string, unknown>;
|
|
462
|
+
state?: never;
|
|
463
|
+
};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { Draft } from "immer";
|
|
2
|
+
import type { GameMoveDefinitions } from "../game-definition/move-definitions";
|
|
3
|
+
import type { PlayerId, ZoneId } from "../types/branded";
|
|
4
|
+
import type { MoveContext } from "./move-system";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for configuring standard moves
|
|
8
|
+
*/
|
|
9
|
+
export type StandardMoveOptions = {
|
|
10
|
+
/** Which standard moves to include */
|
|
11
|
+
include?: StandardMoveName[];
|
|
12
|
+
/** Custom zone names for draw/shuffle operations */
|
|
13
|
+
zones?: {
|
|
14
|
+
deck?: ZoneId;
|
|
15
|
+
hand?: ZoneId;
|
|
16
|
+
discard?: ZoneId;
|
|
17
|
+
};
|
|
18
|
+
/** Custom game-over handling for concede */
|
|
19
|
+
onConcede?: (playerId: PlayerId, context: MoveContext) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Available standard move names
|
|
24
|
+
*/
|
|
25
|
+
export type StandardMoveName =
|
|
26
|
+
| "pass"
|
|
27
|
+
| "concede"
|
|
28
|
+
| "draw"
|
|
29
|
+
| "shuffle"
|
|
30
|
+
| "mulligan";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Type definition for standard moves
|
|
34
|
+
*/
|
|
35
|
+
export type StandardMoves = {
|
|
36
|
+
pass: { playerId: PlayerId };
|
|
37
|
+
concede: { playerId: PlayerId };
|
|
38
|
+
draw: { playerId: PlayerId; count?: number };
|
|
39
|
+
shuffle: { playerId: PlayerId };
|
|
40
|
+
mulligan: { playerId: PlayerId; keepCards?: string[] };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a library of standard TCG moves that games can opt into.
|
|
45
|
+
* Eliminates boilerplate for common operations like pass, concede, draw, shuffle, etc.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const gameDefinition = {
|
|
50
|
+
* moves: {
|
|
51
|
+
* ...standardMoves({ include: ["pass", "concede", "draw"] }),
|
|
52
|
+
* // Custom game-specific moves
|
|
53
|
+
* playCard: { ... }
|
|
54
|
+
* }
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @param options - Configuration for which moves to include and custom settings
|
|
59
|
+
* @returns A GameMoveDefinitions object with the requested standard moves
|
|
60
|
+
*/
|
|
61
|
+
export function standardMoves<TState>(
|
|
62
|
+
options: StandardMoveOptions = {},
|
|
63
|
+
): Partial<GameMoveDefinitions<TState, StandardMoves>> {
|
|
64
|
+
const { include = ["pass", "concede"], zones = {} } = options;
|
|
65
|
+
const moves: Partial<GameMoveDefinitions<TState, StandardMoves>> = {};
|
|
66
|
+
|
|
67
|
+
const deckZone = zones.deck ?? ("deck" as ZoneId);
|
|
68
|
+
const handZone = zones.hand ?? ("hand" as ZoneId);
|
|
69
|
+
const discardZone = zones.discard ?? ("discard" as ZoneId);
|
|
70
|
+
|
|
71
|
+
// Pass move - do nothing, just advance turn
|
|
72
|
+
if (include.includes("pass")) {
|
|
73
|
+
moves.pass = {
|
|
74
|
+
condition: (_state: TState, _context: MoveContext) => {
|
|
75
|
+
// Can always pass (unless game has ended)
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
reducer: (_draft: Draft<TState>, _context: MoveContext) => {
|
|
79
|
+
// No state changes - this is just a signal to advance flow
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Concede move - player forfeits the game
|
|
85
|
+
if (include.includes("concede")) {
|
|
86
|
+
moves.concede = {
|
|
87
|
+
condition: () => true, // Can always concede
|
|
88
|
+
reducer: (_draft: Draft<TState>, context: MoveContext) => {
|
|
89
|
+
if (options.onConcede) {
|
|
90
|
+
options.onConcede(context.playerId, context);
|
|
91
|
+
} else if (context.endGame) {
|
|
92
|
+
// Determine winner (opponent of conceeding player)
|
|
93
|
+
const winner = (
|
|
94
|
+
context.flow?.currentPlayer !== context.playerId
|
|
95
|
+
? context.flow?.currentPlayer
|
|
96
|
+
: undefined
|
|
97
|
+
) as PlayerId | undefined;
|
|
98
|
+
context.endGame({
|
|
99
|
+
winner,
|
|
100
|
+
reason: "concede",
|
|
101
|
+
metadata: { conceedingPlayer: context.playerId },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Draw move - draw N cards from deck to hand
|
|
109
|
+
if (include.includes("draw")) {
|
|
110
|
+
moves.draw = {
|
|
111
|
+
condition: (_state: TState, context: MoveContext) => {
|
|
112
|
+
const count = context.params?.count ?? 1;
|
|
113
|
+
const deckCards = context.zones.getCardsInZone(
|
|
114
|
+
deckZone,
|
|
115
|
+
context.playerId,
|
|
116
|
+
);
|
|
117
|
+
return deckCards.length >= count;
|
|
118
|
+
},
|
|
119
|
+
reducer: (_draft: Draft<TState>, context: MoveContext) => {
|
|
120
|
+
const count = context.params?.count ?? 1;
|
|
121
|
+
context.zones.drawCards({
|
|
122
|
+
from: deckZone,
|
|
123
|
+
to: handZone,
|
|
124
|
+
count,
|
|
125
|
+
playerId: context.playerId,
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Shuffle move - shuffle player's deck
|
|
132
|
+
if (include.includes("shuffle")) {
|
|
133
|
+
moves.shuffle = {
|
|
134
|
+
condition: () => true,
|
|
135
|
+
reducer: (_draft: Draft<TState>, context: MoveContext) => {
|
|
136
|
+
context.zones.shuffleZone(deckZone, context.playerId);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Mulligan move - return hand to deck, shuffle, and redraw
|
|
142
|
+
if (include.includes("mulligan")) {
|
|
143
|
+
moves.mulligan = {
|
|
144
|
+
condition: (_state: TState, context: MoveContext) => {
|
|
145
|
+
// Typically only allowed on first turn or during setup
|
|
146
|
+
return context.flow?.isFirstTurn ?? true;
|
|
147
|
+
},
|
|
148
|
+
reducer: (_draft: Draft<TState>, context: MoveContext) => {
|
|
149
|
+
const keepCards = context.params?.keepCards ?? [];
|
|
150
|
+
const handCards = context.zones.getCardsInZone(
|
|
151
|
+
handZone,
|
|
152
|
+
context.playerId,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Return non-kept cards to deck
|
|
156
|
+
const cardsToReturn = handCards.filter(
|
|
157
|
+
(cardId: string) => !keepCards.includes(cardId),
|
|
158
|
+
);
|
|
159
|
+
for (const cardId of cardsToReturn) {
|
|
160
|
+
context.zones.moveCard({
|
|
161
|
+
cardId,
|
|
162
|
+
targetZoneId: deckZone,
|
|
163
|
+
position: "bottom",
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Shuffle deck
|
|
168
|
+
context.zones.shuffleZone(deckZone, context.playerId);
|
|
169
|
+
|
|
170
|
+
// Draw the same number of cards we returned
|
|
171
|
+
const drawCount = cardsToReturn.length;
|
|
172
|
+
if (drawCount > 0) {
|
|
173
|
+
context.zones.drawCards({
|
|
174
|
+
from: deckZone,
|
|
175
|
+
to: handZone,
|
|
176
|
+
count: drawCount,
|
|
177
|
+
playerId: context.playerId,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return moves;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Helper to create a "discard" standard move
|
|
189
|
+
* This is separate because it requires card selection parameters
|
|
190
|
+
*/
|
|
191
|
+
export function createDiscardMove<TState>(): GameMoveDefinitions<
|
|
192
|
+
TState,
|
|
193
|
+
{ discard: { playerId: PlayerId; cardIds: string[] } }
|
|
194
|
+
>["discard"] {
|
|
195
|
+
return {
|
|
196
|
+
condition: (_state: TState, context: MoveContext) => {
|
|
197
|
+
// Must have cards to discard
|
|
198
|
+
const cardIds = context.params?.cardIds ?? [];
|
|
199
|
+
return cardIds.length > 0;
|
|
200
|
+
},
|
|
201
|
+
reducer: (_draft: Draft<TState>, context: MoveContext) => {
|
|
202
|
+
const cardIds = context.params?.cardIds ?? [];
|
|
203
|
+
for (const cardId of cardIds) {
|
|
204
|
+
context.zones.moveCard({
|
|
205
|
+
cardId,
|
|
206
|
+
targetZoneId: "discard" as ZoneId,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Helper to create an "endTurn" standard move
|
|
215
|
+
* This is separate because it interacts with flow management
|
|
216
|
+
*/
|
|
217
|
+
export function createEndTurnMove<TState>(): GameMoveDefinitions<
|
|
218
|
+
TState,
|
|
219
|
+
{ endTurn: { playerId: PlayerId } }
|
|
220
|
+
>["endTurn"] {
|
|
221
|
+
return {
|
|
222
|
+
condition: (_state: TState, context: MoveContext) => {
|
|
223
|
+
// Only current player can end their turn
|
|
224
|
+
return context.flow?.currentPlayer === context.playerId;
|
|
225
|
+
},
|
|
226
|
+
reducer: (_draft: Draft<TState>, _context: MoveContext) => {
|
|
227
|
+
// The actual turn transition is handled by FlowManager
|
|
228
|
+
// This move just signals that the player is done
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|