@thegeem/protocol 0.1.15 → 0.1.17

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
@@ -21,12 +21,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ClientMsg: () => ClientMsg,
24
+ GAMES: () => GAMES,
24
25
  GAME_IDS: () => GAME_IDS,
25
26
  LIMITS: () => LIMITS,
27
+ MIN_SUPPORTED_PV: () => MIN_SUPPORTED_PV,
26
28
  MODES: () => MODES,
27
29
  MODE_IDS: () => MODE_IDS,
28
30
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
29
31
  ROOM_ALPHABET: () => ROOM_ALPHABET,
32
+ gameIdForPv: () => gameIdForPv,
33
+ gamesForPv: () => gamesForPv,
30
34
  modesForGame: () => modesForGame
31
35
  });
32
36
  module.exports = __toCommonJS(index_exports);
@@ -37,14 +41,25 @@ var LIMITS = {
37
41
  MAX_PLAYERS: 12,
38
42
  MAX_MSG_PER_SEC: 8
39
43
  };
40
- var PROTOCOL_VERSION = 1;
41
- var GAME_IDS = ["trivia"];
44
+ var PROTOCOL_VERSION = 2;
45
+ var MIN_SUPPORTED_PV = 1;
46
+ var GAME_IDS = ["seedha", "snag", "trivia"];
47
+ var GAMES = [
48
+ { id: "seedha", label: "Seedha", minPv: 2 },
49
+ { id: "snag", label: "Snag", minPv: 2 },
50
+ { id: "trivia", label: "Trivia", minPv: 1, deprecated: true }
51
+ ];
52
+ var gamesForPv = (pv) => GAMES.filter((g) => !g.deprecated && g.minPv <= pv);
53
+ var gameIdForPv = (gameId, pv) => {
54
+ const g = GAMES.find((x) => x.id === gameId);
55
+ return g && !g.deprecated && (pv ?? MIN_SUPPORTED_PV) >= g.minPv ? gameId : "trivia";
56
+ };
42
57
  var MODE_IDS = ["solo", "oneDevice", "snag", "multiplayer"];
43
58
  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 }
59
+ { id: "solo", gameId: "seedha", label: "Training", onlineEligible: false },
60
+ { id: "oneDevice", gameId: "seedha", label: "One Device", onlineEligible: false },
61
+ { id: "multiplayer", gameId: "seedha", label: "Multiplayer", onlineEligible: true },
62
+ { id: "snag", gameId: "snag", label: "Snag", onlineEligible: true }
48
63
  ];
49
64
  var modesForGame = (gameId) => MODES.filter((m) => m.gameId === gameId);
50
65
  var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
@@ -143,11 +158,15 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
143
158
  // Annotate the CommonJS export names for ESM import in node:
144
159
  0 && (module.exports = {
145
160
  ClientMsg,
161
+ GAMES,
146
162
  GAME_IDS,
147
163
  LIMITS,
164
+ MIN_SUPPORTED_PV,
148
165
  MODES,
149
166
  MODE_IDS,
150
167
  PROTOCOL_VERSION,
151
168
  ROOM_ALPHABET,
169
+ gameIdForPv,
170
+ gamesForPv,
152
171
  modesForGame
153
172
  });
