@thegeem/protocol 0.1.9 → 0.1.11
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/dist/index.cjs +28 -5
- package/dist/index.d.cts +50 -4
- package/dist/index.d.ts +50 -4
- package/dist/index.js +24 -4
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -23,8 +23,11 @@ __export(index_exports, {
|
|
|
23
23
|
ClientMsg: () => ClientMsg,
|
|
24
24
|
GAME_IDS: () => GAME_IDS,
|
|
25
25
|
LIMITS: () => LIMITS,
|
|
26
|
+
MODES: () => MODES,
|
|
27
|
+
MODE_IDS: () => MODE_IDS,
|
|
26
28
|
PROTOCOL_VERSION: () => PROTOCOL_VERSION,
|
|
27
|
-
ROOM_ALPHABET: () => ROOM_ALPHABET
|
|
29
|
+
ROOM_ALPHABET: () => ROOM_ALPHABET,
|
|
30
|
+
modesForGame: () => modesForGame
|
|
28
31
|
});
|
|
29
32
|
module.exports = __toCommonJS(index_exports);
|
|
30
33
|
var import_zod = require("zod");
|
|
@@ -36,6 +39,14 @@ var LIMITS = {
|
|
|
36
39
|
};
|
|
37
40
|
var PROTOCOL_VERSION = 1;
|
|
38
41
|
var GAME_IDS = ["trivia"];
|
|
42
|
+
var MODE_IDS = ["solo", "oneDevice", "snag", "multiplayer"];
|
|
43
|
+
var MODES = [
|
|
44
|
+
{ id: "solo", gameId: "trivia", label: "Solo", onlineEligible: false },
|
|
45
|
+
{ id: "oneDevice", gameId: "trivia", label: "One Device", onlineEligible: false },
|
|
46
|
+
{ id: "snag", gameId: "trivia", label: "Snag", onlineEligible: true },
|
|
47
|
+
{ id: "multiplayer", gameId: "trivia", label: "Multiplayer", onlineEligible: true }
|
|
48
|
+
];
|
|
49
|
+
var modesForGame = (gameId) => MODES.filter((m) => m.gameId === gameId);
|
|
39
50
|
var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
40
51
|
var UNSAFE_CHARS = /[\p{Cc}<>]/gu;
|
|
41
52
|
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");
|
|
@@ -52,18 +63,19 @@ var HelpType = import_zod.z.enum([
|
|
|
52
63
|
]);
|
|
53
64
|
var Pv = import_zod.z.number().int().min(1).max(1e6).optional();
|
|
54
65
|
var IdToken = import_zod.z.string().max(4096).optional();
|
|
66
|
+
var Platform = import_zod.z.enum(["web", "ios", "android"]).optional();
|
|
55
67
|
var Avatar = import_zod.z.object({
|
|
56
68
|
emoji: import_zod.z.string().min(1).max(16).transform((s) => s.replace(UNSAFE_CHARS, "")).refine((s) => s.length >= 1, "empty emoji"),
|
|
57
69
|
color: import_zod.z.string().regex(/^#[0-9a-fA-F]{6}$/, "color must be #rrggbb")
|
|
58
70
|
});
|
|
59
71
|
var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
60
72
|
// room / lobby
|
|
61
|
-
import_zod.z.object({ t: import_zod.z.literal("createRoom"), name: DisplayName.optional(), avatar: Avatar.optional(), pv: Pv, idToken: IdToken }),
|
|
73
|
+
import_zod.z.object({ t: import_zod.z.literal("createRoom"), name: DisplayName.optional(), avatar: Avatar.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
|
|
62
74
|
// Host sets/changes their display name AFTER createRoom (for anonymous one-tap
|
|
63
75
|
// hosts who started nameless). Host-only; the relay re-broadcasts the lobby.
|
|
64
76
|
import_zod.z.object({ t: import_zod.z.literal("renameHost"), name: DisplayName }),
|
|
65
|
-
import_zod.z.object({ t: import_zod.z.literal("joinRoom"), code: RoomCode, name: DisplayName, avatar: Avatar.optional(), pv: Pv, idToken: IdToken }),
|
|
66
|
-
import_zod.z.object({ t: import_zod.z.literal("resume"), code: RoomCode, token: import_zod.z.string().max(120), avatar: Avatar.optional(), pv: Pv, idToken: IdToken }),
|
|
77
|
+
import_zod.z.object({ t: import_zod.z.literal("joinRoom"), code: RoomCode, name: DisplayName, avatar: Avatar.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
|
|
78
|
+
import_zod.z.object({ t: import_zod.z.literal("resume"), code: RoomCode, token: import_zod.z.string().max(120), avatar: Avatar.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
|
|
67
79
|
import_zod.z.object({ t: import_zod.z.literal("leaveRoom") }),
|
|
68
80
|
import_zod.z.object({ t: import_zod.z.literal("approve"), playerId: PlayerId }),
|
|
69
81
|
import_zod.z.object({ t: import_zod.z.literal("reject"), playerId: PlayerId }),
|
|
@@ -75,6 +87,10 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
|
75
87
|
import_zod.z.object({ t: import_zod.z.literal("setTeams"), teams: import_zod.z.array(DisplayName).min(1).max(LIMITS.MAX_PLAYERS) }),
|
|
76
88
|
// A player joins team slot `teamIndex` (0-based, into the host's `setTeams` list).
|
|
77
89
|
import_zod.z.object({ t: import_zod.z.literal("pickTeam"), teamIndex: import_zod.z.number().int().min(0).max(LIMITS.MAX_PLAYERS - 1) }),
|
|
90
|
+
// Host's IN-PROGRESS mode selection in the lobby — a live preview so waiting players see which mode
|
|
91
|
+
// is coming before start. Distinct from startGame.modeId (which commits it); the relay defaults
|
|
92
|
+
// startGame to this when modeId is omitted. Re-broadcasts the lobby.
|
|
93
|
+
import_zod.z.object({ t: import_zod.z.literal("setMode"), modeId: import_zod.z.enum(MODE_IDS) }),
|
|
78
94
|
// Host signals it's busy configuring the game (categories/rounds/mode) so waiting players can show
|
|
79
95
|
// a "host is setting up…" state instead of a frozen-looking lobby. 'idle' when the settings close.
|
|
80
96
|
import_zod.z.object({ t: import_zod.z.literal("hostActivity"), activity: import_zod.z.enum(["configuring", "idle"]) }),
|
|
@@ -85,6 +101,10 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
|
85
101
|
// which game to play; defaults to 'trivia'
|
|
86
102
|
totalRounds: import_zod.z.number().int().min(1).max(50),
|
|
87
103
|
categoryIds: import_zod.z.array(import_zod.z.string().max(60)).min(1).max(12),
|
|
104
|
+
// PREFERRED: pick a mode from the registry (see MODES). The server maps it to the engine
|
|
105
|
+
// rules. Send this and omit buzzerMode/noHost. Falls back to the legacy flags below if omitted.
|
|
106
|
+
modeId: import_zod.z.enum(MODE_IDS).optional(),
|
|
107
|
+
// LEGACY (pre-modeId clients) — the raw engine flags. Ignored when modeId is present.
|
|
88
108
|
// How to play (maps to the engine). Omit → controller (each player answers from
|
|
89
109
|
// their own device). 'off' = one device drives (Me-Controller); pair with noHost.
|
|
90
110
|
// solo=Solo · race=Snag/buzz · controller=each-player-from-device · off=Me-Controller
|
|
@@ -120,6 +140,9 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
|
|
|
120
140
|
ClientMsg,
|
|
121
141
|
GAME_IDS,
|
|
122
142
|
LIMITS,
|
|
143
|
+
MODES,
|
|
144
|
+
MODE_IDS,
|
|
123
145
|
PROTOCOL_VERSION,
|
|
124
|
-
ROOM_ALPHABET
|
|
146
|
+
ROOM_ALPHABET,
|
|
147
|
+
modesForGame
|
|
125
148
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -37,6 +37,26 @@ declare const PROTOCOL_VERSION = 1;
|
|
|
37
37
|
*/
|
|
38
38
|
declare const GAME_IDS: readonly ["trivia"];
|
|
39
39
|
type GameId = (typeof GAME_IDS)[number];
|
|
40
|
+
/**
|
|
41
|
+
* The MODE REGISTRY — the list of ways to play each game. A client renders its mode picker from this
|
|
42
|
+
* and sends the chosen `modeId` on `startGame`; the SERVER owns the modeId → engine-rules mapping (the
|
|
43
|
+
* flags are an internal relay detail, not the client's job). Adding a mode = one entry here. Game #2
|
|
44
|
+
* gets its own `gameId` rows. `label` is a short English label — clients may localize by `id` (Arabic
|
|
45
|
+
* UI is Kuwaiti). `onlineEligible` = a finished game in this mode counts toward the global leaderboard
|
|
46
|
+
* (only server-arbitrated, multi-device modes; single-device/solo are local-only — the host controls
|
|
47
|
+
* input, so the server can't trust the score).
|
|
48
|
+
*/
|
|
49
|
+
declare const MODE_IDS: readonly ["solo", "oneDevice", "snag", "multiplayer"];
|
|
50
|
+
type ModeId = (typeof MODE_IDS)[number];
|
|
51
|
+
interface ModeInfo {
|
|
52
|
+
id: ModeId;
|
|
53
|
+
gameId: GameId;
|
|
54
|
+
label: string;
|
|
55
|
+
onlineEligible: boolean;
|
|
56
|
+
}
|
|
57
|
+
declare const MODES: readonly ModeInfo[];
|
|
58
|
+
/** The modes available for a given game (for a client's mode picker). */
|
|
59
|
+
declare const modesForGame: (gameId: GameId) => ModeInfo[];
|
|
40
60
|
/** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
|
|
41
61
|
declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
42
62
|
declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
@@ -55,6 +75,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
55
75
|
}>>;
|
|
56
76
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
57
77
|
idToken: z.ZodOptional<z.ZodString>;
|
|
78
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
58
79
|
}, "strip", z.ZodTypeAny, {
|
|
59
80
|
t: "createRoom";
|
|
60
81
|
name?: string | undefined;
|
|
@@ -64,6 +85,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
64
85
|
} | undefined;
|
|
65
86
|
pv?: number | undefined;
|
|
66
87
|
idToken?: string | undefined;
|
|
88
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
67
89
|
}, {
|
|
68
90
|
t: "createRoom";
|
|
69
91
|
name?: string | undefined;
|
|
@@ -73,6 +95,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
73
95
|
} | undefined;
|
|
74
96
|
pv?: number | undefined;
|
|
75
97
|
idToken?: string | undefined;
|
|
98
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
76
99
|
}>, z.ZodObject<{
|
|
77
100
|
t: z.ZodLiteral<"renameHost">;
|
|
78
101
|
name: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
@@ -98,6 +121,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
98
121
|
}>>;
|
|
99
122
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
100
123
|
idToken: z.ZodOptional<z.ZodString>;
|
|
124
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
101
125
|
}, "strip", z.ZodTypeAny, {
|
|
102
126
|
code: string;
|
|
103
127
|
t: "joinRoom";
|
|
@@ -108,6 +132,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
108
132
|
} | undefined;
|
|
109
133
|
pv?: number | undefined;
|
|
110
134
|
idToken?: string | undefined;
|
|
135
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
111
136
|
}, {
|
|
112
137
|
code: string;
|
|
113
138
|
t: "joinRoom";
|
|
@@ -118,6 +143,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
118
143
|
} | undefined;
|
|
119
144
|
pv?: number | undefined;
|
|
120
145
|
idToken?: string | undefined;
|
|
146
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
121
147
|
}>, z.ZodObject<{
|
|
122
148
|
t: z.ZodLiteral<"resume">;
|
|
123
149
|
code: z.ZodString;
|
|
@@ -134,6 +160,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
134
160
|
}>>;
|
|
135
161
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
136
162
|
idToken: z.ZodOptional<z.ZodString>;
|
|
163
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
137
164
|
}, "strip", z.ZodTypeAny, {
|
|
138
165
|
code: string;
|
|
139
166
|
t: "resume";
|
|
@@ -144,6 +171,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
144
171
|
} | undefined;
|
|
145
172
|
pv?: number | undefined;
|
|
146
173
|
idToken?: string | undefined;
|
|
174
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
147
175
|
}, {
|
|
148
176
|
code: string;
|
|
149
177
|
t: "resume";
|
|
@@ -154,6 +182,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
154
182
|
} | undefined;
|
|
155
183
|
pv?: number | undefined;
|
|
156
184
|
idToken?: string | undefined;
|
|
185
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
157
186
|
}>, z.ZodObject<{
|
|
158
187
|
t: z.ZodLiteral<"leaveRoom">;
|
|
159
188
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -214,6 +243,15 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
214
243
|
}, {
|
|
215
244
|
t: "pickTeam";
|
|
216
245
|
teamIndex: number;
|
|
246
|
+
}>, z.ZodObject<{
|
|
247
|
+
t: z.ZodLiteral<"setMode">;
|
|
248
|
+
modeId: z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>;
|
|
249
|
+
}, "strip", z.ZodTypeAny, {
|
|
250
|
+
t: "setMode";
|
|
251
|
+
modeId: "solo" | "oneDevice" | "snag" | "multiplayer";
|
|
252
|
+
}, {
|
|
253
|
+
t: "setMode";
|
|
254
|
+
modeId: "solo" | "oneDevice" | "snag" | "multiplayer";
|
|
217
255
|
}>, z.ZodObject<{
|
|
218
256
|
t: z.ZodLiteral<"hostActivity">;
|
|
219
257
|
activity: z.ZodEnum<["configuring", "idle"]>;
|
|
@@ -228,21 +266,24 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
228
266
|
game: z.ZodOptional<z.ZodEnum<["trivia"]>>;
|
|
229
267
|
totalRounds: z.ZodNumber;
|
|
230
268
|
categoryIds: z.ZodArray<z.ZodString, "many">;
|
|
269
|
+
modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
|
|
231
270
|
buzzerMode: z.ZodOptional<z.ZodEnum<["off", "solo", "race", "controller"]>>;
|
|
232
271
|
noHost: z.ZodOptional<z.ZodBoolean>;
|
|
233
272
|
}, "strip", z.ZodTypeAny, {
|
|
234
273
|
t: "startGame";
|
|
235
274
|
categoryIds: string[];
|
|
236
275
|
totalRounds: number;
|
|
276
|
+
modeId?: "solo" | "oneDevice" | "snag" | "multiplayer" | undefined;
|
|
237
277
|
game?: "trivia" | undefined;
|
|
238
|
-
buzzerMode?: "
|
|
278
|
+
buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
|
|
239
279
|
noHost?: boolean | undefined;
|
|
240
280
|
}, {
|
|
241
281
|
t: "startGame";
|
|
242
282
|
categoryIds: string[];
|
|
243
283
|
totalRounds: number;
|
|
284
|
+
modeId?: "solo" | "oneDevice" | "snag" | "multiplayer" | undefined;
|
|
244
285
|
game?: "trivia" | undefined;
|
|
245
|
-
buzzerMode?: "
|
|
286
|
+
buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
|
|
246
287
|
noHost?: boolean | undefined;
|
|
247
288
|
}>, z.ZodObject<{
|
|
248
289
|
t: z.ZodLiteral<"pickLevel">;
|
|
@@ -383,6 +424,8 @@ interface CategoryView {
|
|
|
383
424
|
packName: string;
|
|
384
425
|
icon: string;
|
|
385
426
|
hasImage: boolean;
|
|
427
|
+
/** Category's brand color (hex, e.g. '#E8473F') for board coloring; absent if the category has none. */
|
|
428
|
+
color?: string;
|
|
386
429
|
}
|
|
387
430
|
type GameStatus = 'setup' | 'categorySelection' | 'inProgress' | 'completed';
|
|
388
431
|
type GameplayFlow = 'classic' | 'openJudged' | 'noHost' | 'buzzer';
|
|
@@ -402,11 +445,13 @@ interface TeamView {
|
|
|
402
445
|
type: HelpName;
|
|
403
446
|
isUsed: boolean;
|
|
404
447
|
}[];
|
|
405
|
-
/** The team's players in rotation order
|
|
448
|
+
/** The team's players in rotation order (`connected` = that player's socket is online right now).
|
|
449
|
+
* Empty for host-run single-device teams. */
|
|
406
450
|
players: {
|
|
407
451
|
id: string;
|
|
408
452
|
name: string;
|
|
409
453
|
emoji: string;
|
|
454
|
+
connected: boolean;
|
|
410
455
|
}[];
|
|
411
456
|
/** The player on THIS team whose turn it is to answer (rotates each turn); null if the team has
|
|
412
457
|
* no seated players. In controller mode only this player may `answer` on the team's turn. */
|
|
@@ -531,6 +576,7 @@ type ServerMsg = {
|
|
|
531
576
|
selectedCategoryIds: string[];
|
|
532
577
|
teams: TeamSlotView[];
|
|
533
578
|
hostActivity: 'configuring' | 'idle';
|
|
579
|
+
modeId: ModeId | null;
|
|
534
580
|
you: YouContext;
|
|
535
581
|
} | {
|
|
536
582
|
t: 'state';
|
|
@@ -549,4 +595,4 @@ type ServerMsg = {
|
|
|
549
595
|
message: string;
|
|
550
596
|
};
|
|
551
597
|
|
|
552
|
-
export { type AvatarView, type BuzzerGameMode, type CategoryView, 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 TeamSlotView, type TeamView, type YouContext };
|
|
598
|
+
export { type AvatarView, type BuzzerGameMode, type CategoryView, ClientMsg, type Effect, GAME_IDS, type GameId, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type ServerMsg, type SoundName, type TeamSlotView, type TeamView, type YouContext, modesForGame };
|
package/dist/index.d.ts
CHANGED
|
@@ -37,6 +37,26 @@ declare const PROTOCOL_VERSION = 1;
|
|
|
37
37
|
*/
|
|
38
38
|
declare const GAME_IDS: readonly ["trivia"];
|
|
39
39
|
type GameId = (typeof GAME_IDS)[number];
|
|
40
|
+
/**
|
|
41
|
+
* The MODE REGISTRY — the list of ways to play each game. A client renders its mode picker from this
|
|
42
|
+
* and sends the chosen `modeId` on `startGame`; the SERVER owns the modeId → engine-rules mapping (the
|
|
43
|
+
* flags are an internal relay detail, not the client's job). Adding a mode = one entry here. Game #2
|
|
44
|
+
* gets its own `gameId` rows. `label` is a short English label — clients may localize by `id` (Arabic
|
|
45
|
+
* UI is Kuwaiti). `onlineEligible` = a finished game in this mode counts toward the global leaderboard
|
|
46
|
+
* (only server-arbitrated, multi-device modes; single-device/solo are local-only — the host controls
|
|
47
|
+
* input, so the server can't trust the score).
|
|
48
|
+
*/
|
|
49
|
+
declare const MODE_IDS: readonly ["solo", "oneDevice", "snag", "multiplayer"];
|
|
50
|
+
type ModeId = (typeof MODE_IDS)[number];
|
|
51
|
+
interface ModeInfo {
|
|
52
|
+
id: ModeId;
|
|
53
|
+
gameId: GameId;
|
|
54
|
+
label: string;
|
|
55
|
+
onlineEligible: boolean;
|
|
56
|
+
}
|
|
57
|
+
declare const MODES: readonly ModeInfo[];
|
|
58
|
+
/** The modes available for a given game (for a client's mode picker). */
|
|
59
|
+
declare const modesForGame: (gameId: GameId) => ModeInfo[];
|
|
40
60
|
/** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
|
|
41
61
|
declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
42
62
|
declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
|
|
@@ -55,6 +75,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
55
75
|
}>>;
|
|
56
76
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
57
77
|
idToken: z.ZodOptional<z.ZodString>;
|
|
78
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
58
79
|
}, "strip", z.ZodTypeAny, {
|
|
59
80
|
t: "createRoom";
|
|
60
81
|
name?: string | undefined;
|
|
@@ -64,6 +85,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
64
85
|
} | undefined;
|
|
65
86
|
pv?: number | undefined;
|
|
66
87
|
idToken?: string | undefined;
|
|
88
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
67
89
|
}, {
|
|
68
90
|
t: "createRoom";
|
|
69
91
|
name?: string | undefined;
|
|
@@ -73,6 +95,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
73
95
|
} | undefined;
|
|
74
96
|
pv?: number | undefined;
|
|
75
97
|
idToken?: string | undefined;
|
|
98
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
76
99
|
}>, z.ZodObject<{
|
|
77
100
|
t: z.ZodLiteral<"renameHost">;
|
|
78
101
|
name: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
|
|
@@ -98,6 +121,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
98
121
|
}>>;
|
|
99
122
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
100
123
|
idToken: z.ZodOptional<z.ZodString>;
|
|
124
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
101
125
|
}, "strip", z.ZodTypeAny, {
|
|
102
126
|
code: string;
|
|
103
127
|
t: "joinRoom";
|
|
@@ -108,6 +132,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
108
132
|
} | undefined;
|
|
109
133
|
pv?: number | undefined;
|
|
110
134
|
idToken?: string | undefined;
|
|
135
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
111
136
|
}, {
|
|
112
137
|
code: string;
|
|
113
138
|
t: "joinRoom";
|
|
@@ -118,6 +143,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
118
143
|
} | undefined;
|
|
119
144
|
pv?: number | undefined;
|
|
120
145
|
idToken?: string | undefined;
|
|
146
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
121
147
|
}>, z.ZodObject<{
|
|
122
148
|
t: z.ZodLiteral<"resume">;
|
|
123
149
|
code: z.ZodString;
|
|
@@ -134,6 +160,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
134
160
|
}>>;
|
|
135
161
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
136
162
|
idToken: z.ZodOptional<z.ZodString>;
|
|
163
|
+
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
|
|
137
164
|
}, "strip", z.ZodTypeAny, {
|
|
138
165
|
code: string;
|
|
139
166
|
t: "resume";
|
|
@@ -144,6 +171,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
144
171
|
} | undefined;
|
|
145
172
|
pv?: number | undefined;
|
|
146
173
|
idToken?: string | undefined;
|
|
174
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
147
175
|
}, {
|
|
148
176
|
code: string;
|
|
149
177
|
t: "resume";
|
|
@@ -154,6 +182,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
154
182
|
} | undefined;
|
|
155
183
|
pv?: number | undefined;
|
|
156
184
|
idToken?: string | undefined;
|
|
185
|
+
platform?: "web" | "ios" | "android" | undefined;
|
|
157
186
|
}>, z.ZodObject<{
|
|
158
187
|
t: z.ZodLiteral<"leaveRoom">;
|
|
159
188
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -214,6 +243,15 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
214
243
|
}, {
|
|
215
244
|
t: "pickTeam";
|
|
216
245
|
teamIndex: number;
|
|
246
|
+
}>, z.ZodObject<{
|
|
247
|
+
t: z.ZodLiteral<"setMode">;
|
|
248
|
+
modeId: z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>;
|
|
249
|
+
}, "strip", z.ZodTypeAny, {
|
|
250
|
+
t: "setMode";
|
|
251
|
+
modeId: "solo" | "oneDevice" | "snag" | "multiplayer";
|
|
252
|
+
}, {
|
|
253
|
+
t: "setMode";
|
|
254
|
+
modeId: "solo" | "oneDevice" | "snag" | "multiplayer";
|
|
217
255
|
}>, z.ZodObject<{
|
|
218
256
|
t: z.ZodLiteral<"hostActivity">;
|
|
219
257
|
activity: z.ZodEnum<["configuring", "idle"]>;
|
|
@@ -228,21 +266,24 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
228
266
|
game: z.ZodOptional<z.ZodEnum<["trivia"]>>;
|
|
229
267
|
totalRounds: z.ZodNumber;
|
|
230
268
|
categoryIds: z.ZodArray<z.ZodString, "many">;
|
|
269
|
+
modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
|
|
231
270
|
buzzerMode: z.ZodOptional<z.ZodEnum<["off", "solo", "race", "controller"]>>;
|
|
232
271
|
noHost: z.ZodOptional<z.ZodBoolean>;
|
|
233
272
|
}, "strip", z.ZodTypeAny, {
|
|
234
273
|
t: "startGame";
|
|
235
274
|
categoryIds: string[];
|
|
236
275
|
totalRounds: number;
|
|
276
|
+
modeId?: "solo" | "oneDevice" | "snag" | "multiplayer" | undefined;
|
|
237
277
|
game?: "trivia" | undefined;
|
|
238
|
-
buzzerMode?: "
|
|
278
|
+
buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
|
|
239
279
|
noHost?: boolean | undefined;
|
|
240
280
|
}, {
|
|
241
281
|
t: "startGame";
|
|
242
282
|
categoryIds: string[];
|
|
243
283
|
totalRounds: number;
|
|
284
|
+
modeId?: "solo" | "oneDevice" | "snag" | "multiplayer" | undefined;
|
|
244
285
|
game?: "trivia" | undefined;
|
|
245
|
-
buzzerMode?: "
|
|
286
|
+
buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
|
|
246
287
|
noHost?: boolean | undefined;
|
|
247
288
|
}>, z.ZodObject<{
|
|
248
289
|
t: z.ZodLiteral<"pickLevel">;
|
|
@@ -383,6 +424,8 @@ interface CategoryView {
|
|
|
383
424
|
packName: string;
|
|
384
425
|
icon: string;
|
|
385
426
|
hasImage: boolean;
|
|
427
|
+
/** Category's brand color (hex, e.g. '#E8473F') for board coloring; absent if the category has none. */
|
|
428
|
+
color?: string;
|
|
386
429
|
}
|
|
387
430
|
type GameStatus = 'setup' | 'categorySelection' | 'inProgress' | 'completed';
|
|
388
431
|
type GameplayFlow = 'classic' | 'openJudged' | 'noHost' | 'buzzer';
|
|
@@ -402,11 +445,13 @@ interface TeamView {
|
|
|
402
445
|
type: HelpName;
|
|
403
446
|
isUsed: boolean;
|
|
404
447
|
}[];
|
|
405
|
-
/** The team's players in rotation order
|
|
448
|
+
/** The team's players in rotation order (`connected` = that player's socket is online right now).
|
|
449
|
+
* Empty for host-run single-device teams. */
|
|
406
450
|
players: {
|
|
407
451
|
id: string;
|
|
408
452
|
name: string;
|
|
409
453
|
emoji: string;
|
|
454
|
+
connected: boolean;
|
|
410
455
|
}[];
|
|
411
456
|
/** The player on THIS team whose turn it is to answer (rotates each turn); null if the team has
|
|
412
457
|
* no seated players. In controller mode only this player may `answer` on the team's turn. */
|
|
@@ -531,6 +576,7 @@ type ServerMsg = {
|
|
|
531
576
|
selectedCategoryIds: string[];
|
|
532
577
|
teams: TeamSlotView[];
|
|
533
578
|
hostActivity: 'configuring' | 'idle';
|
|
579
|
+
modeId: ModeId | null;
|
|
534
580
|
you: YouContext;
|
|
535
581
|
} | {
|
|
536
582
|
t: 'state';
|
|
@@ -549,4 +595,4 @@ type ServerMsg = {
|
|
|
549
595
|
message: string;
|
|
550
596
|
};
|
|
551
597
|
|
|
552
|
-
export { type AvatarView, type BuzzerGameMode, type CategoryView, 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 TeamSlotView, type TeamView, type YouContext };
|
|
598
|
+
export { type AvatarView, type BuzzerGameMode, type CategoryView, ClientMsg, type Effect, GAME_IDS, type GameId, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type ServerMsg, type SoundName, type TeamSlotView, type TeamView, type YouContext, modesForGame };
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,14 @@ var LIMITS = {
|
|
|
8
8
|
};
|
|
9
9
|
var PROTOCOL_VERSION = 1;
|
|
10
10
|
var GAME_IDS = ["trivia"];
|
|
11
|
+
var MODE_IDS = ["solo", "oneDevice", "snag", "multiplayer"];
|
|
12
|
+
var MODES = [
|
|
13
|
+
{ id: "solo", gameId: "trivia", label: "Solo", onlineEligible: false },
|
|
14
|
+
{ id: "oneDevice", gameId: "trivia", label: "One Device", onlineEligible: false },
|
|
15
|
+
{ id: "snag", gameId: "trivia", label: "Snag", onlineEligible: true },
|
|
16
|
+
{ id: "multiplayer", gameId: "trivia", label: "Multiplayer", onlineEligible: true }
|
|
17
|
+
];
|
|
18
|
+
var modesForGame = (gameId) => MODES.filter((m) => m.gameId === gameId);
|
|
11
19
|
var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
|
|
12
20
|
var UNSAFE_CHARS = /[\p{Cc}<>]/gu;
|
|
13
21
|
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");
|
|
@@ -24,18 +32,19 @@ var HelpType = z.enum([
|
|
|
24
32
|
]);
|
|
25
33
|
var Pv = z.number().int().min(1).max(1e6).optional();
|
|
26
34
|
var IdToken = z.string().max(4096).optional();
|
|
35
|
+
var Platform = z.enum(["web", "ios", "android"]).optional();
|
|
27
36
|
var Avatar = z.object({
|
|
28
37
|
emoji: z.string().min(1).max(16).transform((s) => s.replace(UNSAFE_CHARS, "")).refine((s) => s.length >= 1, "empty emoji"),
|
|
29
38
|
color: z.string().regex(/^#[0-9a-fA-F]{6}$/, "color must be #rrggbb")
|
|
30
39
|
});
|
|
31
40
|
var ClientMsg = z.discriminatedUnion("t", [
|
|
32
41
|
// room / lobby
|
|
33
|
-
z.object({ t: z.literal("createRoom"), name: DisplayName.optional(), avatar: Avatar.optional(), pv: Pv, idToken: IdToken }),
|
|
42
|
+
z.object({ t: z.literal("createRoom"), name: DisplayName.optional(), avatar: Avatar.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
|
|
34
43
|
// Host sets/changes their display name AFTER createRoom (for anonymous one-tap
|
|
35
44
|
// hosts who started nameless). Host-only; the relay re-broadcasts the lobby.
|
|
36
45
|
z.object({ t: z.literal("renameHost"), name: DisplayName }),
|
|
37
|
-
z.object({ t: z.literal("joinRoom"), code: RoomCode, name: DisplayName, avatar: Avatar.optional(), pv: Pv, idToken: IdToken }),
|
|
38
|
-
z.object({ t: z.literal("resume"), code: RoomCode, token: z.string().max(120), avatar: Avatar.optional(), pv: Pv, idToken: IdToken }),
|
|
46
|
+
z.object({ t: z.literal("joinRoom"), code: RoomCode, name: DisplayName, avatar: Avatar.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
|
|
47
|
+
z.object({ t: z.literal("resume"), code: RoomCode, token: z.string().max(120), avatar: Avatar.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
|
|
39
48
|
z.object({ t: z.literal("leaveRoom") }),
|
|
40
49
|
z.object({ t: z.literal("approve"), playerId: PlayerId }),
|
|
41
50
|
z.object({ t: z.literal("reject"), playerId: PlayerId }),
|
|
@@ -47,6 +56,10 @@ var ClientMsg = z.discriminatedUnion("t", [
|
|
|
47
56
|
z.object({ t: z.literal("setTeams"), teams: z.array(DisplayName).min(1).max(LIMITS.MAX_PLAYERS) }),
|
|
48
57
|
// A player joins team slot `teamIndex` (0-based, into the host's `setTeams` list).
|
|
49
58
|
z.object({ t: z.literal("pickTeam"), teamIndex: z.number().int().min(0).max(LIMITS.MAX_PLAYERS - 1) }),
|
|
59
|
+
// Host's IN-PROGRESS mode selection in the lobby — a live preview so waiting players see which mode
|
|
60
|
+
// is coming before start. Distinct from startGame.modeId (which commits it); the relay defaults
|
|
61
|
+
// startGame to this when modeId is omitted. Re-broadcasts the lobby.
|
|
62
|
+
z.object({ t: z.literal("setMode"), modeId: z.enum(MODE_IDS) }),
|
|
50
63
|
// Host signals it's busy configuring the game (categories/rounds/mode) so waiting players can show
|
|
51
64
|
// a "host is setting up…" state instead of a frozen-looking lobby. 'idle' when the settings close.
|
|
52
65
|
z.object({ t: z.literal("hostActivity"), activity: z.enum(["configuring", "idle"]) }),
|
|
@@ -57,6 +70,10 @@ var ClientMsg = z.discriminatedUnion("t", [
|
|
|
57
70
|
// which game to play; defaults to 'trivia'
|
|
58
71
|
totalRounds: z.number().int().min(1).max(50),
|
|
59
72
|
categoryIds: z.array(z.string().max(60)).min(1).max(12),
|
|
73
|
+
// PREFERRED: pick a mode from the registry (see MODES). The server maps it to the engine
|
|
74
|
+
// rules. Send this and omit buzzerMode/noHost. Falls back to the legacy flags below if omitted.
|
|
75
|
+
modeId: z.enum(MODE_IDS).optional(),
|
|
76
|
+
// LEGACY (pre-modeId clients) — the raw engine flags. Ignored when modeId is present.
|
|
60
77
|
// How to play (maps to the engine). Omit → controller (each player answers from
|
|
61
78
|
// their own device). 'off' = one device drives (Me-Controller); pair with noHost.
|
|
62
79
|
// solo=Solo · race=Snag/buzz · controller=each-player-from-device · off=Me-Controller
|
|
@@ -91,6 +108,9 @@ export {
|
|
|
91
108
|
ClientMsg,
|
|
92
109
|
GAME_IDS,
|
|
93
110
|
LIMITS,
|
|
111
|
+
MODES,
|
|
112
|
+
MODE_IDS,
|
|
94
113
|
PROTOCOL_VERSION,
|
|
95
|
-
ROOM_ALPHABET
|
|
114
|
+
ROOM_ALPHABET,
|
|
115
|
+
modesForGame
|
|
96
116
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegeem/protocol",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
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
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://geem.tv",
|