@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,565 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { FlowDefinition } from "../flow-definition";
3
+ import { FlowManager } from "../flow-manager";
4
+
5
+ /**
6
+ * End-to-End Serialization Tests
7
+ *
8
+ * Use case: When a game ends, we store a serialized version of the state in a database.
9
+ * Players can later recover this state and check their replay.
10
+ *
11
+ * These tests verify that:
12
+ * - Game state (including flow state) can be serialized to JSON
13
+ * - Serialized state can be deserialized
14
+ * - Flow can continue from deserialized state
15
+ * - Flow position (phase, segment, turn) is preserved
16
+ */
17
+
18
+ type GameState = {
19
+ currentPlayer: number;
20
+ players: Array<{ id: string; name: string; score: number }>;
21
+ turnCount: number;
22
+ phase?: string;
23
+ step?: string;
24
+ log: string[];
25
+ // Flow state that needs to be preserved
26
+ flowState?: {
27
+ currentPhase?: string;
28
+ currentStep?: string;
29
+ turnNumber: number;
30
+ };
31
+ };
32
+
33
+ describe("Flow Serialization - End to End", () => {
34
+ it("should serialize and deserialize complete game state with flow position", () => {
35
+ // Setup: Create a game with flow
36
+ const flow: FlowDefinition<GameState> = {
37
+ gameSegments: {
38
+ mainGame: {
39
+ order: 1,
40
+ turn: {
41
+ onBegin: (context) => {
42
+ context.state.turnCount += 1;
43
+ context.state.log.push(`turn-${context.state.turnCount}-begin`);
44
+ },
45
+ phases: {
46
+ ready: {
47
+ order: 0,
48
+ next: "draw",
49
+ onBegin: (context) => {
50
+ context.state.log.push("ready-phase");
51
+ },
52
+ },
53
+ draw: {
54
+ order: 1,
55
+ next: "main",
56
+ onBegin: (context) => {
57
+ context.state.log.push("draw-phase");
58
+ },
59
+ },
60
+ main: {
61
+ order: 2,
62
+ next: "end",
63
+ onBegin: (context) => {
64
+ context.state.log.push("main-phase");
65
+ },
66
+ },
67
+ end: {
68
+ order: 3,
69
+ next: undefined,
70
+ onBegin: (context) => {
71
+ context.state.log.push("end-phase");
72
+ },
73
+ },
74
+ },
75
+ },
76
+ },
77
+ },
78
+ };
79
+
80
+ const initialState: GameState = {
81
+ currentPlayer: 0,
82
+ players: [
83
+ { id: "p1", name: "Alice", score: 0 },
84
+ { id: "p2", name: "Bob", score: 0 },
85
+ ],
86
+ turnCount: 0,
87
+ log: [],
88
+ };
89
+
90
+ const manager = new FlowManager(flow, initialState);
91
+
92
+ // Progress through some phases
93
+ manager.nextPhase(); // ready → draw
94
+ manager.nextPhase(); // draw → main
95
+
96
+ const gameStateBeforeSerialization = manager.getGameState();
97
+
98
+ // Serialize: Capture both game state and flow state
99
+ const serializedState = JSON.stringify({
100
+ gameState: gameStateBeforeSerialization,
101
+ flowState: manager.serializeFlowState(),
102
+ });
103
+
104
+ // Simulate: Save to database, then load later
105
+ expect(serializedState).toBeDefined();
106
+ expect(serializedState.length).toBeGreaterThan(0);
107
+
108
+ // Deserialize: Parse from JSON
109
+ const deserialized = JSON.parse(serializedState);
110
+
111
+ expect(deserialized.gameState).toBeDefined();
112
+ expect(deserialized.flowState).toBeDefined();
113
+
114
+ // Verify flow state was preserved
115
+ expect(deserialized.flowState.currentPhase).toBe("main");
116
+ expect(deserialized.flowState.turnNumber).toBeGreaterThan(0);
117
+ });
118
+
119
+ it("should restore flow manager from serialized state and continue playing", () => {
120
+ const flow: FlowDefinition<GameState> = {
121
+ gameSegments: {
122
+ mainGame: {
123
+ order: 1,
124
+ turn: {
125
+ onBegin: (context) => {
126
+ context.state.turnCount += 1;
127
+ },
128
+ phases: {
129
+ ready: { order: 0, next: "draw" },
130
+ draw: { order: 1, next: "main" },
131
+ main: { order: 2, next: "end" },
132
+ end: { order: 3, next: undefined },
133
+ },
134
+ },
135
+ },
136
+ },
137
+ };
138
+
139
+ // Step 1: Original game session
140
+ const originalState: GameState = {
141
+ currentPlayer: 0,
142
+ players: [
143
+ { id: "p1", name: "Alice", score: 10 },
144
+ { id: "p2", name: "Bob", score: 15 },
145
+ ],
146
+ turnCount: 0,
147
+ log: [],
148
+ };
149
+
150
+ const originalManager = new FlowManager(flow, originalState);
151
+ originalManager.nextPhase(); // ready → draw
152
+ originalManager.nextPhase(); // draw → main
153
+
154
+ // Step 2: Serialize (save to database)
155
+ const savedState = {
156
+ gameState: originalManager.getGameState(),
157
+ flowState: originalManager.serializeFlowState(),
158
+ };
159
+
160
+ const serialized = JSON.stringify(savedState);
161
+
162
+ // Step 3: Later... deserialize (load from database)
163
+ const loaded = JSON.parse(serialized);
164
+
165
+ // Step 4: Restore game state
166
+ const restoredState = loaded.gameState;
167
+ const restoredFlowState = loaded.flowState;
168
+
169
+ // Step 5: Create new FlowManager with restored state
170
+ const restoredManager = new FlowManager(flow, restoredState, {
171
+ restoreFrom: restoredFlowState,
172
+ });
173
+
174
+ // Verify: Restored state matches original
175
+ expect(restoredManager.getGameState().players[0].score).toBe(10);
176
+ expect(restoredManager.getGameState().players[1].score).toBe(15);
177
+
178
+ // Step 6: Continue playing from restored state
179
+ restoredManager.nextPhase(); // main → end
180
+
181
+ expect(restoredManager.getCurrentPhase()).toBe("end");
182
+
183
+ // Game continues normally after restoration
184
+ restoredManager.nextPhase(); // end → new turn (ready phase)
185
+ expect(restoredManager.getCurrentPhase()).toBe("ready");
186
+ expect(restoredManager.getGameState().turnCount).toBeGreaterThan(1);
187
+ });
188
+
189
+ it("should preserve step state during serialization", () => {
190
+ const flow: FlowDefinition<GameState> = {
191
+ gameSegments: {
192
+ mainGame: {
193
+ order: 1,
194
+ turn: {
195
+ phases: {
196
+ combat: {
197
+ order: 0,
198
+ next: undefined,
199
+ steps: {
200
+ declare: {
201
+ order: 0,
202
+ next: "target",
203
+ onBegin: (context) => {
204
+ context.state.log.push("declare-attackers");
205
+ },
206
+ },
207
+ target: {
208
+ order: 1,
209
+ next: "damage",
210
+ onBegin: (context) => {
211
+ context.state.log.push("declare-targets");
212
+ },
213
+ },
214
+ damage: {
215
+ order: 2,
216
+ next: undefined,
217
+ onBegin: (context) => {
218
+ context.state.log.push("deal-damage");
219
+ },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ },
227
+ };
228
+
229
+ const initialState: GameState = {
230
+ currentPlayer: 0,
231
+ players: [{ id: "p1", name: "Alice", score: 0 }],
232
+ turnCount: 0,
233
+ log: [],
234
+ };
235
+
236
+ const manager = new FlowManager(flow, initialState);
237
+
238
+ // Progress to middle of combat
239
+ manager.nextStep(); // declare → target
240
+
241
+ // Serialize with step information
242
+ const snapshot = {
243
+ game: manager.getGameState(),
244
+ flow: manager.serializeFlowState(),
245
+ };
246
+
247
+ const serialized = JSON.stringify(snapshot);
248
+ const restored = JSON.parse(serialized);
249
+
250
+ // Verify step was preserved
251
+ expect(restored.flow.currentPhase).toBe("combat");
252
+ expect(restored.flow.currentStep).toBe("target");
253
+ expect(restored.game.log).toContain("declare-attackers");
254
+ expect(restored.game.log).toContain("declare-targets");
255
+
256
+ // Create new manager with restored state
257
+ const restoredManager = new FlowManager(flow, restored.game, {
258
+ restoreFrom: restored.flow,
259
+ });
260
+
261
+ // Continue from where we left off
262
+ restoredManager.nextStep(); // target → damage
263
+
264
+ expect(restoredManager.getCurrentStep()).toBe("damage");
265
+ expect(restoredManager.getGameState().log).toContain("deal-damage");
266
+ });
267
+
268
+ it("should handle replay scenario: deserialize multiple snapshots in sequence", () => {
269
+ const flow: FlowDefinition<GameState> = {
270
+ gameSegments: {
271
+ mainGame: {
272
+ order: 1,
273
+ turn: {
274
+ onBegin: (context) => {
275
+ context.state.turnCount += 1;
276
+ context.state.currentPlayer =
277
+ (context.state.currentPlayer + 1) %
278
+ context.state.players.length;
279
+ },
280
+ phases: {
281
+ main: {
282
+ order: 0,
283
+ next: undefined,
284
+ onBegin: (context) => {
285
+ context.state.log.push(
286
+ `player-${context.state.currentPlayer}-main`,
287
+ );
288
+ },
289
+ },
290
+ },
291
+ },
292
+ },
293
+ },
294
+ };
295
+
296
+ const initialState: GameState = {
297
+ currentPlayer: 0,
298
+ players: [
299
+ { id: "p1", name: "Alice", score: 0 },
300
+ { id: "p2", name: "Bob", score: 0 },
301
+ ],
302
+ turnCount: 0,
303
+ log: [],
304
+ };
305
+
306
+ const manager = new FlowManager(flow, initialState);
307
+
308
+ // Simulate game progression with snapshots
309
+ const snapshots: string[] = [];
310
+
311
+ // Snapshot 1: After turn 1
312
+ manager.nextTurn();
313
+ snapshots.push(
314
+ JSON.stringify({
315
+ game: manager.getGameState(),
316
+ flow: manager.serializeFlowState(),
317
+ }),
318
+ );
319
+
320
+ // Snapshot 2: After turn 2
321
+ manager.nextTurn();
322
+ snapshots.push(
323
+ JSON.stringify({
324
+ game: manager.getGameState(),
325
+ flow: manager.serializeFlowState(),
326
+ }),
327
+ );
328
+
329
+ // Snapshot 3: After turn 3
330
+ manager.nextTurn();
331
+ snapshots.push(
332
+ JSON.stringify({
333
+ game: manager.getGameState(),
334
+ flow: manager.serializeFlowState(),
335
+ }),
336
+ );
337
+
338
+ // Replay scenario: Load and verify each snapshot
339
+ const snapshot1 = JSON.parse(snapshots[0]);
340
+ expect(snapshot1.game.turnCount).toBe(2); // Initial turn + 1
341
+ expect(snapshot1.flow.turnNumber).toBe(2);
342
+
343
+ const snapshot2 = JSON.parse(snapshots[1]);
344
+ expect(snapshot2.game.turnCount).toBe(3);
345
+ expect(snapshot2.flow.turnNumber).toBe(3);
346
+
347
+ const snapshot3 = JSON.parse(snapshots[2]);
348
+ expect(snapshot3.game.turnCount).toBe(4);
349
+ expect(snapshot3.flow.turnNumber).toBe(4);
350
+
351
+ // Verify log progression
352
+ expect(snapshot1.game.log.length).toBeLessThan(snapshot2.game.log.length);
353
+ expect(snapshot2.game.log.length).toBeLessThan(snapshot3.game.log.length);
354
+ });
355
+
356
+ it("should preserve complex game state with nested objects during serialization", () => {
357
+ type ComplexGameState = GameState & {
358
+ cards: Record<string, { id: string; owner: string; zone: string }>;
359
+ zones: Record<string, string[]>;
360
+ };
361
+
362
+ const flow: FlowDefinition<ComplexGameState> = {
363
+ gameSegments: {
364
+ mainGame: {
365
+ order: 1,
366
+ turn: {
367
+ phases: {
368
+ main: {
369
+ order: 0,
370
+ next: undefined,
371
+ onBegin: (context) => {
372
+ // Modify nested structures
373
+ context.state.cards.card1 = {
374
+ id: "card1",
375
+ owner: "p1",
376
+ zone: "hand",
377
+ };
378
+ context.state.zones.hand = ["card1"];
379
+ },
380
+ },
381
+ },
382
+ },
383
+ },
384
+ },
385
+ };
386
+
387
+ const initialState: ComplexGameState = {
388
+ currentPlayer: 0,
389
+ players: [{ id: "p1", name: "Alice", score: 0 }],
390
+ turnCount: 0,
391
+ log: [],
392
+ cards: {},
393
+ zones: { hand: [], deck: [], discard: [] },
394
+ };
395
+
396
+ const manager = new FlowManager(flow, initialState);
397
+
398
+ const gameState = manager.getGameState();
399
+
400
+ // Serialize complex nested state
401
+ const serialized = JSON.stringify({
402
+ game: gameState,
403
+ flow: manager.serializeFlowState(),
404
+ });
405
+
406
+ // Deserialize
407
+ const restored = JSON.parse(serialized);
408
+
409
+ // Verify nested structures preserved
410
+ expect(restored.game.cards.card1).toBeDefined();
411
+ expect(restored.game.cards.card1.owner).toBe("p1");
412
+ expect(restored.game.zones.hand).toEqual(["card1"]);
413
+ expect(Array.isArray(restored.game.zones.deck)).toBe(true);
414
+
415
+ // Create new manager and verify it works
416
+ const restoredManager = new FlowManager(flow, restored.game, {
417
+ restoreFrom: restored.flow,
418
+ });
419
+ expect(restoredManager.getGameState().cards.card1).toBeDefined();
420
+ });
421
+
422
+ it("should handle serialization with automatic transitions (endIf)", () => {
423
+ const flow: FlowDefinition<GameState> = {
424
+ gameSegments: {
425
+ mainGame: {
426
+ order: 1,
427
+ turn: {
428
+ phases: {
429
+ waiting: {
430
+ order: 0,
431
+ next: "ready",
432
+ endIf: (context) => {
433
+ // Auto-transition when all players ready
434
+ return context.state.players.every((p) => p.score > 0);
435
+ },
436
+ onBegin: (context) => {
437
+ context.state.log.push("waiting-for-players");
438
+ },
439
+ },
440
+ ready: {
441
+ order: 1,
442
+ next: undefined,
443
+ onBegin: (context) => {
444
+ context.state.log.push("all-players-ready");
445
+ },
446
+ },
447
+ },
448
+ },
449
+ },
450
+ },
451
+ };
452
+
453
+ const initialState: GameState = {
454
+ currentPlayer: 0,
455
+ players: [
456
+ { id: "p1", name: "Alice", score: 0 },
457
+ { id: "p2", name: "Bob", score: 0 },
458
+ ],
459
+ turnCount: 0,
460
+ log: [],
461
+ };
462
+
463
+ const manager = new FlowManager(flow, initialState);
464
+
465
+ expect(manager.getCurrentPhase()).toBe("waiting");
466
+
467
+ // Trigger state change that will cause endIf to activate
468
+ manager.updateState((draft) => {
469
+ draft.players[0].score = 10;
470
+ draft.players[1].score = 15;
471
+ });
472
+
473
+ // endIf should have triggered automatic transition
474
+ expect(manager.getCurrentPhase()).toBe("ready");
475
+
476
+ // Serialize after automatic transition
477
+ const snapshot = {
478
+ game: manager.getGameState(),
479
+ flow: manager.serializeFlowState(),
480
+ };
481
+
482
+ const serialized = JSON.stringify(snapshot);
483
+ const restored = JSON.parse(serialized);
484
+
485
+ // Verify state after automatic transition was preserved
486
+ expect(restored.flow.currentPhase).toBe("ready");
487
+ expect(restored.game.log).toContain("waiting-for-players");
488
+ expect(restored.game.log).toContain("all-players-ready");
489
+ expect(restored.game.players[0].score).toBe(10);
490
+ });
491
+
492
+ it("should validate that FlowManager state is fully reconstructible", () => {
493
+ // This test verifies that we can reconstruct a FlowManager
494
+ // with the exact same state and continue from any point
495
+
496
+ const flow: FlowDefinition<GameState> = {
497
+ gameSegments: {
498
+ mainGame: {
499
+ order: 1,
500
+ turn: {
501
+ onBegin: (context) => {
502
+ context.state.turnCount += 1;
503
+ },
504
+ phases: {
505
+ phase1: {
506
+ order: 0,
507
+ next: "phase2",
508
+ steps: {
509
+ step1: { order: 0, next: "step2" },
510
+ step2: { order: 1, next: undefined },
511
+ },
512
+ },
513
+ phase2: {
514
+ order: 1,
515
+ next: undefined,
516
+ },
517
+ },
518
+ },
519
+ },
520
+ },
521
+ };
522
+
523
+ const initialState: GameState = {
524
+ currentPlayer: 0,
525
+ players: [{ id: "p1", name: "Alice", score: 0 }],
526
+ turnCount: 0,
527
+ log: [],
528
+ };
529
+
530
+ // Original manager at specific state
531
+ const original = new FlowManager(flow, initialState);
532
+ original.nextStep(); // phase1.step1 → phase1.step2
533
+
534
+ // Capture full state
535
+ const fullState = {
536
+ gameState: original.getGameState(),
537
+ flowState: original.serializeFlowState(),
538
+ };
539
+
540
+ // Serialize and deserialize
541
+ const serialized = JSON.stringify(fullState);
542
+ const deserialized = JSON.parse(serialized);
543
+
544
+ // Create new manager from deserialized state
545
+ const reconstructed = new FlowManager(flow, deserialized.gameState, {
546
+ restoreFrom: deserialized.flowState,
547
+ });
548
+
549
+ // Verify reconstruction is accurate
550
+ expect(reconstructed.getGameState()).toEqual(original.getGameState());
551
+
552
+ // Both managers should be able to continue identically
553
+ original.nextStep(); // phase1.step2 → phase2
554
+ reconstructed.nextStep(); // phase1.step2 → phase2
555
+
556
+ const origPhase = original.getCurrentPhase();
557
+ const origStep = original.getCurrentStep();
558
+ if (origPhase) {
559
+ expect(reconstructed.getCurrentPhase()).toBe(origPhase);
560
+ }
561
+ if (origStep) {
562
+ expect(reconstructed.getCurrentStep()).toBe(origStep);
563
+ }
564
+ });
565
+ });