@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,571 @@
1
+ /**
2
+ * MultiplayerEngine Usage Examples
3
+ *
4
+ * This file demonstrates how to use the MultiplayerEngine for
5
+ * server-authoritative multiplayer gameplay with network synchronization.
6
+ */
7
+
8
+ import type { Patch } from "immer";
9
+ import type { GameDefinition } from "../game-definition/game-definition";
10
+ import type { GameMoveDefinitions } from "../game-definition/move-definitions";
11
+ import { createPlayerId } from "../types";
12
+ import { MultiplayerEngine } from "./multiplayer-engine";
13
+
14
+ // ============================================================================
15
+ // Example 1: Basic Server Setup
16
+ // ============================================================================
17
+
18
+ type CardGameState = {
19
+ players: Array<{
20
+ id: string;
21
+ name: string;
22
+ hand: string[];
23
+ deck: string[];
24
+ score: number;
25
+ }>;
26
+ currentPlayerIndex: number;
27
+ turnNumber: number;
28
+ };
29
+
30
+ type CardGameMoves = {
31
+ drawCard: Record<string, never>;
32
+ playCard: { cardId: string };
33
+ endTurn: Record<string, never>;
34
+ };
35
+
36
+ function createCardGame(): GameDefinition<CardGameState, CardGameMoves> {
37
+ const moves: GameMoveDefinitions<CardGameState, CardGameMoves> = {
38
+ drawCard: {
39
+ condition: (state) => {
40
+ const player = state.players[state.currentPlayerIndex];
41
+ return player ? player.deck.length > 0 : false;
42
+ },
43
+ reducer: (draft) => {
44
+ const player = draft.players[draft.currentPlayerIndex];
45
+ if (player) {
46
+ const card = player.deck.pop();
47
+ if (card) {
48
+ player.hand.push(card);
49
+ }
50
+ }
51
+ },
52
+ },
53
+ playCard: {
54
+ condition: (state, context) => {
55
+ const player = state.players[state.currentPlayerIndex];
56
+ return player
57
+ ? player.hand.includes(context.params?.cardId as string)
58
+ : false;
59
+ },
60
+ reducer: (draft, context) => {
61
+ const player = draft.players[draft.currentPlayerIndex];
62
+ if (player && context.params?.cardId) {
63
+ const cardId = context.params.cardId as string;
64
+ const index = player.hand.indexOf(cardId);
65
+ if (index >= 0) {
66
+ player.hand.splice(index, 1);
67
+ player.score += 1;
68
+ }
69
+ }
70
+ },
71
+ },
72
+ endTurn: {
73
+ reducer: (draft) => {
74
+ draft.currentPlayerIndex =
75
+ (draft.currentPlayerIndex + 1) % draft.players.length;
76
+ draft.turnNumber += 1;
77
+ },
78
+ },
79
+ };
80
+
81
+ return {
82
+ name: "Example Card Game",
83
+ setup: (players) => ({
84
+ players: players.map((p) => ({
85
+ id: p.id,
86
+ name: p.name || "Player",
87
+ hand: [],
88
+ deck: Array.from({ length: 20 }, (_, i) => `card-${i + 1}`),
89
+ score: 0,
90
+ })),
91
+ currentPlayerIndex: 0,
92
+ turnNumber: 1,
93
+ }),
94
+ moves,
95
+ endIf: (state) => {
96
+ const winner = state.players.find((p) => p.score >= 10);
97
+ return winner
98
+ ? { winner: winner.id, reason: "First to 10 points" }
99
+ : undefined;
100
+ },
101
+ };
102
+ }
103
+
104
+ // ============================================================================
105
+ // Example 2: Server with WebSocket Broadcasting
106
+ // ============================================================================
107
+
108
+ /**
109
+ * Example server implementation using WebSockets
110
+ *
111
+ * This shows how to integrate MultiplayerEngine with a real network layer.
112
+ */
113
+ class GameServer {
114
+ private engine: MultiplayerEngine<CardGameState, CardGameMoves>;
115
+ private clients: Map<string, any> = new Map(); // WebSocket clients
116
+
117
+ constructor() {
118
+ const gameDefinition = createCardGame();
119
+ const players = [
120
+ { id: createPlayerId("p1"), name: "Alice" },
121
+ { id: createPlayerId("p2"), name: "Bob" },
122
+ ];
123
+
124
+ this.engine = new MultiplayerEngine(gameDefinition, players, {
125
+ mode: "server",
126
+ seed: "game-123-seed",
127
+ onPatchBroadcast: (broadcast) => {
128
+ // Broadcast patches to all connected clients
129
+ this.broadcastToAllClients({
130
+ type: "PATCH_UPDATE",
131
+ patches: broadcast.patches,
132
+ historyIndex: broadcast.historyIndex,
133
+ moveId: broadcast.moveId,
134
+ });
135
+
136
+ console.log(
137
+ `[Server] Move ${broadcast.moveId} executed, broadcasting ${broadcast.patches.length} patches`,
138
+ );
139
+ },
140
+ onMoveRejected: (moveId, error, errorCode) => {
141
+ console.error(
142
+ `[Server] Move ${moveId} rejected: ${error} (${errorCode})`,
143
+ );
144
+ },
145
+ });
146
+ }
147
+
148
+ handleClientConnection(clientId: string, websocket: any) {
149
+ console.log(`[Server] Client ${clientId} connected`);
150
+
151
+ // Register client
152
+ this.clients.set(clientId, websocket);
153
+ this.engine.registerClient(clientId);
154
+
155
+ // Send initial state
156
+ websocket.send(
157
+ JSON.stringify({
158
+ type: "INITIAL_STATE",
159
+ state: this.engine.getState(),
160
+ historyIndex: this.engine.getCurrentHistoryIndex(),
161
+ }),
162
+ );
163
+ }
164
+
165
+ handleClientReconnection(clientId: string, lastKnownIndex: number) {
166
+ console.log(
167
+ `[Server] Client ${clientId} reconnecting from ${lastKnownIndex}`,
168
+ );
169
+
170
+ const client = this.engine.getClientState(clientId);
171
+ if (client) {
172
+ // Client reconnecting - send catchup patches
173
+ const catchupPatches = this.engine.getCatchupPatches(lastKnownIndex + 1);
174
+
175
+ return {
176
+ type: "CATCHUP",
177
+ patches: catchupPatches,
178
+ currentIndex: this.engine.getCurrentHistoryIndex(),
179
+ };
180
+ }
181
+
182
+ // New client - send full state
183
+ return {
184
+ type: "INITIAL_STATE",
185
+ state: this.engine.getState(),
186
+ historyIndex: this.engine.getCurrentHistoryIndex(),
187
+ };
188
+ }
189
+
190
+ handleClientMove(clientId: string, moveId: string, params: any) {
191
+ console.log(`[Server] Client ${clientId} attempting move ${moveId}`);
192
+
193
+ const result = this.engine.executeMove(moveId, {
194
+ playerId: createPlayerId(clientId),
195
+ params,
196
+ });
197
+
198
+ if (!result.success) {
199
+ // Send error back to client
200
+ const client = this.clients.get(clientId);
201
+ if (client) {
202
+ client.send(
203
+ JSON.stringify({
204
+ type: "MOVE_ERROR",
205
+ moveId,
206
+ error: result.error,
207
+ errorCode: result.errorCode,
208
+ }),
209
+ );
210
+ }
211
+ }
212
+
213
+ // On success, patches are automatically broadcast via onPatchBroadcast callback
214
+
215
+ return result;
216
+ }
217
+
218
+ handleClientDisconnection(clientId: string) {
219
+ console.log(`[Server] Client ${clientId} disconnected`);
220
+
221
+ this.clients.delete(clientId);
222
+ this.engine.unregisterClient(clientId);
223
+ }
224
+
225
+ private broadcastToAllClients(message: any) {
226
+ const json = JSON.stringify(message);
227
+ for (const [clientId, websocket] of this.clients.entries()) {
228
+ try {
229
+ websocket.send(json);
230
+ } catch (error) {
231
+ console.error(`[Server] Failed to send to ${clientId}:`, error);
232
+ }
233
+ }
234
+ }
235
+
236
+ getGameState() {
237
+ return this.engine.getState();
238
+ }
239
+
240
+ checkGameEnd() {
241
+ return this.engine.checkGameEnd();
242
+ }
243
+ }
244
+
245
+ // ============================================================================
246
+ // Example 3: Client with Network Synchronization
247
+ // ============================================================================
248
+
249
+ /**
250
+ * Example client implementation
251
+ *
252
+ * This shows how to integrate MultiplayerEngine on the client side.
253
+ */
254
+ class GameClient {
255
+ private engine: MultiplayerEngine<CardGameState, CardGameMoves>;
256
+ private websocket?: any;
257
+ private playerId: string;
258
+ private lastSyncedIndex = -1;
259
+
260
+ constructor(playerId: string) {
261
+ this.playerId = playerId;
262
+
263
+ const gameDefinition = createCardGame();
264
+ const players = [
265
+ { id: createPlayerId("p1"), name: "Alice" },
266
+ { id: createPlayerId("p2"), name: "Bob" },
267
+ ];
268
+
269
+ this.engine = new MultiplayerEngine(gameDefinition, players, {
270
+ mode: "client",
271
+ onPatchesApplied: (patches) => {
272
+ console.log(`[Client] Applied ${patches.length} patches from server`);
273
+
274
+ // Update UI after state changes
275
+ this.updateUI();
276
+ },
277
+ });
278
+ }
279
+
280
+ connect(websocket: any) {
281
+ this.websocket = websocket;
282
+
283
+ // Setup message handlers
284
+ websocket.on("message", (data: string) => {
285
+ const message = JSON.parse(data);
286
+
287
+ switch (message.type) {
288
+ case "INITIAL_STATE":
289
+ this.handleInitialState(message.state, message.historyIndex);
290
+ break;
291
+
292
+ case "PATCH_UPDATE":
293
+ this.handlePatchUpdate(message.patches, message.historyIndex);
294
+ break;
295
+
296
+ case "CATCHUP":
297
+ this.handleCatchup(message.patches, message.currentIndex);
298
+ break;
299
+
300
+ case "MOVE_ERROR":
301
+ this.handleMoveError(
302
+ message.moveId,
303
+ message.error,
304
+ message.errorCode,
305
+ );
306
+ break;
307
+ }
308
+ });
309
+ }
310
+
311
+ private handleInitialState(state: CardGameState, historyIndex: number) {
312
+ console.log(`[Client] Received initial state at index ${historyIndex}`);
313
+
314
+ // Note: For initial state, we might need to fully replace the state
315
+ // This is a simplified example - production code might use a different approach
316
+ this.lastSyncedIndex = historyIndex;
317
+ this.updateUI();
318
+ }
319
+
320
+ private handlePatchUpdate(patches: Patch[], historyIndex: number) {
321
+ console.log(`[Client] Received patch update for index ${historyIndex}`);
322
+
323
+ this.engine.applyServerPatches(patches);
324
+ this.lastSyncedIndex = historyIndex;
325
+ }
326
+
327
+ private handleCatchup(patches: Patch[], currentIndex: number) {
328
+ console.log(
329
+ `[Client] Catching up from ${this.lastSyncedIndex} to ${currentIndex}`,
330
+ );
331
+
332
+ if (patches.length > 0) {
333
+ this.engine.applyServerPatches(patches);
334
+ }
335
+
336
+ this.lastSyncedIndex = currentIndex;
337
+ }
338
+
339
+ private handleMoveError(moveId: string, error: string, errorCode?: string) {
340
+ console.error(`[Client] Move ${moveId} rejected: ${error} (${errorCode})`);
341
+
342
+ // Show error to user
343
+ this.showError(`Cannot ${moveId}: ${error}`);
344
+ }
345
+
346
+ // Client-side move request (sends to server)
347
+ requestMove(moveId: string, params?: any) {
348
+ if (!this.websocket) {
349
+ console.error("[Client] Not connected to server");
350
+ return;
351
+ }
352
+
353
+ // Optional: Check if move is valid before sending to server
354
+ // This provides immediate UI feedback
355
+ const canExecute = this.engine.canExecuteMove(moveId, {
356
+ playerId: createPlayerId(this.playerId),
357
+ params,
358
+ });
359
+
360
+ if (!canExecute) {
361
+ this.showError(`Move ${moveId} is not valid right now`);
362
+ return;
363
+ }
364
+
365
+ // Send move request to server
366
+ this.websocket.send(
367
+ JSON.stringify({
368
+ type: "MOVE",
369
+ moveId,
370
+ params,
371
+ }),
372
+ );
373
+
374
+ console.log(`[Client] Sent move request: ${moveId}`);
375
+ }
376
+
377
+ // Get current game state
378
+ getState() {
379
+ return this.engine.getState();
380
+ }
381
+
382
+ // Get player-specific view (hides opponent's hand, etc.)
383
+ getPlayerView() {
384
+ return this.engine.getPlayerView(this.playerId);
385
+ }
386
+
387
+ // Get valid moves for UI (enable/disable buttons)
388
+ getValidMoves() {
389
+ return this.engine.getValidMoves(createPlayerId(this.playerId));
390
+ }
391
+
392
+ // Check if game has ended
393
+ checkGameEnd() {
394
+ return this.engine.checkGameEnd();
395
+ }
396
+
397
+ private updateUI() {
398
+ // Update game UI with new state
399
+ const state = this.getPlayerView();
400
+ console.log("[Client] UI Updated:", state);
401
+ }
402
+
403
+ private showError(message: string) {
404
+ console.error("[Client] Error:", message);
405
+ // Show error in UI
406
+ }
407
+
408
+ disconnect() {
409
+ if (this.websocket) {
410
+ this.websocket.close();
411
+ this.websocket = undefined;
412
+ }
413
+ }
414
+ }
415
+
416
+ // ============================================================================
417
+ // Example 4: Testing/Simulation
418
+ // ============================================================================
419
+
420
+ /**
421
+ * Example usage for testing or local simulation
422
+ */
423
+ function simulateMultiplayerGame() {
424
+ console.log("=== Simulating Multiplayer Game ===\n");
425
+
426
+ // Create server
427
+ const gameDefinition = createCardGame();
428
+ const players = [
429
+ { id: createPlayerId("p1"), name: "Alice" },
430
+ { id: createPlayerId("p2"), name: "Bob" },
431
+ ];
432
+
433
+ const server = new MultiplayerEngine(gameDefinition, players, {
434
+ mode: "server",
435
+ seed: "simulation-seed",
436
+ onPatchBroadcast: (broadcast) => {
437
+ console.log(
438
+ `[Server] Broadcasting move ${broadcast.moveId} (${broadcast.patches.length} patches)`,
439
+ );
440
+
441
+ // Simulate network broadcast to clients
442
+ client1.applyServerPatches(broadcast.patches);
443
+ client2.applyServerPatches(broadcast.patches);
444
+ },
445
+ });
446
+
447
+ // Create clients
448
+ const client1 = new MultiplayerEngine(gameDefinition, players, {
449
+ mode: "client",
450
+ onPatchesApplied: (patches) => {
451
+ console.log(`[Client 1] Synced (${patches.length} patches)`);
452
+ },
453
+ });
454
+
455
+ const client2 = new MultiplayerEngine(gameDefinition, players, {
456
+ mode: "client",
457
+ onPatchesApplied: (patches) => {
458
+ console.log(`[Client 2] Synced (${patches.length} patches)`);
459
+ },
460
+ });
461
+
462
+ // Simulate game flow
463
+ console.log("\nPlayer 1 draws a card:");
464
+ server.executeMove("drawCard", {
465
+ playerId: createPlayerId("p1"),
466
+ params: {},
467
+ });
468
+
469
+ console.log("\nPlayer 1 plays a card:");
470
+ server.executeMove("playCard", {
471
+ playerId: createPlayerId("p1"),
472
+ params: { cardId: "card-20" },
473
+ });
474
+
475
+ console.log("\nPlayer 1 ends turn:");
476
+ server.executeMove("endTurn", { playerId: createPlayerId("p1"), params: {} });
477
+
478
+ console.log("\nVerifying all clients are synchronized:");
479
+ const serverState = server.getState();
480
+ const client1State = client1.getState();
481
+ const client2State = client2.getState();
482
+
483
+ console.log("Server state matches Client 1:", serverState === client1State);
484
+ console.log("Server state matches Client 2:", serverState === client2State);
485
+
486
+ console.log("\nFinal game state:");
487
+ console.log("Turn number:", serverState.turnNumber);
488
+ console.log("Current player:", serverState.currentPlayerIndex);
489
+ console.log(
490
+ "Player 1 score:",
491
+ serverState.players[0]?.score,
492
+ "(hand size:",
493
+ serverState.players[0]?.hand.length,
494
+ ")",
495
+ );
496
+ }
497
+
498
+ // ============================================================================
499
+ // Example 5: Reconnection Handling
500
+ // ============================================================================
501
+
502
+ function demonstrateReconnection() {
503
+ console.log("\n=== Demonstrating Client Reconnection ===\n");
504
+
505
+ const gameDefinition = createCardGame();
506
+ const players = [
507
+ { id: createPlayerId("p1"), name: "Alice" },
508
+ { id: createPlayerId("p2"), name: "Bob" },
509
+ ];
510
+
511
+ // Create server
512
+ const server = new MultiplayerEngine(gameDefinition, players, {
513
+ mode: "server",
514
+ onPatchBroadcast: (broadcast) => {
515
+ console.log(`[Server] Broadcast at index ${broadcast.historyIndex}`);
516
+ },
517
+ });
518
+
519
+ // Execute some moves while client is disconnected
520
+ console.log("Executing moves while client is offline:");
521
+ server.executeMove("drawCard", {
522
+ playerId: createPlayerId("p1"),
523
+ params: {},
524
+ });
525
+ server.executeMove("endTurn", { playerId: createPlayerId("p1"), params: {} });
526
+ server.executeMove("drawCard", {
527
+ playerId: createPlayerId("p2"),
528
+ params: {},
529
+ });
530
+
531
+ const serverState = server.getState();
532
+ console.log("Server state:", {
533
+ turn: serverState.turnNumber,
534
+ currentPlayer: serverState.currentPlayerIndex,
535
+ });
536
+
537
+ // Client reconnects
538
+ console.log("\nClient reconnecting...");
539
+ const reconnectedClient = new MultiplayerEngine(gameDefinition, players, {
540
+ mode: "client",
541
+ });
542
+
543
+ // Get catchup patches
544
+ const catchupPatches = server.getCatchupPatches(0);
545
+ console.log(`Sending ${catchupPatches.length} catchup patches to client`);
546
+
547
+ // Apply patches
548
+ reconnectedClient.applyServerPatches(catchupPatches);
549
+
550
+ const clientState = reconnectedClient.getState();
551
+ console.log("Client state after catchup:", {
552
+ turn: clientState.turnNumber,
553
+ currentPlayer: clientState.currentPlayerIndex,
554
+ });
555
+
556
+ console.log(
557
+ "States match:",
558
+ JSON.stringify(serverState) === JSON.stringify(clientState),
559
+ );
560
+ }
561
+
562
+ // Uncomment to run simulations:
563
+ // simulateMultiplayerGame();
564
+ // demonstrateReconnection();
565
+
566
+ export {
567
+ GameServer,
568
+ GameClient,
569
+ simulateMultiplayerGame,
570
+ demonstrateReconnection,
571
+ };