@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,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
|
+
}
|