package/dist/index.d.cts CHANGED
@@ -23,20 +23,43 @@ declare const LIMITS: {
23
23
  *
24
24
  * Why this exists: web auto-updates, but an App-Store / Play build is FROZEN on
25
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.
26
+ * its first message.
27
+ *
28
+ * Versioning model (D-025) ADAPT within a support window, don't hard-gate:
29
+ * - `PROTOCOL_VERSION` = the CURRENT capability level (bump when new wire features ship).
30
+ * - `MIN_SUPPORTED_PV` = the REJECT FLOOR: the server rejects only `pv < MIN_SUPPORTED_PV`
31
+ * (`{ t:'error', code:'client_outdated' }`); builds between the floor and current keep working —
32
+ * the server tailors its output to their `pv` (e.g. sends the legacy `gameId:'trivia'` instead of
33
+ * `seedha`/`snag`). Raise the floor SLOWLY to retire ancient builds. Unversioned clients (no `pv`,
34
+ * e.g. the legacy web harness) are treated as the floor and never rejected.
30
35
  */
31
- declare const PROTOCOL_VERSION = 1;
36
+ declare const PROTOCOL_VERSION = 2;
37
+ declare const MIN_SUPPORTED_PV = 1;
32
38
  /**
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.)
39
+ * Geem is a PLATFORM of GAMES (jackbox-style). The trivia board split (D-023) into TWO player-facing
40
+ * games: `seedha` (turn-based: Training/One Device/Multiplayer) and `snag` (buzzer race one mode,
41
+ * needs ≥2 devices). `trivia` is kept as a DEPRECATED legacy alias: it's what the relay sends to
42
+ * pre-pv2 clients (they never saw seedha/snag), and what old clients send on startGame. Every room
43
+ * runs one game, identified by `gameId`.
37
44
  */
38
- declare const GAME_IDS: readonly ["trivia"];
45
+ declare const GAME_IDS: readonly ["seedha", "snag", "trivia"];
39
46
  type GameId = (typeof GAME_IDS)[number];
47
+ /**
48
+ * The GAME REGISTRY — player-facing games + the version gating (D-024/D-025). A client renders a tile
49
+ * per game whose `minPv <= its own pv` (so old builds are never offered a game they can't run); the
50
+ * client maps its `pv` → an "update to play X" hint for games above it. Adding Game #N = one row here.
51
+ */
52
+ interface GameInfo {
53
+ id: GameId;
54
+ label: string;
55
+ minPv: number;
56
+ deprecated?: boolean;
57
+ }
58
+ declare const GAMES: readonly GameInfo[];
59
+ /** Games to OFFER a client of version `pv` (tiles). Excludes the deprecated alias + anything too new. */
60
+ declare const gamesForPv: (pv: number) => GameInfo[];
61
+ /** The gameId to SEND a client of version `pv`: new games need pv >= their minPv; older clients see 'trivia'. */
62
+ declare const gameIdForPv: (gameId: GameId, pv: number | undefined) => GameId;
40
63
  /**
41
64
  * The MODE REGISTRY — the list of ways to play each game. A client renders its mode picker from this
42
65
  * and sends the chosen `modeId` on `startGame`; the SERVER owns the modeId → engine-rules mapping (the
@@ -248,10 +271,10 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
248
271
  modeId: z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>;
249
272
  }, "strip", z.ZodTypeAny, {
250
273
  t: "setMode";
251
- modeId: "solo" | "oneDevice" | "snag" | "multiplayer";
274
+ modeId: "snag" | "solo" | "oneDevice" | "multiplayer";
252
275
  }, {
253
276
  t: "setMode";
254
- modeId: "solo" | "oneDevice" | "snag" | "multiplayer";
277
+ modeId: "snag" | "solo" | "oneDevice" | "multiplayer";
255
278
  }>, z.ZodObject<{
256
279
  t: z.ZodLiteral<"hostActivity">;
257
280
  activity: z.ZodEnum<["configuring", "idle"]>;
@@ -263,7 +286,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
263
286
  activity: "configuring" | "idle";
264
287
  }>, z.ZodObject<{
265
288
  t: z.ZodLiteral<"startGame">;
266
- game: z.ZodOptional<z.ZodEnum<["trivia"]>>;
289
+ game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia"]>>;
267
290
  totalRounds: z.ZodNumber;
268
291
  categoryIds: z.ZodArray<z.ZodString, "many">;
269
292
  modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
@@ -273,16 +296,16 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
273
296
  t: "startGame";
274
297
  categoryIds: string[];
275
298
  totalRounds: number;
276
- modeId?: "solo" | "oneDevice" | "snag" | "multiplayer" | undefined;
277
- game?: "trivia" | undefined;
299
+ modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
300
+ game?: "seedha" | "snag" | "trivia" | undefined;
278
301
  buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
279
302
  noHost?: boolean | undefined;
280
303
  }, {
281
304
  t: "startGame";
282
305
  categoryIds: string[];
283
306
  totalRounds: number;
284
- modeId?: "solo" | "oneDevice" | "snag" | "multiplayer" | undefined;
285
- game?: "trivia" | undefined;
307
+ modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
308
+ game?: "seedha" | "snag" | "trivia" | undefined;
286
309
  buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
287
310
  noHost?: boolean | undefined;
288
311
  }>, z.ZodObject<{
@@ -478,6 +501,8 @@ interface QuestionView {
478
501
  correctAnswerIndex: number | null;
479
502
  imageName?: string;
480
503
  videoName?: string;
504
+ /** For video questions: seconds into the clip to stop playback (so the answer isn't shown). Absent = play through. */
505
+ videoStopTime?: number;
481
506
  }
