@magicworks/client-party-game 0.1.0-rc.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 +91 -0
- package/dist/client.d.ts +210 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +659 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +407 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +232 -0
- package/dist/protocol.js.map +1 -0
- package/dist/socket.d.ts +30 -0
- package/dist/socket.d.ts.map +1 -0
- package/dist/socket.js +3 -0
- package/dist/socket.js.map +1 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# @magicworks/client-party-game
|
|
2
|
+
|
|
3
|
+
Host TV client and wire protocol for the multiplayer party games (Name the Artist, Name the Song). The ROXi app uses `PartyGameClient` to open a room, run rounds and get scores from `@magicworks/service-party-game`. The protocol module is the one definition of events, payloads, limits and scoring, shared by the server and the app.
|
|
4
|
+
|
|
5
|
+
## Tech Stack
|
|
6
|
+
|
|
7
|
+
- **socket.io-client (bring your own)**: the client takes socket.io-client's `io` as an option instead of bundling it. Version 2.x (what the app ships) and 4.x both work.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Opens, reopens and re-attaches to rooms, remembering the last room code so *Play again* within 20 minutes keeps the same phones
|
|
12
|
+
- Reaches a room in its own region: codes say which region holds them (`PARTY_REGIONS`), and with `regionUrls` the client connects for a code to that region's servers. Only opening a new room goes to the global, latency-routed `url`
|
|
13
|
+
- Routes by room: every connection made for a room carries its code in the `c` query parameter, which the load balancer uses to reach the server holding the room. A connection made before a room exists reconnects with the code once it has one, during the lobby, where the reconnect costs nothing
|
|
14
|
+
- Retries on another server when one is draining for a deploy (`host:create` refused with `reason: 'draining'`)
|
|
15
|
+
- Holds host calls while a reconnect re-attaches, so nothing reaches the server from a socket it doesn't know the room of
|
|
16
|
+
- Measures the clock offset to the server NTP-style (fastest of 5 pings, every 60s), for starting rounds at an agreed moment
|
|
17
|
+
- Follows its room between servers: when a draining server asks (`room:move`), the client takes the room to a server still taking parties and opens it there under a new code. The phones follow on their own, and host calls made meanwhile wait and are sent to the new server. It also follows `movedTo` when it asks for a room by an old code
|
|
18
|
+
- Reports a session change (`onSession`) whenever the room's code changes: it moved, or its server went and a reconnect was given a new room. The lobby should show the new code
|
|
19
|
+
- `restart()` on a server past its drain deadline opens a fresh room elsewhere; the phones have to scan the new code
|
|
20
|
+
|
|
21
|
+
## Reference
|
|
22
|
+
|
|
23
|
+
| Type | Function | Arguments | Returns | Description |
|
|
24
|
+
|------|----------|-----------|---------|-------------|
|
|
25
|
+
| Session | host | gameType: PartyGameType, genre: string, genreName: string \| null, fresh?: boolean | Promise\<PartyHostSession\> | Open a room, or reopen the last one; `fresh` walks away from it |
|
|
26
|
+
| Session | getSession | - | PartyHostSession \| null | The current room |
|
|
27
|
+
| Session | getPlayers | - | PartyPlayer[] | Last roster pushed by the server |
|
|
28
|
+
| Session | isConnected | - | boolean | Whether the socket is up |
|
|
29
|
+
| Session | close | - | void | Leave the room and drop the socket |
|
|
30
|
+
| Game | start | totalRounds?: number | Promise\<PartyPlayer[]\> | Close the lobby; unready players are dropped |
|
|
31
|
+
| Game | readyForGame | - | void | This TV is ready for the countdown |
|
|
32
|
+
| Game | waitForCountdown | - | Promise\<void\> | Resolves when the server starts the countdown |
|
|
33
|
+
| Game | startRound | config: PartyRoundConfig | Promise\<void\> | Options and answer for a round whose video is preloaded |
|
|
34
|
+
| Game | waitForRoundGo | round: number | Promise\<PartyRoundGo\> | When the round starts, on the server's clock |
|
|
35
|
+
| Game | endRound | - | Promise\<PartyRoundResults \| null\> | Score the round |
|
|
36
|
+
| Game | endGame | - | Promise\<PartyPlayer[]\> | Final standings |
|
|
37
|
+
| Game | restart | - | Promise\<PartyPlayer[]\> | Same players, scores back to zero (a fresh, empty room if the server won't play again) |
|
|
38
|
+
| Game | getTotalRounds | - | number | Rounds in this game |
|
|
39
|
+
| Clock | syncClock | - | Promise\<void\> | Measure the offset to the server again |
|
|
40
|
+
| Clock | serverNow | - | number | Now, on the server's clock |
|
|
41
|
+
| Clock | toServerTime / toLocalTime | time: number | number | Convert between this device's clock and the server's |
|
|
42
|
+
| Listener | onPlayers / onAllAnswered / onRoundGo / onSession | listener | () => void | Subscribe; returns an unsubscribe |
|
|
43
|
+
| Util | speedtest | bytes: number | { url, bytes } \| null | The CDN file of at least `bytes` random bytes for the lobby's bandwidth test, and its real size; null until there is a session |
|
|
44
|
+
| Protocol | scoreForAnswer | secondsLeft: number, totalRoundTime: number, modifier: number | number | Points for a right answer |
|
|
45
|
+
| Protocol | normaliseCode | code: unknown | string \| null | A typed code tidied up, or null if it can't be one |
|
|
46
|
+
| Protocol | codePrefixOf | code: string | string | The server prefix a code routes by |
|
|
47
|
+
|
|
48
|
+
The protocol module also exports `Feature`, `HostRefusal` and `RoomSnapshot`, the event names (`HostEvent`, `PlayerEvent`, `GameEvent`, `TimeEvent`, `HostMessage`, `PlayerMessage`, `GameMessage`), the limits (`MAX_PLAYERS`, `MAX_NAME_LENGTH`, `TOTAL_ROUNDS`, `REOPEN_WINDOW_MS`, `ROUND_LEAD_MS`, `GAME_READY_TIMEOUT_MS`, `CODE_ALPHABET`, `CODE_LENGTH`, `CODE_PREFIX_LENGTH`, `ROUTING_QUERY_KEY`), and request/ack/message types for every event.
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import io from 'socket.io-client'; // the app's 2.x
|
|
54
|
+
import { PartyGameClient } from '@magicworks/client-party-game';
|
|
55
|
+
|
|
56
|
+
const client = new PartyGameClient({
|
|
57
|
+
url: 'https://party-ws.prod.cat.roxi.tv',
|
|
58
|
+
regionUrls: {
|
|
59
|
+
'us-east-1': 'https://party-ws.prod-us-east-1.cat.roxi.tv',
|
|
60
|
+
'eu-west-1': 'https://party-ws.prod-eu-west-1.cat.roxi.tv',
|
|
61
|
+
},
|
|
62
|
+
io,
|
|
63
|
+
hostId: deviceBarcodeId, // the same TV across app restarts
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const session = await client.host('nametheartist', 'pop', 'Pop');
|
|
67
|
+
showQrCode(session.joinUrl);
|
|
68
|
+
client.onPlayers(players => renderLobby(players));
|
|
69
|
+
|
|
70
|
+
await client.start(15);
|
|
71
|
+
client.readyForGame();
|
|
72
|
+
await client.waitForCountdown();
|
|
73
|
+
|
|
74
|
+
await client.startRound({ round: 1, options, correctIndex: 2, durationMs: 20000, modifier: 1, notBefore });
|
|
75
|
+
const { startAt } = await client.waitForRoundGo(1);
|
|
76
|
+
schedulePlay(client.toLocalTime(startAt));
|
|
77
|
+
const results = await client.endRound();
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Configuration
|
|
81
|
+
|
|
82
|
+
| Option | Type | Required | Description |
|
|
83
|
+
|--------|------|----------|-------------|
|
|
84
|
+
| url | string | Yes | The party servers' global, latency-routed URL (`party-ws.<stage>.cat.roxi.tv`), not the phone page's: where a new room opens |
|
|
85
|
+
| regionUrls | Record\<string, string\> | No | Each region's own URL. A room is reached at its code's region (`PARTY_REGIONS`), so a TV that latency routing sends to another region still finds its room. Without it, everything goes to `url` |
|
|
86
|
+
| io | PartySocketFactory | Yes | socket.io-client's `io`, either major version |
|
|
87
|
+
| hostId | string | No | Identifies this TV across its connections; one per process by default |
|
|
88
|
+
| roomMemory | RoomMemory | No | Where the last room code is kept; in memory by default |
|
|
89
|
+
| logger | PartyLogger | No | `debug` and `warn`; `console` by default |
|
|
90
|
+
| ackTimeoutMs | number | No | How long to wait for any answer from the server (8000) |
|
|
91
|
+
| clockSyncIntervalMs | number | No | How often the clock offset is measured again (60000) |
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { type PartyGameType, type PartyHostSession, type PartyPlayer, type PartyRoundConfig, type PartyRoundGo, type PartyRoundResults } from './protocol';
|
|
2
|
+
import type { PartySocketFactory } from './socket';
|
|
3
|
+
/**
|
|
4
|
+
* Where the last room this app hosted is remembered, past one client being
|
|
5
|
+
* closed and the next one made.
|
|
6
|
+
*
|
|
7
|
+
* A second game is nearly always the same people in the same room, so the
|
|
8
|
+
* lobby offers this code back to the server: if the room is still recent
|
|
9
|
+
* enough the server reopens it - same code, same names, scores cleared - and
|
|
10
|
+
* everyone readies up on the phone they already have open instead of scanning
|
|
11
|
+
* again. The server owns that "recent enough", so this is kept indefinitely
|
|
12
|
+
* and a stale code simply gets a new room back.
|
|
13
|
+
*/
|
|
14
|
+
export interface RoomMemory {
|
|
15
|
+
get(): string | null;
|
|
16
|
+
set(code: string): void;
|
|
17
|
+
}
|
|
18
|
+
export interface PartyLogger {
|
|
19
|
+
debug(...args: unknown[]): void;
|
|
20
|
+
warn(...args: unknown[]): void;
|
|
21
|
+
}
|
|
22
|
+
export interface PartyGameClientOptions {
|
|
23
|
+
/**
|
|
24
|
+
* The party servers' global name, e.g. `https://party-ws.prod.cat.roxi.tv`:
|
|
25
|
+
* latency-routed, so a new room opens in the nearest region.
|
|
26
|
+
*/
|
|
27
|
+
url: string;
|
|
28
|
+
/**
|
|
29
|
+
* Each region's own name, e.g. `{ 'eu-west-1': 'https://party-ws.prod-eu-west-1.cat.roxi.tv' }`.
|
|
30
|
+
* A room is reached at its code's region (see PARTY_REGIONS) wherever the
|
|
31
|
+
* global name would send this TV now. Without it, everything goes to `url`.
|
|
32
|
+
*/
|
|
33
|
+
regionUrls?: Record<string, string>;
|
|
34
|
+
/** socket.io-client's `io`: see `PartySocketFactory`. */
|
|
35
|
+
io: PartySocketFactory;
|
|
36
|
+
/**
|
|
37
|
+
* Which TV this is. The server will not hand a room to another TV while its
|
|
38
|
+
* host is connected, and a connection closed on the way into the next lobby
|
|
39
|
+
* can still look connected to the server for a moment - so the room is
|
|
40
|
+
* claimed by this, not by the connection. One per app run by default.
|
|
41
|
+
*/
|
|
42
|
+
hostId?: string;
|
|
43
|
+
/** In memory, for as long as this module is loaded, by default. */
|
|
44
|
+
roomMemory?: RoomMemory;
|
|
45
|
+
logger?: PartyLogger;
|
|
46
|
+
/** How long to wait on the server's answer to anything. */
|
|
47
|
+
ackTimeoutMs?: number;
|
|
48
|
+
/** How often the clock offset to the server is measured again. */
|
|
49
|
+
clockSyncIntervalMs?: number;
|
|
50
|
+
}
|
|
51
|
+
type PlayersListener = (players: PartyPlayer[]) => void;
|
|
52
|
+
type AllAnsweredListener = (round: number) => void;
|
|
53
|
+
type RoundGoListener = (go: PartyRoundGo) => void;
|
|
54
|
+
type SessionListener = (session: PartyHostSession) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Host side of the party games protocol. The TV owns the video and therefore
|
|
57
|
+
* owns round timing: it tells the server when a round starts and when it ends,
|
|
58
|
+
* and the server answers with who scored what.
|
|
59
|
+
*
|
|
60
|
+
* One instance is meant to be shared by the lobby screen and the game itself,
|
|
61
|
+
* so navigating from the lobby into the first round keeps the same room and
|
|
62
|
+
* the same players.
|
|
63
|
+
*
|
|
64
|
+
* Rooms live in the memory of one server of several, and the load balancer
|
|
65
|
+
* finds it by the room code: every connection made for a room carries its
|
|
66
|
+
* code (ROUTING_QUERY_KEY). A connection made before there is a room goes to
|
|
67
|
+
* any server, so once that server has opened one the client connects again
|
|
68
|
+
* with its code - in the lobby, where a moment's reconnect costs nothing,
|
|
69
|
+
* rather than leaving the game on a route the load balancer can take away.
|
|
70
|
+
*/
|
|
71
|
+
export declare class PartyGameClient {
|
|
72
|
+
private readonly url;
|
|
73
|
+
private readonly regionUrls;
|
|
74
|
+
private readonly io;
|
|
75
|
+
private readonly hostId;
|
|
76
|
+
private readonly roomMemory;
|
|
77
|
+
private readonly logger;
|
|
78
|
+
private readonly ackTimeoutMs;
|
|
79
|
+
private readonly clockSyncIntervalMs;
|
|
80
|
+
private socket;
|
|
81
|
+
/** The room code the current socket was routed by, if any. */
|
|
82
|
+
private routedCode;
|
|
83
|
+
/**
|
|
84
|
+
* Whether the current connection is attached to the session's room. Host
|
|
85
|
+
* calls wait for it: sent while a reconnect is still re-attaching, they
|
|
86
|
+
* would reach the server from a socket it does not know the room of.
|
|
87
|
+
*/
|
|
88
|
+
private attached;
|
|
89
|
+
private attachedWaiters;
|
|
90
|
+
private session;
|
|
91
|
+
/** Where the session's room is now, when a server has said it moved. */
|
|
92
|
+
private redirectCode;
|
|
93
|
+
/** What the last lobby was opened for, to open another like it. */
|
|
94
|
+
private lastHost;
|
|
95
|
+
private players;
|
|
96
|
+
private playersListeners;
|
|
97
|
+
private allAnsweredListeners;
|
|
98
|
+
private sessionListeners;
|
|
99
|
+
/** Start times the server has given rounds of the current game. */
|
|
100
|
+
private roundGos;
|
|
101
|
+
private roundGoListeners;
|
|
102
|
+
/** Whether the server has started the current game's countdown. */
|
|
103
|
+
private countdownStarted;
|
|
104
|
+
private countdownListeners;
|
|
105
|
+
/** Server clock minus this TV's clock, in ms. */
|
|
106
|
+
private clockOffset;
|
|
107
|
+
private clockSyncTimer;
|
|
108
|
+
constructor(options: PartyGameClientOptions);
|
|
109
|
+
getSession(): PartyHostSession | null;
|
|
110
|
+
getPlayers(): PartyPlayer[];
|
|
111
|
+
isConnected(): boolean;
|
|
112
|
+
/**
|
|
113
|
+
* A download of at least `bytes` random bytes for the TV to time in the
|
|
114
|
+
* lobby, and its real size, which the bandwidth has to be worked out from.
|
|
115
|
+
* It is a file on the CDN the phone page is on - the origin of the
|
|
116
|
+
* session's join URL - so null until there is a session.
|
|
117
|
+
*/
|
|
118
|
+
speedtest(bytes: number): {
|
|
119
|
+
url: string;
|
|
120
|
+
bytes: number;
|
|
121
|
+
} | null;
|
|
122
|
+
/** The socket for `code`'s room (or for no room), connecting a new one if need be. */
|
|
123
|
+
private socketFor;
|
|
124
|
+
private currentSocket;
|
|
125
|
+
private setAttached;
|
|
126
|
+
private whenAttached;
|
|
127
|
+
/** Where `code`'s room is: its region's servers, or for no room, the nearest. */
|
|
128
|
+
private urlFor;
|
|
129
|
+
/** A socket routed by `code`, to `url` (by default, where `code`'s room is). */
|
|
130
|
+
private connectTo;
|
|
131
|
+
private openSocket;
|
|
132
|
+
/**
|
|
133
|
+
* Back on the room we were already hosting, so a dropped connection
|
|
134
|
+
* mid-party does not lose the players or their scores. If the room is gone
|
|
135
|
+
* - its server is - the server opens a new one, and the session follows.
|
|
136
|
+
*/
|
|
137
|
+
private reattach;
|
|
138
|
+
/** Takes on a session the server has handed back, rerouting by its code if need be. */
|
|
139
|
+
private adopt;
|
|
140
|
+
private send;
|
|
141
|
+
private emit;
|
|
142
|
+
private moveRoom;
|
|
143
|
+
/**
|
|
144
|
+
* Measures the offset from this TV's clock to the server's, NTP style: a few
|
|
145
|
+
* round trips, trusting the quickest - the one whose reply spent the least
|
|
146
|
+
* time in flight, and so says most exactly what the server's clock read.
|
|
147
|
+
*/
|
|
148
|
+
syncClock(): Promise<void>;
|
|
149
|
+
/** The time now on the server's clock. */
|
|
150
|
+
serverNow(): number;
|
|
151
|
+
/** A time by this TV's `Date.now()`, as the server's clock would read it. */
|
|
152
|
+
toServerTime(localTime: number): number;
|
|
153
|
+
/** A time on the server's clock, as this TV's `Date.now()` would read it. */
|
|
154
|
+
toLocalTime(serverTime: number): number;
|
|
155
|
+
/**
|
|
156
|
+
* Opens a room and returns the code / join URL to put in the QR code.
|
|
157
|
+
* Passing the code of a room we hosted recently reopens that one instead,
|
|
158
|
+
* which is what a second game does: same phones, same names, and a
|
|
159
|
+
* `reused` session so the lobby can tell people to tap Ready.
|
|
160
|
+
*
|
|
161
|
+
* `fresh` is the way out of that: a brand new room, and the one we were
|
|
162
|
+
* holding is closed behind us so its phones are not left waiting on it.
|
|
163
|
+
*/
|
|
164
|
+
host(gameType: PartyGameType, genre: string, genreName: string | null, fresh?: boolean): Promise<PartyHostSession>;
|
|
165
|
+
/**
|
|
166
|
+
* Closes the lobby - no more players can join after this. The round count is
|
|
167
|
+
* chosen in the lobby, so it is locked in here.
|
|
168
|
+
*/
|
|
169
|
+
start(totalRounds?: number): Promise<PartyPlayer[]>;
|
|
170
|
+
/** How many rounds this party is playing; the default until a game starts. */
|
|
171
|
+
getTotalRounds(): number;
|
|
172
|
+
/**
|
|
173
|
+
* This TV has the game on screen and its player set up: the countdown to
|
|
174
|
+
* round 1 can start as far as it is concerned. It starts on the TV and the
|
|
175
|
+
* phones at once, when they have all said so - see waitForCountdown.
|
|
176
|
+
*/
|
|
177
|
+
readyForGame(): void;
|
|
178
|
+
/** Resolves when the server starts the countdown to round 1 of this game. */
|
|
179
|
+
waitForCountdown(): Promise<void>;
|
|
180
|
+
/**
|
|
181
|
+
* Tells the server the host's video for the round is preloaded, with the
|
|
182
|
+
* options for the phones; the correct index stays between us and the server.
|
|
183
|
+
* The round starts at the time `waitForRoundGo` resolves with.
|
|
184
|
+
*/
|
|
185
|
+
startRound(config: PartyRoundConfig): Promise<void>;
|
|
186
|
+
/** When the server has said a round starts: at once if it already has. */
|
|
187
|
+
waitForRoundGo(round: number): Promise<PartyRoundGo>;
|
|
188
|
+
/** Scores the round. Players who did not answer are scored as wrong. */
|
|
189
|
+
endRound(): Promise<PartyRoundResults | null>;
|
|
190
|
+
endGame(): Promise<PartyPlayer[]>;
|
|
191
|
+
/**
|
|
192
|
+
* Same players, scores back to zero, room back to the lobby. A server past
|
|
193
|
+
* its drain deadline plays no more games, and closes the room instead: the
|
|
194
|
+
* lobby then opens fresh on another server, with a code to scan.
|
|
195
|
+
*/
|
|
196
|
+
restart(): Promise<PartyPlayer[]>;
|
|
197
|
+
onPlayers(listener: PlayersListener): () => boolean;
|
|
198
|
+
onAllAnswered(listener: AllAnsweredListener): () => boolean;
|
|
199
|
+
onRoundGo(listener: RoundGoListener): () => boolean;
|
|
200
|
+
/**
|
|
201
|
+
* The room changed under us: its server went away and a reconnect was
|
|
202
|
+
* handed a new one. The lobby shows the new code; a game in progress has
|
|
203
|
+
* lost its players.
|
|
204
|
+
*/
|
|
205
|
+
onSession(listener: SessionListener): () => boolean;
|
|
206
|
+
/** Leaves the room entirely - the next `host()` opens a fresh one. */
|
|
207
|
+
close(): void;
|
|
208
|
+
}
|
|
209
|
+
export {};
|
|
210
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|