@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 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?: "off" | "solo" | "race" | "controller" | undefined;
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?: "off" | "solo" | "race" | "controller" | undefined;
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. Empty for host-run single-device teams. */
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?: "off" | "solo" | "race" | "controller" | undefined;
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?: "off" | "solo" | "race" | "controller" | undefined;
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. Empty for host-run single-device teams. */
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.9",
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",