@phalanx-engine/server 0.1.0
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 +507 -0
- package/dist/Phalanx.d.ts +85 -0
- package/dist/Phalanx.d.ts.map +1 -0
- package/dist/Phalanx.js +600 -0
- package/dist/Phalanx.js.map +1 -0
- package/dist/config/defaults.d.ts +10 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +51 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/validation.d.ts +15 -0
- package/dist/config/validation.d.ts.map +1 -0
- package/dist/config/validation.js +74 -0
- package/dist/config/validation.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/services/GameRoom.d.ts +359 -0
- package/dist/services/GameRoom.d.ts.map +1 -0
- package/dist/services/GameRoom.js +1272 -0
- package/dist/services/GameRoom.js.map +1 -0
- package/dist/services/MatchmakingService.d.ts +102 -0
- package/dist/services/MatchmakingService.d.ts.map +1 -0
- package/dist/services/MatchmakingService.js +286 -0
- package/dist/services/MatchmakingService.js.map +1 -0
- package/dist/services/OAuthExchangeService.d.ts +49 -0
- package/dist/services/OAuthExchangeService.d.ts.map +1 -0
- package/dist/services/OAuthExchangeService.js +96 -0
- package/dist/services/OAuthExchangeService.js.map +1 -0
- package/dist/services/PrivateRoomService.d.ts +136 -0
- package/dist/services/PrivateRoomService.d.ts.map +1 -0
- package/dist/services/PrivateRoomService.js +395 -0
- package/dist/services/PrivateRoomService.js.map +1 -0
- package/dist/services/TokenValidator.d.ts +60 -0
- package/dist/services/TokenValidator.d.ts.map +1 -0
- package/dist/services/TokenValidator.js +213 -0
- package/dist/services/TokenValidator.js.map +1 -0
- package/dist/types/index.d.ts +390 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +6 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/DeterministicRandom.d.ts +88 -0
- package/dist/utils/DeterministicRandom.d.ts.map +1 -0
- package/dist/utils/DeterministicRandom.js +140 -0
- package/dist/utils/DeterministicRandom.js.map +1 -0
- package/dist/utils/FixedMath.d.ts +22 -0
- package/dist/utils/FixedMath.d.ts.map +1 -0
- package/dist/utils/FixedMath.js +26 -0
- package/dist/utils/FixedMath.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# Phalanx Server
|
|
2
|
+
|
|
3
|
+
Server component of [Phalanx Engine](../README.md) - a game-agnostic deterministic lockstep multiplayer engine with authentication, matchmaking, and command synchronization.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Deterministic Lockstep**: Synchronizes only player commands, game logic runs deterministically on each client
|
|
8
|
+
- **Matchmaking**: Built-in support for various game modes (1v1, 2v2, 3v3, 4v4, FFA)
|
|
9
|
+
- **Tick System**: Configurable tick rate with command batching
|
|
10
|
+
- **Reconnection Support**: Players can rejoin matches after disconnection
|
|
11
|
+
- **TypeScript**: Full TypeScript support with exported types
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @phalanx-engine/server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Phalanx } from '@phalanx-engine/server';
|
|
23
|
+
|
|
24
|
+
const app = new Phalanx({
|
|
25
|
+
port: 3000,
|
|
26
|
+
tickRate: 20,
|
|
27
|
+
gameMode: '3v3',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
app.start().then(() => {
|
|
31
|
+
console.log('Phalanx server running on port 3000');
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { Phalanx, PhalanxConfig } from '@phalanx-engine/server';
|
|
39
|
+
|
|
40
|
+
const config: Partial<PhalanxConfig> = {
|
|
41
|
+
// === Server ===
|
|
42
|
+
port: 3000, // Server port (default: 3000)
|
|
43
|
+
cors: { origin: '*' }, // CORS configuration
|
|
44
|
+
extraRequestHandler: async (req, res) => {
|
|
45
|
+
// Optional: mount trusted webhooks or custom HTTP endpoints on the
|
|
46
|
+
// same HTTP server. Return true when the request was fully handled.
|
|
47
|
+
if (req.method === 'POST' && req.url === '/telegram/webhook') {
|
|
48
|
+
res.writeHead(204);
|
|
49
|
+
res.end();
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// === TLS/SSL (optional) ===
|
|
56
|
+
// See "TLS/WSS Configuration" section below for details
|
|
57
|
+
tls: {
|
|
58
|
+
enabled: true,
|
|
59
|
+
keyPath: '/path/to/privkey.pem',
|
|
60
|
+
certPath: '/path/to/fullchain.pem',
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// === Authentication (optional) ===
|
|
64
|
+
auth: {
|
|
65
|
+
enabled: false, // Set true to require auth tokens
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// === Tick System ===
|
|
69
|
+
tickRate: 20, // Ticks per second (default: 20)
|
|
70
|
+
tickDeadlineMs: 50, // Max wait for commands per tick
|
|
71
|
+
|
|
72
|
+
// === Matchmaking ===
|
|
73
|
+
gameMode: '3v3', // Preset: '1v1' | '2v2' | '3v3' | '4v4' | 'FFA4'
|
|
74
|
+
// OR custom:
|
|
75
|
+
// gameMode: { playersPerMatch: 6, teamsCount: 2 },
|
|
76
|
+
matchmakingIntervalMs: 1000,
|
|
77
|
+
countdownSeconds: 5,
|
|
78
|
+
|
|
79
|
+
// === Timeouts ===
|
|
80
|
+
timeoutTicks: 40, // Ticks before "lagging" warning
|
|
81
|
+
disconnectTicks: 100, // Ticks before disconnect
|
|
82
|
+
reconnectGracePeriodMs: 30000,
|
|
83
|
+
|
|
84
|
+
// === Command Validation ===
|
|
85
|
+
maxTickBehind: 10,
|
|
86
|
+
maxTickAhead: 5,
|
|
87
|
+
|
|
88
|
+
// === Command History (for reconnection) ===
|
|
89
|
+
commandHistoryTicks: 200, // Ticks of command history to keep
|
|
90
|
+
|
|
91
|
+
// === Ready Handshake ===
|
|
92
|
+
readyTimeoutMs: 30000, // Max time for clients to send ready (default: 30000)
|
|
93
|
+
|
|
94
|
+
// === Determinism / Desync Detection ===
|
|
95
|
+
enableStateHashing: true, // Enable server-side hash comparison
|
|
96
|
+
stateHashInterval: 60, // Hint interval for hash submission (default: 60)
|
|
97
|
+
desync: {
|
|
98
|
+
enabled: true,
|
|
99
|
+
action: 'end-match', // 'log-only' | 'end-match'
|
|
100
|
+
gracePeriodTicks: 1, // Consecutive desyncs before taking action
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// === Pause/Resume ===
|
|
104
|
+
pause: {
|
|
105
|
+
maxPausesPerPlayer: 3,
|
|
106
|
+
requireSamePlayerToResume: true,
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
// === Game Types (multi-game-type routing) ===
|
|
110
|
+
tickMode: 'continuous', // Default tick mode: 'continuous' | 'event'
|
|
111
|
+
turnTimeoutMs: 60000, // Turn timeout for event mode (default: 60000)
|
|
112
|
+
gameTypes: [
|
|
113
|
+
{
|
|
114
|
+
gameType: 'moba',
|
|
115
|
+
tickMode: 'continuous',
|
|
116
|
+
tickRate: 20,
|
|
117
|
+
gameMode: '1v1',
|
|
118
|
+
countdownSeconds: 3,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
gameType: 'tactics',
|
|
122
|
+
tickMode: 'event',
|
|
123
|
+
gameMode: '1v1',
|
|
124
|
+
countdownSeconds: 3,
|
|
125
|
+
turnTimeoutMs: 90000,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const app = new Phalanx(config);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Event Hooks
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
app.on('match-created', (match) => {
|
|
137
|
+
console.log(`Match ${match.id} created with ${match.players.length} players`);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
app.on('match-started', (match) => {
|
|
141
|
+
console.log(`Match ${match.id} started!`);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
app.on('player-command', (playerId, command) => {
|
|
145
|
+
// Custom command validation
|
|
146
|
+
return true; // or false to reject
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.on('player-timeout', (playerId, matchId) => {
|
|
150
|
+
console.log(`Player ${playerId} timed out`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
app.on('player-disconnected', (playerId, matchId) => {
|
|
154
|
+
console.log(`Player ${playerId} disconnected from match ${matchId}`);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
app.on('player-reconnected', (playerId, matchId) => {
|
|
158
|
+
console.log(`Player ${playerId} reconnected to match ${matchId}`);
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## HTTP Extension Hooks
|
|
163
|
+
|
|
164
|
+
Use `extraRequestHandler` when an embedding server needs a small HTTP surface on the same port as Phalanx, such as a Telegram webhook, an internal health probe, or a signed partner callback.
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { Phalanx } from '@phalanx-engine/server';
|
|
168
|
+
|
|
169
|
+
const app = new Phalanx({
|
|
170
|
+
port: 3000,
|
|
171
|
+
extraRequestHandler: async (req, res) => {
|
|
172
|
+
if (req.method !== 'POST' || req.url !== '/telegram/webhook') {
|
|
173
|
+
return false; // fall through to Phalanx built-in routes
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (req.headers['x-telegram-bot-api-secret-token'] !== process.env.WEBHOOK_SECRET) {
|
|
177
|
+
res.writeHead(401);
|
|
178
|
+
res.end();
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Parse and handle the request body here.
|
|
183
|
+
res.writeHead(200);
|
|
184
|
+
res.end();
|
|
185
|
+
return true;
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Request routing order is:
|
|
191
|
+
|
|
192
|
+
1. CORS preflight (`OPTIONS`) is answered by Phalanx.
|
|
193
|
+
2. `extraRequestHandler` is called.
|
|
194
|
+
3. Built-in routes run (`/`, `/health`, `/auth/token`).
|
|
195
|
+
4. Unknown routes receive `404`.
|
|
196
|
+
|
|
197
|
+
Handler contract:
|
|
198
|
+
|
|
199
|
+
- Return `true` only after fully handling the request, including writing headers/body and ending the response.
|
|
200
|
+
- Return `false` to let Phalanx continue with its built-in routing.
|
|
201
|
+
- Set any route-specific CORS or content headers yourself for handled custom routes.
|
|
202
|
+
- Parse and size-limit request bodies yourself; Phalanx only parses bodies for its own built-in routes.
|
|
203
|
+
- If the handler throws, Phalanx logs the error and sends a generic `500` response when headers have not already been sent.
|
|
204
|
+
|
|
205
|
+
Treat custom HTTP routes as public internet-facing endpoints. Validate webhook secrets or signatures, avoid exposing trusted administrative operations to browsers, and apply rate limiting where appropriate.
|
|
206
|
+
|
|
207
|
+
## API
|
|
208
|
+
|
|
209
|
+
### Phalanx Class
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
class Phalanx {
|
|
213
|
+
constructor(config?: Partial<PhalanxConfig>);
|
|
214
|
+
|
|
215
|
+
// Lifecycle
|
|
216
|
+
start(): Promise<void>;
|
|
217
|
+
stop(): Promise<void>;
|
|
218
|
+
|
|
219
|
+
// Trusted server-side integrations
|
|
220
|
+
createPrivateRoomForHost(params: {
|
|
221
|
+
playerId: string;
|
|
222
|
+
username?: string;
|
|
223
|
+
gameType?: string;
|
|
224
|
+
}): RoomCreatedEvent;
|
|
225
|
+
|
|
226
|
+
// Events
|
|
227
|
+
on(event: PhalanxEvent, handler: Function): this;
|
|
228
|
+
off(event: PhalanxEvent, handler: Function): this;
|
|
229
|
+
|
|
230
|
+
// Runtime info
|
|
231
|
+
getActiveMatches(): MatchInfo[];
|
|
232
|
+
getQueueSize(): number;
|
|
233
|
+
getConfig(): PhalanxConfig;
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Client Events
|
|
238
|
+
|
|
239
|
+
| Event | Emit | Receive | Description |
|
|
240
|
+
| --------------------- | ---- | ------- | --------------------------------- |
|
|
241
|
+
| `queue-join` | ✅ | | Join matchmaking queue |
|
|
242
|
+
| `queue-leave` | ✅ | | Leave matchmaking queue |
|
|
243
|
+
| `queue-status` | | ✅ | Queue join/leave confirmation |
|
|
244
|
+
| `match-found` | | ✅ | Match created, countdown starting |
|
|
245
|
+
| `match-waiting-for-players` | | ✅ | Match is waiting for disconnected participants before countdown |
|
|
246
|
+
| `game-start` | | ✅ | Countdown finished, load assets |
|
|
247
|
+
| `client-ready` | ✅ | | Client finished loading, ready for ticks |
|
|
248
|
+
| `player-ready` | | ✅ | A player reported ready |
|
|
249
|
+
| `match-end` | | ✅ | Match has ended |
|
|
250
|
+
| `submit-commands` | ✅ | | Send game commands |
|
|
251
|
+
| `submit-commands-ack` | | ✅ | Command acknowledgment |
|
|
252
|
+
| `commands-batch` | | ✅ | All commands for a tick |
|
|
253
|
+
| `tick-sync` | | ✅ | Periodic tick synchronization |
|
|
254
|
+
| `countdown` | | ✅ | Countdown before game starts |
|
|
255
|
+
| `submit-state-hash` | ✅ | | Submit state hash for desync detection |
|
|
256
|
+
| `hash-comparison` | | ✅ | Server hash comparison result |
|
|
257
|
+
| `pause-game` | ✅ | | Request game pause |
|
|
258
|
+
| `resume-game` | ✅ | | Request game resume |
|
|
259
|
+
| `game-paused` | | ✅ | Game was paused |
|
|
260
|
+
| `game-resumed` | | ✅ | Game was resumed |
|
|
261
|
+
| `reconnect-match` | ✅ | | Attempt to rejoin a match |
|
|
262
|
+
| `reconnect-status` | | ✅ | Reconnection result |
|
|
263
|
+
| `reconnect-state` | | ✅ | Game state for reconnection |
|
|
264
|
+
| `player-disconnected` | | ✅ | Another player disconnected |
|
|
265
|
+
| `player-reconnected` | | ✅ | Another player reconnected |
|
|
266
|
+
| `room-create` | ✅ | | Create a private room |
|
|
267
|
+
| `room-join` | ✅ | | Join a private room by invite code |
|
|
268
|
+
| `room-cancel` | ✅ | | Cancel a previously created room |
|
|
269
|
+
| `room-recover` | ✅ | | Reclaim a room after socket drop (see Private Room Recovery) |
|
|
270
|
+
| `room-created` | | ✅ | Room created; contains invite `code` |
|
|
271
|
+
| `room-error` | | ✅ | Room operation failed; contains `message` |
|
|
272
|
+
| `room-expired` | | ✅ | Room TTL exceeded; server evicted the room |
|
|
273
|
+
| `room-cancelled` | | ✅ | Host cancelled the room |
|
|
274
|
+
| `room-recovered` | | ✅ | Room successfully reclaimed after socket drop |
|
|
275
|
+
|
|
276
|
+
### Game Start Synchronization
|
|
277
|
+
|
|
278
|
+
After the countdown completes, the server emits `game-start` and enters a `waiting-for-ready` state. The tick loop does **not** start until all connected clients send `client-ready`. This prevents desync caused by clients with different asset loading times missing early ticks.
|
|
279
|
+
|
|
280
|
+
If a client does not send `client-ready` within 30 seconds, the match ends with reason `'ready-timeout'`.
|
|
281
|
+
|
|
282
|
+
### Private Room Recovery
|
|
283
|
+
|
|
284
|
+
Private rooms support a `room-recover` handshake so a participant whose socket was dropped (e.g. mobile OS suspended the WebSocket while the user was sharing the invite link) can seamlessly reclaim either a waiting room or a private-room match without losing the pending join or countdown state.
|
|
285
|
+
|
|
286
|
+
`room-recover` requires both the deterministic `playerId` and the original room `code`. Treat the room code as a bearer recovery secret: possession of the code is what proves the caller is allowed to recover that waiting room or match.
|
|
287
|
+
|
|
288
|
+
#### Recovery protocol
|
|
289
|
+
|
|
290
|
+
1. Client calls `room-recover` with `{ playerId, code }` after reconnecting.
|
|
291
|
+
2. If the code still maps to a waiting room, only the original host can recover it. The server rebinds the host socket and emits `room-recovered`.
|
|
292
|
+
3. If the code has already been consumed into a match, any participant in that match can recover with the original code. The server emits `room-recovered` and delegates to the match reconnect flow.
|
|
293
|
+
4. If the match is still waiting for disconnected participants, reconnecting everyone starts the countdown. If the match is already in progress, the normal reconnect state is delivered.
|
|
294
|
+
5. Server responds with `room-error: { message: 'Room expired' }` on terminal failure. The same generic message is used for missing, expired, wrong-player, and already-finished cases so callers cannot probe room ownership.
|
|
295
|
+
|
|
296
|
+
#### Server-side room TTL
|
|
297
|
+
|
|
298
|
+
Waiting rooms are kept alive for `ROOM_TTL_MS` (default 5 minutes) from creation. If the host disconnects, the room remains recoverable until that original TTL expires; if no host socket is connected at expiry time, there is no `room-expired` recipient to notify. Clients should mirror this TTL in their local persistence layer so they do not attempt `room-recover` for a room the server has certainly already evicted.
|
|
299
|
+
|
|
300
|
+
The client-side [`RoomRecoveryController`](../phalanx-client/README.md#mobile-friendly-room-recovery) handles the full recovery lifecycle automatically when `roomRecovery.enabled: true` is passed to `PhalanxClient`.
|
|
301
|
+
|
|
302
|
+
### Trusted Server-Side Private Room Creation
|
|
303
|
+
|
|
304
|
+
Trusted integrations can create a private room for a host that is not currently connected over Socket.IO. This is intended for server-side entry points such as bot commands: the bot creates a room, sends the invite link containing the room code, and the host later opens the game and recovers the room with the same deterministic `playerId`.
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
const room = app.createPrivateRoomForHost({
|
|
308
|
+
playerId: `telegram:${telegramUserId}`,
|
|
309
|
+
username: telegramUsername,
|
|
310
|
+
gameType: 'chapayev',
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const inviteUrl = `https://game.example.com/?room=${room.code}`;
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
`createPrivateRoomForHost`:
|
|
317
|
+
|
|
318
|
+
- Requires `playerId` and accepts optional `username` and `gameType`.
|
|
319
|
+
- Defaults `username` to `playerId` and `gameType` to the default game type when omitted.
|
|
320
|
+
- Returns `{ code }`. If the same host already has an active waiting room, the existing room code is reused rather than creating a duplicate room.
|
|
321
|
+
- Throws if Phalanx is not running, if the host is already queued for matchmaking, or if the host is already in an active private-room match.
|
|
322
|
+
- Does not emit `room-created`, because there is no host socket yet.
|
|
323
|
+
|
|
324
|
+
Typical bot/webhook flow:
|
|
325
|
+
|
|
326
|
+
1. A trusted HTTP handler validates the webhook request.
|
|
327
|
+
2. The handler derives a stable server-trusted `playerId` from the external identity, such as `telegram:123456`.
|
|
328
|
+
3. The handler calls `createPrivateRoomForHost` and sends the invite URL/code back through the external channel.
|
|
329
|
+
4. The host opens the game and emits `room-recover` with the same `playerId` and code.
|
|
330
|
+
5. The guest opens the invite and emits `room-join` with the code.
|
|
331
|
+
|
|
332
|
+
Do not expose `createPrivateRoomForHost` as an unauthenticated browser endpoint. Browser clients should continue using the Socket.IO `room-create` flow, while trusted server integrations should authenticate the external request before creating rooms.
|
|
333
|
+
|
|
334
|
+
## Game Types
|
|
335
|
+
|
|
336
|
+
Phalanx supports running multiple game types on a single server instance. Each game type can override base config values like `tickMode`, `tickRate`, `gameMode`, `countdownSeconds`, and `turnTimeoutMs`.
|
|
337
|
+
|
|
338
|
+
### Configuration
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const app = new Phalanx({
|
|
342
|
+
tickRate: 20,
|
|
343
|
+
gameMode: '1v1',
|
|
344
|
+
gameTypes: [
|
|
345
|
+
{
|
|
346
|
+
gameType: 'direct-strike',
|
|
347
|
+
tickMode: 'continuous',
|
|
348
|
+
tickRate: 20,
|
|
349
|
+
gameMode: '1v1',
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
gameType: 'chapayev',
|
|
353
|
+
tickMode: 'event',
|
|
354
|
+
gameMode: '1v1',
|
|
355
|
+
turnTimeoutMs: 90000,
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Client: Joining a Game Type Queue
|
|
362
|
+
|
|
363
|
+
Clients declare which game type they want to join via the `queue-join` event:
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
socket.emit('queue-join', {
|
|
367
|
+
playerId: 'player-123',
|
|
368
|
+
username: 'Alice',
|
|
369
|
+
gameType: 'chapayev', // optional — omit for default queue
|
|
370
|
+
});
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Clients that omit `gameType` are placed in a `'default'` queue and use the base config.
|
|
374
|
+
|
|
375
|
+
### `tickMode: 'event'` (Turn-Based / Tickless)
|
|
376
|
+
|
|
377
|
+
When a game type uses `tickMode: 'event'`, no server tick loop runs. Instead:
|
|
378
|
+
|
|
379
|
+
- On `receivePlayerCommands`, the server immediately broadcasts `commands-batch` to all players in the room.
|
|
380
|
+
- `currentTick` increments by 1 per broadcast.
|
|
381
|
+
- A turn timeout (`turnTimeoutMs`, default 60000ms) starts on game start and resets on each received batch. If the timeout fires, the match ends with reason `'turn-timeout'`.
|
|
382
|
+
- `timeoutTicks` / `disconnectTicks` activity tracking is skipped in event mode.
|
|
383
|
+
- Pause/resume works identically in both modes.
|
|
384
|
+
|
|
385
|
+
This is ideal for turn-based physics games (e.g. Chapayev checkers) where game state transitions are driven by player commands only.
|
|
386
|
+
|
|
387
|
+
### `turnTimeoutMs`
|
|
388
|
+
|
|
389
|
+
Maximum time (in milliseconds) allowed between command batches in event mode. If no commands arrive within this window, the match ends automatically. Default: `60000` (1 minute).
|
|
390
|
+
|
|
391
|
+
## Game Modes
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { GAME_MODES } from '@phalanx-engine/server';
|
|
395
|
+
|
|
396
|
+
// Available presets:
|
|
397
|
+
// '1v1' - 2 players, 2 teams
|
|
398
|
+
// '2v2' - 4 players, 2 teams
|
|
399
|
+
// '3v3' - 6 players, 2 teams
|
|
400
|
+
// '4v4' - 8 players, 2 teams
|
|
401
|
+
// 'FFA4' - 4 players, 4 teams (Free For All)
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## TLS/WSS Configuration
|
|
405
|
+
|
|
406
|
+
Phalanx supports secure WebSocket connections (WSS) for production environments.
|
|
407
|
+
|
|
408
|
+
### Basic TLS Setup
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
import { Phalanx } from '@phalanx-engine/server';
|
|
412
|
+
|
|
413
|
+
const app = new Phalanx({
|
|
414
|
+
port: 443,
|
|
415
|
+
tls: {
|
|
416
|
+
enabled: true,
|
|
417
|
+
keyPath: '/etc/letsencrypt/live/game.example.com/privkey.pem',
|
|
418
|
+
certPath: '/etc/letsencrypt/live/game.example.com/fullchain.pem',
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
await app.start();
|
|
423
|
+
console.log('Phalanx server running with TLS on port 443');
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### TLS Configuration Options
|
|
427
|
+
|
|
428
|
+
```typescript
|
|
429
|
+
interface TlsConfig {
|
|
430
|
+
/** Enable TLS/SSL encryption */
|
|
431
|
+
enabled: boolean;
|
|
432
|
+
/** Path to the private key file (PEM format) */
|
|
433
|
+
keyPath: string;
|
|
434
|
+
/** Path to the certificate file (PEM format) */
|
|
435
|
+
certPath: string;
|
|
436
|
+
/** Optional path to CA certificate chain (for Let's Encrypt) */
|
|
437
|
+
caPath?: string;
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Development Mode (No TLS)
|
|
442
|
+
|
|
443
|
+
When TLS is not configured, the server runs in HTTP/WS mode:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
const app = new Phalanx({
|
|
447
|
+
port: 3000,
|
|
448
|
+
// No tls config = development mode
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Let's Encrypt Setup
|
|
453
|
+
|
|
454
|
+
1. Install certbot:
|
|
455
|
+
```bash
|
|
456
|
+
sudo apt install certbot
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
2. Obtain certificates:
|
|
460
|
+
```bash
|
|
461
|
+
sudo certbot certonly --standalone -d game.example.com
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
3. Configure Phalanx:
|
|
465
|
+
```typescript
|
|
466
|
+
const app = new Phalanx({
|
|
467
|
+
port: 443,
|
|
468
|
+
tls: {
|
|
469
|
+
enabled: true,
|
|
470
|
+
keyPath: '/etc/letsencrypt/live/game.example.com/privkey.pem',
|
|
471
|
+
certPath: '/etc/letsencrypt/live/game.example.com/fullchain.pem',
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
4. Set up certificate auto-renewal:
|
|
477
|
+
```bash
|
|
478
|
+
sudo certbot renew --dry-run
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
> **Note**: You may need to run the server with elevated privileges for port 443, or use a reverse proxy like nginx.
|
|
482
|
+
|
|
483
|
+
### Client Connection (WSS)
|
|
484
|
+
|
|
485
|
+
When connecting to a TLS-enabled server from the client:
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { PhalanxClient } from '@phalanx-engine/client';
|
|
489
|
+
|
|
490
|
+
const client = await PhalanxClient.create({
|
|
491
|
+
serverUrl: 'https://game.example.com', // Use https:// for TLS
|
|
492
|
+
playerId: 'player-123',
|
|
493
|
+
username: 'MyPlayer',
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
## Related Packages
|
|
498
|
+
|
|
499
|
+
- [@phalanx-engine/client](../phalanx-client) - Client library for connecting to Phalanx servers
|
|
500
|
+
|
|
501
|
+
## Requirements
|
|
502
|
+
|
|
503
|
+
- Node.js 18+
|
|
504
|
+
|
|
505
|
+
## License
|
|
506
|
+
|
|
507
|
+
MIT
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import type { PhalanxConfig, MatchInfo, RoomCreatedEvent, PhalanxEventType, PhalanxEventHandlers } from './types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Phalanx Engine
|
|
5
|
+
* A game-agnostic deterministic lockstep multiplayer engine
|
|
6
|
+
*/
|
|
7
|
+
export declare class Phalanx extends EventEmitter {
|
|
8
|
+
private readonly config;
|
|
9
|
+
private httpServer;
|
|
10
|
+
private io;
|
|
11
|
+
private matchmaking;
|
|
12
|
+
private privateRooms;
|
|
13
|
+
private tokenValidator;
|
|
14
|
+
private oauthExchange;
|
|
15
|
+
private isRunning;
|
|
16
|
+
constructor(config?: Partial<PhalanxConfig>);
|
|
17
|
+
/**
|
|
18
|
+
* Create HTTP or HTTPS server based on TLS configuration
|
|
19
|
+
*/
|
|
20
|
+
private createServer;
|
|
21
|
+
/**
|
|
22
|
+
* Set CORS headers on response
|
|
23
|
+
*/
|
|
24
|
+
private setCorsHeaders;
|
|
25
|
+
/**
|
|
26
|
+
* Handle OAuth token exchange request
|
|
27
|
+
*/
|
|
28
|
+
private handleTokenExchange;
|
|
29
|
+
/**
|
|
30
|
+
* Read the full request body as a string
|
|
31
|
+
*/
|
|
32
|
+
private readRequestBody;
|
|
33
|
+
/**
|
|
34
|
+
* Start the Phalanx server
|
|
35
|
+
*/
|
|
36
|
+
start(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Create or reuse a private room for a host that will connect later.
|
|
39
|
+
* Intended for trusted server-side integrations such as Telegram bot
|
|
40
|
+
* commands; browser clients should continue using the Socket.IO flow.
|
|
41
|
+
*/
|
|
42
|
+
createPrivateRoomForHost(params: {
|
|
43
|
+
playerId: string;
|
|
44
|
+
username?: string;
|
|
45
|
+
gameType?: string;
|
|
46
|
+
}): RoomCreatedEvent;
|
|
47
|
+
/**
|
|
48
|
+
* Stop the Phalanx server
|
|
49
|
+
*/
|
|
50
|
+
stop(): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Setup authentication middleware for Socket.IO connections.
|
|
53
|
+
* Validates tokens before allowing connections.
|
|
54
|
+
*/
|
|
55
|
+
private setupAuthMiddleware;
|
|
56
|
+
/**
|
|
57
|
+
* Setup socket event handlers
|
|
58
|
+
*/
|
|
59
|
+
private setupSocketHandlers;
|
|
60
|
+
/**
|
|
61
|
+
* Find a GameRoom by matchId across both matchmaking and private room services.
|
|
62
|
+
*/
|
|
63
|
+
private findMatch;
|
|
64
|
+
/**
|
|
65
|
+
* Register an event handler
|
|
66
|
+
*/
|
|
67
|
+
on<E extends PhalanxEventType>(event: E, handler: PhalanxEventHandlers[E]): this;
|
|
68
|
+
/**
|
|
69
|
+
* Remove an event handler
|
|
70
|
+
*/
|
|
71
|
+
off<E extends PhalanxEventType>(event: E, handler: PhalanxEventHandlers[E]): this;
|
|
72
|
+
/**
|
|
73
|
+
* Get all active matches (from both matchmaking and private rooms)
|
|
74
|
+
*/
|
|
75
|
+
getActiveMatches(): MatchInfo[];
|
|
76
|
+
/**
|
|
77
|
+
* Get current matchmaking queue size
|
|
78
|
+
*/
|
|
79
|
+
getQueueSize(): number;
|
|
80
|
+
/**
|
|
81
|
+
* Get the current configuration
|
|
82
|
+
*/
|
|
83
|
+
getConfig(): PhalanxConfig;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=Phalanx.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Phalanx.d.ts","sourceRoot":"","sources":["../src/Phalanx.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAatC,OAAO,KAAK,EACV,aAAa,EACb,SAAS,EACT,gBAAgB,EAEhB,gBAAgB,EAChB,oBAAoB,EAErB,MAAM,kBAAkB,CAAC;AAyB1B;;;GAGG;AACH,qBAAa,OAAQ,SAAQ,YAAY;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,UAAU,CAAyC;IAC3D,OAAO,CAAC,EAAE,CAA+B;IACzC,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,YAAY,CAAmC;IACvD,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,aAAa,CAAqC;IAC1D,OAAO,CAAC,SAAS,CAAkB;gBAEvB,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC;IAK3C;;OAEG;IACH,OAAO,CAAC,YAAY;IA0EpB;;OAEG;IACH,OAAO,CAAC,cAAc;IA2BtB;;OAEG;YACW,mBAAmB;IA6CjC;;OAEG;IACH,OAAO,CAAC,eAAe;IAevB;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAmE5B;;;;OAIG;IACH,wBAAwB,CAAC,MAAM,EAAE;QAC/B,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,gBAAgB;IAYpB;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmC3B;;;OAGG;IACH,OAAO,CAAC,mBAAmB;IA2C3B;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAqS3B;;OAEG;IACH,OAAO,CAAC,SAAS;IAIjB;;OAEG;IACM,EAAE,CAAC,CAAC,SAAS,gBAAgB,EACpC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,IAAI;IAIP;;OAEG;IACM,GAAG,CAAC,CAAC,SAAS,gBAAgB,EACrC,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC/B,IAAI;IAIP;;OAEG;IACH,gBAAgB,IAAI,SAAS,EAAE;IAM/B;;OAEG;IACH,YAAY,IAAI,MAAM;IAItB;;OAEG;IACH,SAAS,IAAI,aAAa;CAG3B"}
|