@smoregg/sdk 2.1.0 → 2.3.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 +361 -111
- package/dist/cjs/controller.cjs +3 -0
- package/dist/cjs/controller.cjs.map +1 -1
- package/dist/cjs/screen.cjs +31 -0
- package/dist/cjs/screen.cjs.map +1 -1
- package/dist/cjs/testing.cjs +12 -11
- package/dist/cjs/testing.cjs.map +1 -1
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/esm/controller.js +3 -0
- package/dist/esm/controller.js.map +1 -1
- package/dist/esm/screen.js +31 -0
- package/dist/esm/screen.js.map +1 -1
- package/dist/esm/testing.js +12 -11
- package/dist/esm/testing.js.map +1 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/types/controller.d.ts.map +1 -1
- package/dist/types/screen.d.ts.map +1 -1
- package/dist/types/testing.d.ts +5 -0
- package/dist/types/testing.d.ts.map +1 -1
- package/dist/types/types.d.ts +45 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd/smore-sdk.umd.js +34 -0
- package/dist/umd/smore-sdk.umd.js.map +1 -1
- package/dist/umd/smore-sdk.umd.min.js +1 -1
- package/dist/umd/smore-sdk.umd.min.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# @smoregg/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SDK for building multiplayer party games on the S'MORE platform.
|
|
4
|
+
|
|
5
|
+
**v2.3.0** | TypeScript | Zero runtime dependencies | ESM, CJS, UMD
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Overview
|
|
10
|
+
|
|
11
|
+
S'MORE is a multiplayer party game platform where a shared display (the **Screen**, typically a TV or computer) runs the game while each player uses their phone as a **Controller** -- an input device and personal display. Think Jackbox-style games with full developer control.
|
|
12
|
+
|
|
13
|
+
This SDK provides type-safe APIs for communication between the Screen and Controllers. Define your game's event types once in a shared interface, and get full compile-time checking on every `send()`, `broadcast()`, and `on()` call. The SDK handles connection management, reconnection, lifecycle events, and message delivery.
|
|
14
|
+
|
|
15
|
+
The core architectural principle is the **Stateless Controller Pattern**: Controllers are display + input devices only. The Screen holds all game state and is the single source of truth. When a player reconnects, the Screen simply re-pushes the current view -- no state synchronization needed.
|
|
4
16
|
|
|
5
17
|
## Installation
|
|
6
18
|
|
|
@@ -14,177 +26,415 @@ yarn add @smoregg/sdk
|
|
|
14
26
|
|
|
15
27
|
## Quick Start
|
|
16
28
|
|
|
17
|
-
###
|
|
29
|
+
### 1. Define Your Events
|
|
30
|
+
|
|
31
|
+
Create a shared event map that both Screen and Controller will use:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// events.ts (shared between Screen and Controller)
|
|
35
|
+
interface GameEvents {
|
|
36
|
+
// Screen -> Controller (view state)
|
|
37
|
+
'game-state': { phase: string; score: number };
|
|
38
|
+
// Controller -> Screen (input)
|
|
39
|
+
'tap': { timestamp: number };
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 2. Screen (TV / Shared Display)
|
|
18
44
|
|
|
19
|
-
```
|
|
45
|
+
```typescript
|
|
20
46
|
import { createScreen } from '@smoregg/sdk';
|
|
21
47
|
|
|
22
|
-
|
|
23
|
-
const screen = createScreen({
|
|
24
|
-
gameId: 'my-game',
|
|
25
|
-
listeners: {
|
|
26
|
-
tap: (playerIndex, data) => {
|
|
27
|
-
console.log(`Player ${playerIndex} tapped!`);
|
|
28
|
-
screen.broadcast('score-update', { playerIndex, score: 10 });
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
});
|
|
48
|
+
const screen = createScreen<GameEvents>();
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
// Listen for player input
|
|
51
|
+
screen.on('tap', (playerIndex, data) => {
|
|
52
|
+
console.log(`Player ${playerIndex} tapped at ${data.timestamp}`);
|
|
53
|
+
// Update game state and push to all controllers
|
|
54
|
+
screen.broadcast('game-state', { phase: 'playing', score: 10 });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Re-push view to reconnecting players
|
|
58
|
+
screen.onControllerReconnect((playerIndex) => {
|
|
59
|
+
screen.sendToController(playerIndex, 'game-state', getCurrentState());
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await screen.ready;
|
|
35
63
|
```
|
|
36
64
|
|
|
37
|
-
### Controller
|
|
65
|
+
### 3. Controller (Phone)
|
|
38
66
|
|
|
39
|
-
```
|
|
67
|
+
```typescript
|
|
40
68
|
import { createController } from '@smoregg/sdk';
|
|
41
69
|
|
|
42
|
-
|
|
43
|
-
const controller = createController({
|
|
44
|
-
gameId: 'my-game',
|
|
45
|
-
listeners: {
|
|
46
|
-
'state-update': (state) => {
|
|
47
|
-
console.log('Game state:', state);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
});
|
|
70
|
+
const controller = createController<GameEvents>();
|
|
51
71
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
// Render what Screen sends (stateless -- no local game state)
|
|
73
|
+
controller.on('game-state', (data) => {
|
|
74
|
+
renderUI(data.phase, data.score);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Send input to Screen
|
|
78
|
+
function handleTap() {
|
|
79
|
+
controller.send('tap', { timestamp: Date.now() });
|
|
57
80
|
}
|
|
81
|
+
|
|
82
|
+
await controller.ready;
|
|
58
83
|
```
|
|
59
84
|
|
|
60
|
-
##
|
|
85
|
+
## Architecture
|
|
61
86
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
```
|
|
88
|
+
┌─────────┐ events ┌─────────┐ relay ┌──────────────┐
|
|
89
|
+
│ Screen │ <----------> │ Server │ <--------> │ Controller │
|
|
90
|
+
│ (TV) │ │ (relay) │ │ (Phone) │
|
|
91
|
+
│ │ │ │ │ │
|
|
92
|
+
│ Game │ broadcast │ No game │ on() │ Display only │
|
|
93
|
+
│ Logic │ -----------> │ logic │ ---------> │ + Input │
|
|
94
|
+
│ State │ │ │ │ │
|
|
95
|
+
│ Source │ sendToCtrl │ │ send() │ No game │
|
|
96
|
+
│ of Truth │ -----------> │ │ <--------- │ state │
|
|
97
|
+
└─────────┘ └─────────┘ └──────────────┘
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Data flow:**
|
|
101
|
+
|
|
102
|
+
- **Controller to Screen:** Input only via `controller.send()`
|
|
103
|
+
- **Screen to all Controllers:** View state via `screen.broadcast()`
|
|
104
|
+
- **Screen to one Controller:** Targeted view state via `screen.sendToController()`
|
|
105
|
+
- **Reconnection:** Screen re-pushes view in `onControllerReconnect` callback
|
|
106
|
+
|
|
107
|
+
The server is a stateless relay -- it forwards messages without game logic. All game state lives on the Screen.
|
|
67
108
|
|
|
68
109
|
## API Reference
|
|
69
110
|
|
|
70
|
-
###
|
|
111
|
+
### Screen
|
|
71
112
|
|
|
72
|
-
|
|
113
|
+
#### Creating a Screen
|
|
73
114
|
|
|
74
115
|
```typescript
|
|
75
116
|
import { createScreen } from '@smoregg/sdk';
|
|
76
117
|
|
|
77
|
-
const screen = createScreen(
|
|
78
|
-
|
|
79
|
-
listeners?: Record<string, (playerIndex: number, data: any) => void>;
|
|
80
|
-
});
|
|
118
|
+
const screen = createScreen<MyEvents>(config?);
|
|
119
|
+
```
|
|
81
120
|
|
|
82
|
-
|
|
83
|
-
screen.broadcast(event: string, data: any): void
|
|
84
|
-
screen.sendToController(playerIndex: number, event: string, data: any): void
|
|
85
|
-
screen.gameOver(results: GameResults): void
|
|
86
|
-
screen.on(event: string, callback: Function): void
|
|
87
|
-
screen.off(event: string, callback: Function): void
|
|
121
|
+
#### Config Options
|
|
88
122
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
| Option | Type | Default | Description |
|
|
124
|
+
|--------|------|---------|-------------|
|
|
125
|
+
| `debug` | `boolean \| DebugOptions` | `false` | Enable debug logging |
|
|
126
|
+
| `parentOrigin` | `string` | `'*'` | Parent window origin for message validation |
|
|
127
|
+
| `timeout` | `number` | `10000` | Connection timeout in milliseconds |
|
|
128
|
+
| `autoReady` | `boolean` | `true` | Automatically signal ready after initialization |
|
|
93
129
|
|
|
94
|
-
|
|
130
|
+
#### Properties
|
|
95
131
|
|
|
96
|
-
|
|
132
|
+
| Property | Type | Description |
|
|
133
|
+
|----------|------|-------------|
|
|
134
|
+
| `controllers` | `readonly ControllerInfo[]` | All connected controllers (shallow copy per access) |
|
|
135
|
+
| `roomCode` | `string` | Room code for this game session |
|
|
136
|
+
| `isReady` | `boolean` | Whether the screen is initialized |
|
|
137
|
+
| `isDestroyed` | `boolean` | Whether the screen has been destroyed |
|
|
138
|
+
| `isConnected` | `boolean` | Whether the connection is active |
|
|
139
|
+
| `ready` | `Promise<void>` | Resolves when the screen is ready |
|
|
97
140
|
|
|
98
|
-
|
|
99
|
-
import { createController } from '@smoregg/sdk';
|
|
141
|
+
#### Communication
|
|
100
142
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
143
|
+
| Method | Description |
|
|
144
|
+
|--------|-------------|
|
|
145
|
+
| `broadcast(event, data)` | Send to all controllers. Rate limit: 60/sec (shared with `sendToController`). Max payload: 64KB. |
|
|
146
|
+
| `sendToController(playerIndex, event, data)` | Send to one controller. Shares the 60/sec rate limit with `broadcast`. |
|
|
147
|
+
| `gameOver(results?)` | End the game. Accepts optional `GameResults` with scores, winner, rankings. |
|
|
148
|
+
| `signalReady()` | Signal ready to the server. Auto-called if `autoReady` is `true`. |
|
|
105
149
|
|
|
106
|
-
|
|
107
|
-
controller.send(event: string, data: any): void
|
|
108
|
-
controller.on(event: string, callback: Function): void
|
|
109
|
-
controller.off(event: string, callback: Function): void
|
|
150
|
+
#### Lifecycle Callbacks
|
|
110
151
|
|
|
111
|
-
|
|
112
|
-
controller.myIndex: number
|
|
113
|
-
controller.isLeader: boolean
|
|
114
|
-
```
|
|
152
|
+
All lifecycle methods return an unsubscribe function.
|
|
115
153
|
|
|
116
|
-
|
|
154
|
+
| Method | Callback Signature | Description |
|
|
155
|
+
|--------|--------------------|-------------|
|
|
156
|
+
| `onAllReady(cb)` | `() => void` | All participants are ready. Fires immediately if already ready. |
|
|
157
|
+
| `onControllerJoin(cb)` | `(playerIndex, info) => void` | A player joined the room |
|
|
158
|
+
| `onControllerLeave(cb)` | `(playerIndex) => void` | A player left the room |
|
|
159
|
+
| `onControllerDisconnect(cb)` | `(playerIndex) => void` | A player temporarily disconnected |
|
|
160
|
+
| `onControllerReconnect(cb)` | `(playerIndex, info) => void` | A player reconnected |
|
|
161
|
+
| `onCharacterUpdated(cb)` | `(playerIndex, appearance) => void` | A player's character appearance changed |
|
|
162
|
+
| `onError(cb)` | `(error: SmoreError) => void` | An SDK error occurred |
|
|
163
|
+
| `onConnectionChange(cb)` | `(connected: boolean) => void` | Connection status changed |
|
|
117
164
|
|
|
118
|
-
|
|
165
|
+
#### Event Subscription
|
|
166
|
+
|
|
167
|
+
| Method | Description |
|
|
168
|
+
|--------|-------------|
|
|
169
|
+
| `on(event, handler)` | Subscribe to an event. Handler receives `(playerIndex, data)`. Returns unsubscribe function. |
|
|
170
|
+
| `once(event, handler)` | Subscribe once. Auto-removes after first call. |
|
|
171
|
+
| `off(event, handler?)` | Remove a specific handler, or all handlers for an event. |
|
|
172
|
+
| `removeAllListeners(event?)` | Remove all user event listeners, or all for a specific event. |
|
|
173
|
+
|
|
174
|
+
#### Utilities
|
|
175
|
+
|
|
176
|
+
| Method | Description |
|
|
177
|
+
|--------|-------------|
|
|
178
|
+
| `getController(playerIndex)` | Get a `ControllerInfo` by player index, or `undefined` |
|
|
179
|
+
| `getControllerCount()` | Number of currently connected controllers |
|
|
180
|
+
| `destroy()` | Clean up all resources and disconnect |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### Controller
|
|
185
|
+
|
|
186
|
+
#### Creating a Controller
|
|
119
187
|
|
|
120
188
|
```typescript
|
|
121
|
-
|
|
122
|
-
import type { SmoreHost, SmorePlayer } from '@smoregg/sdk';
|
|
189
|
+
import { createController } from '@smoregg/sdk';
|
|
123
190
|
|
|
124
|
-
|
|
125
|
-
import type { SmoreScreen, SmoreController } from '@smoregg/sdk';
|
|
191
|
+
const controller = createController<MyEvents>(config?);
|
|
126
192
|
```
|
|
127
193
|
|
|
128
|
-
|
|
194
|
+
Config options are the same as Screen.
|
|
129
195
|
|
|
130
|
-
|
|
196
|
+
#### Properties
|
|
131
197
|
|
|
132
|
-
|
|
133
|
-
|
|
198
|
+
| Property | Type | Description |
|
|
199
|
+
|----------|------|-------------|
|
|
200
|
+
| `myPlayerIndex` | `number` | This player's index (0, 1, 2, ...) |
|
|
201
|
+
| `me` | `ControllerInfo \| undefined` | This player's info |
|
|
202
|
+
| `roomCode` | `string` | Room code for this game session |
|
|
203
|
+
| `controllers` | `readonly ControllerInfo[]` | All known controllers in the room |
|
|
204
|
+
| `isReady` | `boolean` | Whether the controller is initialized |
|
|
205
|
+
| `isDestroyed` | `boolean` | Whether the controller has been destroyed |
|
|
206
|
+
| `isConnected` | `boolean` | Whether the connection is active |
|
|
207
|
+
| `ready` | `Promise<void>` | Resolves when the controller is ready |
|
|
134
208
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
209
|
+
#### Communication
|
|
210
|
+
|
|
211
|
+
| Method | Description |
|
|
212
|
+
|--------|-------------|
|
|
213
|
+
| `send(event, data)` | Send to Screen. Rate limit: 60/sec. Max payload: 64KB. |
|
|
214
|
+
| `signalReady()` | Signal ready to the server. Auto-called if `autoReady` is `true`. |
|
|
215
|
+
|
|
216
|
+
Controller has no `broadcast()` -- all communication goes through Screen. Controller-to-Controller messaging is not supported; route through Screen instead.
|
|
141
217
|
|
|
142
|
-
|
|
143
|
-
screen.simulateEvent(0, 'tap', { x: 100, y: 200 });
|
|
218
|
+
#### Lifecycle Callbacks
|
|
144
219
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
220
|
+
Same as Screen, plus:
|
|
221
|
+
|
|
222
|
+
| Method | Callback Signature | Description |
|
|
223
|
+
|--------|--------------------|-------------|
|
|
224
|
+
| `onGameOver(cb)` | `(results?) => void` | The game has ended (Screen called `gameOver()`) |
|
|
225
|
+
|
|
226
|
+
#### Event Subscription
|
|
227
|
+
|
|
228
|
+
Same API as Screen. Handler receives `(data)` only -- no `playerIndex` parameter.
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
controller.on('game-state', (data) => {
|
|
232
|
+
// data is type-safe: { phase: string; score: number }
|
|
233
|
+
renderUI(data.phase, data.score);
|
|
149
234
|
});
|
|
150
235
|
```
|
|
151
236
|
|
|
152
|
-
|
|
237
|
+
#### Utilities
|
|
238
|
+
|
|
239
|
+
| Method | Description |
|
|
240
|
+
|--------|-------------|
|
|
241
|
+
| `getController(playerIndex)` | Get a `ControllerInfo` by player index, or `undefined` |
|
|
242
|
+
| `getControllerCount()` | Number of currently connected controllers |
|
|
243
|
+
| `destroy()` | Clean up all resources and disconnect |
|
|
153
244
|
|
|
154
|
-
|
|
245
|
+
---
|
|
155
246
|
|
|
156
|
-
|
|
247
|
+
### Types
|
|
157
248
|
|
|
158
249
|
```typescript
|
|
159
250
|
import type {
|
|
160
|
-
|
|
161
|
-
|
|
251
|
+
EventMap,
|
|
252
|
+
ControllerInfo,
|
|
253
|
+
GameResults,
|
|
254
|
+
SmoreError,
|
|
255
|
+
SmoreErrorCode,
|
|
162
256
|
CharacterAppearance,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
257
|
+
PlayerIndex,
|
|
258
|
+
Screen,
|
|
259
|
+
Controller,
|
|
166
260
|
} from '@smoregg/sdk';
|
|
261
|
+
|
|
262
|
+
import { SmoreSDKError, LifecycleEvent } from '@smoregg/sdk';
|
|
167
263
|
```
|
|
168
264
|
|
|
169
|
-
|
|
265
|
+
#### ControllerInfo
|
|
170
266
|
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
|
|
267
|
+
```typescript
|
|
268
|
+
interface ControllerInfo {
|
|
269
|
+
readonly playerIndex: number;
|
|
270
|
+
readonly nickname: string;
|
|
271
|
+
readonly connected: boolean;
|
|
272
|
+
readonly appearance?: CharacterAppearance | null;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
#### GameResults
|
|
174
277
|
|
|
175
|
-
|
|
176
|
-
|
|
278
|
+
```typescript
|
|
279
|
+
interface GameResults {
|
|
280
|
+
scores?: Record<number, number>;
|
|
281
|
+
winner?: number;
|
|
282
|
+
rankings?: number[];
|
|
283
|
+
custom?: Record<string, unknown>;
|
|
284
|
+
}
|
|
285
|
+
```
|
|
177
286
|
|
|
178
|
-
|
|
179
|
-
|
|
287
|
+
#### SmoreSDKError
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
class SmoreSDKError extends Error {
|
|
291
|
+
readonly code: SmoreErrorCode;
|
|
292
|
+
readonly cause?: Error;
|
|
293
|
+
readonly details?: Record<string, unknown>;
|
|
294
|
+
}
|
|
180
295
|
```
|
|
181
296
|
|
|
182
|
-
|
|
297
|
+
**Error codes:** `TIMEOUT`, `NOT_READY`, `DESTROYED`, `INVALID_EVENT`, `INVALID_PLAYER`, `CONNECTION_LOST`, `INIT_FAILED`, `RATE_LIMITED`, `PAYLOAD_TOO_LARGE`, `UNKNOWN`
|
|
183
298
|
|
|
184
|
-
|
|
185
|
-
|
|
299
|
+
#### LifecycleEvent Constants
|
|
300
|
+
|
|
301
|
+
Subscribe to lifecycle events via `on()` using `$`-prefixed constants:
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
import { LifecycleEvent } from '@smoregg/sdk';
|
|
305
|
+
|
|
306
|
+
// These are equivalent:
|
|
307
|
+
screen.onControllerJoin((playerIndex, info) => { /* ... */ });
|
|
308
|
+
screen.on(LifecycleEvent.CONTROLLER_JOIN, (playerIndex, info) => { /* ... */ });
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Available constants: `ALL_READY`, `CONTROLLER_JOIN`, `CONTROLLER_LEAVE`, `CONTROLLER_DISCONNECT`, `CONTROLLER_RECONNECT`, `CHARACTER_UPDATED`, `ERROR`, `GAME_OVER`, `CONNECTION_CHANGE`
|
|
312
|
+
|
|
313
|
+
#### EventMap
|
|
314
|
+
|
|
315
|
+
Event data values must be plain objects, not primitives. The fields `playerIndex` and `targetPlayerIndex` are reserved by the SDK.
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// Good
|
|
319
|
+
interface MyEvents {
|
|
320
|
+
'tap': { x: number; y: number };
|
|
321
|
+
'answer': { choice: number };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Bad -- primitives are not allowed
|
|
325
|
+
interface MyEvents {
|
|
326
|
+
'tap': number; // Will break type safety
|
|
327
|
+
'answer': string; // Use { value: string } instead
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Testing
|
|
332
|
+
|
|
333
|
+
Import test utilities from `@smoregg/sdk/testing`:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
import { createMockScreen, createMockController } from '@smoregg/sdk/testing';
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Example Test
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { describe, it, expect } from 'vitest';
|
|
343
|
+
import { createMockScreen } from '@smoregg/sdk/testing';
|
|
344
|
+
|
|
345
|
+
interface GameEvents {
|
|
346
|
+
'tap': { x: number; y: number };
|
|
347
|
+
'score-update': { scores: Record<number, number> };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
describe('My Game', () => {
|
|
351
|
+
it('broadcasts score on tap', () => {
|
|
352
|
+
const screen = createMockScreen<GameEvents>({
|
|
353
|
+
controllers: [
|
|
354
|
+
{ playerIndex: 0, nickname: 'Alice', connected: true },
|
|
355
|
+
],
|
|
356
|
+
});
|
|
357
|
+
screen.triggerReady();
|
|
358
|
+
|
|
359
|
+
// Register game logic
|
|
360
|
+
screen.on('tap', (playerIndex, data) => {
|
|
361
|
+
screen.broadcast('score-update', { scores: { [playerIndex]: 10 } });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Simulate player input
|
|
365
|
+
screen.simulateEvent(0, 'tap', { x: 100, y: 200 });
|
|
366
|
+
|
|
367
|
+
// Assert game logic response
|
|
368
|
+
const broadcasts = screen.getBroadcasts();
|
|
369
|
+
expect(broadcasts).toHaveLength(1);
|
|
370
|
+
expect(broadcasts[0]).toEqual({
|
|
371
|
+
event: 'score-update',
|
|
372
|
+
data: { scores: { 0: 10 } },
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
});
|
|
186
376
|
```
|
|
187
377
|
|
|
378
|
+
### MockScreen Methods
|
|
379
|
+
|
|
380
|
+
| Method | Description |
|
|
381
|
+
|--------|-------------|
|
|
382
|
+
| `triggerReady()` | Manually trigger the ready state (synchronous) |
|
|
383
|
+
| `simulateEvent(playerIndex, event, data)` | Simulate a controller sending an event |
|
|
384
|
+
| `simulateControllerJoin(info)` | Simulate a player joining |
|
|
385
|
+
| `simulateControllerLeave(playerIndex)` | Simulate a player leaving |
|
|
386
|
+
| `simulateControllerDisconnect(playerIndex)` | Simulate a player disconnecting |
|
|
387
|
+
| `simulateControllerReconnect(playerIndex)` | Simulate a player reconnecting |
|
|
388
|
+
| `simulateAllReady()` | Trigger the all-ready event |
|
|
389
|
+
| `simulateCharacterUpdate(playerIndex, appearance)` | Simulate a character appearance change |
|
|
390
|
+
| `simulateConnectionChange(connected)` | Simulate connection status change |
|
|
391
|
+
| `simulateError(error)` | Simulate an error event |
|
|
392
|
+
| `getBroadcasts()` | Get all recorded `broadcast()` calls |
|
|
393
|
+
| `getSentToController(playerIndex)` | Get recorded `sendToController()` calls for a player |
|
|
394
|
+
| `getAllSentToController()` | Get all recorded `sendToController()` calls |
|
|
395
|
+
| `clearRecordedEvents()` | Clear all recorded broadcasts and sends |
|
|
396
|
+
|
|
397
|
+
### MockController Methods
|
|
398
|
+
|
|
399
|
+
| Method | Description |
|
|
400
|
+
|--------|-------------|
|
|
401
|
+
| `triggerReady()` | Manually trigger the ready state (synchronous) |
|
|
402
|
+
| `simulateEvent(event, data)` | Simulate the Screen sending an event |
|
|
403
|
+
| `simulateGameOver(results?)` | Simulate the game ending |
|
|
404
|
+
| `simulatePlayerJoin(playerIndex, info)` | Simulate a player joining |
|
|
405
|
+
| `simulatePlayerLeave(playerIndex)` | Simulate a player leaving |
|
|
406
|
+
| `simulatePlayerDisconnect(playerIndex)` | Simulate a player disconnecting |
|
|
407
|
+
| `simulatePlayerReconnect(playerIndex, info)` | Simulate a player reconnecting |
|
|
408
|
+
| `simulateAllReady()` | Trigger the all-ready event |
|
|
409
|
+
| `simulateCharacterUpdate(playerIndex, appearance)` | Simulate a character appearance change |
|
|
410
|
+
| `simulateConnectionChange(connected)` | Simulate connection status change |
|
|
411
|
+
| `simulateError(error)` | Simulate an error event |
|
|
412
|
+
| `getSentEvents()` | Get all recorded `send()` calls |
|
|
413
|
+
| `clearRecordedEvents()` | Clear all recorded sends |
|
|
414
|
+
|
|
415
|
+
**Note:** Default `autoReady` is `false` in mocks. Use `triggerReady()` for synchronous test control. If you pass `autoReady: true`, ready fires asynchronously on the next tick.
|
|
416
|
+
|
|
417
|
+
## Event Naming Rules
|
|
418
|
+
|
|
419
|
+
| Rule | Example |
|
|
420
|
+
|------|---------|
|
|
421
|
+
| Must start with a letter | `tap` (valid), `123tap` (invalid) |
|
|
422
|
+
| Letters, numbers, hyphens, underscores only | `player-move` (valid), `player.move` (invalid) |
|
|
423
|
+
| No colons | `my-event` (valid), `my:event` (invalid, reserved for platform) |
|
|
424
|
+
| Max 128 characters | - |
|
|
425
|
+
|
|
426
|
+
## Limits and Constraints
|
|
427
|
+
|
|
428
|
+
| Constraint | Value |
|
|
429
|
+
|------------|-------|
|
|
430
|
+
| Rate limit | 60 events/sec per socket (shared across all send methods) |
|
|
431
|
+
| Max payload | 64KB per event |
|
|
432
|
+
| Message ordering | Guaranteed for a single sender |
|
|
433
|
+
| Event data | Must be objects, not primitives |
|
|
434
|
+
| Reserved fields | `playerIndex` and `targetPlayerIndex` in event data |
|
|
435
|
+
|
|
436
|
+
Events exceeding the rate limit or payload size are silently dropped by the server.
|
|
437
|
+
|
|
188
438
|
## License
|
|
189
439
|
|
|
190
|
-
MIT
|
|
440
|
+
MIT
|
package/dist/cjs/controller.cjs
CHANGED