@thegeem/protocol 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 +26 -0
- package/dist/index.cjs +94 -0
- package/dist/index.d.cts +396 -0
- package/dist/index.d.ts +396 -0
- package/dist/index.js +65 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @thegeem/protocol
|
|
2
|
+
|
|
3
|
+
The **wire contract** for [Geem](https://geem.tv) — jackbox.tv-style multiplayer trivia.
|
|
4
|
+
|
|
5
|
+
Shared **zod schemas + TypeScript types** for talking to the Geem game server over a
|
|
6
|
+
single Socket.IO connection: `ClientMsg` in (validated), `ServerMsg` out (authoritative).
|
|
7
|
+
This package is the **source of truth** every client (web + native) implements against.
|
|
8
|
+
|
|
9
|
+
The server is the referee (server-authoritative): clients send *inputs* and *render* the
|
|
10
|
+
authoritative state the server pushes. There is no REST gameplay API — this protocol is the
|
|
11
|
+
only gate.
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { ClientMsg, ServerMsg, PROTOCOL_VERSION } from '@thegeem/protocol';
|
|
15
|
+
|
|
16
|
+
// validate anything you send
|
|
17
|
+
const msg = ClientMsg.parse({ t: 'joinRoom', code: 'ABCD', name: 'Sara', pv: PROTOCOL_VERSION });
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Versioning
|
|
21
|
+
|
|
22
|
+
`PROTOCOL_VERSION` is a breaking-change counter. Clients send `pv` on entry; the server
|
|
23
|
+
rejects `pv < PROTOCOL_VERSION` with `client_outdated`. The npm package version is separate
|
|
24
|
+
(normal semver for the package itself).
|
|
25
|
+
|
|
26
|
+
UNLICENSED — published for Geem's own clients; not for third-party use.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ClientMsg: () => ClientMsg,
|
|
24
|
+
GAME_IDS: () => GAME_IDS,
|
|
25
|
+
LIMITS: () => LIMITS,
|
|
26
|
+
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
27
|
+
ROOM_ALPHABET: () => ROOM_ALPHABET
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
var import_zod = require("zod");
|
|
31
|
+
var LIMITS = {
|
|
32
|
+
MAX_NAME_LEN: 20,
|
|
33
|
+
ROOM_CODE_LEN: 4,
|
|
34
|
+
MAX_PLAYERS: 12,
|
|
35
|
+
MAX_MSG_PER_SEC: 8
|
|
36
|
+
};
|
|
37
|
+
var PROTOCOL_VERSION = 1;
|
|
38
|
+
var GAME_IDS = ["trivia"];
|
|
39
|
+
var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
40
|
+
var UNSAFE_CHARS = /[\p{Cc}<>]/gu;
|
|
41
|
+
var DisplayName = import_zod.z.string().trim().min(1).max(LIMITS.MAX_NAME_LEN).transform((s) => s.replace(UNSAFE_CHARS, "").trim()).refine((s) => s.length >= 1, "name is empty after sanitizing");
|
|
42
|
+
var RoomCode = import_zod.z.string().trim().toUpperCase().regex(new RegExp(`^[${ROOM_ALPHABET}]{${LIMITS.ROOM_CODE_LEN}}$`), "bad room code");
|
|
43
|
+
var PlayerId = import_zod.z.string().max(40);
|
|
44
|
+
var HelpType = import_zod.z.enum([
|
|
45
|
+
"removeTwoAnswers",
|
|
46
|
+
"changeQuestion",
|
|
47
|
+
"extraTime",
|
|
48
|
+
"doublePoints",
|
|
49
|
+
"stealPoints",
|
|
50
|
+
"restPlayer",
|
|
51
|
+
"tripLevel"
|
|
52
|
+
]);
|
|
53
|
+
var Pv = import_zod.z.number().int().min(1).max(1e6).optional();
|
|
54
|
+
var IdToken = import_zod.z.string().max(4096).optional();
|
|
55
|
+
var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
56
|
+
// room / lobby
|
|
57
|
+
import_zod.z.object({ t: import_zod.z.literal("createRoom"), name: DisplayName.optional(), pv: Pv, idToken: IdToken }),
|
|
58
|
+
import_zod.z.object({ t: import_zod.z.literal("joinRoom"), code: RoomCode, name: DisplayName, pv: Pv, idToken: IdToken }),
|
|
59
|
+
import_zod.z.object({ t: import_zod.z.literal("resume"), code: RoomCode, token: import_zod.z.string().max(120), pv: Pv, idToken: IdToken }),
|
|
60
|
+
import_zod.z.object({ t: import_zod.z.literal("leaveRoom") }),
|
|
61
|
+
import_zod.z.object({ t: import_zod.z.literal("approve"), playerId: PlayerId }),
|
|
62
|
+
import_zod.z.object({ t: import_zod.z.literal("reject"), playerId: PlayerId }),
|
|
63
|
+
import_zod.z.object({ t: import_zod.z.literal("kick"), playerId: PlayerId }),
|
|
64
|
+
// host game actions
|
|
65
|
+
import_zod.z.object({
|
|
66
|
+
t: import_zod.z.literal("startGame"),
|
|
67
|
+
game: import_zod.z.enum(GAME_IDS).optional(),
|
|
68
|
+
// which game to play; defaults to 'trivia'
|
|
69
|
+
totalRounds: import_zod.z.number().int().min(1).max(50),
|
|
70
|
+
categoryIds: import_zod.z.array(import_zod.z.string().max(60)).min(1).max(12)
|
|
71
|
+
}),
|
|
72
|
+
import_zod.z.object({ t: import_zod.z.literal("pickLevel"), categoryId: import_zod.z.string().max(60), level: import_zod.z.number().int().min(1).max(6) }),
|
|
73
|
+
import_zod.z.object({ t: import_zod.z.literal("nextTurn") }),
|
|
74
|
+
import_zod.z.object({ t: import_zod.z.literal("playAgain") }),
|
|
75
|
+
// host: after a game ends, return the SAME players to the lobby
|
|
76
|
+
import_zod.z.object({ t: import_zod.z.literal("markOpen"), correct: import_zod.z.boolean() }),
|
|
77
|
+
import_zod.z.object({ t: import_zod.z.literal("revealNoHost") }),
|
|
78
|
+
import_zod.z.object({ t: import_zod.z.literal("awardNoHost"), teamIndex: import_zod.z.number().int().min(0).max(LIMITS.MAX_PLAYERS) }),
|
|
79
|
+
import_zod.z.object({ t: import_zod.z.literal("noHostNobody") }),
|
|
80
|
+
// player actions
|
|
81
|
+
import_zod.z.object({ t: import_zod.z.literal("buzz") }),
|
|
82
|
+
import_zod.z.object({ t: import_zod.z.literal("answer"), index: import_zod.z.number().int().min(0).max(10) }),
|
|
83
|
+
import_zod.z.object({ t: import_zod.z.literal("useHelp"), helpType: HelpType }),
|
|
84
|
+
// sabotage a rival (active team only): spend a sabotage help to debuff targetTeam's next question
|
|
85
|
+
import_zod.z.object({ t: import_zod.z.literal("sabotage"), helpType: HelpType, targetTeam: import_zod.z.number().int().min(0).max(LIMITS.MAX_PLAYERS) })
|
|
86
|
+
]);
|
|
87
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
88
|
+
0 && (module.exports = {
|
|
89
|
+
ClientMsg,
|
|
90
|
+
GAME_IDS,
|
|
91
|
+
LIMITS,
|
|
92
|
+
PROTOCOL_VERSION,
|
|
93
|
+
ROOM_ALPHABET
|
|
94
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Geem Online wire format — server-authoritative (Shape B).
|
|
5
|
+
* Clients send INPUTS (`ClientMsg`, always validated server-side); the server
|
|
6
|
+
* runs @thegeem/engine and pushes authoritative STATE (`ServerMsg`).
|
|
7
|
+
*
|
|
8
|
+
* SELF-CONTAINED: this package owns the wire contract and depends only on `zod`
|
|
9
|
+
* (no @thegeem/engine import) so it can be published publicly + generated into
|
|
10
|
+
* Swift/Kotlin. The engine re-exports these wire types and PRODUCES them.
|
|
11
|
+
*/
|
|
12
|
+
declare const LIMITS: {
|
|
13
|
+
readonly MAX_NAME_LEN: 20;
|
|
14
|
+
readonly ROOM_CODE_LEN: 4;
|
|
15
|
+
readonly MAX_PLAYERS: 12;
|
|
16
|
+
readonly MAX_MSG_PER_SEC: 8;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Wire-format generation. **Bump ONLY on a breaking change** to ClientMsg /
|
|
20
|
+
* ServerMsg / GameStateView (a field removed/renamed/retyped, or a semantic
|
|
21
|
+
* change a frozen client would mis-handle). Additive, backward-compatible
|
|
22
|
+
* changes (a new optional field, a new effect a client can ignore) do NOT bump.
|
|
23
|
+
*
|
|
24
|
+
* Why this exists: web auto-updates, but an App-Store / Play build is FROZEN on
|
|
25
|
+
* the user's device. A client sends the version it was built against (`pv`) on
|
|
26
|
+
* its first message; the server rejects `pv < PROTOCOL_VERSION` with
|
|
27
|
+
* `{ t:'error', code:'client_outdated' }` so old native builds get a clean
|
|
28
|
+
* "please update" instead of silently mis-rendering. Unversioned clients (no
|
|
29
|
+
* `pv`, e.g. the legacy web harness) are never rejected.
|
|
30
|
+
*/
|
|
31
|
+
declare const PROTOCOL_VERSION = 1;
|
|
32
|
+
/**
|
|
33
|
+
* Geem is a PLATFORM of GAMES (jackbox-style); the trivia board is Game #1.
|
|
34
|
+
* Every room runs one game, identified by `gameId`. Add ids here as games ship.
|
|
35
|
+
* (NOTE: "modes" — controller/race/no-host — are variants WITHIN the trivia
|
|
36
|
+
* game, not separate games. See the platform-of-games decision.)
|
|
37
|
+
*/
|
|
38
|
+
declare const GAME_IDS: readonly ["trivia"];
|
|
39
|
+
type GameId = (typeof GAME_IDS)[number];
|
|
40
|
+
/** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
|
|
41
|
+
declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
42
|
+
declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
43
|
+
declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
44
|
+
t: z.ZodLiteral<"createRoom">;
|
|
45
|
+
name: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
46
|
+
pv: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
idToken: z.ZodOptional<z.ZodString>;
|
|
48
|
+
}, "strip", z.ZodTypeAny, {
|
|
49
|
+
t: "createRoom";
|
|
50
|
+
name?: string | undefined;
|
|
51
|
+
pv?: number | undefined;
|
|
52
|
+
idToken?: string | undefined;
|
|
53
|
+
}, {
|
|
54
|
+
t: "createRoom";
|
|
55
|
+
name?: string | undefined;
|
|
56
|
+
pv?: number | undefined;
|
|
57
|
+
idToken?: string | undefined;
|
|
58
|
+
}>, z.ZodObject<{
|
|
59
|
+
t: z.ZodLiteral<"joinRoom">;
|
|
60
|
+
code: z.ZodString;
|
|
61
|
+
name: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
62
|
+
pv: z.ZodOptional<z.ZodNumber>;
|
|
63
|
+
idToken: z.ZodOptional<z.ZodString>;
|
|
64
|
+
}, "strip", z.ZodTypeAny, {
|
|
65
|
+
code: string;
|
|
66
|
+
t: "joinRoom";
|
|
67
|
+
name: string;
|
|
68
|
+
pv?: number | undefined;
|
|
69
|
+
idToken?: string | undefined;
|
|
70
|
+
}, {
|
|
71
|
+
code: string;
|
|
72
|
+
t: "joinRoom";
|
|
73
|
+
name: string;
|
|
74
|
+
pv?: number | undefined;
|
|
75
|
+
idToken?: string | undefined;
|
|
76
|
+
}>, z.ZodObject<{
|
|
77
|
+
t: z.ZodLiteral<"resume">;
|
|
78
|
+
code: z.ZodString;
|
|
79
|
+
token: z.ZodString;
|
|
80
|
+
pv: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
idToken: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, "strip", z.ZodTypeAny, {
|
|
83
|
+
code: string;
|
|
84
|
+
t: "resume";
|
|
85
|
+
token: string;
|
|
86
|
+
pv?: number | undefined;
|
|
87
|
+
idToken?: string | undefined;
|
|
88
|
+
}, {
|
|
89
|
+
code: string;
|
|
90
|
+
t: "resume";
|
|
91
|
+
token: string;
|
|
92
|
+
pv?: number | undefined;
|
|
93
|
+
idToken?: string | undefined;
|
|
94
|
+
}>, z.ZodObject<{
|
|
95
|
+
t: z.ZodLiteral<"leaveRoom">;
|
|
96
|
+
}, "strip", z.ZodTypeAny, {
|
|
97
|
+
t: "leaveRoom";
|
|
98
|
+
}, {
|
|
99
|
+
t: "leaveRoom";
|
|
100
|
+
}>, z.ZodObject<{
|
|
101
|
+
t: z.ZodLiteral<"approve">;
|
|
102
|
+
playerId: z.ZodString;
|
|
103
|
+
}, "strip", z.ZodTypeAny, {
|
|
104
|
+
t: "approve";
|
|
105
|
+
playerId: string;
|
|
106
|
+
}, {
|
|
107
|
+
t: "approve";
|
|
108
|
+
playerId: string;
|
|
109
|
+
}>, z.ZodObject<{
|
|
110
|
+
t: z.ZodLiteral<"reject">;
|
|
111
|
+
playerId: z.ZodString;
|
|
112
|
+
}, "strip", z.ZodTypeAny, {
|
|
113
|
+
t: "reject";
|
|
114
|
+
playerId: string;
|
|
115
|
+
}, {
|
|
116
|
+
t: "reject";
|
|
117
|
+
playerId: string;
|
|
118
|
+
}>, z.ZodObject<{
|
|
119
|
+
t: z.ZodLiteral<"kick">;
|
|
120
|
+
playerId: z.ZodString;
|
|
121
|
+
}, "strip", z.ZodTypeAny, {
|
|
122
|
+
t: "kick";
|
|
123
|
+
playerId: string;
|
|
124
|
+
}, {
|
|
125
|
+
t: "kick";
|
|
126
|
+
playerId: string;
|
|
127
|
+
}>, z.ZodObject<{
|
|
128
|
+
t: z.ZodLiteral<"startGame">;
|
|
129
|
+
game: z.ZodOptional<z.ZodEnum<["trivia"]>>;
|
|
130
|
+
totalRounds: z.ZodNumber;
|
|
131
|
+
categoryIds: z.ZodArray<z.ZodString, "many">;
|
|
132
|
+
}, "strip", z.ZodTypeAny, {
|
|
133
|
+
t: "startGame";
|
|
134
|
+
totalRounds: number;
|
|
135
|
+
categoryIds: string[];
|
|
136
|
+
game?: "trivia" | undefined;
|
|
137
|
+
}, {
|
|
138
|
+
t: "startGame";
|
|
139
|
+
totalRounds: number;
|
|
140
|
+
categoryIds: string[];
|
|
141
|
+
game?: "trivia" | undefined;
|
|
142
|
+
}>, z.ZodObject<{
|
|
143
|
+
t: z.ZodLiteral<"pickLevel">;
|
|
144
|
+
categoryId: z.ZodString;
|
|
145
|
+
level: z.ZodNumber;
|
|
146
|
+
}, "strip", z.ZodTypeAny, {
|
|
147
|
+
t: "pickLevel";
|
|
148
|
+
categoryId: string;
|
|
149
|
+
level: number;
|
|
150
|
+
}, {
|
|
151
|
+
t: "pickLevel";
|
|
152
|
+
categoryId: string;
|
|
153
|
+
level: number;
|
|
154
|
+
}>, z.ZodObject<{
|
|
155
|
+
t: z.ZodLiteral<"nextTurn">;
|
|
156
|
+
}, "strip", z.ZodTypeAny, {
|
|
157
|
+
t: "nextTurn";
|
|
158
|
+
}, {
|
|
159
|
+
t: "nextTurn";
|
|
160
|
+
}>, z.ZodObject<{
|
|
161
|
+
t: z.ZodLiteral<"playAgain">;
|
|
162
|
+
}, "strip", z.ZodTypeAny, {
|
|
163
|
+
t: "playAgain";
|
|
164
|
+
}, {
|
|
165
|
+
t: "playAgain";
|
|
166
|
+
}>, z.ZodObject<{
|
|
167
|
+
t: z.ZodLiteral<"markOpen">;
|
|
168
|
+
correct: z.ZodBoolean;
|
|
169
|
+
}, "strip", z.ZodTypeAny, {
|
|
170
|
+
t: "markOpen";
|
|
171
|
+
correct: boolean;
|
|
172
|
+
}, {
|
|
173
|
+
t: "markOpen";
|
|
174
|
+
correct: boolean;
|
|
175
|
+
}>, z.ZodObject<{
|
|
176
|
+
t: z.ZodLiteral<"revealNoHost">;
|
|
177
|
+
}, "strip", z.ZodTypeAny, {
|
|
178
|
+
t: "revealNoHost";
|
|
179
|
+
}, {
|
|
180
|
+
t: "revealNoHost";
|
|
181
|
+
}>, z.ZodObject<{
|
|
182
|
+
t: z.ZodLiteral<"awardNoHost">;
|
|
183
|
+
teamIndex: z.ZodNumber;
|
|
184
|
+
}, "strip", z.ZodTypeAny, {
|
|
185
|
+
t: "awardNoHost";
|
|
186
|
+
teamIndex: number;
|
|
187
|
+
}, {
|
|
188
|
+
t: "awardNoHost";
|
|
189
|
+
teamIndex: number;
|
|
190
|
+
}>, z.ZodObject<{
|
|
191
|
+
t: z.ZodLiteral<"noHostNobody">;
|
|
192
|
+
}, "strip", z.ZodTypeAny, {
|
|
193
|
+
t: "noHostNobody";
|
|
194
|
+
}, {
|
|
195
|
+
t: "noHostNobody";
|
|
196
|
+
}>, z.ZodObject<{
|
|
197
|
+
t: z.ZodLiteral<"buzz">;
|
|
198
|
+
}, "strip", z.ZodTypeAny, {
|
|
199
|
+
t: "buzz";
|
|
200
|
+
}, {
|
|
201
|
+
t: "buzz";
|
|
202
|
+
}>, z.ZodObject<{
|
|
203
|
+
t: z.ZodLiteral<"answer">;
|
|
204
|
+
index: z.ZodNumber;
|
|
205
|
+
}, "strip", z.ZodTypeAny, {
|
|
206
|
+
t: "answer";
|
|
207
|
+
index: number;
|
|
208
|
+
}, {
|
|
209
|
+
t: "answer";
|
|
210
|
+
index: number;
|
|
211
|
+
}>, z.ZodObject<{
|
|
212
|
+
t: z.ZodLiteral<"useHelp">;
|
|
213
|
+
helpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
214
|
+
}, "strip", z.ZodTypeAny, {
|
|
215
|
+
t: "useHelp";
|
|
216
|
+
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
217
|
+
}, {
|
|
218
|
+
t: "useHelp";
|
|
219
|
+
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
220
|
+
}>, z.ZodObject<{
|
|
221
|
+
t: z.ZodLiteral<"sabotage">;
|
|
222
|
+
helpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
223
|
+
targetTeam: z.ZodNumber;
|
|
224
|
+
}, "strip", z.ZodTypeAny, {
|
|
225
|
+
t: "sabotage";
|
|
226
|
+
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
227
|
+
targetTeam: number;
|
|
228
|
+
}, {
|
|
229
|
+
t: "sabotage";
|
|
230
|
+
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
231
|
+
targetTeam: number;
|
|
232
|
+
}>]>;
|
|
233
|
+
type ClientMsg = z.infer<typeof ClientMsg>;
|
|
234
|
+
interface LobbyPlayer {
|
|
235
|
+
id: string;
|
|
236
|
+
name: string;
|
|
237
|
+
approved: boolean;
|
|
238
|
+
connected: boolean;
|
|
239
|
+
}
|
|
240
|
+
/** Per-recipient context attached to lobby/state pushes. */
|
|
241
|
+
interface YouContext {
|
|
242
|
+
id: string;
|
|
243
|
+
isHost: boolean;
|
|
244
|
+
/** This player's team index once the game has started; null in the lobby / for the host. */
|
|
245
|
+
teamIndex: number | null;
|
|
246
|
+
}
|
|
247
|
+
type GameStatus = 'setup' | 'categorySelection' | 'inProgress' | 'completed';
|
|
248
|
+
type GameplayFlow = 'classic' | 'openJudged' | 'noHost' | 'buzzer';
|
|
249
|
+
type BuzzerGameMode = 'off' | 'solo' | 'race' | 'controller';
|
|
250
|
+
/** The help/lifeline ids (same set as the inbound `HelpType` zod enum above). */
|
|
251
|
+
type HelpName = z.infer<typeof HelpType>;
|
|
252
|
+
type SoundName = 'correct' | 'wrong' | 'timeup' | 'warning' | 'help' | 'ding' | 'pop' | 'swoosh';
|
|
253
|
+
interface TeamView {
|
|
254
|
+
id: string;
|
|
255
|
+
name: string;
|
|
256
|
+
emoji: string;
|
|
257
|
+
color: string;
|
|
258
|
+
score: number;
|
|
259
|
+
currentStreak: number;
|
|
260
|
+
/** This team's lifelines and whether each has been spent. */
|
|
261
|
+
helps: {
|
|
262
|
+
type: HelpName;
|
|
263
|
+
isUsed: boolean;
|
|
264
|
+
}[];
|
|
265
|
+
}
|
|
266
|
+
interface QuestionView {
|
|
267
|
+
text: string;
|
|
268
|
+
options: string[];
|
|
269
|
+
hiddenOptionIndices: number[];
|
|
270
|
+
revealed: boolean;
|
|
271
|
+
selectedAnswerIndex: number | null;
|
|
272
|
+
/** Only present once revealed (anti-cheat). Null while the question is live. */
|
|
273
|
+
correctAnswerIndex: number | null;
|
|
274
|
+
imageName?: string;
|
|
275
|
+
videoName?: string;
|
|
276
|
+
}
|
|
277
|
+
interface GameStateView {
|
|
278
|
+
status: GameStatus;
|
|
279
|
+
flow: GameplayFlow;
|
|
280
|
+
mode: BuzzerGameMode;
|
|
281
|
+
round: number;
|
|
282
|
+
totalRounds: number;
|
|
283
|
+
currentTeamIndex: number;
|
|
284
|
+
answeringTeamIndex: number;
|
|
285
|
+
teams: TeamView[];
|
|
286
|
+
question: QuestionView | null;
|
|
287
|
+
/** Race: the team that buzzed first (null otherwise). */
|
|
288
|
+
firstBuzzTeamIndex: number | null;
|
|
289
|
+
lockedOutTeamIndices: number[];
|
|
290
|
+
isOpenStealIntro: boolean;
|
|
291
|
+
doublePointsActive: boolean;
|
|
292
|
+
stealPointsActive: boolean;
|
|
293
|
+
trippedTeamIndices: number[];
|
|
294
|
+
/** Shared board: per category id, the level numbers already played. */
|
|
295
|
+
usedLevels: Record<string, number[]>;
|
|
296
|
+
}
|
|
297
|
+
/** Transient presentation effects the relay emits; clients play them once. */
|
|
298
|
+
type Effect = {
|
|
299
|
+
t: 'stopTimer';
|
|
300
|
+
} | {
|
|
301
|
+
t: 'startTimer';
|
|
302
|
+
duration: number;
|
|
303
|
+
} | {
|
|
304
|
+
t: 'resumeTimer';
|
|
305
|
+
} | {
|
|
306
|
+
t: 'addTime';
|
|
307
|
+
seconds: number;
|
|
308
|
+
} | {
|
|
309
|
+
t: 'closeBuzzer';
|
|
310
|
+
} | {
|
|
311
|
+
t: 'openBuzzer';
|
|
312
|
+
} | {
|
|
313
|
+
t: 'serveQuestion';
|
|
314
|
+
} | {
|
|
315
|
+
t: 'sound';
|
|
316
|
+
name: SoundName;
|
|
317
|
+
} | {
|
|
318
|
+
t: 'reveal';
|
|
319
|
+
} | {
|
|
320
|
+
t: 'celebrate';
|
|
321
|
+
hard: boolean;
|
|
322
|
+
} | {
|
|
323
|
+
t: 'scorePop';
|
|
324
|
+
amount: number;
|
|
325
|
+
streakBonus: number;
|
|
326
|
+
} | {
|
|
327
|
+
t: 'chargeCredit';
|
|
328
|
+
} | {
|
|
329
|
+
t: 'stealPause';
|
|
330
|
+
ms: number;
|
|
331
|
+
} | {
|
|
332
|
+
t: 'openStealIntro';
|
|
333
|
+
ms: number;
|
|
334
|
+
} | {
|
|
335
|
+
t: 'noHostAdvance';
|
|
336
|
+
ms: number;
|
|
337
|
+
} | {
|
|
338
|
+
t: 'endGame';
|
|
339
|
+
};
|
|
340
|
+
type ServerMsg = {
|
|
341
|
+
t: 'roomCreated';
|
|
342
|
+
code: string;
|
|
343
|
+
youId: string;
|
|
344
|
+
token: string;
|
|
345
|
+
pv: number;
|
|
346
|
+
} | {
|
|
347
|
+
t: 'joinPending';
|
|
348
|
+
} | {
|
|
349
|
+
t: 'joinRequest';
|
|
350
|
+
playerId: string;
|
|
351
|
+
name: string;
|
|
352
|
+
} | {
|
|
353
|
+
t: 'joined';
|
|
354
|
+
youId: string;
|
|
355
|
+
token: string;
|
|
356
|
+
pv: number;
|
|
357
|
+
} | {
|
|
358
|
+
t: 'joinRejected';
|
|
359
|
+
reason: string;
|
|
360
|
+
} | {
|
|
361
|
+
t: 'kicked';
|
|
362
|
+
} | {
|
|
363
|
+
t: 'roomClosed';
|
|
364
|
+
reason: string;
|
|
365
|
+
} | {
|
|
366
|
+
t: 'lobby';
|
|
367
|
+
code: string;
|
|
368
|
+
gameId: GameId;
|
|
369
|
+
hostId: string;
|
|
370
|
+
hostName: string;
|
|
371
|
+
players: LobbyPlayer[];
|
|
372
|
+
categories: {
|
|
373
|
+
id: string;
|
|
374
|
+
name: string;
|
|
375
|
+
packId: string;
|
|
376
|
+
packName: string;
|
|
377
|
+
icon: string;
|
|
378
|
+
hasImage: boolean;
|
|
379
|
+
}[];
|
|
380
|
+
you: YouContext;
|
|
381
|
+
} | {
|
|
382
|
+
t: 'state';
|
|
383
|
+
gameId: GameId;
|
|
384
|
+
view: GameStateView;
|
|
385
|
+
you: YouContext;
|
|
386
|
+
teamConnected: boolean[];
|
|
387
|
+
} | {
|
|
388
|
+
t: 'fx';
|
|
389
|
+
effects: Effect[];
|
|
390
|
+
} | {
|
|
391
|
+
t: 'error';
|
|
392
|
+
code: string;
|
|
393
|
+
message: string;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
export { type BuzzerGameMode, ClientMsg, type Effect, GAME_IDS, type GameId, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type ServerMsg, type SoundName, type TeamView, type YouContext };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Geem Online wire format — server-authoritative (Shape B).
|
|
5
|
+
* Clients send INPUTS (`ClientMsg`, always validated server-side); the server
|
|
6
|
+
* runs @thegeem/engine and pushes authoritative STATE (`ServerMsg`).
|
|
7
|
+
*
|
|
8
|
+
* SELF-CONTAINED: this package owns the wire contract and depends only on `zod`
|
|
9
|
+
* (no @thegeem/engine import) so it can be published publicly + generated into
|
|
10
|
+
* Swift/Kotlin. The engine re-exports these wire types and PRODUCES them.
|
|
11
|
+
*/
|
|
12
|
+
declare const LIMITS: {
|
|
13
|
+
readonly MAX_NAME_LEN: 20;
|
|
14
|
+
readonly ROOM_CODE_LEN: 4;
|
|
15
|
+
readonly MAX_PLAYERS: 12;
|
|
16
|
+
readonly MAX_MSG_PER_SEC: 8;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Wire-format generation. **Bump ONLY on a breaking change** to ClientMsg /
|
|
20
|
+
* ServerMsg / GameStateView (a field removed/renamed/retyped, or a semantic
|
|
21
|
+
* change a frozen client would mis-handle). Additive, backward-compatible
|
|
22
|
+
* changes (a new optional field, a new effect a client can ignore) do NOT bump.
|
|
23
|
+
*
|
|
24
|
+
* Why this exists: web auto-updates, but an App-Store / Play build is FROZEN on
|
|
25
|
+
* the user's device. A client sends the version it was built against (`pv`) on
|
|
26
|
+
* its first message; the server rejects `pv < PROTOCOL_VERSION` with
|
|
27
|
+
* `{ t:'error', code:'client_outdated' }` so old native builds get a clean
|
|
28
|
+
* "please update" instead of silently mis-rendering. Unversioned clients (no
|
|
29
|
+
* `pv`, e.g. the legacy web harness) are never rejected.
|
|
30
|
+
*/
|
|
31
|
+
declare const PROTOCOL_VERSION = 1;
|
|
32
|
+
/**
|
|
33
|
+
* Geem is a PLATFORM of GAMES (jackbox-style); the trivia board is Game #1.
|
|
34
|
+
* Every room runs one game, identified by `gameId`. Add ids here as games ship.
|
|
35
|
+
* (NOTE: "modes" — controller/race/no-host — are variants WITHIN the trivia
|
|
36
|
+
* game, not separate games. See the platform-of-games decision.)
|
|
37
|
+
*/
|
|
38
|
+
declare const GAME_IDS: readonly ["trivia"];
|
|
39
|
+
type GameId = (typeof GAME_IDS)[number];
|
|
40
|
+
/** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
|
|
41
|
+
declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
42
|
+
declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
43
|
+
declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
44
|
+
t: z.ZodLiteral<"createRoom">;
|
|
45
|
+
name: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
|
|
46
|
+
pv: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
idToken: z.ZodOptional<z.ZodString>;
|
|
48
|
+
}, "strip", z.ZodTypeAny, {
|
|
49
|
+
t: "createRoom";
|
|
50
|
+
name?: string | undefined;
|
|
51
|
+
pv?: number | undefined;
|
|
52
|
+
idToken?: string | undefined;
|
|
53
|
+
}, {
|
|
54
|
+
t: "createRoom";
|
|
55
|
+
name?: string | undefined;
|
|
56
|
+
pv?: number | undefined;
|
|
57
|
+
idToken?: string | undefined;
|
|
58
|
+
}>, z.ZodObject<{
|
|
59
|
+
t: z.ZodLiteral<"joinRoom">;
|
|
60
|
+
code: z.ZodString;
|
|
61
|
+
name: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
62
|
+
pv: z.ZodOptional<z.ZodNumber>;
|
|
63
|
+
idToken: z.ZodOptional<z.ZodString>;
|
|
64
|
+
}, "strip", z.ZodTypeAny, {
|
|
65
|
+
code: string;
|
|
66
|
+
t: "joinRoom";
|
|
67
|
+
name: string;
|
|
68
|
+
pv?: number | undefined;
|
|
69
|
+
idToken?: string | undefined;
|
|
70
|
+
}, {
|
|
71
|
+
code: string;
|
|
72
|
+
t: "joinRoom";
|
|
73
|
+
name: string;
|
|
74
|
+
pv?: number | undefined;
|
|
75
|
+
idToken?: string | undefined;
|
|
76
|
+
}>, z.ZodObject<{
|
|
77
|
+
t: z.ZodLiteral<"resume">;
|
|
78
|
+
code: z.ZodString;
|
|
79
|
+
token: z.ZodString;
|
|
80
|
+
pv: z.ZodOptional<z.ZodNumber>;
|
|
81
|
+
idToken: z.ZodOptional<z.ZodString>;
|
|
82
|
+
}, "strip", z.ZodTypeAny, {
|
|
83
|
+
code: string;
|
|
84
|
+
t: "resume";
|
|
85
|
+
token: string;
|
|
86
|
+
pv?: number | undefined;
|
|
87
|
+
idToken?: string | undefined;
|
|
88
|
+
}, {
|
|
89
|
+
code: string;
|
|
90
|
+
t: "resume";
|
|
91
|
+
token: string;
|
|
92
|
+
pv?: number | undefined;
|
|
93
|
+
idToken?: string | undefined;
|
|
94
|
+
}>, z.ZodObject<{
|
|
95
|
+
t: z.ZodLiteral<"leaveRoom">;
|
|
96
|
+
}, "strip", z.ZodTypeAny, {
|
|
97
|
+
t: "leaveRoom";
|
|
98
|
+
}, {
|
|
99
|
+
t: "leaveRoom";
|
|
100
|
+
}>, z.ZodObject<{
|
|
101
|
+
t: z.ZodLiteral<"approve">;
|
|
102
|
+
playerId: z.ZodString;
|
|
103
|
+
}, "strip", z.ZodTypeAny, {
|
|
104
|
+
t: "approve";
|
|
105
|
+
playerId: string;
|
|
106
|
+
}, {
|
|
107
|
+
t: "approve";
|
|
108
|
+
playerId: string;
|
|
109
|
+
}>, z.ZodObject<{
|
|
110
|
+
t: z.ZodLiteral<"reject">;
|
|
111
|
+
playerId: z.ZodString;
|
|
112
|
+
}, "strip", z.ZodTypeAny, {
|
|
113
|
+
t: "reject";
|
|
114
|
+
playerId: string;
|
|
115
|
+
}, {
|
|
116
|
+
t: "reject";
|
|
117
|
+
playerId: string;
|
|
118
|
+
}>, z.ZodObject<{
|
|
119
|
+
t: z.ZodLiteral<"kick">;
|
|
120
|
+
playerId: z.ZodString;
|
|
121
|
+
}, "strip", z.ZodTypeAny, {
|
|
122
|
+
t: "kick";
|
|
123
|
+
playerId: string;
|
|
124
|
+
}, {
|
|
125
|
+
t: "kick";
|
|
126
|
+
playerId: string;
|
|
127
|
+
}>, z.ZodObject<{
|
|
128
|
+
t: z.ZodLiteral<"startGame">;
|
|
129
|
+
game: z.ZodOptional<z.ZodEnum<["trivia"]>>;
|
|
130
|
+
totalRounds: z.ZodNumber;
|
|
131
|
+
categoryIds: z.ZodArray<z.ZodString, "many">;
|
|
132
|
+
}, "strip", z.ZodTypeAny, {
|
|
133
|
+
t: "startGame";
|
|
134
|
+
totalRounds: number;
|
|
135
|
+
categoryIds: string[];
|
|
136
|
+
game?: "trivia" | undefined;
|
|
137
|
+
}, {
|
|
138
|
+
t: "startGame";
|
|
139
|
+
totalRounds: number;
|
|
140
|
+
categoryIds: string[];
|
|
141
|
+
game?: "trivia" | undefined;
|
|
142
|
+
}>, z.ZodObject<{
|
|
143
|
+
t: z.ZodLiteral<"pickLevel">;
|
|
144
|
+
categoryId: z.ZodString;
|
|
145
|
+
level: z.ZodNumber;
|
|
146
|
+
}, "strip", z.ZodTypeAny, {
|
|
147
|
+
t: "pickLevel";
|
|
148
|
+
categoryId: string;
|
|
149
|
+
level: number;
|
|
150
|
+
}, {
|
|
151
|
+
t: "pickLevel";
|
|
152
|
+
categoryId: string;
|
|
153
|
+
level: number;
|
|
154
|
+
}>, z.ZodObject<{
|
|
155
|
+
t: z.ZodLiteral<"nextTurn">;
|
|
156
|
+
}, "strip", z.ZodTypeAny, {
|
|
157
|
+
t: "nextTurn";
|
|
158
|
+
}, {
|
|
159
|
+
t: "nextTurn";
|
|
160
|
+
}>, z.ZodObject<{
|
|
161
|
+
t: z.ZodLiteral<"playAgain">;
|
|
162
|
+
}, "strip", z.ZodTypeAny, {
|
|
163
|
+
t: "playAgain";
|
|
164
|
+
}, {
|
|
165
|
+
t: "playAgain";
|
|
166
|
+
}>, z.ZodObject<{
|
|
167
|
+
t: z.ZodLiteral<"markOpen">;
|
|
168
|
+
correct: z.ZodBoolean;
|
|
169
|
+
}, "strip", z.ZodTypeAny, {
|
|
170
|
+
t: "markOpen";
|
|
171
|
+
correct: boolean;
|
|
172
|
+
}, {
|
|
173
|
+
t: "markOpen";
|
|
174
|
+
correct: boolean;
|
|
175
|
+
}>, z.ZodObject<{
|
|
176
|
+
t: z.ZodLiteral<"revealNoHost">;
|
|
177
|
+
}, "strip", z.ZodTypeAny, {
|
|
178
|
+
t: "revealNoHost";
|
|
179
|
+
}, {
|
|
180
|
+
t: "revealNoHost";
|
|
181
|
+
}>, z.ZodObject<{
|
|
182
|
+
t: z.ZodLiteral<"awardNoHost">;
|
|
183
|
+
teamIndex: z.ZodNumber;
|
|
184
|
+
}, "strip", z.ZodTypeAny, {
|
|
185
|
+
t: "awardNoHost";
|
|
186
|
+
teamIndex: number;
|
|
187
|
+
}, {
|
|
188
|
+
t: "awardNoHost";
|
|
189
|
+
teamIndex: number;
|
|
190
|
+
}>, z.ZodObject<{
|
|
191
|
+
t: z.ZodLiteral<"noHostNobody">;
|
|
192
|
+
}, "strip", z.ZodTypeAny, {
|
|
193
|
+
t: "noHostNobody";
|
|
194
|
+
}, {
|
|
195
|
+
t: "noHostNobody";
|
|
196
|
+
}>, z.ZodObject<{
|
|
197
|
+
t: z.ZodLiteral<"buzz">;
|
|
198
|
+
}, "strip", z.ZodTypeAny, {
|
|
199
|
+
t: "buzz";
|
|
200
|
+
}, {
|
|
201
|
+
t: "buzz";
|
|
202
|
+
}>, z.ZodObject<{
|
|
203
|
+
t: z.ZodLiteral<"answer">;
|
|
204
|
+
index: z.ZodNumber;
|
|
205
|
+
}, "strip", z.ZodTypeAny, {
|
|
206
|
+
t: "answer";
|
|
207
|
+
index: number;
|
|
208
|
+
}, {
|
|
209
|
+
t: "answer";
|
|
210
|
+
index: number;
|
|
211
|
+
}>, z.ZodObject<{
|
|
212
|
+
t: z.ZodLiteral<"useHelp">;
|
|
213
|
+
helpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
214
|
+
}, "strip", z.ZodTypeAny, {
|
|
215
|
+
t: "useHelp";
|
|
216
|
+
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
217
|
+
}, {
|
|
218
|
+
t: "useHelp";
|
|
219
|
+
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
220
|
+
}>, z.ZodObject<{
|
|
221
|
+
t: z.ZodLiteral<"sabotage">;
|
|
222
|
+
helpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
223
|
+
targetTeam: z.ZodNumber;
|
|
224
|
+
}, "strip", z.ZodTypeAny, {
|
|
225
|
+
t: "sabotage";
|
|
226
|
+
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
227
|
+
targetTeam: number;
|
|
228
|
+
}, {
|
|
229
|
+
t: "sabotage";
|
|
230
|
+
helpType: "removeTwoAnswers" | "changeQuestion" | "extraTime" | "doublePoints" | "stealPoints" | "restPlayer" | "tripLevel";
|
|
231
|
+
targetTeam: number;
|
|
232
|
+
}>]>;
|
|
233
|
+
type ClientMsg = z.infer<typeof ClientMsg>;
|
|
234
|
+
interface LobbyPlayer {
|
|
235
|
+
id: string;
|
|
236
|
+
name: string;
|
|
237
|
+
approved: boolean;
|
|
238
|
+
connected: boolean;
|
|
239
|
+
}
|
|
240
|
+
/** Per-recipient context attached to lobby/state pushes. */
|
|
241
|
+
interface YouContext {
|
|
242
|
+
id: string;
|
|
243
|
+
isHost: boolean;
|
|
244
|
+
/** This player's team index once the game has started; null in the lobby / for the host. */
|
|
245
|
+
teamIndex: number | null;
|
|
246
|
+
}
|
|
247
|
+
type GameStatus = 'setup' | 'categorySelection' | 'inProgress' | 'completed';
|
|
248
|
+
type GameplayFlow = 'classic' | 'openJudged' | 'noHost' | 'buzzer';
|
|
249
|
+
type BuzzerGameMode = 'off' | 'solo' | 'race' | 'controller';
|
|
250
|
+
/** The help/lifeline ids (same set as the inbound `HelpType` zod enum above). */
|
|
251
|
+
type HelpName = z.infer<typeof HelpType>;
|
|
252
|
+
type SoundName = 'correct' | 'wrong' | 'timeup' | 'warning' | 'help' | 'ding' | 'pop' | 'swoosh';
|
|
253
|
+
interface TeamView {
|
|
254
|
+
id: string;
|
|
255
|
+
name: string;
|
|
256
|
+
emoji: string;
|
|
257
|
+
color: string;
|
|
258
|
+
score: number;
|
|
259
|
+
currentStreak: number;
|
|
260
|
+
/** This team's lifelines and whether each has been spent. */
|
|
261
|
+
helps: {
|
|
262
|
+
type: HelpName;
|
|
263
|
+
isUsed: boolean;
|
|
264
|
+
}[];
|
|
265
|
+
}
|
|
266
|
+
interface QuestionView {
|
|
267
|
+
text: string;
|
|
268
|
+
options: string[];
|
|
269
|
+
hiddenOptionIndices: number[];
|
|
270
|
+
revealed: boolean;
|
|
271
|
+
selectedAnswerIndex: number | null;
|
|
272
|
+
/** Only present once revealed (anti-cheat). Null while the question is live. */
|
|
273
|
+
correctAnswerIndex: number | null;
|
|
274
|
+
imageName?: string;
|
|
275
|
+
videoName?: string;
|
|
276
|
+
}
|
|
277
|
+
interface GameStateView {
|
|
278
|
+
status: GameStatus;
|
|
279
|
+
flow: GameplayFlow;
|
|
280
|
+
mode: BuzzerGameMode;
|
|
281
|
+
round: number;
|
|
282
|
+
totalRounds: number;
|
|
283
|
+
currentTeamIndex: number;
|
|
284
|
+
answeringTeamIndex: number;
|
|
285
|
+
teams: TeamView[];
|
|
286
|
+
question: QuestionView | null;
|
|
287
|
+
/** Race: the team that buzzed first (null otherwise). */
|
|
288
|
+
firstBuzzTeamIndex: number | null;
|
|
289
|
+
lockedOutTeamIndices: number[];
|
|
290
|
+
isOpenStealIntro: boolean;
|
|
291
|
+
doublePointsActive: boolean;
|
|
292
|
+
stealPointsActive: boolean;
|
|
293
|
+
trippedTeamIndices: number[];
|
|
294
|
+
/** Shared board: per category id, the level numbers already played. */
|
|
295
|
+
usedLevels: Record<string, number[]>;
|
|
296
|
+
}
|
|
297
|
+
/** Transient presentation effects the relay emits; clients play them once. */
|
|
298
|
+
type Effect = {
|
|
299
|
+
t: 'stopTimer';
|
|
300
|
+
} | {
|
|
301
|
+
t: 'startTimer';
|
|
302
|
+
duration: number;
|
|
303
|
+
} | {
|
|
304
|
+
t: 'resumeTimer';
|
|
305
|
+
} | {
|
|
306
|
+
t: 'addTime';
|
|
307
|
+
seconds: number;
|
|
308
|
+
} | {
|
|
309
|
+
t: 'closeBuzzer';
|
|
310
|
+
} | {
|
|
311
|
+
t: 'openBuzzer';
|
|
312
|
+
} | {
|
|
313
|
+
t: 'serveQuestion';
|
|
314
|
+
} | {
|
|
315
|
+
t: 'sound';
|
|
316
|
+
name: SoundName;
|
|
317
|
+
} | {
|
|
318
|
+
t: 'reveal';
|
|
319
|
+
} | {
|
|
320
|
+
t: 'celebrate';
|
|
321
|
+
hard: boolean;
|
|
322
|
+
} | {
|
|
323
|
+
t: 'scorePop';
|
|
324
|
+
amount: number;
|
|
325
|
+
streakBonus: number;
|
|
326
|
+
} | {
|
|
327
|
+
t: 'chargeCredit';
|
|
328
|
+
} | {
|
|
329
|
+
t: 'stealPause';
|
|
330
|
+
ms: number;
|
|
331
|
+
} | {
|
|
332
|
+
t: 'openStealIntro';
|
|
333
|
+
ms: number;
|
|
334
|
+
} | {
|
|
335
|
+
t: 'noHostAdvance';
|
|
336
|
+
ms: number;
|
|
337
|
+
} | {
|
|
338
|
+
t: 'endGame';
|
|
339
|
+
};
|
|
340
|
+
type ServerMsg = {
|
|
341
|
+
t: 'roomCreated';
|
|
342
|
+
code: string;
|
|
343
|
+
youId: string;
|
|
344
|
+
token: string;
|
|
345
|
+
pv: number;
|
|
346
|
+
} | {
|
|
347
|
+
t: 'joinPending';
|
|
348
|
+
} | {
|
|
349
|
+
t: 'joinRequest';
|
|
350
|
+
playerId: string;
|
|
351
|
+
name: string;
|
|
352
|
+
} | {
|
|
353
|
+
t: 'joined';
|
|
354
|
+
youId: string;
|
|
355
|
+
token: string;
|
|
356
|
+
pv: number;
|
|
357
|
+
} | {
|
|
358
|
+
t: 'joinRejected';
|
|
359
|
+
reason: string;
|
|
360
|
+
} | {
|
|
361
|
+
t: 'kicked';
|
|
362
|
+
} | {
|
|
363
|
+
t: 'roomClosed';
|
|
364
|
+
reason: string;
|
|
365
|
+
} | {
|
|
366
|
+
t: 'lobby';
|
|
367
|
+
code: string;
|
|
368
|
+
gameId: GameId;
|
|
369
|
+
hostId: string;
|
|
370
|
+
hostName: string;
|
|
371
|
+
players: LobbyPlayer[];
|
|
372
|
+
categories: {
|
|
373
|
+
id: string;
|
|
374
|
+
name: string;
|
|
375
|
+
packId: string;
|
|
376
|
+
packName: string;
|
|
377
|
+
icon: string;
|
|
378
|
+
hasImage: boolean;
|
|
379
|
+
}[];
|
|
380
|
+
you: YouContext;
|
|
381
|
+
} | {
|
|
382
|
+
t: 'state';
|
|
383
|
+
gameId: GameId;
|
|
384
|
+
view: GameStateView;
|
|
385
|
+
you: YouContext;
|
|
386
|
+
teamConnected: boolean[];
|
|
387
|
+
} | {
|
|
388
|
+
t: 'fx';
|
|
389
|
+
effects: Effect[];
|
|
390
|
+
} | {
|
|
391
|
+
t: 'error';
|
|
392
|
+
code: string;
|
|
393
|
+
message: string;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
export { type BuzzerGameMode, ClientMsg, type Effect, GAME_IDS, type GameId, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type ServerMsg, type SoundName, type TeamView, type YouContext };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var LIMITS = {
|
|
4
|
+
MAX_NAME_LEN: 20,
|
|
5
|
+
ROOM_CODE_LEN: 4,
|
|
6
|
+
MAX_PLAYERS: 12,
|
|
7
|
+
MAX_MSG_PER_SEC: 8
|
|
8
|
+
};
|
|
9
|
+
var PROTOCOL_VERSION = 1;
|
|
10
|
+
var GAME_IDS = ["trivia"];
|
|
11
|
+
var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
12
|
+
var UNSAFE_CHARS = /[\p{Cc}<>]/gu;
|
|
13
|
+
var DisplayName = z.string().trim().min(1).max(LIMITS.MAX_NAME_LEN).transform((s) => s.replace(UNSAFE_CHARS, "").trim()).refine((s) => s.length >= 1, "name is empty after sanitizing");
|
|
14
|
+
var RoomCode = z.string().trim().toUpperCase().regex(new RegExp(`^[${ROOM_ALPHABET}]{${LIMITS.ROOM_CODE_LEN}}$`), "bad room code");
|
|
15
|
+
var PlayerId = z.string().max(40);
|
|
16
|
+
var HelpType = z.enum([
|
|
17
|
+
"removeTwoAnswers",
|
|
18
|
+
"changeQuestion",
|
|
19
|
+
"extraTime",
|
|
20
|
+
"doublePoints",
|
|
21
|
+
"stealPoints",
|
|
22
|
+
"restPlayer",
|
|
23
|
+
"tripLevel"
|
|
24
|
+
]);
|
|
25
|
+
var Pv = z.number().int().min(1).max(1e6).optional();
|
|
26
|
+
var IdToken = z.string().max(4096).optional();
|
|
27
|
+
var ClientMsg = z.discriminatedUnion("t", [
|
|
28
|
+
// room / lobby
|
|
29
|
+
z.object({ t: z.literal("createRoom"), name: DisplayName.optional(), pv: Pv, idToken: IdToken }),
|
|
30
|
+
z.object({ t: z.literal("joinRoom"), code: RoomCode, name: DisplayName, pv: Pv, idToken: IdToken }),
|
|
31
|
+
z.object({ t: z.literal("resume"), code: RoomCode, token: z.string().max(120), pv: Pv, idToken: IdToken }),
|
|
32
|
+
z.object({ t: z.literal("leaveRoom") }),
|
|
33
|
+
z.object({ t: z.literal("approve"), playerId: PlayerId }),
|
|
34
|
+
z.object({ t: z.literal("reject"), playerId: PlayerId }),
|
|
35
|
+
z.object({ t: z.literal("kick"), playerId: PlayerId }),
|
|
36
|
+
// host game actions
|
|
37
|
+
z.object({
|
|
38
|
+
t: z.literal("startGame"),
|
|
39
|
+
game: z.enum(GAME_IDS).optional(),
|
|
40
|
+
// which game to play; defaults to 'trivia'
|
|
41
|
+
totalRounds: z.number().int().min(1).max(50),
|
|
42
|
+
categoryIds: z.array(z.string().max(60)).min(1).max(12)
|
|
43
|
+
}),
|
|
44
|
+
z.object({ t: z.literal("pickLevel"), categoryId: z.string().max(60), level: z.number().int().min(1).max(6) }),
|
|
45
|
+
z.object({ t: z.literal("nextTurn") }),
|
|
46
|
+
z.object({ t: z.literal("playAgain") }),
|
|
47
|
+
// host: after a game ends, return the SAME players to the lobby
|
|
48
|
+
z.object({ t: z.literal("markOpen"), correct: z.boolean() }),
|
|
49
|
+
z.object({ t: z.literal("revealNoHost") }),
|
|
50
|
+
z.object({ t: z.literal("awardNoHost"), teamIndex: z.number().int().min(0).max(LIMITS.MAX_PLAYERS) }),
|
|
51
|
+
z.object({ t: z.literal("noHostNobody") }),
|
|
52
|
+
// player actions
|
|
53
|
+
z.object({ t: z.literal("buzz") }),
|
|
54
|
+
z.object({ t: z.literal("answer"), index: z.number().int().min(0).max(10) }),
|
|
55
|
+
z.object({ t: z.literal("useHelp"), helpType: HelpType }),
|
|
56
|
+
// sabotage a rival (active team only): spend a sabotage help to debuff targetTeam's next question
|
|
57
|
+
z.object({ t: z.literal("sabotage"), helpType: HelpType, targetTeam: z.number().int().min(0).max(LIMITS.MAX_PLAYERS) })
|
|
58
|
+
]);
|
|
59
|
+
export {
|
|
60
|
+
ClientMsg,
|
|
61
|
+
GAME_IDS,
|
|
62
|
+
LIMITS,
|
|
63
|
+
PROTOCOL_VERSION,
|
|
64
|
+
ROOM_ALPHABET
|
|
65
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thegeem/protocol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Geem's wire protocol — shared zod schemas + TypeScript types for talking to the Geem game server over Socket.IO. The source of truth all clients implement against.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"homepage": "https://geem.tv",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"geem",
|
|
24
|
+
"protocol",
|
|
25
|
+
"socket.io",
|
|
26
|
+
"zod"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"release": "node scripts/release.mjs"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"zod": "^3.23.8"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.5.4"
|
|
38
|
+
},
|
|
39
|
+
"module": "./dist/index.js",
|
|
40
|
+
"types": "./dist/index.d.ts"
|
|
41
|
+
}
|