@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,756 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { FlowDefinition } from "../flow-definition";
3
+ import { FlowManager } from "../flow-manager";
4
+
5
+ /**
6
+ * Task 9.3, 9.4: Tests for FlowManager
7
+ *
8
+ * Tests verify:
9
+ * - Turn/phase/step state machine construction
10
+ * - Lifecycle hook execution
11
+ * - Automatic and programmatic transitions
12
+ * - Hierarchical state management
13
+ * - Event handling
14
+ */
15
+
16
+ type GameState = {
17
+ currentPlayer: number;
18
+ players: Array<{ id: string; ready: boolean }>;
19
+ turnCount: number;
20
+ phase?: string;
21
+ step?: string;
22
+ log: string[];
23
+ };
24
+
25
+ describe("FlowManager - State Machine", () => {
26
+ describe("Task 9.3, 9.4: Turn/Phase/Step State Machine", () => {
27
+ it("should initialize with turn → phase hierarchy", () => {
28
+ // Red: Test initialization
29
+ const flow: FlowDefinition<GameState> = {
30
+ gameSegments: {
31
+ mainGame: {
32
+ order: 1,
33
+ turn: {
34
+ phases: {
35
+ ready: { order: 0, next: "draw" },
36
+ draw: { order: 1, next: "main" },
37
+ main: { order: 2, next: undefined },
38
+ },
39
+ },
40
+ },
41
+ },
42
+ };
43
+
44
+ const initialState: GameState = {
45
+ currentPlayer: 0,
46
+ players: [{ id: "p1", ready: false }],
47
+ turnCount: 0,
48
+ log: [],
49
+ };
50
+
51
+ const manager = new FlowManager(flow, initialState);
52
+
53
+ const state = manager.getState();
54
+ expect(state).toBeDefined();
55
+ });
56
+
57
+ it("should progress through phases sequentially", () => {
58
+ const flow: FlowDefinition<GameState> = {
59
+ gameSegments: {
60
+ mainGame: {
61
+ order: 1,
62
+ turn: {
63
+ phases: {
64
+ ready: { order: 0, next: "draw" },
65
+ draw: { order: 1, next: "main" },
66
+ main: { order: 2, next: undefined },
67
+ },
68
+ },
69
+ },
70
+ },
71
+ };
72
+
73
+ const initialState: GameState = {
74
+ currentPlayer: 0,
75
+ players: [{ id: "p1", ready: false }],
76
+ turnCount: 0,
77
+ log: [],
78
+ };
79
+
80
+ const manager = new FlowManager(flow, initialState);
81
+
82
+ // Should start in ready phase
83
+ expect(manager.getCurrentPhase()).toBe("ready");
84
+
85
+ // Transition to draw
86
+ manager.nextPhase();
87
+ expect(manager.getCurrentPhase()).toBe("draw");
88
+
89
+ // Transition to main
90
+ manager.nextPhase();
91
+ expect(manager.getCurrentPhase()).toBe("main");
92
+
93
+ // Next phase is undefined, should end turn
94
+ manager.nextPhase();
95
+ expect(manager.getCurrentPhase()).toBe("ready"); // New turn
96
+ });
97
+
98
+ it("should support hierarchical states (phase → steps)", () => {
99
+ // Task 9.13: Hierarchical states
100
+ const flow: FlowDefinition<GameState> = {
101
+ gameSegments: {
102
+ mainGame: {
103
+ order: 1,
104
+ turn: {
105
+ phases: {
106
+ main: {
107
+ order: 0,
108
+ next: undefined,
109
+ steps: {
110
+ declare: { order: 0, next: "target" },
111
+ target: { order: 1, next: "damage" },
112
+ damage: { order: 2, next: undefined },
113
+ },
114
+ },
115
+ },
116
+ },
117
+ },
118
+ },
119
+ };
120
+
121
+ const initialState: GameState = {
122
+ currentPlayer: 0,
123
+ players: [{ id: "p1", ready: false }],
124
+ turnCount: 0,
125
+ log: [],
126
+ };
127
+
128
+ const manager = new FlowManager(flow, initialState);
129
+
130
+ expect(manager.getCurrentPhase()).toBe("main");
131
+ expect(manager.getCurrentStep()).toBe("declare");
132
+
133
+ manager.nextStep();
134
+ expect(manager.getCurrentStep()).toBe("target");
135
+
136
+ manager.nextStep();
137
+ expect(manager.getCurrentStep()).toBe("damage");
138
+ });
139
+ });
140
+
141
+ describe("Task 9.5, 9.6: Lifecycle Hooks", () => {
142
+ it("should execute onBegin hook when phase starts", () => {
143
+ const flow: FlowDefinition<GameState> = {
144
+ gameSegments: {
145
+ mainGame: {
146
+ order: 1,
147
+ turn: {
148
+ onBegin: (context) => {
149
+ context.state.turnCount += 1;
150
+ context.state.log.push("turn-begin");
151
+ },
152
+ phases: {
153
+ ready: {
154
+ order: 0,
155
+ next: undefined,
156
+ onBegin: (context) => {
157
+ context.state.log.push("ready-begin");
158
+ },
159
+ },
160
+ },
161
+ },
162
+ },
163
+ },
164
+ };
165
+
166
+ const initialState: GameState = {
167
+ currentPlayer: 0,
168
+ players: [{ id: "p1", ready: false }],
169
+ turnCount: 0,
170
+ log: [],
171
+ };
172
+
173
+ const manager = new FlowManager(flow, initialState);
174
+ const state = manager.getGameState();
175
+
176
+ expect(state.turnCount).toBe(1);
177
+ expect(state.log).toContain("turn-begin");
178
+ expect(state.log).toContain("ready-begin");
179
+ });
180
+
181
+ it("should execute onEnd hook when phase ends", () => {
182
+ const flow: FlowDefinition<GameState> = {
183
+ gameSegments: {
184
+ mainGame: {
185
+ order: 1,
186
+ turn: {
187
+ phases: {
188
+ ready: {
189
+ order: 0,
190
+ next: "draw",
191
+ onEnd: (context) => {
192
+ context.state.log.push("ready-end");
193
+ },
194
+ },
195
+ draw: {
196
+ order: 1,
197
+ next: undefined,
198
+ },
199
+ },
200
+ },
201
+ },
202
+ },
203
+ };
204
+
205
+ const initialState: GameState = {
206
+ currentPlayer: 0,
207
+ players: [{ id: "p1", ready: false }],
208
+ turnCount: 0,
209
+ log: [],
210
+ };
211
+
212
+ const manager = new FlowManager(flow, initialState);
213
+
214
+ manager.nextPhase();
215
+ const state = manager.getGameState();
216
+
217
+ expect(state.log).toContain("ready-end");
218
+ expect(manager.getCurrentPhase()).toBe("draw");
219
+ });
220
+
221
+ it("should execute step lifecycle hooks", () => {
222
+ const flow: FlowDefinition<GameState> = {
223
+ gameSegments: {
224
+ mainGame: {
225
+ order: 1,
226
+ turn: {
227
+ phases: {
228
+ main: {
229
+ order: 0,
230
+ next: undefined,
231
+ steps: {
232
+ declare: {
233
+ order: 0,
234
+ next: "target",
235
+ onBegin: (context) => {
236
+ context.state.log.push("declare-begin");
237
+ },
238
+ onEnd: (context) => {
239
+ context.state.log.push("declare-end");
240
+ },
241
+ },
242
+ target: {
243
+ order: 1,
244
+ next: undefined,
245
+ },
246
+ },
247
+ },
248
+ },
249
+ },
250
+ },
251
+ },
252
+ };
253
+
254
+ const initialState: GameState = {
255
+ currentPlayer: 0,
256
+ players: [{ id: "p1", ready: false }],
257
+ turnCount: 0,
258
+ log: [],
259
+ };
260
+
261
+ const manager = new FlowManager(flow, initialState);
262
+ let state = manager.getGameState();
263
+
264
+ expect(state.log).toContain("declare-begin");
265
+
266
+ manager.nextStep();
267
+ state = manager.getGameState();
268
+
269
+ expect(state.log).toContain("declare-end");
270
+ });
271
+ });
272
+
273
+ describe("Task 9.7, 9.8: EndIf Conditions", () => {
274
+ it("should automatically transition when endIf returns true", () => {
275
+ const flow: FlowDefinition<GameState> = {
276
+ gameSegments: {
277
+ mainGame: {
278
+ order: 1,
279
+ turn: {
280
+ phases: {
281
+ ready: {
282
+ order: 0,
283
+ next: "draw",
284
+ endIf: (context) => {
285
+ // Auto-end when all players are ready
286
+ return context.state.players.every((p) => p.ready);
287
+ },
288
+ },
289
+ draw: {
290
+ order: 1,
291
+ next: undefined,
292
+ },
293
+ },
294
+ },
295
+ },
296
+ },
297
+ };
298
+
299
+ const initialState: GameState = {
300
+ currentPlayer: 0,
301
+ players: [
302
+ { id: "p1", ready: false },
303
+ { id: "p2", ready: false },
304
+ ],
305
+ turnCount: 0,
306
+ log: [],
307
+ };
308
+
309
+ const manager = new FlowManager(flow, initialState);
310
+
311
+ expect(manager.getCurrentPhase()).toBe("ready");
312
+
313
+ // Make all players ready
314
+ manager.updateState((draft) => {
315
+ for (const player of draft.players) {
316
+ player.ready = true;
317
+ }
318
+ });
319
+
320
+ // Should auto-transition to draw
321
+ expect(manager.getCurrentPhase()).toBe("draw");
322
+ });
323
+
324
+ it("should check endIf after state updates", () => {
325
+ const flow: FlowDefinition<GameState> = {
326
+ gameSegments: {
327
+ mainGame: {
328
+ order: 1,
329
+ turn: {
330
+ phases: {
331
+ main: {
332
+ order: 0,
333
+ next: "end",
334
+ endIf: (context) => context.state.turnCount >= 5,
335
+ },
336
+ end: {
337
+ order: 1,
338
+ next: undefined,
339
+ },
340
+ },
341
+ },
342
+ },
343
+ },
344
+ };
345
+
346
+ const initialState: GameState = {
347
+ currentPlayer: 0,
348
+ players: [{ id: "p1", ready: false }],
349
+ turnCount: 0,
350
+ log: [],
351
+ };
352
+
353
+ const manager = new FlowManager(flow, initialState);
354
+
355
+ expect(manager.getCurrentPhase()).toBe("main");
356
+
357
+ // Increment turn count
358
+ manager.updateState((draft) => {
359
+ draft.turnCount = 5;
360
+ });
361
+
362
+ // Should auto-transition to end
363
+ expect(manager.getCurrentPhase()).toBe("end");
364
+ });
365
+ });
366
+
367
+ describe("Task 9.9, 9.10: FlowContext", () => {
368
+ it("should provide programmatic endPhase control", () => {
369
+ const flow: FlowDefinition<GameState> = {
370
+ gameSegments: {
371
+ mainGame: {
372
+ order: 1,
373
+ turn: {
374
+ phases: {
375
+ ready: {
376
+ order: 0,
377
+ next: "draw",
378
+ onBegin: (context) => {
379
+ // Skip this phase if no players
380
+ if (context.state.players.length === 0) {
381
+ context.endPhase();
382
+ }
383
+ },
384
+ },
385
+ draw: {
386
+ order: 1,
387
+ next: undefined,
388
+ },
389
+ },
390
+ },
391
+ },
392
+ },
393
+ };
394
+
395
+ const initialState: GameState = {
396
+ currentPlayer: 0,
397
+ players: [],
398
+ turnCount: 0,
399
+ log: [],
400
+ };
401
+
402
+ const manager = new FlowManager(flow, initialState);
403
+
404
+ // Should have skipped ready phase via endPhase()
405
+ expect(manager.getCurrentPhase()).toBe("draw");
406
+ });
407
+
408
+ it("should provide programmatic endStep control", () => {
409
+ const flow: FlowDefinition<GameState> = {
410
+ gameSegments: {
411
+ mainGame: {
412
+ order: 1,
413
+ turn: {
414
+ phases: {
415
+ main: {
416
+ order: 0,
417
+ next: undefined,
418
+ steps: {
419
+ declare: {
420
+ order: 0,
421
+ next: "target",
422
+ onBegin: (context) => {
423
+ if (context.state.players.length === 0) {
424
+ context.endStep();
425
+ }
426
+ },
427
+ },
428
+ target: {
429
+ order: 1,
430
+ next: undefined,
431
+ },
432
+ },
433
+ },
434
+ },
435
+ },
436
+ },
437
+ },
438
+ };
439
+
440
+ const initialState: GameState = {
441
+ currentPlayer: 0,
442
+ players: [],
443
+ turnCount: 0,
444
+ log: [],
445
+ };
446
+
447
+ const manager = new FlowManager(flow, initialState);
448
+
449
+ // Should have skipped declare step
450
+ expect(manager.getCurrentStep()).toBe("target");
451
+ });
452
+
453
+ it("should provide programmatic endTurn control", () => {
454
+ const flow: FlowDefinition<GameState> = {
455
+ gameSegments: {
456
+ mainGame: {
457
+ order: 1,
458
+ turn: {
459
+ onBegin: (context) => {
460
+ context.state.turnCount += 1;
461
+ // End turn immediately if turn count is 5
462
+ if (context.state.turnCount === 5) {
463
+ context.endTurn();
464
+ }
465
+ },
466
+ phases: {
467
+ main: { order: 0, next: undefined },
468
+ },
469
+ },
470
+ },
471
+ },
472
+ };
473
+
474
+ const initialState: GameState = {
475
+ currentPlayer: 0,
476
+ players: [{ id: "p1", ready: false }],
477
+ turnCount: 4, // Next turn will be 5
478
+ log: [],
479
+ };
480
+
481
+ const manager = new FlowManager(flow, initialState);
482
+
483
+ manager.nextTurn();
484
+ const state = manager.getGameState();
485
+
486
+ // Turn should have ended immediately
487
+ expect(state.turnCount).toBeGreaterThan(5);
488
+ });
489
+
490
+ it("should provide current flow information", () => {
491
+ const flow: FlowDefinition<GameState> = {
492
+ gameSegments: {
493
+ mainGame: {
494
+ order: 1,
495
+ turn: {
496
+ phases: {
497
+ main: {
498
+ order: 0,
499
+ next: undefined,
500
+ onBegin: (context) => {
501
+ // For games that don't have special setup, we should set currentPlayer
502
+ // in the turn onBegin. For testing, we'll set it here.
503
+ if (!context.getCurrentPlayer()) {
504
+ context.setCurrentPlayer("p1");
505
+ }
506
+
507
+ // Access flow information
508
+ const phase = context.getCurrentPhase();
509
+ const turn = context.getTurnNumber();
510
+ const player = context.getCurrentPlayer();
511
+
512
+ expect(phase).toBe("main");
513
+ expect(turn).toBeGreaterThan(0);
514
+ expect(player).toBeDefined();
515
+ },
516
+ },
517
+ },
518
+ },
519
+ },
520
+ },
521
+ };
522
+
523
+ const initialState: GameState = {
524
+ currentPlayer: 0,
525
+ players: [{ id: "p1", ready: false }],
526
+ turnCount: 0,
527
+ log: [],
528
+ };
529
+
530
+ const manager = new FlowManager(flow, initialState);
531
+ manager.getGameState(); // Trigger onBegin
532
+ });
533
+ });
534
+
535
+ describe("Task 9.11, 9.12: Flow Event Handling", () => {
536
+ it("should handle NEXT_PHASE event", () => {
537
+ const flow: FlowDefinition<GameState> = {
538
+ gameSegments: {
539
+ mainGame: {
540
+ order: 1,
541
+ turn: {
542
+ phases: {
543
+ ready: { order: 0, next: "draw" },
544
+ draw: { order: 1, next: undefined },
545
+ },
546
+ },
547
+ },
548
+ },
549
+ };
550
+
551
+ const initialState: GameState = {
552
+ currentPlayer: 0,
553
+ players: [{ id: "p1", ready: false }],
554
+ turnCount: 0,
555
+ log: [],
556
+ };
557
+
558
+ const manager = new FlowManager(flow, initialState);
559
+
560
+ expect(manager.getCurrentPhase()).toBe("ready");
561
+
562
+ manager.send({ type: "NEXT_PHASE" });
563
+
564
+ expect(manager.getCurrentPhase()).toBe("draw");
565
+ });
566
+
567
+ it("should handle END_TURN event", () => {
568
+ const flow: FlowDefinition<GameState> = {
569
+ gameSegments: {
570
+ mainGame: {
571
+ order: 1,
572
+ turn: {
573
+ onBegin: (context) => {
574
+ context.state.turnCount += 1;
575
+ },
576
+ phases: {
577
+ main: { order: 0, next: undefined },
578
+ },
579
+ },
580
+ },
581
+ },
582
+ };
583
+
584
+ const initialState: GameState = {
585
+ currentPlayer: 0,
586
+ players: [{ id: "p1", ready: false }],
587
+ turnCount: 0,
588
+ log: [],
589
+ };
590
+
591
+ const manager = new FlowManager(flow, initialState);
592
+
593
+ const initialTurnCount = manager.getGameState().turnCount;
594
+
595
+ manager.send({ type: "END_TURN" });
596
+
597
+ expect(manager.getGameState().turnCount).toBeGreaterThan(
598
+ initialTurnCount,
599
+ );
600
+ });
601
+
602
+ it("should handle END_STEP event", () => {
603
+ const flow: FlowDefinition<GameState> = {
604
+ gameSegments: {
605
+ mainGame: {
606
+ order: 1,
607
+ turn: {
608
+ phases: {
609
+ main: {
610
+ order: 0,
611
+ next: undefined,
612
+ steps: {
613
+ declare: { order: 0, next: "target" },
614
+ target: { order: 1, next: undefined },
615
+ },
616
+ },
617
+ },
618
+ },
619
+ },
620
+ },
621
+ };
622
+
623
+ const initialState: GameState = {
624
+ currentPlayer: 0,
625
+ players: [{ id: "p1", ready: false }],
626
+ turnCount: 0,
627
+ log: [],
628
+ };
629
+
630
+ const manager = new FlowManager(flow, initialState);
631
+
632
+ expect(manager.getCurrentStep()).toBe("declare");
633
+
634
+ manager.send({ type: "END_STEP" });
635
+
636
+ expect(manager.getCurrentStep()).toBe("target");
637
+ });
638
+ });
639
+
640
+ describe("Task 9.13, 9.14: Hierarchical States", () => {
641
+ it("should support nested phase → step hierarchy", () => {
642
+ const flow: FlowDefinition<GameState> = {
643
+ gameSegments: {
644
+ mainGame: {
645
+ order: 1,
646
+ turn: {
647
+ phases: {
648
+ main: {
649
+ order: 0,
650
+ next: "end",
651
+ steps: {
652
+ start: { order: 0, next: "middle" },
653
+ middle: { order: 1, next: "finish" },
654
+ finish: { order: 2, next: undefined },
655
+ },
656
+ },
657
+ end: {
658
+ order: 1,
659
+ next: undefined,
660
+ },
661
+ },
662
+ },
663
+ },
664
+ },
665
+ };
666
+
667
+ const initialState: GameState = {
668
+ currentPlayer: 0,
669
+ players: [{ id: "p1", ready: false }],
670
+ turnCount: 0,
671
+ log: [],
672
+ };
673
+
674
+ const manager = new FlowManager(flow, initialState);
675
+
676
+ // Should start in main.start
677
+ expect(manager.getCurrentPhase()).toBe("main");
678
+ expect(manager.getCurrentStep()).toBe("start");
679
+
680
+ // Progress through steps
681
+ manager.nextStep();
682
+ expect(manager.getCurrentStep()).toBe("middle");
683
+
684
+ manager.nextStep();
685
+ expect(manager.getCurrentStep()).toBe("finish");
686
+
687
+ // End step should end phase
688
+ manager.nextStep();
689
+ expect(manager.getCurrentPhase()).toBe("end");
690
+ expect(manager.getCurrentStep()).toBeUndefined();
691
+ });
692
+
693
+ it("should handle lifecycle hooks at all levels", () => {
694
+ const flow: FlowDefinition<GameState> = {
695
+ gameSegments: {
696
+ mainGame: {
697
+ order: 1,
698
+ turn: {
699
+ onBegin: (context) => {
700
+ context.state.log.push("turn-begin");
701
+ },
702
+ onEnd: (context) => {
703
+ context.state.log.push("turn-end");
704
+ },
705
+ phases: {
706
+ main: {
707
+ order: 0,
708
+ next: undefined,
709
+ onBegin: (context) => {
710
+ context.state.log.push("phase-begin");
711
+ },
712
+ onEnd: (context) => {
713
+ context.state.log.push("phase-end");
714
+ },
715
+ steps: {
716
+ start: {
717
+ order: 0,
718
+ next: undefined,
719
+ onBegin: (context) => {
720
+ context.state.log.push("step-begin");
721
+ },
722
+ onEnd: (context) => {
723
+ context.state.log.push("step-end");
724
+ },
725
+ },
726
+ },
727
+ },
728
+ },
729
+ },
730
+ },
731
+ },
732
+ };
733
+
734
+ const initialState: GameState = {
735
+ currentPlayer: 0,
736
+ players: [{ id: "p1", ready: false }],
737
+ turnCount: 0,
738
+ log: [],
739
+ };
740
+
741
+ const manager = new FlowManager(flow, initialState);
742
+ let state = manager.getGameState();
743
+
744
+ expect(state.log).toContain("turn-begin");
745
+ expect(state.log).toContain("phase-begin");
746
+ expect(state.log).toContain("step-begin");
747
+
748
+ manager.nextTurn();
749
+ state = manager.getGameState();
750
+
751
+ expect(state.log).toContain("step-end");
752
+ expect(state.log).toContain("phase-end");
753
+ expect(state.log).toContain("turn-end");
754
+ });
755
+ });
756
+ });