482
507
  interface GameStateView {
483
508
  status: GameStatus;
@@ -609,4 +634,4 @@ type ServerMsg = {
609
634
  message: string;
610
635
  };
611
636
 
612
- 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 };
637
+ export { type AvatarView, type BuzzerGameMode, type CategoryView, ClientMsg, type Effect, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type ServerMsg, type SoundName, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
package/dist/index.d.ts CHANGED
@@ -23,20 +23,43 @@ declare const LIMITS: {
23
23
  *
24
24
  * Why this exists: web auto-updates, but an App-Store / Play build is FROZEN on
25
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.
26
+ * its first message.
27
+ *
28
+ * Versioning model (D-025) ADAPT within a support window, don't hard-gate:
29
+ * - `PROTOCOL_VERSION` = the CURRENT capability level (bump when new wire features ship).
30
+ * - `MIN_SUPPORTED_PV` = the REJECT FLOOR: the server rejects only `pv < MIN_SUPPORTED_PV`
31
+ * (`{ t:'error', code:'client_outdated' }`); builds between the floor and current keep working —
32
+ * the server tailors its output to their `pv` (e.g. sends the legacy `gameId:'trivia'` instead of
33
+ * `seedha`/`snag`). Raise the floor SLOWLY to retire ancient builds. Unversioned clients (no `pv`,
34
+ * e.g. the legacy web harness) are treated as the floor and never rejected.
30
35
  */
31
- declare const PROTOCOL_VERSION = 1;
36
+ declare const PROTOCOL_VERSION = 2;
37
+ declare const MIN_SUPPORTED_PV = 1;
32
38
  /**
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.)
39
+ * Geem is a PLATFORM of GAMES (jackbox-style). The trivia board split (D-023) into TWO player-facing
40
+ * games: `seedha` (turn-based: Training/One Device/Multiplayer) and `snag` (buzzer race one mode,
41
+ * needs ≥2 devices). `trivia` is kept as a DEPRECATED legacy alias: it's what the relay sends to
42
+ * pre-pv2 clients (they never saw seedha/snag), and what old clients send on startGame. Every room
43
+ * runs one game, identified by `gameId`.
37
44
  */
38
- declare const GAME_IDS: readonly ["trivia"];
45
+ declare const GAME_IDS: readonly ["seedha", "snag", "trivia"];
39
46
  type GameId = (typeof GAME_IDS)[number];
47
+ /**
48
+ * The GAME REGISTRY — player-facing games + the version gating (D-024/D-025). A client renders a tile
49
+ * per game whose `minPv <= its own pv` (so old builds are never offered a game they can't run); the
50
+ * client maps its `pv` → an "update to play X" hint for games above it. Adding Game #N = one row here.
51
+ */
52
+ interface GameInfo {
53
+ id: GameId;
54
+ label: string;
55
+ minPv: number;
56
+ deprecated?: boolean;
57
+ }
58
+ declare const GAMES: readonly GameInfo[];
59
+ /** Games to OFFER a client of version `pv` (tiles). Excludes the deprecated alias + anything too new. */
60
+ declare const gamesForPv: (pv: number) => GameInfo[];
61
+ /** The gameId to SEND a client of version `pv`: new games need pv >= their minPv; older clients see 'trivia'. */
62
+ declare const gameIdForPv: (gameId: GameId, pv: number | undefined) => GameId;
40
63
  /**
41
64
  * The MODE REGISTRY — the list of ways to play each game. A client renders its mode picker from this
42
65
  * and sends the chosen `modeId` on `startGame`; the SERVER owns the modeId → engine-rules mapping (the
@@ -248,10 +271,10 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
248
271
  modeId: z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>;
249
272
  }, "strip", z.ZodTypeAny, {
250
273
  t: "setMode";
251
- modeId: "solo" | "oneDevice" | "snag" | "multiplayer";
274
+ modeId: "snag" | "solo" | "oneDevice" | "multiplayer";
252
275
  }, {
253
276
  t: "setMode";
254
- modeId: "solo" | "oneDevice" | "snag" | "multiplayer";
277
+ modeId: "snag" | "solo" | "oneDevice" | "multiplayer";
255
278
  }>, z.ZodObject<{
256
279
  t: z.ZodLiteral<"hostActivity">;
257
280
  activity: z.ZodEnum<["configuring", "idle"]>;
@@ -263,7 +286,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
263
286
  activity: "configuring" | "idle";
264
287
  }>, z.ZodObject<{
265
288
  t: z.ZodLiteral<"startGame">;
266
- game: z.ZodOptional<z.ZodEnum<["trivia"]>>;
289
+ game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia"]>>;
267
290
  totalRounds: z.ZodNumber;
268
291
  categoryIds: z.ZodArray<z.ZodString, "many">;
269
292
  modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
@@ -273,16 +296,16 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
273
296
  t: "startGame";
274
297
  categoryIds: string[];
275
298
  totalRounds: number;
276
- modeId?: "solo" | "oneDevice" | "snag" | "multiplayer" | undefined;
277
- game?: "trivia" | undefined;
299
+ modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
300
+ game?: "seedha" | "snag" | "trivia" | undefined;
278
301
  buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
279
302
  noHost?: boolean | undefined;
280
303
  }, {
281
304
  t: "startGame";
282
305
  categoryIds: string[];
283
306
  totalRounds: number;
284
- modeId?: "solo" | "oneDevice" | "snag" | "multiplayer" | undefined;
285
- game?: "trivia" | undefined;
307
+ modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
308
+ game?: "seedha" | "snag" | "trivia" | undefined;
286
309
  buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
287
310
  noHost?: boolean | undefined;
288
311
  }>, z.ZodObject<{
@@ -478,6 +501,8 @@ interface QuestionView {
478
501
  correctAnswerIndex: number | null;
479
502
  imageName?: string;
480
503
  videoName?: string;
504
+ /** For video questions: seconds into the clip to stop playback (so the answer isn't shown). Absent = play through. */
505
+ videoStopTime?: number;
481
506
  }
482
507
  interface GameStateView {
483
508
  status: GameStatus;
@@ -609,4 +634,4 @@ type ServerMsg = {
609
634
  message: string;
610
635
  };
611
636
 
612
- 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 };
637
+ export { type AvatarView, type BuzzerGameMode, type CategoryView, ClientMsg, type Effect, GAMES, GAME_IDS, type GameId, type GameInfo, type GameStateView, type GameStatus, type GameplayFlow, type HelpName, LIMITS, type LobbyPlayer, MIN_SUPPORTED_PV, MODES, MODE_IDS, type ModeId, type ModeInfo, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type ServerMsg, type SoundName, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
package/dist/index.js CHANGED
@@ -6,14 +6,25 @@ var LIMITS = {
6
6
  MAX_PLAYERS: 12,
7
7
  MAX_MSG_PER_SEC: 8
8
8
  };
9
- var PROTOCOL_VERSION = 1;
10
- var GAME_IDS = ["trivia"];
9
+ var PROTOCOL_VERSION = 2;
10
+ var MIN_SUPPORTED_PV = 1;
11
+ var GAME_IDS = ["seedha", "snag", "trivia"];
12
+ var GAMES = [
13
+ { id: "seedha", label: "Seedha", minPv: 2 },
14
+ { id: "snag", label: "Snag", minPv: 2 },
15
+ { id: "trivia", label: "Trivia", minPv: 1, deprecated: true }
16
+ ];
17
+ var gamesForPv = (pv) => GAMES.filter((g) => !g.deprecated && g.minPv <= pv);
18
+ var gameIdForPv = (gameId, pv) => {
19
+ const g = GAMES.find((x) => x.id === gameId);
20
+ return g && !g.deprecated && (pv ?? MIN_SUPPORTED_PV) >= g.minPv ? gameId : "trivia";
21
+ };
11
22
  var MODE_IDS = ["solo", "oneDevice", "snag", "multiplayer"];
12
23
  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 }
24
+ { id: "solo", gameId: "seedha", label: "Training", onlineEligible: false },
25
+ { id: "oneDevice", gameId: "seedha", label: "One Device", onlineEligible: false },
26
+ { id: "multiplayer", gameId: "seedha", label: "Multiplayer", onlineEligible: true },
27
+ { id: "snag", gameId: "snag", label: "Snag", onlineEligible: true }
17
28
  ];
18
29
  var modesForGame = (gameId) => MODES.filter((m) => m.gameId === gameId);
19
30
  var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
@@ -111,11 +122,15 @@ var ClientMsg = z.discriminatedUnion("t", [
111
122
  ]);
112
123
  export {
113
124
  ClientMsg,
125
+ GAMES,
114
126
  GAME_IDS,
115
127
  LIMITS,
128
+ MIN_SUPPORTED_PV,
116
129
  MODES,
117
130
  MODE_IDS,
118
131
  PROTOCOL_VERSION,
119
132
  ROOM_ALPHABET,
133
+ gameIdForPv,
134
+ gamesForPv,
120
135
  modesForGame
121
136
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegeem/protocol",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
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",