@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,51 @@
|
|
|
1
|
+
import type { CardId } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Modifier type definitions
|
|
5
|
+
* - stat: Modifies numerical stats (power, toughness, cost, etc.)
|
|
6
|
+
* - ability: Grants or removes abilities
|
|
7
|
+
* - type: Changes card types or subtypes
|
|
8
|
+
* - keyword: Grants keywords (flying, haste, trample, etc.)
|
|
9
|
+
*/
|
|
10
|
+
export type ModifierType = "stat" | "ability" | "type" | "keyword";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Modifier duration options
|
|
14
|
+
* - permanent: Lasts indefinitely
|
|
15
|
+
* - until-end-of-turn: Expires at end of turn
|
|
16
|
+
* - while-condition: Active only while condition is true
|
|
17
|
+
*/
|
|
18
|
+
export type ModifierDuration =
|
|
19
|
+
| "permanent"
|
|
20
|
+
| "until-end-of-turn"
|
|
21
|
+
| "while-condition";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Modifier represents a temporary or permanent change to a card's properties
|
|
25
|
+
* @template TGameState - Game state type for condition evaluation (defaults to unknown)
|
|
26
|
+
*/
|
|
27
|
+
export type Modifier<TGameState = unknown> = {
|
|
28
|
+
/** Unique identifier for this modifier */
|
|
29
|
+
id: string;
|
|
30
|
+
|
|
31
|
+
/** Type of modification */
|
|
32
|
+
type: ModifierType;
|
|
33
|
+
|
|
34
|
+
/** Which property to modify (e.g., 'power', 'toughness', 'flying') */
|
|
35
|
+
property: string;
|
|
36
|
+
|
|
37
|
+
/** The modification value (can be number, string, or boolean) */
|
|
38
|
+
value: number | string | boolean;
|
|
39
|
+
|
|
40
|
+
/** How long this modifier lasts */
|
|
41
|
+
duration: ModifierDuration;
|
|
42
|
+
|
|
43
|
+
/** Optional condition function - modifier only applies when this returns true */
|
|
44
|
+
condition?: (state: TGameState) => boolean;
|
|
45
|
+
|
|
46
|
+
/** Card that created this modifier */
|
|
47
|
+
source: CardId;
|
|
48
|
+
|
|
49
|
+
/** Optional layer for complex modifier interactions (e.g., MTG layer system) */
|
|
50
|
+
layer?: number;
|
|
51
|
+
};
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# MultiplayerEngine - Network Synchronization Guide
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`MultiplayerEngine` is a reusable wrapper around `RuleEngine` that encapsulates multiplayer patterns for server-authoritative gameplay. It provides a clean abstraction for network synchronization using Immer patches.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
- **Server-Authoritative Architecture**: Server is the source of truth
|
|
10
|
+
- **Patch-Based Synchronization**: Efficient delta updates using Immer patches
|
|
11
|
+
- **Client Tracking**: Monitor which clients are synchronized
|
|
12
|
+
- **Reconnection Support**: Batch patch application for catching up
|
|
13
|
+
- **Type-Safe**: Full TypeScript support with branded types
|
|
14
|
+
- **Network Agnostic**: Works with any transport layer (WebSocket, HTTP, etc.)
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
┌─────────────────────────────────────────────────────────┐
|
|
20
|
+
│ Server │
|
|
21
|
+
│ ┌───────────────────────────────────────────────────┐ │
|
|
22
|
+
│ │ MultiplayerEngine (Server Mode) │ │
|
|
23
|
+
│ │ - Executes moves authoritatively │ │
|
|
24
|
+
│ │ - Generates patches │ │
|
|
25
|
+
│ │ - Broadcasts patches to clients │ │
|
|
26
|
+
│ │ - Tracks client sync state │ │
|
|
27
|
+
│ └───────────────────────────────────────────────────┘ │
|
|
28
|
+
└─────────────────────────────────────────────────────────┘
|
|
29
|
+
│
|
|
30
|
+
│ Patches
|
|
31
|
+
▼
|
|
32
|
+
┌──────────────────────────────────────────┐
|
|
33
|
+
│ Network Layer │
|
|
34
|
+
│ (WebSocket, HTTP, Custom Protocol) │
|
|
35
|
+
└──────────────────────────────────────────┘
|
|
36
|
+
│
|
|
37
|
+
│ Patches
|
|
38
|
+
▼
|
|
39
|
+
┌─────────────────────────────────────────────────────────┐
|
|
40
|
+
│ Clients │
|
|
41
|
+
│ ┌───────────────────────────────────────────────────┐ │
|
|
42
|
+
│ │ MultiplayerEngine (Client Mode) │ │
|
|
43
|
+
│ │ - Applies patches from server │ │
|
|
44
|
+
│ │ - Maintains synced state │ │
|
|
45
|
+
│ │ - Validates moves locally (optional) │ │
|
|
46
|
+
│ │ - Provides player views │ │
|
|
47
|
+
│ └───────────────────────────────────────────────────┘ │
|
|
48
|
+
└─────────────────────────────────────────────────────────┘
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
### Server Setup
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { MultiplayerEngine } from "@drmxrcy/tcg-core";
|
|
57
|
+
|
|
58
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
59
|
+
mode: "server",
|
|
60
|
+
seed: "game-123-seed",
|
|
61
|
+
|
|
62
|
+
// Callback when moves generate patches
|
|
63
|
+
onPatchBroadcast: (broadcast) => {
|
|
64
|
+
// Send patches to all connected clients via your network layer
|
|
65
|
+
websocket.broadcast({
|
|
66
|
+
type: "PATCH_UPDATE",
|
|
67
|
+
patches: broadcast.patches,
|
|
68
|
+
historyIndex: broadcast.historyIndex,
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// Callback when moves are rejected
|
|
73
|
+
onMoveRejected: (moveId, error, errorCode) => {
|
|
74
|
+
console.error(`Move ${moveId} rejected: ${error}`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Execute moves (server only)
|
|
79
|
+
const result = server.executeMove("playCard", {
|
|
80
|
+
playerId: createPlayerId("p1"),
|
|
81
|
+
data: { cardId: "card-123" }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Patches are automatically broadcast via onPatchBroadcast callback
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Client Setup
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { MultiplayerEngine } from "@drmxrcy/tcg-core";
|
|
91
|
+
|
|
92
|
+
const client = new MultiplayerEngine(gameDefinition, players, {
|
|
93
|
+
mode: "client",
|
|
94
|
+
|
|
95
|
+
// Callback when patches are applied
|
|
96
|
+
onPatchesApplied: (patches) => {
|
|
97
|
+
console.log(`Applied ${patches.length} patches from server`);
|
|
98
|
+
updateUI(); // Refresh your game UI
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Receive and apply patches from server
|
|
103
|
+
websocket.on("message", (data) => {
|
|
104
|
+
if (data.type === "PATCH_UPDATE") {
|
|
105
|
+
client.applyServerPatches(data.patches);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Get current state
|
|
110
|
+
const state = client.getState();
|
|
111
|
+
|
|
112
|
+
// Get player-specific view (hides private information)
|
|
113
|
+
const playerView = client.getPlayerView(playerId);
|
|
114
|
+
|
|
115
|
+
// Check valid moves for UI (enable/disable buttons)
|
|
116
|
+
const validMoves = client.getValidMoves(playerId);
|
|
117
|
+
const canDraw = client.canExecuteMove("drawCard", { playerId });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Common Patterns
|
|
121
|
+
|
|
122
|
+
### Pattern 1: Move Request Flow
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
// CLIENT SIDE
|
|
126
|
+
// User clicks "Draw Card" button
|
|
127
|
+
function handleDrawCard() {
|
|
128
|
+
// Optional: Check if move is valid (for immediate UI feedback)
|
|
129
|
+
const canDraw = client.canExecuteMove("drawCard", { playerId });
|
|
130
|
+
|
|
131
|
+
if (!canDraw) {
|
|
132
|
+
showError("Cannot draw card right now");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Send move request to server
|
|
137
|
+
websocket.send({
|
|
138
|
+
type: "MOVE_REQUEST",
|
|
139
|
+
moveId: "drawCard",
|
|
140
|
+
playerId: "p1"
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// SERVER SIDE
|
|
145
|
+
// Receive move request from client
|
|
146
|
+
websocket.on("message", (msg) => {
|
|
147
|
+
if (msg.type === "MOVE_REQUEST") {
|
|
148
|
+
const result = server.executeMove(msg.moveId, {
|
|
149
|
+
playerId: msg.playerId
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// On success, patches automatically broadcast to all clients
|
|
153
|
+
// On failure, send error back to requesting client
|
|
154
|
+
if (!result.success) {
|
|
155
|
+
websocket.send({
|
|
156
|
+
type: "MOVE_ERROR",
|
|
157
|
+
error: result.error,
|
|
158
|
+
errorCode: result.errorCode
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Pattern 2: Client Reconnection
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// SERVER SIDE
|
|
169
|
+
function handleReconnection(clientId: string, lastKnownIndex: number) {
|
|
170
|
+
// Get all patches since client's last known state
|
|
171
|
+
const catchupPatches = server.getCatchupPatches(lastKnownIndex + 1);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
type: "CATCHUP",
|
|
175
|
+
patches: catchupPatches,
|
|
176
|
+
currentIndex: server.getCurrentHistoryIndex()
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// CLIENT SIDE
|
|
181
|
+
function reconnect(lastKnownIndex: number) {
|
|
182
|
+
websocket.send({
|
|
183
|
+
type: "RECONNECT",
|
|
184
|
+
lastKnownIndex
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
websocket.on("message", (msg) => {
|
|
189
|
+
if (msg.type === "CATCHUP") {
|
|
190
|
+
// Apply all missed patches at once
|
|
191
|
+
client.applyServerPatches(msg.patches);
|
|
192
|
+
console.log(`Caught up from ${lastKnownIndex} to ${msg.currentIndex}`);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Pattern 3: Client State Tracking
|
|
198
|
+
|
|
199
|
+
```typescript
|
|
200
|
+
// SERVER SIDE
|
|
201
|
+
// Register client when they connect
|
|
202
|
+
server.registerClient("client-123", -1);
|
|
203
|
+
|
|
204
|
+
// Update sync index when client acknowledges patches
|
|
205
|
+
websocket.on("message", (msg) => {
|
|
206
|
+
if (msg.type === "PATCH_ACK") {
|
|
207
|
+
server.updateClientSyncIndex(msg.clientId, msg.historyIndex);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Check client sync state
|
|
212
|
+
const clientState = server.getClientState("client-123");
|
|
213
|
+
if (clientState && !clientState.connected) {
|
|
214
|
+
console.log("Client disconnected, can remove from game");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Get all clients for monitoring
|
|
218
|
+
const allClients = server.getAllClients();
|
|
219
|
+
console.log(`${allClients.filter(c => c.connected).length} clients online`);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Pattern 4: Optimistic UI Updates
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// CLIENT SIDE
|
|
226
|
+
function handlePlayCard(cardId: string) {
|
|
227
|
+
// Check if move is valid
|
|
228
|
+
if (!client.canExecuteMove("playCard", { playerId, data: { cardId } })) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Optimistic UI update (optional)
|
|
233
|
+
showCardPlaying(cardId);
|
|
234
|
+
|
|
235
|
+
// Send to server
|
|
236
|
+
websocket.send({
|
|
237
|
+
type: "MOVE_REQUEST",
|
|
238
|
+
moveId: "playCard",
|
|
239
|
+
data: { cardId }
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Server will broadcast patches if successful
|
|
243
|
+
// If rejected, patches won't arrive and server sends error
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
websocket.on("message", (msg) => {
|
|
247
|
+
if (msg.type === "MOVE_ERROR") {
|
|
248
|
+
// Revert optimistic update
|
|
249
|
+
hideCardPlaying(msg.data?.cardId);
|
|
250
|
+
showError(msg.error);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## API Reference
|
|
256
|
+
|
|
257
|
+
### Server-Only Methods
|
|
258
|
+
|
|
259
|
+
| Method | Description |
|
|
260
|
+
|--------|-------------|
|
|
261
|
+
| `executeMove(moveId, context)` | Execute a move and generate patches |
|
|
262
|
+
| `getCatchupPatches(sinceIndex)` | Get patches for reconnecting clients |
|
|
263
|
+
| `registerClient(clientId, lastSyncedIndex)` | Track a connected client |
|
|
264
|
+
| `unregisterClient(clientId)` | Mark client as disconnected |
|
|
265
|
+
| `updateClientSyncIndex(clientId, index)` | Update client's sync state |
|
|
266
|
+
| `getClientState(clientId)` | Get a client's sync state |
|
|
267
|
+
| `getAllClients()` | Get all registered clients |
|
|
268
|
+
| `getHistory()` | Get full move history |
|
|
269
|
+
| `getCurrentHistoryIndex()` | Get current history position |
|
|
270
|
+
|
|
271
|
+
### Client-Only Methods
|
|
272
|
+
|
|
273
|
+
| Method | Description |
|
|
274
|
+
|--------|-------------|
|
|
275
|
+
| `applyServerPatches(patches)` | Apply patches from server |
|
|
276
|
+
|
|
277
|
+
### Common Methods (Both Server & Client)
|
|
278
|
+
|
|
279
|
+
| Method | Description |
|
|
280
|
+
|--------|-------------|
|
|
281
|
+
| `getState()` | Get current game state |
|
|
282
|
+
| `getPlayerView(playerId)` | Get player-specific view |
|
|
283
|
+
| `canExecuteMove(moveId, context)` | Check if move is valid |
|
|
284
|
+
| `getValidMoves(playerId)` | Get all valid moves for player |
|
|
285
|
+
| `checkGameEnd()` | Check if game has ended |
|
|
286
|
+
| `getEngine()` | Access underlying RuleEngine |
|
|
287
|
+
| `getMode()` | Get current mode (server/client) |
|
|
288
|
+
|
|
289
|
+
## Examples
|
|
290
|
+
|
|
291
|
+
See `multiplayer-engine.example.ts` for comprehensive examples including:
|
|
292
|
+
|
|
293
|
+
- Basic server setup
|
|
294
|
+
- Client implementation
|
|
295
|
+
- WebSocket integration
|
|
296
|
+
- Reconnection handling
|
|
297
|
+
- Full game simulation
|
|
298
|
+
|
|
299
|
+
## Integration with Existing Projects
|
|
300
|
+
|
|
301
|
+
### Using in Lorcana Engine
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// packages/lorcana-engine/src/multiplayer/server.ts
|
|
305
|
+
import { MultiplayerEngine } from "@drmxrcy/tcg-core";
|
|
306
|
+
import { lorcanaGameDefinition } from "../game-definition";
|
|
307
|
+
|
|
308
|
+
export function createLorcanaServer(players) {
|
|
309
|
+
return new MultiplayerEngine(lorcanaGameDefinition, players, {
|
|
310
|
+
mode: "server",
|
|
311
|
+
seed: generateGameSeed(),
|
|
312
|
+
onPatchBroadcast: broadcastToClients
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Using in Gundam Engine
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// packages/gundam-engine/src/multiplayer/server.ts
|
|
321
|
+
import { MultiplayerEngine } from "@drmxrcy/tcg-core";
|
|
322
|
+
import { gundamGameDefinition } from "../game-definition";
|
|
323
|
+
|
|
324
|
+
export function createGundamServer(players) {
|
|
325
|
+
return new MultiplayerEngine(gundamGameDefinition, players, {
|
|
326
|
+
mode: "server",
|
|
327
|
+
seed: generateGameSeed(),
|
|
328
|
+
onPatchBroadcast: broadcastToClients
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Benefits Over Direct RuleEngine Usage
|
|
334
|
+
|
|
335
|
+
1. **Encapsulation**: Hides multiplayer complexity behind clean API
|
|
336
|
+
2. **Safety**: Server/client separation prevents invalid operations
|
|
337
|
+
3. **Callbacks**: Built-in hooks for network integration
|
|
338
|
+
4. **Client Tracking**: Automatic sync state management
|
|
339
|
+
5. **Reconnection**: Built-in catchup patch support
|
|
340
|
+
6. **Type Safety**: Full TypeScript support with mode enforcement
|
|
341
|
+
7. **Reusability**: Works across all game projects
|
|
342
|
+
|
|
343
|
+
## Testing
|
|
344
|
+
|
|
345
|
+
See `multiplayer-engine.test.ts` for comprehensive test suite covering:
|
|
346
|
+
|
|
347
|
+
- Server mode operations
|
|
348
|
+
- Client mode operations
|
|
349
|
+
- Server-client synchronization
|
|
350
|
+
- Client reconnection
|
|
351
|
+
- Error handling
|
|
352
|
+
- Callbacks and hooks
|
|
353
|
+
|
|
354
|
+
## Performance Considerations
|
|
355
|
+
|
|
356
|
+
1. **Patch Size**: Immer patches are minimal - only changes are transmitted
|
|
357
|
+
2. **Batching**: Consider batching multiple patches for better network efficiency
|
|
358
|
+
3. **Compression**: Use gzip/compression for network transmission
|
|
359
|
+
4. **ACK Protocol**: Track client acknowledgments to prevent packet loss
|
|
360
|
+
5. **Delta Encoding**: Patches are already delta-encoded by Immer
|
|
361
|
+
|
|
362
|
+
## Best Practices
|
|
363
|
+
|
|
364
|
+
1. **Always use server mode for authoritative engine**
|
|
365
|
+
2. **Use client mode for all connected players**
|
|
366
|
+
3. **Implement reconnection logic for network stability**
|
|
367
|
+
4. **Validate moves on both client (UI) and server (authority)**
|
|
368
|
+
5. **Use player views to hide private information**
|
|
369
|
+
6. **Track client sync state for monitoring**
|
|
370
|
+
7. **Implement proper error handling and user feedback**
|
|
371
|
+
8. **Test with network latency simulation**
|
|
372
|
+
|
|
373
|
+
## Troubleshooting
|
|
374
|
+
|
|
375
|
+
### States Out of Sync
|
|
376
|
+
|
|
377
|
+
- Ensure all clients apply patches in order
|
|
378
|
+
- Check that server seed is set for deterministic RNG
|
|
379
|
+
- Verify no client-side state mutations outside patches
|
|
380
|
+
|
|
381
|
+
### Patches Not Broadcasting
|
|
382
|
+
|
|
383
|
+
- Check `onPatchBroadcast` callback is registered
|
|
384
|
+
- Verify move execution returned `success: true`
|
|
385
|
+
- Check network layer is transmitting messages
|
|
386
|
+
|
|
387
|
+
### Client Can't Execute Moves
|
|
388
|
+
|
|
389
|
+
- Ensure client is in "client" mode
|
|
390
|
+
- Moves should only be executed on server
|
|
391
|
+
- Clients should only `applyServerPatches`
|
|
392
|
+
|
|
393
|
+
### Reconnection Issues
|
|
394
|
+
|
|
395
|
+
- Use `getCatchupPatches(lastKnownIndex)` correctly
|
|
396
|
+
- Ensure `lastKnownIndex` is tracked on client
|
|
397
|
+
- Verify patches are applied in correct order
|
|
398
|
+
|
|
399
|
+
## Migration from RuleEngine
|
|
400
|
+
|
|
401
|
+
If you're currently using `RuleEngine` directly:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
// OLD CODE
|
|
405
|
+
const server = new RuleEngine(gameDefinition, players);
|
|
406
|
+
const result = server.executeMove("playCard", context);
|
|
407
|
+
if (result.success) {
|
|
408
|
+
broadcastToClients(result.patches);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// NEW CODE
|
|
412
|
+
const server = new MultiplayerEngine(gameDefinition, players, {
|
|
413
|
+
mode: "server",
|
|
414
|
+
onPatchBroadcast: (broadcast) => broadcastToClients(broadcast.patches)
|
|
415
|
+
});
|
|
416
|
+
const result = server.executeMove("playCard", context);
|
|
417
|
+
// Patches automatically broadcast via callback
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## Related Documentation
|
|
421
|
+
|
|
422
|
+
- `rule-engine.ts` - Core game engine implementation
|
|
423
|
+
- `integration-network-sync.test.ts` - Integration test patterns
|
|
424
|
+
- `multiplayer-engine.example.ts` - Comprehensive examples
|
|
425
|
+
- Game Definition documentation for defining moves and state
|