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