@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,428 @@
|
|
|
1
|
+
# Flow State Serialization
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The FlowManager supports full serialization and deserialization of game state for persistence and replay functionality. This enables:
|
|
6
|
+
|
|
7
|
+
- **Game Saving**: Store complete game state to database
|
|
8
|
+
- **Game Loading**: Restore and continue from any saved point
|
|
9
|
+
- **Replay System**: Step through game history
|
|
10
|
+
- **Cross-session Play**: Players can resume games later
|
|
11
|
+
|
|
12
|
+
## Core Concepts
|
|
13
|
+
|
|
14
|
+
### Serializable State
|
|
15
|
+
|
|
16
|
+
All flow state is JSON-serializable:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
type SerializedFlowState = {
|
|
20
|
+
currentPhase?: string;
|
|
21
|
+
currentSegment?: string;
|
|
22
|
+
turnNumber: number;
|
|
23
|
+
currentPlayer: string;
|
|
24
|
+
};
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Restoration Options
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
type FlowManagerOptions = {
|
|
31
|
+
/** Skip initialization hooks when restoring */
|
|
32
|
+
skipInitialization?: boolean;
|
|
33
|
+
/** Restore from serialized flow state */
|
|
34
|
+
restoreFrom?: SerializedFlowState;
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage Examples
|
|
39
|
+
|
|
40
|
+
### Basic Save/Load Pattern
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { FlowManager, type FlowDefinition, type SerializedFlowState } from "@drmxrcy/tcg-core/flow";
|
|
44
|
+
|
|
45
|
+
// Define your game flow
|
|
46
|
+
const flow: FlowDefinition<GameState> = {
|
|
47
|
+
turn: {
|
|
48
|
+
phases: {
|
|
49
|
+
ready: { order: 0, next: "draw" },
|
|
50
|
+
draw: { order: 1, next: "main" },
|
|
51
|
+
main: { order: 2, next: "end" },
|
|
52
|
+
end: { order: 3, next: undefined },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Play game
|
|
58
|
+
const manager = new FlowManager(flow, initialState);
|
|
59
|
+
manager.nextPhase(); // Progress game
|
|
60
|
+
|
|
61
|
+
// === Save to database ===
|
|
62
|
+
const saveData = {
|
|
63
|
+
gameState: manager.getGameState(),
|
|
64
|
+
flowState: manager.serializeFlowState(),
|
|
65
|
+
timestamp: Date.now(),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
await db.games.insert(saveData);
|
|
69
|
+
|
|
70
|
+
// === Later: Load from database ===
|
|
71
|
+
const loaded = await db.games.findById(gameId);
|
|
72
|
+
|
|
73
|
+
// Restore exact state
|
|
74
|
+
const restored = new FlowManager(flow, loaded.gameState, {
|
|
75
|
+
restoreFrom: loaded.flowState,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Continue playing from where you left off
|
|
79
|
+
restored.nextPhase();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Database Schema Example
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
type SavedGame = {
|
|
86
|
+
id: string;
|
|
87
|
+
gameState: GameState; // Your game-specific state
|
|
88
|
+
flowState: SerializedFlowState; // Flow position
|
|
89
|
+
players: string[];
|
|
90
|
+
createdAt: Date;
|
|
91
|
+
updatedAt: Date;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
async function saveGame(manager: FlowManager<GameState>): Promise<string> {
|
|
95
|
+
const saveData: SavedGame = {
|
|
96
|
+
id: generateId(),
|
|
97
|
+
gameState: manager.getGameState(),
|
|
98
|
+
flowState: manager.serializeFlowState(),
|
|
99
|
+
players: /* extract from state */,
|
|
100
|
+
createdAt: new Date(),
|
|
101
|
+
updatedAt: new Date(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
await db.collection('games').insertOne(saveData);
|
|
105
|
+
return saveData.id;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function loadGame(gameId: string): Promise<FlowManager<GameState>> {
|
|
109
|
+
const saved = await db.collection('games').findOne({ id: gameId });
|
|
110
|
+
|
|
111
|
+
if (!saved) throw new Error('Game not found');
|
|
112
|
+
|
|
113
|
+
return new FlowManager(gameFlow, saved.gameState, {
|
|
114
|
+
restoreFrom: saved.flowState,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Replay System
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
type GameSnapshot = {
|
|
123
|
+
gameState: GameState;
|
|
124
|
+
flowState: SerializedFlowState;
|
|
125
|
+
moveNumber: number;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
class ReplaySystem {
|
|
129
|
+
private snapshots: GameSnapshot[] = [];
|
|
130
|
+
|
|
131
|
+
// Record snapshot after each move
|
|
132
|
+
recordSnapshot(manager: FlowManager<GameState>) {
|
|
133
|
+
this.snapshots.push({
|
|
134
|
+
gameState: manager.getGameState(),
|
|
135
|
+
flowState: manager.serializeFlowState(),
|
|
136
|
+
moveNumber: this.snapshots.length,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Jump to specific move
|
|
141
|
+
jumpToMove(moveNumber: number): FlowManager<GameState> {
|
|
142
|
+
const snapshot = this.snapshots[moveNumber];
|
|
143
|
+
if (!snapshot) throw new Error('Snapshot not found');
|
|
144
|
+
|
|
145
|
+
return new FlowManager(gameFlow, snapshot.gameState, {
|
|
146
|
+
restoreFrom: snapshot.flowState,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Step forward/backward
|
|
151
|
+
stepForward(current: number) { return this.jumpToMove(current + 1); }
|
|
152
|
+
stepBackward(current: number) { return this.jumpToMove(current - 1); }
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Network Synchronization
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// Server: Send snapshot to clients
|
|
160
|
+
socket.on('requestGameState', async (gameId) => {
|
|
161
|
+
const manager = activeGames.get(gameId);
|
|
162
|
+
|
|
163
|
+
socket.emit('gameState', {
|
|
164
|
+
game: manager.getGameState(),
|
|
165
|
+
flow: manager.serializeFlowState(),
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Client: Receive and restore
|
|
170
|
+
socket.on('gameState', (snapshot) => {
|
|
171
|
+
const manager = new FlowManager(gameFlow, snapshot.game, {
|
|
172
|
+
restoreFrom: snapshot.flow,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Client now has exact game state
|
|
176
|
+
renderGame(manager);
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Spectator Mode
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
async function createSpectatorView(gameId: string): Promise<FlowManager<GameState>> {
|
|
184
|
+
const liveGame = await db.games.findById(gameId);
|
|
185
|
+
|
|
186
|
+
// Spectators get read-only copy at current state
|
|
187
|
+
return new FlowManager(gameFlow, liveGame.gameState, {
|
|
188
|
+
restoreFrom: liveGame.flowState,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Auto-save Every N Turns
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
class AutoSaveManager {
|
|
197
|
+
private saveInterval = 5; // Save every 5 turns
|
|
198
|
+
|
|
199
|
+
async checkAndSave(manager: FlowManager<GameState>, gameId: string) {
|
|
200
|
+
const flowState = manager.serializeFlowState();
|
|
201
|
+
|
|
202
|
+
if (flowState.turnNumber % this.saveInterval === 0) {
|
|
203
|
+
await this.saveGame(gameId, manager);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async saveGame(gameId: string, manager: FlowManager<GameState>) {
|
|
208
|
+
await db.games.update(gameId, {
|
|
209
|
+
gameState: manager.getGameState(),
|
|
210
|
+
flowState: manager.serializeFlowState(),
|
|
211
|
+
updatedAt: new Date(),
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Important Considerations
|
|
218
|
+
|
|
219
|
+
### 1. Hooks Are Not Re-executed on Restore
|
|
220
|
+
|
|
221
|
+
When restoring from serialized state, lifecycle hooks (`onBegin`, `onEnd`) are **not** executed. This is intentional because:
|
|
222
|
+
|
|
223
|
+
- The hooks already executed in the original game session
|
|
224
|
+
- Their effects are already in the game state
|
|
225
|
+
- Re-executing would duplicate side effects
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
// Original game - hooks execute
|
|
229
|
+
const original = new FlowManager(flow, state);
|
|
230
|
+
// onBegin hooks run, modify state
|
|
231
|
+
|
|
232
|
+
// Save
|
|
233
|
+
const saved = {
|
|
234
|
+
game: original.getGameState(), // Contains hook effects
|
|
235
|
+
flow: original.serializeFlowState(),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// Restore - hooks DON'T re-execute
|
|
239
|
+
const restored = new FlowManager(flow, saved.game, {
|
|
240
|
+
restoreFrom: saved.flow, // Skips initialization
|
|
241
|
+
});
|
|
242
|
+
// State already contains all hook effects
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### 2. Phase/Segment Position Preserved
|
|
246
|
+
|
|
247
|
+
The exact flow position is maintained:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Original at main phase, damage segment
|
|
251
|
+
manager.getCurrentPhase(); // "main"
|
|
252
|
+
manager.getCurrentSegment(); // "damage"
|
|
253
|
+
|
|
254
|
+
// After save/restore
|
|
255
|
+
restored.getCurrentPhase(); // "main"
|
|
256
|
+
restored.getCurrentSegment(); // "damage"
|
|
257
|
+
|
|
258
|
+
// Can continue exactly where left off
|
|
259
|
+
restored.nextSegment();
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 3. All Game State Must Be Serializable
|
|
263
|
+
|
|
264
|
+
Ensure your game state only contains JSON-serializable data:
|
|
265
|
+
|
|
266
|
+
✅ **Serializable**:
|
|
267
|
+
- Primitives (string, number, boolean)
|
|
268
|
+
- Plain objects
|
|
269
|
+
- Arrays
|
|
270
|
+
- null
|
|
271
|
+
|
|
272
|
+
❌ **Not Serializable**:
|
|
273
|
+
- Functions
|
|
274
|
+
- Dates (convert to timestamp)
|
|
275
|
+
- Maps/Sets (convert to arrays)
|
|
276
|
+
- Class instances (use plain objects)
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
// ❌ Bad
|
|
280
|
+
type BadGameState = {
|
|
281
|
+
createdAt: Date; // Loses type on serialize
|
|
282
|
+
players: Map<string, Player>; // Not serializable
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// ✅ Good
|
|
286
|
+
type GoodGameState = {
|
|
287
|
+
createdAt: number; // Unix timestamp
|
|
288
|
+
players: Record<string, Player>; // Plain object
|
|
289
|
+
};
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 4. Flow Definition Must Match
|
|
293
|
+
|
|
294
|
+
The same `FlowDefinition` must be used when restoring:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// ❌ Won't work correctly
|
|
298
|
+
const v1Flow = { /* old definition */ };
|
|
299
|
+
const v2Flow = { /* updated definition */ };
|
|
300
|
+
|
|
301
|
+
const manager = new FlowManager(v1Flow, state);
|
|
302
|
+
const saved = manager.serializeFlowState();
|
|
303
|
+
|
|
304
|
+
// Phase names might not exist in v2
|
|
305
|
+
const restored = new FlowManager(v2Flow, state, {
|
|
306
|
+
restoreFrom: saved, // Mismatch!
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// ✅ Use matching definition
|
|
310
|
+
const restored = new FlowManager(v1Flow, state, {
|
|
311
|
+
restoreFrom: saved,
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Best Practices
|
|
316
|
+
|
|
317
|
+
### 1. Version Your Saves
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
type VersionedSave = {
|
|
321
|
+
version: number;
|
|
322
|
+
gameState: GameState;
|
|
323
|
+
flowState: SerializedFlowState;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
async function saveGameWithVersion(manager: FlowManager<GameState>) {
|
|
327
|
+
const save: VersionedSave = {
|
|
328
|
+
version: CURRENT_GAME_VERSION,
|
|
329
|
+
gameState: manager.getGameState(),
|
|
330
|
+
flowState: manager.serializeFlowState(),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
await db.games.insert(save);
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### 2. Validate Before Restore
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
function validateSavedGame(save: any): save is SavedGame {
|
|
341
|
+
return (
|
|
342
|
+
typeof save.gameState === 'object' &&
|
|
343
|
+
typeof save.flowState === 'object' &&
|
|
344
|
+
typeof save.flowState.turnNumber === 'number'
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function safeLoadGame(gameId: string) {
|
|
349
|
+
const saved = await db.games.findById(gameId);
|
|
350
|
+
|
|
351
|
+
if (!validateSavedGame(saved)) {
|
|
352
|
+
throw new Error('Invalid save data');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return new FlowManager(gameFlow, saved.gameState, {
|
|
356
|
+
restoreFrom: saved.flowState,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### 3. Compress Large States
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import { compress, decompress } from 'lz-string';
|
|
365
|
+
|
|
366
|
+
async function saveCompressed(manager: FlowManager<GameState>) {
|
|
367
|
+
const data = {
|
|
368
|
+
game: manager.getGameState(),
|
|
369
|
+
flow: manager.serializeFlowState(),
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const compressed = compress(JSON.stringify(data));
|
|
373
|
+
await db.games.insert({ id, data: compressed });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function loadCompressed(gameId: string) {
|
|
377
|
+
const saved = await db.games.findById(gameId);
|
|
378
|
+
const data = JSON.parse(decompress(saved.data));
|
|
379
|
+
|
|
380
|
+
return new FlowManager(gameFlow, data.game, {
|
|
381
|
+
restoreFrom: data.flow,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## Testing Serialization
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
import { describe, it, expect } from 'bun:test';
|
|
390
|
+
|
|
391
|
+
describe('Game Serialization', () => {
|
|
392
|
+
it('should survive round-trip serialization', () => {
|
|
393
|
+
const original = new FlowManager(flow, initialState);
|
|
394
|
+
original.nextPhase();
|
|
395
|
+
original.nextPhase();
|
|
396
|
+
|
|
397
|
+
// Serialize
|
|
398
|
+
const serialized = JSON.stringify({
|
|
399
|
+
game: original.getGameState(),
|
|
400
|
+
flow: original.serializeFlowState(),
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Deserialize
|
|
404
|
+
const data = JSON.parse(serialized);
|
|
405
|
+
const restored = new FlowManager(flow, data.game, {
|
|
406
|
+
restoreFrom: data.flow,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Verify exact match
|
|
410
|
+
expect(restored.getGameState()).toEqual(original.getGameState());
|
|
411
|
+
expect(restored.getCurrentPhase()).toBe(original.getCurrentPhase());
|
|
412
|
+
expect(restored.getCurrentSegment()).toBe(original.getCurrentSegment());
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Performance Considerations
|
|
418
|
+
|
|
419
|
+
- **Serialization**: O(n) where n is state size
|
|
420
|
+
- **Deserialization**: O(n) for parsing + O(1) for flow restoration
|
|
421
|
+
- **Memory**: Keep snapshots minimal (delta compression for large histories)
|
|
422
|
+
- **Database**: Index on `gameId`, `updatedAt` for fast lookups
|
|
423
|
+
|
|
424
|
+
## See Also
|
|
425
|
+
|
|
426
|
+
- [Flow Definition Guide](./flow-definition.ts)
|
|
427
|
+
- [Flow Manager API](./flow-manager.ts)
|
|
428
|
+
- [Serialization Tests](./flow/__tests__/flow-serialization.test.ts)
|