@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.
Files changed (157) hide show
  1. package/README.md +882 -0
  2. package/package.json +58 -0
  3. package/src/__tests__/alpha-clash-engine-definition.test.ts +319 -0
  4. package/src/__tests__/createMockAlphaClashGame.ts +462 -0
  5. package/src/__tests__/createMockGrandArchiveGame.ts +373 -0
  6. package/src/__tests__/createMockGundamGame.ts +379 -0
  7. package/src/__tests__/createMockLorcanaGame.ts +328 -0
  8. package/src/__tests__/createMockOnePieceGame.ts +429 -0
  9. package/src/__tests__/createMockRiftboundGame.ts +462 -0
  10. package/src/__tests__/grand-archive-engine-definition.test.ts +118 -0
  11. package/src/__tests__/gundam-engine-definition.test.ts +110 -0
  12. package/src/__tests__/integration-complete-game.test.ts +508 -0
  13. package/src/__tests__/integration-network-sync.test.ts +469 -0
  14. package/src/__tests__/lorcana-engine-definition.test.ts +100 -0
  15. package/src/__tests__/move-enumeration.test.ts +725 -0
  16. package/src/__tests__/multiplayer-engine.test.ts +555 -0
  17. package/src/__tests__/one-piece-engine-definition.test.ts +114 -0
  18. package/src/__tests__/riftbound-engine-definition.test.ts +124 -0
  19. package/src/actions/action-definition.test.ts +201 -0
  20. package/src/actions/action-definition.ts +122 -0
  21. package/src/actions/action-timing.test.ts +490 -0
  22. package/src/actions/action-timing.ts +257 -0
  23. package/src/cards/card-definition.test.ts +268 -0
  24. package/src/cards/card-definition.ts +27 -0
  25. package/src/cards/card-instance.test.ts +422 -0
  26. package/src/cards/card-instance.ts +49 -0
  27. package/src/cards/computed-properties.test.ts +530 -0
  28. package/src/cards/computed-properties.ts +84 -0
  29. package/src/cards/conditional-modifiers.test.ts +390 -0
  30. package/src/cards/modifiers.test.ts +286 -0
  31. package/src/cards/modifiers.ts +51 -0
  32. package/src/engine/MULTIPLAYER.md +425 -0
  33. package/src/engine/__tests__/rule-engine-flow.test.ts +348 -0
  34. package/src/engine/__tests__/rule-engine-history.test.ts +535 -0
  35. package/src/engine/__tests__/rule-engine-moves.test.ts +488 -0
  36. package/src/engine/__tests__/rule-engine.test.ts +366 -0
  37. package/src/engine/index.ts +14 -0
  38. package/src/engine/multiplayer-engine.example.ts +571 -0
  39. package/src/engine/multiplayer-engine.ts +409 -0
  40. package/src/engine/rule-engine.test.ts +286 -0
  41. package/src/engine/rule-engine.ts +1539 -0
  42. package/src/engine/tracker-system.ts +172 -0
  43. package/src/examples/__tests__/coin-flip-game.test.ts +641 -0
  44. package/src/filtering/card-filter.test.ts +230 -0
  45. package/src/filtering/card-filter.ts +91 -0
  46. package/src/filtering/card-query.test.ts +901 -0
  47. package/src/filtering/card-query.ts +273 -0
  48. package/src/filtering/filter-matching.test.ts +944 -0
  49. package/src/filtering/filter-matching.ts +315 -0
  50. package/src/flow/SERIALIZATION.md +428 -0
  51. package/src/flow/__tests__/flow-definition.test.ts +427 -0
  52. package/src/flow/__tests__/flow-manager.test.ts +756 -0
  53. package/src/flow/__tests__/flow-serialization.test.ts +565 -0
  54. package/src/flow/flow-definition.ts +453 -0
  55. package/src/flow/flow-manager.ts +1044 -0
  56. package/src/flow/index.ts +35 -0
  57. package/src/game-definition/__tests__/game-definition-validation.test.ts +359 -0
  58. package/src/game-definition/__tests__/game-definition.test.ts +291 -0
  59. package/src/game-definition/__tests__/move-definitions.test.ts +328 -0
  60. package/src/game-definition/game-definition.ts +261 -0
  61. package/src/game-definition/index.ts +28 -0
  62. package/src/game-definition/move-definitions.ts +188 -0
  63. package/src/game-definition/validation.ts +183 -0
  64. package/src/history/history-manager.test.ts +497 -0
  65. package/src/history/history-manager.ts +312 -0
  66. package/src/history/history-operations.ts +122 -0
  67. package/src/history/index.ts +9 -0
  68. package/src/history/types.ts +255 -0
  69. package/src/index.ts +32 -0
  70. package/src/logging/index.ts +27 -0
  71. package/src/logging/log-formatter.ts +187 -0
  72. package/src/logging/logger.ts +276 -0
  73. package/src/logging/types.ts +148 -0
  74. package/src/moves/create-move.test.ts +331 -0
  75. package/src/moves/create-move.ts +64 -0
  76. package/src/moves/move-enumeration.ts +228 -0
  77. package/src/moves/move-executor.test.ts +431 -0
  78. package/src/moves/move-executor.ts +195 -0
  79. package/src/moves/move-system.test.ts +380 -0
  80. package/src/moves/move-system.ts +463 -0
  81. package/src/moves/standard-moves.ts +231 -0
  82. package/src/operations/card-operations.test.ts +236 -0
  83. package/src/operations/card-operations.ts +116 -0
  84. package/src/operations/card-registry-impl.test.ts +251 -0
  85. package/src/operations/card-registry-impl.ts +70 -0
  86. package/src/operations/card-registry.test.ts +234 -0
  87. package/src/operations/card-registry.ts +106 -0
  88. package/src/operations/counter-operations.ts +152 -0
  89. package/src/operations/game-operations.test.ts +280 -0
  90. package/src/operations/game-operations.ts +140 -0
  91. package/src/operations/index.ts +24 -0
  92. package/src/operations/operations-impl.test.ts +354 -0
  93. package/src/operations/operations-impl.ts +468 -0
  94. package/src/operations/zone-operations.test.ts +295 -0
  95. package/src/operations/zone-operations.ts +223 -0
  96. package/src/rng/seeded-rng.test.ts +339 -0
  97. package/src/rng/seeded-rng.ts +123 -0
  98. package/src/targeting/index.ts +48 -0
  99. package/src/targeting/target-definition.test.ts +273 -0
  100. package/src/targeting/target-definition.ts +37 -0
  101. package/src/targeting/target-dsl.ts +279 -0
  102. package/src/targeting/target-resolver.ts +486 -0
  103. package/src/targeting/target-validation.test.ts +994 -0
  104. package/src/targeting/target-validation.ts +286 -0
  105. package/src/telemetry/events.ts +202 -0
  106. package/src/telemetry/index.ts +21 -0
  107. package/src/telemetry/telemetry-manager.ts +127 -0
  108. package/src/telemetry/types.ts +68 -0
  109. package/src/testing/__tests__/testing-utilities-integration.test.ts +161 -0
  110. package/src/testing/index.ts +88 -0
  111. package/src/testing/test-assertions.test.ts +341 -0
  112. package/src/testing/test-assertions.ts +256 -0
  113. package/src/testing/test-card-factory.test.ts +228 -0
  114. package/src/testing/test-card-factory.ts +111 -0
  115. package/src/testing/test-context-factory.ts +187 -0
  116. package/src/testing/test-end-assertions.test.ts +262 -0
  117. package/src/testing/test-end-assertions.ts +95 -0
  118. package/src/testing/test-engine-builder.test.ts +389 -0
  119. package/src/testing/test-engine-builder.ts +46 -0
  120. package/src/testing/test-flow-assertions.test.ts +284 -0
  121. package/src/testing/test-flow-assertions.ts +115 -0
  122. package/src/testing/test-player-builder.test.ts +132 -0
  123. package/src/testing/test-player-builder.ts +46 -0
  124. package/src/testing/test-replay-assertions.test.ts +356 -0
  125. package/src/testing/test-replay-assertions.ts +164 -0
  126. package/src/testing/test-rng-helpers.test.ts +260 -0
  127. package/src/testing/test-rng-helpers.ts +190 -0
  128. package/src/testing/test-state-builder.test.ts +373 -0
  129. package/src/testing/test-state-builder.ts +99 -0
  130. package/src/testing/test-zone-factory.test.ts +295 -0
  131. package/src/testing/test-zone-factory.ts +224 -0
  132. package/src/types/branded-utils.ts +54 -0
  133. package/src/types/branded.test.ts +175 -0
  134. package/src/types/branded.ts +33 -0
  135. package/src/types/index.ts +8 -0
  136. package/src/types/state.test.ts +198 -0
  137. package/src/types/state.ts +154 -0
  138. package/src/validation/card-type-guards.test.ts +242 -0
  139. package/src/validation/card-type-guards.ts +179 -0
  140. package/src/validation/index.ts +40 -0
  141. package/src/validation/schema-builders.test.ts +403 -0
  142. package/src/validation/schema-builders.ts +345 -0
  143. package/src/validation/type-guard-builder.test.ts +216 -0
  144. package/src/validation/type-guard-builder.ts +109 -0
  145. package/src/validation/validator-builder.test.ts +375 -0
  146. package/src/validation/validator-builder.ts +273 -0
  147. package/src/zones/index.ts +28 -0
  148. package/src/zones/zone-factory.test.ts +183 -0
  149. package/src/zones/zone-factory.ts +44 -0
  150. package/src/zones/zone-operations.test.ts +800 -0
  151. package/src/zones/zone-operations.ts +306 -0
  152. package/src/zones/zone-state-helpers.test.ts +337 -0
  153. package/src/zones/zone-state-helpers.ts +128 -0
  154. package/src/zones/zone-visibility.test.ts +156 -0
  155. package/src/zones/zone-visibility.ts +36 -0
  156. package/src/zones/zone.test.ts +186 -0
  157. 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
+ }