@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,1044 @@
1
+ import { type Draft, produce } from "immer";
2
+ import type { Logger } from "../logging";
3
+ import type { CardOperations } from "../operations/card-operations";
4
+ import type { GameOperations } from "../operations/game-operations";
5
+ import type { ZoneOperations } from "../operations/zone-operations";
6
+ import type { TelemetryManager } from "../telemetry";
7
+ import type {
8
+ FlowContext,
9
+ FlowDefinition,
10
+ GameSegmentDefinition,
11
+ } from "./flow-definition";
12
+
13
+ /**
14
+ * Task 9.4: FlowManager - Flow orchestration
15
+ *
16
+ * Manages game flow using a simple, explicit state machine:
17
+ * - Constructs hierarchical state machine from FlowDefinition
18
+ * - Executes lifecycle hooks with FlowContext
19
+ * - Handles automatic transitions (endIf conditions)
20
+ * - Provides programmatic control (endPhase, endSegment, endTurn)
21
+ * - Maintains game state with Immer
22
+ *
23
+ * User requirements:
24
+ * - Flexible turn/phase/segment progression
25
+ * - Rich context API for hooks
26
+ * - Both automatic and programmatic control
27
+ * - Default behaviors with customization
28
+ *
29
+ * Note: Originally planned to use XState, but a simple state machine
30
+ * is more appropriate for this use case. No need for external dependencies.
31
+ */
32
+
33
+ /**
34
+ * Flow event types
35
+ *
36
+ * Task 9.11: Flow event handling
37
+ */
38
+ type FlowEvent =
39
+ | { type: "NEXT_GAME_SEGMENT" }
40
+ | { type: "NEXT_PHASE" }
41
+ | { type: "NEXT_STEP" }
42
+ | { type: "END_TURN" }
43
+ | { type: "END_STEP" }
44
+ | { type: "END_PHASE" }
45
+ | { type: "END_GAME_SEGMENT" }
46
+ | { type: "STATE_UPDATED" };
47
+
48
+ /**
49
+ * Flow state snapshot for querying
50
+ */
51
+ export type FlowStateSnapshot = {
52
+ gameSegment?: string;
53
+ phase?: string;
54
+ step?: string;
55
+ turn: number;
56
+ currentPlayer?: string;
57
+ };
58
+
59
+ /**
60
+ * Serializable flow state for persistence
61
+ *
62
+ * Use case: Save game state to database for later replay/restoration
63
+ */
64
+ export type SerializedFlowState = {
65
+ currentGameSegment?: string;
66
+ currentPhase?: string;
67
+ currentStep?: string;
68
+ turnNumber: number;
69
+ currentPlayer?: string;
70
+ };
71
+
72
+ /**
73
+ * Options for FlowManager construction
74
+ */
75
+ export type FlowManagerOptions<TCardMeta = any> = {
76
+ /** Skip initialization hooks (used when restoring from serialized state) */
77
+ skipInitialization?: boolean;
78
+ /** Restore from serialized flow state */
79
+ restoreFrom?: SerializedFlowState;
80
+ /** Callback invoked at turn end (before transition) */
81
+ onTurnEnd?: () => void;
82
+ /** Callback invoked at phase end (before transition) */
83
+ onPhaseEnd?: (phaseName: string) => void;
84
+ /** Game operations API (required for flow hooks) */
85
+ gameOperations?: GameOperations;
86
+ /** Zone operations API (required for flow hooks) */
87
+ zoneOperations?: ZoneOperations;
88
+ /** Card operations API (required for flow hooks) */
89
+ cardOperations?: CardOperations<TCardMeta>;
90
+ /** Logger instance for structured logging */
91
+ logger?: Logger;
92
+ /** Telemetry manager for event tracking */
93
+ telemetry?: TelemetryManager;
94
+ };
95
+
96
+ /**
97
+ * Task 9.4: FlowManager implementation
98
+ */
99
+ export class FlowManager<TState, TCardMeta = any> {
100
+ private flowDefinition: FlowDefinition<TState, TCardMeta>;
101
+ private normalizedGameSegments: Record<
102
+ string,
103
+ GameSegmentDefinition<TState, TCardMeta>
104
+ >;
105
+ private initialGameSegment?: string;
106
+ private gameState: TState;
107
+ private currentGameSegment?: string;
108
+ private currentPhase?: string;
109
+ private currentStep?: string;
110
+ private turnNumber = 1;
111
+ private currentPlayer?: string = undefined;
112
+ private pendingEndGameSegment = false;
113
+ private pendingEndPhase = false;
114
+ private pendingEndStep = false;
115
+ private pendingEndTurn = false;
116
+ private isTransitioning = false; // Guard against nested transitions
117
+ private onTurnEndCallback?: () => void;
118
+ private onPhaseEndCallback?: (phaseName: string) => void;
119
+ private gameOperations?: GameOperations;
120
+ private zoneOperations?: ZoneOperations;
121
+ private cardOperations?: CardOperations<TCardMeta>;
122
+ private logger?: Logger;
123
+ private telemetry?: TelemetryManager;
124
+
125
+ constructor(
126
+ flowDefinition: FlowDefinition<TState, TCardMeta>,
127
+ initialState: TState,
128
+ options?: FlowManagerOptions<TCardMeta>,
129
+ ) {
130
+ this.flowDefinition = flowDefinition;
131
+ this.gameState = initialState;
132
+ this.onTurnEndCallback = options?.onTurnEnd;
133
+ this.onPhaseEndCallback = options?.onPhaseEnd;
134
+ this.gameOperations = options?.gameOperations;
135
+ this.zoneOperations = options?.zoneOperations;
136
+ this.cardOperations = options?.cardOperations;
137
+ this.logger = options?.logger;
138
+ this.telemetry = options?.telemetry;
139
+
140
+ // Normalize flow definition (handle both simplified and full syntax)
141
+ const normalized = this.normalizeFlowDefinition(flowDefinition);
142
+ this.normalizedGameSegments = normalized.gameSegments;
143
+ this.initialGameSegment = normalized.initialGameSegment;
144
+
145
+ // Restore from serialized state if provided
146
+ if (options?.restoreFrom) {
147
+ this.restoreFromSerialized(options.restoreFrom);
148
+ } else if (!options?.skipInitialization) {
149
+ // Initialize flow normally
150
+ this.initializeFlow();
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Normalize flow definition to always use gameSegments structure
156
+ *
157
+ * If flow uses simplified syntax (just `turn`), convert it to a single
158
+ * "mainGame" segment.
159
+ */
160
+ private normalizeFlowDefinition(flowDef: FlowDefinition<TState, TCardMeta>): {
161
+ gameSegments: Record<string, GameSegmentDefinition<TState, TCardMeta>>;
162
+ initialGameSegment?: string;
163
+ } {
164
+ // Check if it's the simplified syntax (has `turn` property)
165
+ if ("turn" in flowDef) {
166
+ // Create implicit mainGame segment
167
+ return {
168
+ gameSegments: {
169
+ mainGame: {
170
+ order: 0,
171
+ turn: flowDef.turn,
172
+ },
173
+ },
174
+ initialGameSegment: "mainGame",
175
+ };
176
+ }
177
+
178
+ // It's the full syntax with gameSegments
179
+ return {
180
+ gameSegments: flowDef.gameSegments,
181
+ initialGameSegment: flowDef.initialGameSegment,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Restore flow manager from serialized state
187
+ *
188
+ * Use case: Load a saved game from database and continue playing
189
+ */
190
+ private restoreFromSerialized(state: SerializedFlowState): void {
191
+ this.currentGameSegment = state.currentGameSegment;
192
+ this.currentPhase = state.currentPhase;
193
+ this.currentStep = state.currentStep;
194
+ this.turnNumber = state.turnNumber;
195
+ this.currentPlayer = state.currentPlayer;
196
+
197
+ // Don't execute hooks when restoring - state already contains their effects
198
+ }
199
+
200
+ /**
201
+ * Serialize current flow state for persistence
202
+ *
203
+ * Use case: Save game state to database for later replay/restoration
204
+ */
205
+ serializeFlowState(): SerializedFlowState {
206
+ return {
207
+ currentGameSegment: this.currentGameSegment,
208
+ currentPhase: this.currentPhase,
209
+ currentStep: this.currentStep,
210
+ turnNumber: this.turnNumber,
211
+ currentPlayer: this.currentPlayer,
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Task 9.3: Initialize the flow state machine
217
+ */
218
+ private initializeFlow(): void {
219
+ const gameSegments = this.normalizedGameSegments;
220
+
221
+ // Determine initial game segment
222
+ const sortedGameSegments = Object.entries(gameSegments).sort(
223
+ ([, a], [, b]) => a.order - b.order,
224
+ );
225
+ this.currentGameSegment =
226
+ this.initialGameSegment ?? sortedGameSegments[0]?.[0];
227
+
228
+ if (!this.currentGameSegment) {
229
+ throw new Error("No game segments defined in flow definition");
230
+ }
231
+
232
+ const gameSegmentDef = gameSegments[this.currentGameSegment];
233
+ if (!gameSegmentDef) {
234
+ throw new Error(`Game segment "${this.currentGameSegment}" not found`);
235
+ }
236
+
237
+ // Execute game segment onBegin
238
+ this.executeHook(gameSegmentDef.onBegin);
239
+
240
+ // Initialize turn structure for this game segment
241
+ const phases = gameSegmentDef.turn.phases;
242
+ if (phases) {
243
+ const sortedPhases = Object.entries(phases).sort(
244
+ ([, a], [, b]) => a.order - b.order,
245
+ );
246
+ this.currentPhase =
247
+ gameSegmentDef.turn.initialPhase ?? sortedPhases[0]?.[0];
248
+
249
+ // Check for steps in initial phase
250
+ if (this.currentPhase) {
251
+ const initialPhaseDef = phases[this.currentPhase];
252
+ if (initialPhaseDef?.steps) {
253
+ const sortedSteps = Object.entries(initialPhaseDef.steps).sort(
254
+ ([, a], [, b]) => {
255
+ const aOrder = a?.order ?? 0;
256
+ const bOrder = b?.order ?? 0;
257
+ return aOrder - bOrder;
258
+ },
259
+ );
260
+ this.currentStep = initialPhaseDef.initialStep ?? sortedSteps[0]?.[0];
261
+ }
262
+ }
263
+ }
264
+
265
+ // Execute turn onBegin
266
+ this.executeHook(gameSegmentDef.turn.onBegin);
267
+
268
+ // Execute phase onBegin
269
+ if (this.currentPhase && phases) {
270
+ this.executeHook(phases[this.currentPhase]?.onBegin);
271
+ }
272
+
273
+ // Execute step onBegin
274
+ if (this.currentPhase && this.currentStep && phases) {
275
+ const phaseDef = phases[this.currentPhase];
276
+ if (phaseDef?.steps) {
277
+ this.executeHook(phaseDef.steps[this.currentStep]?.onBegin);
278
+ }
279
+ }
280
+
281
+ // Check initial endIf conditions
282
+ this.checkEndConditions();
283
+ }
284
+
285
+ /**
286
+ * Task 9.5: Execute a lifecycle hook with FlowContext
287
+ */
288
+ private executeHook(
289
+ hook: ((context: FlowContext<TState>) => void) | undefined,
290
+ ): void {
291
+ if (!hook) return;
292
+
293
+ this.gameState = produce(this.gameState, (draft) => {
294
+ const context = this.createFlowContext(draft);
295
+ hook(context);
296
+ });
297
+
298
+ // Handle pending programmatic transitions OUTSIDE of produce
299
+ // Order matters: step → phase → turn → game segment
300
+ // Skip if we're already transitioning to avoid nested transitions
301
+ if (this.isTransitioning) {
302
+ // Pending flags remain set, will be processed after current transition
303
+ return;
304
+ }
305
+
306
+ if (this.pendingEndStep) {
307
+ this.pendingEndStep = false;
308
+ this.transitionToNextStep();
309
+ }
310
+ if (this.pendingEndPhase) {
311
+ this.pendingEndPhase = false;
312
+ this.transitionToNextPhase();
313
+ }
314
+ if (this.pendingEndTurn) {
315
+ this.pendingEndTurn = false;
316
+ this.transitionToNextTurn();
317
+ }
318
+ if (this.pendingEndGameSegment) {
319
+ this.pendingEndGameSegment = false;
320
+ this.transitionToNextGameSegment();
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Process any pending transitions that accumulated during a transition
326
+ * Called after setting isTransitioning = false
327
+ */
328
+ private processPendingTransitions(): void {
329
+ // Process in order: step → phase → turn → segment
330
+ while (
331
+ this.pendingEndStep ||
332
+ this.pendingEndPhase ||
333
+ this.pendingEndTurn ||
334
+ this.pendingEndGameSegment
335
+ ) {
336
+ if (this.pendingEndStep) {
337
+ this.pendingEndStep = false;
338
+ this.transitionToNextStep();
339
+ } else if (this.pendingEndPhase) {
340
+ this.pendingEndPhase = false;
341
+ this.transitionToNextPhase();
342
+ } else if (this.pendingEndTurn) {
343
+ this.pendingEndTurn = false;
344
+ this.transitionToNextTurn();
345
+ } else if (this.pendingEndGameSegment) {
346
+ this.pendingEndGameSegment = false;
347
+ this.transitionToNextGameSegment();
348
+ }
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Create stub operations for backward compatibility
354
+ */
355
+ private createStubOperations(): {
356
+ game: GameOperations;
357
+ zones: ZoneOperations;
358
+ cards: CardOperations<TCardMeta>;
359
+ } {
360
+ const stubGameOperations: GameOperations = {
361
+ setOTP: () => {},
362
+ getOTP: () => undefined,
363
+ setChoosingFirstPlayer: () => {},
364
+ getChoosingFirstPlayer: () => undefined,
365
+ setPendingMulligan: () => {
366
+ console.log("stub called");
367
+ },
368
+ getPendingMulligan: () => [],
369
+ addPendingMulligan: () => {
370
+ console.log("stub called");
371
+ },
372
+ removePendingMulligan: () => {
373
+ console.log("stub called");
374
+ },
375
+ };
376
+
377
+ const stubZoneOperations: ZoneOperations = {
378
+ moveCard: () => {
379
+ console.log("stub called");
380
+ },
381
+ getCardsInZone: () => [],
382
+ shuffleZone: () => {
383
+ console.log("stub called");
384
+ },
385
+ getCardZone: () => undefined,
386
+ drawCards: () => [],
387
+ mulligan: () => {
388
+ console.log("stub called");
389
+ },
390
+ bulkMove: () => [],
391
+ createDeck: () => [],
392
+ };
393
+
394
+ const stubCardOperations: CardOperations<TCardMeta> = {
395
+ getCardMeta: () => ({}) as TCardMeta,
396
+ updateCardMeta: () => {
397
+ console.log("stub called");
398
+ },
399
+ setCardMeta: () => {
400
+ console.log("stub called");
401
+ },
402
+ getCardOwner: () => {
403
+ console.log("stub called");
404
+ return undefined;
405
+ },
406
+ queryCards: () => [],
407
+ };
408
+
409
+ return {
410
+ game: stubGameOperations,
411
+ zones: stubZoneOperations,
412
+ cards: stubCardOperations,
413
+ };
414
+ }
415
+
416
+ /**
417
+ * Task 9.9: Create FlowContext for hooks
418
+ */
419
+ private createFlowContext(
420
+ draft: Draft<TState>,
421
+ ): FlowContext<TState, TCardMeta> {
422
+ const stubs = this.createStubOperations();
423
+
424
+ return {
425
+ state: draft,
426
+ game: this.gameOperations || stubs.game,
427
+ zones: this.zoneOperations || stubs.zones,
428
+ cards: this.cardOperations || stubs.cards,
429
+ endGameSegment: () => {
430
+ this.pendingEndGameSegment = true;
431
+ },
432
+ endPhase: () => {
433
+ this.pendingEndPhase = true;
434
+ },
435
+ endStep: () => {
436
+ this.pendingEndStep = true;
437
+ },
438
+ endTurn: () => {
439
+ this.pendingEndTurn = true;
440
+ },
441
+ getCurrentGameSegment: () => this.currentGameSegment,
442
+ getCurrentPhase: () => this.currentPhase,
443
+ getCurrentStep: () => this.currentStep,
444
+ getCurrentPlayer: () => this.currentPlayer ?? "",
445
+ getTurnNumber: () => this.turnNumber,
446
+ setCurrentPlayer: (playerId?: string) => {
447
+ this.currentPlayer = playerId;
448
+ },
449
+ };
450
+ }
451
+
452
+ /**
453
+ * Task 9.7: Check and execute endIf conditions
454
+ */
455
+ public checkEndConditions(): void {
456
+ if (!this.currentGameSegment) return;
457
+
458
+ const gameSegments = this.normalizedGameSegments;
459
+ const gameSegmentDef = gameSegments[this.currentGameSegment];
460
+ if (!gameSegmentDef) return;
461
+
462
+ const phases = gameSegmentDef.turn.phases;
463
+
464
+ // Check step endIf
465
+ if (this.currentPhase && this.currentStep && phases) {
466
+ const phaseDef = phases[this.currentPhase];
467
+ if (phaseDef?.steps) {
468
+ const stepDef = phaseDef.steps[this.currentStep];
469
+ if (stepDef?.endIf) {
470
+ const context = this.createReadOnlyContext();
471
+ if (stepDef.endIf(context)) {
472
+ // Call private transition to avoid recursive checkEndConditions
473
+ this.transitionToNextStep();
474
+ return;
475
+ }
476
+ }
477
+ }
478
+ }
479
+
480
+ // Check phase endIf
481
+ if (this.currentPhase && phases) {
482
+ const phaseDef = phases[this.currentPhase];
483
+ if (phaseDef?.endIf) {
484
+ const context = this.createReadOnlyContext();
485
+ if (phaseDef.endIf(context)) {
486
+ // Call private transition to avoid recursive checkEndConditions
487
+ this.transitionToNextPhase();
488
+ return;
489
+ }
490
+ }
491
+ }
492
+
493
+ // Check turn endIf
494
+ if (gameSegmentDef.turn.endIf) {
495
+ const context = this.createReadOnlyContext();
496
+ if (gameSegmentDef.turn.endIf(context)) {
497
+ // Call private transition to avoid recursive checkEndConditions
498
+ this.transitionToNextTurn();
499
+ return;
500
+ }
501
+ }
502
+
503
+ // Check game segment endIf
504
+ if (gameSegmentDef.endIf) {
505
+ const context = this.createReadOnlyContext();
506
+ if (gameSegmentDef.endIf(context)) {
507
+ // Call private transition to avoid recursive checkEndConditions
508
+ this.transitionToNextGameSegment();
509
+ }
510
+ }
511
+ }
512
+
513
+ /**
514
+ * Create read-only context for conditions
515
+ *
516
+ * Note: We pass the actual state, not a Draft cast, to avoid
517
+ * potential mutations in read-only contexts (as noted by Copilot review).
518
+ * The state should not be mutated in condition functions.
519
+ */
520
+ private createReadOnlyContext(): FlowContext<TState, TCardMeta> {
521
+ const stubs = this.createStubOperations();
522
+
523
+ return {
524
+ state: this.gameState as any as Draft<TState>, // Safe: conditions shouldn't mutate
525
+ game: this.gameOperations || stubs.game,
526
+ zones: this.zoneOperations || stubs.zones,
527
+ cards: this.cardOperations || stubs.cards,
528
+ endGameSegment: () => {},
529
+ endPhase: () => {},
530
+ endStep: () => {},
531
+ endTurn: () => {},
532
+ getCurrentGameSegment: () => this.currentGameSegment,
533
+ getCurrentPhase: () => this.currentPhase,
534
+ getCurrentStep: () => this.currentStep,
535
+ getCurrentPlayer: () => this.currentPlayer ?? "",
536
+ getTurnNumber: () => this.turnNumber,
537
+ setCurrentPlayer: (playerId?: string) => {
538
+ this.currentPlayer = playerId;
539
+ },
540
+ };
541
+ }
542
+
543
+ /**
544
+ * Task 9.13: Transition to next step
545
+ */
546
+ private transitionToNextStep(): void {
547
+ if (!this.currentGameSegment) return;
548
+
549
+ const gameSegments = this.normalizedGameSegments;
550
+ const gameSegmentDef = gameSegments[this.currentGameSegment];
551
+ if (!gameSegmentDef) return;
552
+
553
+ const phases = gameSegmentDef.turn.phases;
554
+ if (!(this.currentPhase && this.currentStep && phases)) return;
555
+
556
+ // Set guard to prevent nested transitions
557
+ this.isTransitioning = true;
558
+
559
+ const phaseDef = phases[this.currentPhase];
560
+ if (!phaseDef?.steps) {
561
+ this.isTransitioning = false;
562
+ return;
563
+ }
564
+
565
+ const stepDef = phaseDef.steps[this.currentStep];
566
+
567
+ // Execute step onEnd
568
+ this.executeHook(stepDef?.onEnd);
569
+
570
+ // Determine next step
571
+ const nextStep = stepDef?.next;
572
+
573
+ if (nextStep && phaseDef.steps[nextStep]) {
574
+ this.currentStep = nextStep;
575
+ // Execute new step onBegin
576
+ this.executeHook(phaseDef.steps[nextStep]?.onBegin);
577
+ } else {
578
+ // No more steps, end phase
579
+ this.currentStep = undefined;
580
+ this.transitionToNextPhase();
581
+ }
582
+
583
+ // Clear guard and process any accumulated pending transitions
584
+ this.isTransitioning = false;
585
+ this.processPendingTransitions();
586
+ }
587
+
588
+ /**
589
+ * Task 9.13: Transition to next phase
590
+ */
591
+ private transitionToNextPhase(): void {
592
+ if (!this.currentGameSegment) return;
593
+
594
+ const gameSegments = this.normalizedGameSegments;
595
+ const gameSegmentDef = gameSegments[this.currentGameSegment];
596
+ if (!gameSegmentDef) return;
597
+
598
+ const phases = gameSegmentDef.turn.phases;
599
+ if (!(this.currentPhase && phases)) return;
600
+
601
+ // Set guard to prevent nested transitions
602
+ this.isTransitioning = true;
603
+
604
+ const phaseDef = phases[this.currentPhase];
605
+ const previousPhase = this.currentPhase;
606
+
607
+ // Execute phase onEnd
608
+ this.executeHook(phaseDef?.onEnd);
609
+
610
+ // Invoke tracker reset callback for the ending phase
611
+ if (this.onPhaseEndCallback && previousPhase) {
612
+ this.onPhaseEndCallback(previousPhase);
613
+ }
614
+
615
+ // Determine next phase
616
+ const nextPhase = phaseDef?.next;
617
+
618
+ if (nextPhase && phases[nextPhase]) {
619
+ this.currentPhase = nextPhase;
620
+ const nextPhaseDef = phases[nextPhase];
621
+
622
+ // Log phase transition (INFO level)
623
+ this.logger?.info("Phase transition", {
624
+ from: previousPhase,
625
+ to: this.currentPhase,
626
+ turn: this.turnNumber,
627
+ });
628
+
629
+ // Emit telemetry event
630
+ this.telemetry?.emitEvent({
631
+ type: "flowTransition",
632
+ transitionType: "phase",
633
+ from: previousPhase,
634
+ to: this.currentPhase,
635
+ turn: this.turnNumber,
636
+ timestamp: Date.now(),
637
+ });
638
+
639
+ // Initialize steps if any
640
+ if (nextPhaseDef.steps) {
641
+ const sortedSteps = Object.entries(nextPhaseDef.steps).sort(
642
+ ([, a], [, b]) => a.order - b.order,
643
+ );
644
+ this.currentStep = nextPhaseDef.initialStep ?? sortedSteps[0]?.[0];
645
+
646
+ // Execute step onBegin
647
+ if (this.currentStep) {
648
+ this.executeHook(nextPhaseDef.steps[this.currentStep]?.onBegin);
649
+ }
650
+ }
651
+
652
+ // Execute phase onBegin
653
+ this.executeHook(nextPhaseDef?.onBegin);
654
+ } else {
655
+ // No more phases, end turn
656
+ this.transitionToNextTurn();
657
+ }
658
+
659
+ // Clear guard and process any accumulated pending transitions
660
+ this.isTransitioning = false;
661
+ this.processPendingTransitions();
662
+ }
663
+
664
+ /**
665
+ * Transition to next turn
666
+ */
667
+ private transitionToNextTurn(): void {
668
+ if (!this.currentGameSegment) return;
669
+
670
+ const gameSegments = this.normalizedGameSegments;
671
+ const gameSegmentDef = gameSegments[this.currentGameSegment];
672
+ if (!gameSegmentDef) return;
673
+
674
+ // Set guard to prevent nested transitions
675
+ this.isTransitioning = true;
676
+
677
+ const phases = gameSegmentDef.turn.phases;
678
+
679
+ // Execute step onEnd if in step
680
+ if (this.currentPhase && this.currentStep && phases) {
681
+ const phaseDef = phases[this.currentPhase];
682
+ if (phaseDef?.steps) {
683
+ const stepDef = phaseDef.steps[this.currentStep];
684
+ this.executeHook(stepDef?.onEnd);
685
+ }
686
+ }
687
+
688
+ // Execute phase onEnd if in phase
689
+ if (this.currentPhase && phases) {
690
+ const phaseDef = phases[this.currentPhase];
691
+ this.executeHook(phaseDef?.onEnd);
692
+ }
693
+
694
+ // Execute turn onEnd
695
+ this.executeHook(gameSegmentDef.turn.onEnd);
696
+
697
+ // Invoke tracker reset callback at turn end
698
+ if (this.onTurnEndCallback) {
699
+ this.onTurnEndCallback();
700
+ }
701
+
702
+ // Increment turn number
703
+ const previousTurn = this.turnNumber;
704
+ this.turnNumber += 1;
705
+
706
+ // Log turn transition (INFO level)
707
+ this.logger?.info("Turn transition", {
708
+ turn: previousTurn,
709
+ nextTurn: this.turnNumber,
710
+ });
711
+
712
+ // Emit telemetry event
713
+ this.telemetry?.emitEvent({
714
+ type: "flowTransition",
715
+ transitionType: "turn",
716
+ from: `turn-${previousTurn}`,
717
+ to: `turn-${this.turnNumber}`,
718
+ turn: this.turnNumber,
719
+ timestamp: Date.now(),
720
+ });
721
+
722
+ // Reset to first phase
723
+ if (phases) {
724
+ const sortedPhases = Object.entries(phases).sort(
725
+ ([, a], [, b]) => a.order - b.order,
726
+ );
727
+ this.currentPhase =
728
+ gameSegmentDef.turn.initialPhase ?? sortedPhases[0]?.[0];
729
+
730
+ // Initialize steps
731
+ if (this.currentPhase) {
732
+ const phaseDef = phases[this.currentPhase];
733
+ if (phaseDef?.steps) {
734
+ const sortedSteps = Object.entries(phaseDef.steps).sort(
735
+ ([, a], [, b]) => a.order - b.order,
736
+ );
737
+ this.currentStep = phaseDef.initialStep ?? sortedSteps[0]?.[0];
738
+ } else {
739
+ this.currentStep = undefined;
740
+ }
741
+ }
742
+ }
743
+
744
+ // Execute turn onBegin
745
+ this.executeHook(gameSegmentDef.turn.onBegin);
746
+
747
+ // Execute phase onBegin
748
+ if (this.currentPhase && phases) {
749
+ this.executeHook(phases[this.currentPhase]?.onBegin);
750
+
751
+ // Execute step onBegin
752
+ if (this.currentStep) {
753
+ const phaseDef = phases[this.currentPhase];
754
+ if (phaseDef?.steps) {
755
+ this.executeHook(phaseDef.steps[this.currentStep]?.onBegin);
756
+ }
757
+ }
758
+ }
759
+
760
+ // Clear guard and process any accumulated pending transitions
761
+ this.isTransitioning = false;
762
+ this.processPendingTransitions();
763
+ }
764
+
765
+ /**
766
+ * Transition to next game segment
767
+ */
768
+ private transitionToNextGameSegment(): void {
769
+ if (!this.currentGameSegment) return;
770
+
771
+ const gameSegments = this.normalizedGameSegments;
772
+ const gameSegmentDef = gameSegments[this.currentGameSegment];
773
+ if (!gameSegmentDef) return;
774
+
775
+ // Set guard to prevent nested transitions
776
+ this.isTransitioning = true;
777
+
778
+ const phases = gameSegmentDef.turn.phases;
779
+
780
+ // Execute step onEnd if in step
781
+ if (this.currentPhase && this.currentStep && phases) {
782
+ const phaseDef = phases[this.currentPhase];
783
+ if (phaseDef?.steps) {
784
+ const stepDef = phaseDef.steps[this.currentStep];
785
+ this.executeHook(stepDef?.onEnd);
786
+ }
787
+ }
788
+
789
+ // Execute phase onEnd if in phase
790
+ if (this.currentPhase && phases) {
791
+ const phaseDef = phases[this.currentPhase];
792
+ this.executeHook(phaseDef?.onEnd);
793
+ }
794
+
795
+ // Execute turn onEnd
796
+ this.executeHook(gameSegmentDef.turn.onEnd);
797
+
798
+ // Execute game segment onEnd
799
+ this.executeHook(gameSegmentDef.onEnd);
800
+
801
+ // Determine next game segment
802
+ const nextGameSegment = gameSegmentDef.next;
803
+ const previousSegment = this.currentGameSegment;
804
+
805
+ if (nextGameSegment && gameSegments[nextGameSegment]) {
806
+ this.currentGameSegment = nextGameSegment;
807
+ const nextGameSegmentDef = gameSegments[nextGameSegment];
808
+
809
+ // Log game segment transition (INFO level)
810
+ this.logger?.info("Game segment transition", {
811
+ from: previousSegment,
812
+ to: this.currentGameSegment,
813
+ turn: this.turnNumber,
814
+ });
815
+
816
+ // Emit telemetry event
817
+ this.telemetry?.emitEvent({
818
+ type: "flowTransition",
819
+ transitionType: "segment",
820
+ from: previousSegment || "none",
821
+ to: this.currentGameSegment || "none",
822
+ turn: this.turnNumber,
823
+ timestamp: Date.now(),
824
+ });
825
+
826
+ // Reset turn number for new game segment (optional - depends on game rules)
827
+ // this.turnNumber = 1;
828
+
829
+ // Execute game segment onBegin
830
+ this.executeHook(nextGameSegmentDef.onBegin);
831
+
832
+ // Initialize turn structure for new game segment
833
+ const nextPhases = nextGameSegmentDef.turn.phases;
834
+ if (nextPhases) {
835
+ const sortedPhases = Object.entries(nextPhases).sort(
836
+ ([, a], [, b]) => a.order - b.order,
837
+ );
838
+ this.currentPhase =
839
+ nextGameSegmentDef.turn.initialPhase ?? sortedPhases[0]?.[0];
840
+
841
+ // Initialize steps
842
+ if (this.currentPhase) {
843
+ const phaseDef = nextPhases[this.currentPhase];
844
+ if (phaseDef?.steps) {
845
+ const sortedSteps = Object.entries(phaseDef.steps).sort(
846
+ ([, a], [, b]) => a.order - b.order,
847
+ );
848
+ this.currentStep = phaseDef.initialStep ?? sortedSteps[0]?.[0];
849
+ } else {
850
+ this.currentStep = undefined;
851
+ }
852
+ }
853
+ }
854
+
855
+ // Execute turn onBegin
856
+ this.executeHook(nextGameSegmentDef.turn.onBegin);
857
+
858
+ // Execute phase onBegin
859
+ if (this.currentPhase && nextPhases) {
860
+ this.executeHook(nextPhases[this.currentPhase]?.onBegin);
861
+
862
+ // Execute step onBegin
863
+ if (this.currentStep) {
864
+ const phaseDef = nextPhases[this.currentPhase];
865
+ if (phaseDef?.steps) {
866
+ this.executeHook(phaseDef.steps[this.currentStep]?.onBegin);
867
+ }
868
+ }
869
+ }
870
+ } else {
871
+ // No more game segments, game ends
872
+ this.currentGameSegment = undefined;
873
+ this.currentPhase = undefined;
874
+ this.currentStep = undefined;
875
+ }
876
+
877
+ // Clear guard and process any accumulated pending transitions
878
+ this.isTransitioning = false;
879
+ this.processPendingTransitions();
880
+ }
881
+
882
+ /**
883
+ * Public API
884
+ */
885
+
886
+ /**
887
+ * Get current phase name
888
+ */
889
+ getCurrentPhase(): string | undefined {
890
+ return this.currentPhase;
891
+ }
892
+
893
+ /**
894
+ * Get current step name
895
+ */
896
+ getCurrentStep(): string | undefined {
897
+ return this.currentStep;
898
+ }
899
+
900
+ /**
901
+ * Get current game segment name
902
+ */
903
+ getCurrentGameSegment(): string | undefined {
904
+ return this.currentGameSegment;
905
+ }
906
+
907
+ /**
908
+ * Get current segment name (alias for getCurrentGameSegment)
909
+ */
910
+ getCurrentSegment(): string | undefined {
911
+ return this.currentGameSegment;
912
+ }
913
+
914
+ /**
915
+ * Get current game state
916
+ */
917
+ getGameState(): TState {
918
+ return this.gameState;
919
+ }
920
+
921
+ /**
922
+ * Get current flow state snapshot
923
+ */
924
+ getState(): FlowStateSnapshot {
925
+ return {
926
+ gameSegment: this.currentGameSegment,
927
+ phase: this.currentPhase,
928
+ step: this.currentStep,
929
+ turn: this.turnNumber,
930
+ };
931
+ }
932
+
933
+ /**
934
+ * Get current turn number (1-indexed)
935
+ */
936
+ getTurnNumber(): number {
937
+ return this.turnNumber;
938
+ }
939
+
940
+ /**
941
+ * Get current player ID
942
+ */
943
+ getCurrentPlayer(): string | undefined {
944
+ return this.currentPlayer;
945
+ }
946
+
947
+ /**
948
+ * Set current player ID
949
+ *
950
+ * This allows explicit control over which player is "active" or has "priority".
951
+ * Useful for game segments where priority doesn't follow standard turn order
952
+ * (e.g., during game setup, mulligan phases, or special action sequences).
953
+ *
954
+ * @param playerId - Player ID to set as current, or undefined to clear
955
+ */
956
+ setCurrentPlayer(playerId?: string): void {
957
+ this.currentPlayer = playerId;
958
+ }
959
+
960
+ /**
961
+ * Check if this is the first turn of the game
962
+ */
963
+ isFirstTurn(): boolean {
964
+ return this.turnNumber === 1;
965
+ }
966
+
967
+ /**
968
+ * Transition to next phase
969
+ */
970
+ nextPhase(): void {
971
+ this.transitionToNextPhase();
972
+ this.checkEndConditions();
973
+ }
974
+
975
+ /**
976
+ * Transition to next step
977
+ */
978
+ nextStep(): void {
979
+ this.transitionToNextStep();
980
+ this.checkEndConditions();
981
+ }
982
+
983
+ /**
984
+ * Transition to next game segment
985
+ */
986
+ nextGameSegment(): void {
987
+ this.transitionToNextGameSegment();
988
+ this.checkEndConditions();
989
+ }
990
+
991
+ /**
992
+ * Transition to next turn
993
+ */
994
+ nextTurn(): void {
995
+ this.transitionToNextTurn();
996
+ this.checkEndConditions();
997
+ }
998
+
999
+ /**
1000
+ * Task 9.11: Send event to flow machine
1001
+ */
1002
+ send(event: FlowEvent): void {
1003
+ switch (event.type) {
1004
+ case "NEXT_GAME_SEGMENT":
1005
+ case "END_GAME_SEGMENT":
1006
+ this.nextGameSegment();
1007
+ break;
1008
+ case "NEXT_PHASE":
1009
+ case "END_PHASE":
1010
+ this.nextPhase();
1011
+ break;
1012
+ case "END_STEP":
1013
+ case "NEXT_STEP":
1014
+ this.nextStep();
1015
+ break;
1016
+ case "END_TURN":
1017
+ this.nextTurn();
1018
+ break;
1019
+ case "STATE_UPDATED":
1020
+ this.checkEndConditions();
1021
+ break;
1022
+ }
1023
+ }
1024
+
1025
+ /**
1026
+ * Update game state and check endIf conditions
1027
+ */
1028
+ updateState(updater: (draft: Draft<TState>) => void): void {
1029
+ this.gameState = produce(this.gameState, updater);
1030
+ this.checkEndConditions();
1031
+ }
1032
+
1033
+ /**
1034
+ * Sync internal game state with external state
1035
+ *
1036
+ * Called by RuleEngine after move execution to ensure FlowManager
1037
+ * has the latest state when checking endIf conditions.
1038
+ *
1039
+ * @param newState - The new game state after move execution
1040
+ */
1041
+ public syncState(newState: TState): void {
1042
+ this.gameState = newState;
1043
+ }
1044
+ }