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