@thegeem/protocol 0.1.41 → 0.1.42

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
@@ -24,6 +24,7 @@ __export(index_exports, {
24
24
  COMEBACK_BONUS_MIN_PV: () => COMEBACK_BONUS_MIN_PV,
25
25
  COMEBACK_EFFECTS: () => COMEBACK_EFFECTS,
26
26
  ClientMsg: () => ClientMsg,
27
+ EFFECT_MIN_PV: () => EFFECT_MIN_PV,
27
28
  EMOTE_IDS: () => EMOTE_IDS,
28
29
  GAMES: () => GAMES,
29
30
  GAME_IDS: () => GAME_IDS,
@@ -34,15 +35,21 @@ __export(index_exports, {
34
35
  PROTOCOL_VERSION: () => PROTOCOL_VERSION,
35
36
  REJECT_REASONS: () => REJECT_REASONS,
36
37
  ROOM_ALPHABET: () => ROOM_ALPHABET,
38
+ SEEDHA_MODE_IDS: () => SEEDHA_MODE_IDS,
39
+ SNAG_MODES_MIN_PV: () => SNAG_MODES_MIN_PV,
40
+ SNAG_MODE_IDS: () => SNAG_MODE_IDS,
37
41
  SUBMISSION_KINDS: () => SUBMISSION_KINDS,
38
42
  SUBMISSION_STATUSES: () => SUBMISSION_STATUSES,
39
43
  effectsForPv: () => effectsForPv,
40
44
  gameIdForPv: () => gameIdForPv,
41
45
  gamesForPv: () => gamesForPv,
42
46
  hostActivityForPv: () => hostActivityForPv,
47
+ internalGameOf: () => internalGameOf,
43
48
  modesForGame: () => modesForGame,
49
+ modesOf: () => modesOf,
44
50
  normalizeWesternDigits: () => normalizeWesternDigits,
45
- sanitizeName: () => sanitizeName
51
+ sanitizeName: () => sanitizeName,
52
+ wireShapeOf: () => wireShapeOf
46
53
  });
47
54
  module.exports = __toCommonJS(index_exports);
48
55
  var import_zod = require("zod");
@@ -55,7 +62,7 @@ var LIMITS = {
55
62
  * Clients batch pen points to stay under this — ~150ms batching lands at ~7/sec. */
56
63
  STROKE_BATCH_PER_SEC: 20
57
64
  };
58
- var PROTOCOL_VERSION = 7;
65
+ var PROTOCOL_VERSION = 8;
59
66
  var MIN_SUPPORTED_PV = 1;
60
67
  var GAME_IDS = ["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"];
61
68
  var GAMES = [
@@ -87,21 +94,55 @@ var GAMES = [
87
94
  ];
88
95
  var hostActivityForPv = (activity, pv) => activity === "options" && (pv ?? MIN_SUPPORTED_PV) < 6 ? "configuring" : activity;
89
96
  var COMEBACK_BONUS_MIN_PV = 7;
97
+ var EFFECT_MIN_PV = {
98
+ comebackOffer: COMEBACK_BONUS_MIN_PV,
99
+ comebackBonus: COMEBACK_BONUS_MIN_PV,
100
+ comebackDeclined: COMEBACK_BONUS_MIN_PV
101
+ };
90
102
  var COMEBACK_EFFECTS = ["comebackOffer", "comebackBonus", "comebackDeclined"];
91
- var COMEBACK_EFFECT_SET = new Set(COMEBACK_EFFECTS);
92
- var effectsForPv = (effects, pv) => (pv ?? MIN_SUPPORTED_PV) >= COMEBACK_BONUS_MIN_PV ? effects : effects.filter((e) => !COMEBACK_EFFECT_SET.has(e.t));
103
+ var effectsForPv = (effects, pv) => {
104
+ const have = pv ?? MIN_SUPPORTED_PV;
105
+ return effects.filter((e) => (EFFECT_MIN_PV[e.t] ?? MIN_SUPPORTED_PV) <= have);
106
+ };
93
107
  var gamesForPv = (pv) => GAMES.filter((g) => !g.deprecated && g.minPv <= pv);
94
108
  var gameIdForPv = (gameId, pv) => {
95
109
  const g = GAMES.find((x) => x.id === gameId);
96
110
  return g && !g.deprecated && (pv ?? MIN_SUPPORTED_PV) >= g.minPv ? gameId : "trivia";
97
111
  };
98
- var MODE_IDS = ["solo", "oneDevice", "snag", "multiplayer"];
112
+ var SEEDHA_MODE_IDS = ["solo", "oneDevice", "multiplayer"];
113
+ var SNAG_MODE_IDS = ["normal", "sebaq"];
114
+ var MODE_IDS = [...SEEDHA_MODE_IDS, ...SNAG_MODE_IDS, "snag"];
115
+ var SNAG_MODES_MIN_PV = 8;
116
+ function modesOf(gameId) {
117
+ return MODES.filter((m) => m.gameId === gameId && !m.legacyAlias);
118
+ }
99
119
  var MODES = [
100
120
  { id: "solo", gameId: "seedha", label: "Training", onlineEligible: false },
101
121
  { id: "oneDevice", gameId: "seedha", label: "One Device", onlineEligible: false },
102
122
  { id: "multiplayer", gameId: "seedha", label: "Multiplayer", onlineEligible: true },
103
- { id: "snag", gameId: "snag", label: "Snag", onlineEligible: true }
123
+ // D-102: today's buzz Snag, by its real name. OFF the boards (owner 2026-08-29): the host awards 1-answer
124
+ // questions and the Comeback +100, so its score is not the relay's alone.
125
+ { id: "normal", gameId: "snag", label: "Normal", onlineEligible: false },
126
+ { id: "sebaq", gameId: "snag", label: "Sebaq", onlineEligible: true },
127
+ // D-102: the race — relay-scored end to end; THE Snag board
128
+ // NOT a mode — the legacy spelling of «snag / normal» (see MODE_IDS). Kept so old clients' `MODES.find` still maps it.
129
+ { id: "snag", gameId: "snag", label: "Snag", onlineEligible: false, legacyAlias: true }
104
130
  ];
131
+ function internalGameOf(game, modeId) {
132
+ if (modeId === "sebaq" || game === "sebaq") return { gameId: "sebaq", modeId: void 0 };
133
+ if (modeId === "snag") return { gameId: "snag", modeId: "normal" };
134
+ if (modeId === "normal") return { gameId: "snag", modeId: "normal" };
135
+ return { gameId: game, modeId };
136
+ }
137
+ function wireShapeOf(gameId, modeId, pv) {
138
+ const v = pv ?? MIN_SUPPORTED_PV;
139
+ if (gameId === "sebaq") return v >= SNAG_MODES_MIN_PV ? { gameId: "snag", modeId: "sebaq" } : { gameId: gameIdForPv("sebaq", pv), modeId: null };
140
+ if (gameId === "snag") {
141
+ const m = modeId === "normal" || modeId === "snag" ? v >= SNAG_MODES_MIN_PV ? "normal" : "snag" : modeId;
142
+ return { gameId: gameIdForPv("snag", pv), modeId: m };
143
+ }
144
+ return { gameId: gameIdForPv(gameId, pv), modeId };
145
+ }
105
146
  var modesForGame = (gameId) => MODES.filter((m) => m.gameId === gameId);
106
147
  var EMOTE_IDS = ["laugh", "wow", "think", "gg"];
107
148
  var CHARACTER_IDS = ["m", "f"];
@@ -366,6 +407,7 @@ var REJECT_REASONS = ["duplicate", "unclear_answer", "not_family", "inaccurate",
366
407
  COMEBACK_BONUS_MIN_PV,
367
408
  COMEBACK_EFFECTS,
368
409
  ClientMsg,
410
+ EFFECT_MIN_PV,
369
411
  EMOTE_IDS,
370
412
  GAMES,
371
413
  GAME_IDS,
@@ -376,13 +418,19 @@ var REJECT_REASONS = ["duplicate", "unclear_answer", "not_family", "inaccurate",
376
418
  PROTOCOL_VERSION,
377
419
  REJECT_REASONS,
378
420
  ROOM_ALPHABET,
421
+ SEEDHA_MODE_IDS,
422
+ SNAG_MODES_MIN_PV,
423
+ SNAG_MODE_IDS,
379
424
  SUBMISSION_KINDS,
380
425
  SUBMISSION_STATUSES,
381
426
  effectsForPv,
382
427
  gameIdForPv,
383
428
  gamesForPv,
384
429
  hostActivityForPv,
430
+ internalGameOf,
385
431
  modesForGame,
432
+ modesOf,
386
433
  normalizeWesternDigits,
387
- sanitizeName
434
+ sanitizeName,
435
+ wireShapeOf
388
436
  });
package/dist/index.d.cts CHANGED
@@ -36,7 +36,7 @@ declare const LIMITS: {
36
36
  * `seedha`/`snag`). Raise the floor SLOWLY to retire ancient builds. Unversioned clients (no `pv`,
37
37
  * e.g. the legacy web harness) are treated as the floor and never rejected.
38
38
  */
39
- declare const PROTOCOL_VERSION = 7;
39
+ declare const PROTOCOL_VERSION = 8;
40
40
  declare const MIN_SUPPORTED_PV = 1;
41
41
  /**
42
42
  * Geem is a PLATFORM of GAMES (jackbox-style). The trivia board split (D-023) into TWO player-facing
@@ -105,10 +105,24 @@ declare const COMEBACK_BONUS_MIN_PV = 7;
105
105
  * inventing one (sending `scorePop` instead) would make the client animate a normal score change for
106
106
  * an event that is not one.
107
107
  */
108
- /** Every comeback effect, as ONE list. ⚠️ Exported because the relay's `CLIENT_FX` allow-list must be
109
- * built from it rather than re-typed: I re-typed it, listed one of the three, and shipped a feature
110
- * whose refusal rendered as silence on prod. A hand-list that fails by OMISSION errors nowhere. */
108
+ /**
109
+ * The pv each VERSION-GATED effect first became decodable at. Absent = decodable since the floor.
110
+ *
111
+ * ⚠️ ONE TABLE, THE SHAPE `GAMES` ALREADY USES (`minPv` per entry) — not one constant, one set and one
112
+ * `||` per feature. The comeback bonus was the first gated effect and arrived as three of each; the
113
+ * next would have added three more. ⚠️ The relay's `CLIENT_FX` spreads EVERY key of this table, whatever
114
+ * its pv — the gate withholds, the allow-list must never — so a gated effect cannot be forgotten in one
115
+ * of the two places. I re-typed that list once, listed one of three, and shipped a feature whose refusal
116
+ * rendered as silence on prod. A hand-list fails by OMISSION.
117
+ */
118
+ declare const EFFECT_MIN_PV: Partial<Record<Effect['t'], number>>;
119
+ /** The comeback FAMILY, explicit. ⚠️ Not «every effect whose minPv is 7» — that derivation would silently
120
+ * drop a fourth comeback effect gated at pv8, and the test that checks this list would stay green because
121
+ * it derives from the same list (code-review C7). Family membership is a fact about the feature; the pv is
122
+ * a fact about the wire. Two tables, on purpose. */
111
123
  declare const COMEBACK_EFFECTS: readonly ["comebackOffer", "comebackBonus", "comebackDeclined"];
124
+ /** The effects a client of version `pv` can decode. Withholding is the only option for an effect — an
125
+ * unknown variant has no older equivalent, and a strict Swift enum drops the WHOLE frame on one. */
112
126
  declare const effectsForPv: (effects: Effect[], pv: number | undefined) => Effect[];
113
127
  declare const gamesForPv: (pv: number) => GameInfo[];
114
128
  /** The gameId to SEND a client of version `pv`: new games need pv >= their minPv; older clients see 'trivia'. */
@@ -122,15 +136,53 @@ declare const gameIdForPv: (gameId: GameId, pv: number | undefined) => GameId;
122
136
  * (only server-arbitrated, multi-device modes; single-device/solo are local-only — the host controls
123
137
  * input, so the server can't trust the score).
124
138
  */
125
- declare const MODE_IDS: readonly ["solo", "oneDevice", "snag", "multiplayer"];
139
+ /** صيدها's modes one name each, the ONLY name anything outside the engine uses (guide §9 item 1, owner «go» 2026-08-29). */
140
+ declare const SEEDHA_MODE_IDS: readonly ["solo", "oneDevice", "multiplayer"];
141
+ /** سناق (من سبق لبق)'s modes — D-102, owner 2026-08-29: `normal` = buzz-to-claim on ONE shared question (what
142
+ * every shipped build calls «Snag»), `sebaq` = everyone races the SAME set on their own phone (D-099's rules).
143
+ * ⚠️ The word ديوانية is RESERVED for a future «Group Diwaniya» — it names nothing today. */
144
+ declare const SNAG_MODE_IDS: readonly ["normal", "sebaq"];
145
+ /** The wire union. `'snag'` is NOT a mode: it is the LEGACY SPELLING of «game snag, mode normal» that every
146
+ * shipped iOS and web build sends, and a frozen build must keep working — see internalGameOf(). The new ids
147
+ * reach only pv ≥ SNAG_MODES_MIN_PV clients (`lobby.modeId` is a STRICT Swift enum: an unknown value drops
148
+ * the whole frame) — wireShapeOf() down-maps for everyone else. */
149
+ declare const MODE_IDS: readonly ["solo", "oneDevice", "multiplayer", "normal", "sebaq", "snag"];
150
+ declare const SNAG_MODES_MIN_PV = 8;
151
+ type SnagModeId = (typeof SNAG_MODE_IDS)[number];
126
152
  type ModeId = (typeof MODE_IDS)[number];
153
+ type SeedhaModeId = (typeof SEEDHA_MODE_IDS)[number];
154
+ /** The modes a game offers, from the registry. Games with no modes (دامة, ارسمها, سباق) → []. */
155
+ declare function modesOf(gameId: GameId): ModeInfo[];
127
156
  interface ModeInfo {
128
157
  id: ModeId;
129
158
  gameId: GameId;
130
159
  label: string;
131
160
  onlineEligible: boolean;
161
+ /** A row kept only so `MODES.find(id)` keeps resolving for builds that send a game as a modeId. Not a mode:
162
+ * never render it in a mode picker — `modesOf()` filters it out. */
163
+ legacyAlias?: true;
132
164
  }
133
165
  declare const MODES: readonly ModeInfo[];
166
+ /**
167
+ * D-102 — the two directions between the PRODUCT shape (game Snag → mode normal | sebaq) and the ENGINE
168
+ * shape the relay runs (`gameId 'snag'` = the trivia engine in race flow; `gameId 'sebaq'` = its own
169
+ * engine/driver, D-026 tile-vs-engine). Both spellings stay on the wire forever: `game: 'sebaq'` and
170
+ * `modeId: 'snag'` are what every shipped build sends.
171
+ *
172
+ * INBOUND — `internalGameOf(game, modeId)`: what the client SAID → what the relay RUNS.
173
+ */
174
+ declare function internalGameOf(game: GameId | undefined, modeId: ModeId | undefined): {
175
+ gameId: GameId | undefined;
176
+ modeId: ModeId | undefined;
177
+ };
178
+ /**
179
+ * OUTBOUND — `wireShapeOf(internalGameId, modeId, pv)`: what the relay RUNS → what THIS client can DECODE.
180
+ * pv ≥ 8 sees the product shape; below it, the pv7 shape it shipped with (`sebaq` as a game, `snag` as a mode).
181
+ */
182
+ declare function wireShapeOf(gameId: GameId, modeId: ModeId | null, pv: number | undefined): {
183
+ gameId: GameId;
184
+ modeId: ModeId | null;
185
+ };
134
186
  /** The modes available for a given game (for a client's mode picker). */
135
187
  declare const modesForGame: (gameId: GameId) => ModeInfo[];
136
188
  /**
@@ -279,8 +331,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
279
331
  games: z.ZodOptional<z.ZodArray<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>, "many">>;
280
332
  appVersion: z.ZodOptional<z.ZodString>;
281
333
  }, "strip", z.ZodTypeAny, {
282
- code: string;
283
334
  t: "joinRoom";
335
+ code: string;
284
336
  name: string;
285
337
  avatar?: {
286
338
  emoji: string;
@@ -293,8 +345,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
293
345
  games?: ("seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq")[] | undefined;
294
346
  appVersion?: string | undefined;
295
347
  }, {
296
- code: string;
297
348
  t: "joinRoom";
349
+ code: string;
298
350
  name: string;
299
351
  avatar?: {
300
352
  emoji: string;
@@ -327,8 +379,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
327
379
  games: z.ZodOptional<z.ZodArray<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>, "many">>;
328
380
  appVersion: z.ZodOptional<z.ZodString>;
329
381
  }, "strip", z.ZodTypeAny, {
330
- code: string;
331
382
  t: "resume";
383
+ code: string;
332
384
  token: string;
333
385
  avatar?: {
334
386
  emoji: string;
@@ -341,8 +393,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
341
393
  games?: ("seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq")[] | undefined;
342
394
  appVersion?: string | undefined;
343
395
  }, {
344
- code: string;
345
396
  t: "resume";
397
+ code: string;
346
398
  token: string;
347
399
  avatar?: {
348
400
  emoji: string;
@@ -459,15 +511,15 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
459
511
  }>, z.ZodObject<{
460
512
  t: z.ZodLiteral<"setMode">;
461
513
  game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>>;
462
- modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
514
+ modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "multiplayer", "normal", "sebaq", "snag"]>>;
463
515
  }, "strip", z.ZodTypeAny, {
464
516
  t: "setMode";
465
517
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
466
- modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
518
+ modeId?: "snag" | "sebaq" | "solo" | "oneDevice" | "multiplayer" | "normal" | undefined;
467
519
  }, {
468
520
  t: "setMode";
469
521
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
470
- modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
522
+ modeId?: "snag" | "sebaq" | "solo" | "oneDevice" | "multiplayer" | "normal" | undefined;
471
523
  }>, z.ZodObject<{
472
524
  t: z.ZodLiteral<"hostActivity">;
473
525
  activity: z.ZodEnum<["options", "configuring", "idle"]>;
@@ -491,7 +543,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
491
543
  }, {
492
544
  difficulty?: "easy" | "medium" | "hard" | undefined;
493
545
  }>>;
494
- modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
546
+ modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "multiplayer", "normal", "sebaq", "snag"]>>;
495
547
  buzzerMode: z.ZodOptional<z.ZodEnum<["off", "solo", "race", "controller"]>>;
496
548
  noHost: z.ZodOptional<z.ZodBoolean>;
497
549
  }, "strip", z.ZodTypeAny, {
@@ -499,7 +551,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
499
551
  categoryIds: string[];
500
552
  totalRounds: number;
501
553
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
502
- modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
554
+ modeId?: "snag" | "sebaq" | "solo" | "oneDevice" | "multiplayer" | "normal" | undefined;
503
555
  hostPlays?: boolean | undefined;
504
556
  players?: string[] | undefined;
505
557
  bot?: {
@@ -512,7 +564,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
512
564
  categoryIds: string[];
513
565
  totalRounds: number;
514
566
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
515
- modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
567
+ modeId?: "snag" | "sebaq" | "solo" | "oneDevice" | "multiplayer" | "normal" | undefined;
516
568
  hostPlays?: boolean | undefined;
517
569
  players?: string[] | undefined;
518
570
  bot?: {
@@ -726,8 +778,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
726
778
  color: z.ZodString;
727
779
  width: z.ZodNumber;
728
780
  }, "strip", z.ZodTypeAny, {
729
- color: string;
730
781
  t: "strokeBatch";
782
+ color: string;
731
783
  strokeId: string;
732
784
  seq: number;
733
785
  points: {
@@ -736,8 +788,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
736
788
  }[];
737
789
  width: number;
738
790
  }, {
739
- color: string;
740
791
  t: "strokeBatch";
792
+ color: string;
741
793
  strokeId: string;
742
794
  seq: number;
743
795
  points: {
@@ -896,7 +948,7 @@ interface PackView {
896
948
  hasImage?: boolean;
897
949
  }
898
950
  type GameStatus = 'setup' | 'categorySelection' | 'inProgress' | 'completed';
899
- type GameplayFlow = 'classic' | 'openJudged' | 'noHost' | 'buzzer';
951
+ type GameplayFlow = 'classic' | 'noHost' | 'buzzer';
900
952
  type BuzzerGameMode = 'off' | 'solo' | 'race' | 'controller';
901
953
  /** The help/lifeline ids (same set as the inbound `HelpType` zod enum above). */
902
954
  type HelpName = z.infer<typeof HelpType>;
@@ -912,6 +964,7 @@ interface TeamView {
912
964
  helps: {
913
965
  type: HelpName;
914
966
  isUsed: boolean;
967
+ unusable?: true;
915
968
  }[];
916
969
  /** The team's players in rotation order (`connected` = that player's socket is online right now).
917
970
  * Empty for host-run single-device teams. */
@@ -1251,6 +1304,7 @@ type ServerMsg = {
1251
1304
  hostFrame?: string;
1252
1305
  hostPlaying?: boolean;
1253
1306
  hostPinned?: boolean;
1307
+ hostTeamIndex?: number | null;
1254
1308
  players: LobbyPlayer[];
1255
1309
  categories: CategoryView[];
1256
1310
  packs?: PackView[];
@@ -1362,4 +1416,4 @@ interface MyQuestionRow {
1362
1416
  };
1363
1417
  }
1364
1418
 
1365
- export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, COMEBACK_BONUS_MIN_PV, COMEBACK_EFFECTS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, 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, type MyQuestionRow, PROTOCOL_VERSION, type PackView, type QuestionView, REJECT_REASONS, ROOM_ALPHABET, type RejectReason, type RosterEntry, SUBMISSION_KINDS, SUBMISSION_STATUSES, type SebaqStandingRow, type SebaqStateView, type ServerMsg, type SoundName, type StrokePointView, type SubmissionKind, type SubmissionStatus, type TeamSlotView, type TeamView, type YouContext, effectsForPv, gameIdForPv, gamesForPv, hostActivityForPv, modesForGame, normalizeWesternDigits, sanitizeName };
1419
+ export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, COMEBACK_BONUS_MIN_PV, COMEBACK_EFFECTS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EFFECT_MIN_PV, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, 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, type MyQuestionRow, PROTOCOL_VERSION, type PackView, type QuestionView, REJECT_REASONS, ROOM_ALPHABET, type RejectReason, type RosterEntry, SEEDHA_MODE_IDS, SNAG_MODES_MIN_PV, SNAG_MODE_IDS, SUBMISSION_KINDS, SUBMISSION_STATUSES, type SebaqStandingRow, type SebaqStateView, type SeedhaModeId, type ServerMsg, type SnagModeId, type SoundName, type StrokePointView, type SubmissionKind, type SubmissionStatus, type TeamSlotView, type TeamView, type YouContext, effectsForPv, gameIdForPv, gamesForPv, hostActivityForPv, internalGameOf, modesForGame, modesOf, normalizeWesternDigits, sanitizeName, wireShapeOf };
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ declare const LIMITS: {
36
36
  * `seedha`/`snag`). Raise the floor SLOWLY to retire ancient builds. Unversioned clients (no `pv`,
37
37
  * e.g. the legacy web harness) are treated as the floor and never rejected.
38
38
  */
39
- declare const PROTOCOL_VERSION = 7;
39
+ declare const PROTOCOL_VERSION = 8;
40
40
  declare const MIN_SUPPORTED_PV = 1;
41
41
  /**
42
42
  * Geem is a PLATFORM of GAMES (jackbox-style). The trivia board split (D-023) into TWO player-facing
@@ -105,10 +105,24 @@ declare const COMEBACK_BONUS_MIN_PV = 7;
105
105
  * inventing one (sending `scorePop` instead) would make the client animate a normal score change for
106
106
  * an event that is not one.
107
107
  */
108
- /** Every comeback effect, as ONE list. ⚠️ Exported because the relay's `CLIENT_FX` allow-list must be
109
- * built from it rather than re-typed: I re-typed it, listed one of the three, and shipped a feature
110
- * whose refusal rendered as silence on prod. A hand-list that fails by OMISSION errors nowhere. */
108
+ /**
109
+ * The pv each VERSION-GATED effect first became decodable at. Absent = decodable since the floor.
110
+ *
111
+ * ⚠️ ONE TABLE, THE SHAPE `GAMES` ALREADY USES (`minPv` per entry) — not one constant, one set and one
112
+ * `||` per feature. The comeback bonus was the first gated effect and arrived as three of each; the
113
+ * next would have added three more. ⚠️ The relay's `CLIENT_FX` spreads EVERY key of this table, whatever
114
+ * its pv — the gate withholds, the allow-list must never — so a gated effect cannot be forgotten in one
115
+ * of the two places. I re-typed that list once, listed one of three, and shipped a feature whose refusal
116
+ * rendered as silence on prod. A hand-list fails by OMISSION.
117
+ */
118
+ declare const EFFECT_MIN_PV: Partial<Record<Effect['t'], number>>;
119
+ /** The comeback FAMILY, explicit. ⚠️ Not «every effect whose minPv is 7» — that derivation would silently
120
+ * drop a fourth comeback effect gated at pv8, and the test that checks this list would stay green because
121
+ * it derives from the same list (code-review C7). Family membership is a fact about the feature; the pv is
122
+ * a fact about the wire. Two tables, on purpose. */
111
123
  declare const COMEBACK_EFFECTS: readonly ["comebackOffer", "comebackBonus", "comebackDeclined"];
124
+ /** The effects a client of version `pv` can decode. Withholding is the only option for an effect — an
125
+ * unknown variant has no older equivalent, and a strict Swift enum drops the WHOLE frame on one. */
112
126
  declare const effectsForPv: (effects: Effect[], pv: number | undefined) => Effect[];
113
127
  declare const gamesForPv: (pv: number) => GameInfo[];
114
128
  /** The gameId to SEND a client of version `pv`: new games need pv >= their minPv; older clients see 'trivia'. */
@@ -122,15 +136,53 @@ declare const gameIdForPv: (gameId: GameId, pv: number | undefined) => GameId;
122
136
  * (only server-arbitrated, multi-device modes; single-device/solo are local-only — the host controls
123
137
  * input, so the server can't trust the score).
124
138
  */
125
- declare const MODE_IDS: readonly ["solo", "oneDevice", "snag", "multiplayer"];
139
+ /** صيدها's modes one name each, the ONLY name anything outside the engine uses (guide §9 item 1, owner «go» 2026-08-29). */
140
+ declare const SEEDHA_MODE_IDS: readonly ["solo", "oneDevice", "multiplayer"];
141
+ /** سناق (من سبق لبق)'s modes — D-102, owner 2026-08-29: `normal` = buzz-to-claim on ONE shared question (what
142
+ * every shipped build calls «Snag»), `sebaq` = everyone races the SAME set on their own phone (D-099's rules).
143
+ * ⚠️ The word ديوانية is RESERVED for a future «Group Diwaniya» — it names nothing today. */
144
+ declare const SNAG_MODE_IDS: readonly ["normal", "sebaq"];
145
+ /** The wire union. `'snag'` is NOT a mode: it is the LEGACY SPELLING of «game snag, mode normal» that every
146
+ * shipped iOS and web build sends, and a frozen build must keep working — see internalGameOf(). The new ids
147
+ * reach only pv ≥ SNAG_MODES_MIN_PV clients (`lobby.modeId` is a STRICT Swift enum: an unknown value drops
148
+ * the whole frame) — wireShapeOf() down-maps for everyone else. */
149
+ declare const MODE_IDS: readonly ["solo", "oneDevice", "multiplayer", "normal", "sebaq", "snag"];
150
+ declare const SNAG_MODES_MIN_PV = 8;
151
+ type SnagModeId = (typeof SNAG_MODE_IDS)[number];
126
152
  type ModeId = (typeof MODE_IDS)[number];
153
+ type SeedhaModeId = (typeof SEEDHA_MODE_IDS)[number];
154
+ /** The modes a game offers, from the registry. Games with no modes (دامة, ارسمها, سباق) → []. */
155
+ declare function modesOf(gameId: GameId): ModeInfo[];
127
156
  interface ModeInfo {
128
157
  id: ModeId;
129
158
  gameId: GameId;
130
159
  label: string;
131
160
  onlineEligible: boolean;
161
+ /** A row kept only so `MODES.find(id)` keeps resolving for builds that send a game as a modeId. Not a mode:
162
+ * never render it in a mode picker — `modesOf()` filters it out. */
163
+ legacyAlias?: true;
132
164
  }
133
165
  declare const MODES: readonly ModeInfo[];
166
+ /**
167
+ * D-102 — the two directions between the PRODUCT shape (game Snag → mode normal | sebaq) and the ENGINE
168
+ * shape the relay runs (`gameId 'snag'` = the trivia engine in race flow; `gameId 'sebaq'` = its own
169
+ * engine/driver, D-026 tile-vs-engine). Both spellings stay on the wire forever: `game: 'sebaq'` and
170
+ * `modeId: 'snag'` are what every shipped build sends.
171
+ *
172
+ * INBOUND — `internalGameOf(game, modeId)`: what the client SAID → what the relay RUNS.
173
+ */
174
+ declare function internalGameOf(game: GameId | undefined, modeId: ModeId | undefined): {
175
+ gameId: GameId | undefined;
176
+ modeId: ModeId | undefined;
177
+ };
178
+ /**
179
+ * OUTBOUND — `wireShapeOf(internalGameId, modeId, pv)`: what the relay RUNS → what THIS client can DECODE.
180
+ * pv ≥ 8 sees the product shape; below it, the pv7 shape it shipped with (`sebaq` as a game, `snag` as a mode).
181
+ */
182
+ declare function wireShapeOf(gameId: GameId, modeId: ModeId | null, pv: number | undefined): {
183
+ gameId: GameId;
184
+ modeId: ModeId | null;
185
+ };
134
186
  /** The modes available for a given game (for a client's mode picker). */
135
187
  declare const modesForGame: (gameId: GameId) => ModeInfo[];
136
188
  /**
@@ -279,8 +331,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
279
331
  games: z.ZodOptional<z.ZodArray<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>, "many">>;
280
332
  appVersion: z.ZodOptional<z.ZodString>;
281
333
  }, "strip", z.ZodTypeAny, {
282
- code: string;
283
334
  t: "joinRoom";
335
+ code: string;
284
336
  name: string;
285
337
  avatar?: {
286
338
  emoji: string;
@@ -293,8 +345,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
293
345
  games?: ("seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq")[] | undefined;
294
346
  appVersion?: string | undefined;
295
347
  }, {
296
- code: string;
297
348
  t: "joinRoom";
349
+ code: string;
298
350
  name: string;
299
351
  avatar?: {
300
352
  emoji: string;
@@ -327,8 +379,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
327
379
  games: z.ZodOptional<z.ZodArray<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>, "many">>;
328
380
  appVersion: z.ZodOptional<z.ZodString>;
329
381
  }, "strip", z.ZodTypeAny, {
330
- code: string;
331
382
  t: "resume";
383
+ code: string;
332
384
  token: string;
333
385
  avatar?: {
334
386
  emoji: string;
@@ -341,8 +393,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
341
393
  games?: ("seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq")[] | undefined;
342
394
  appVersion?: string | undefined;
343
395
  }, {
344
- code: string;
345
396
  t: "resume";
397
+ code: string;
346
398
  token: string;
347
399
  avatar?: {
348
400
  emoji: string;
@@ -459,15 +511,15 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
459
511
  }>, z.ZodObject<{
460
512
  t: z.ZodLiteral<"setMode">;
461
513
  game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>>;
462
- modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
514
+ modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "multiplayer", "normal", "sebaq", "snag"]>>;
463
515
  }, "strip", z.ZodTypeAny, {
464
516
  t: "setMode";
465
517
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
466
- modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
518
+ modeId?: "snag" | "sebaq" | "solo" | "oneDevice" | "multiplayer" | "normal" | undefined;
467
519
  }, {
468
520
  t: "setMode";
469
521
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
470
- modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
522
+ modeId?: "snag" | "sebaq" | "solo" | "oneDevice" | "multiplayer" | "normal" | undefined;
471
523
  }>, z.ZodObject<{
472
524
  t: z.ZodLiteral<"hostActivity">;
473
525
  activity: z.ZodEnum<["options", "configuring", "idle"]>;
@@ -491,7 +543,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
491
543
  }, {
492
544
  difficulty?: "easy" | "medium" | "hard" | undefined;
493
545
  }>>;
494
- modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
546
+ modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "multiplayer", "normal", "sebaq", "snag"]>>;
495
547
  buzzerMode: z.ZodOptional<z.ZodEnum<["off", "solo", "race", "controller"]>>;
496
548
  noHost: z.ZodOptional<z.ZodBoolean>;
497
549
  }, "strip", z.ZodTypeAny, {
@@ -499,7 +551,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
499
551
  categoryIds: string[];
500
552
  totalRounds: number;
501
553
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
502
- modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
554
+ modeId?: "snag" | "sebaq" | "solo" | "oneDevice" | "multiplayer" | "normal" | undefined;
503
555
  hostPlays?: boolean | undefined;
504
556
  players?: string[] | undefined;
505
557
  bot?: {
@@ -512,7 +564,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
512
564
  categoryIds: string[];
513
565
  totalRounds: number;
514
566
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
515
- modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
567
+ modeId?: "snag" | "sebaq" | "solo" | "oneDevice" | "multiplayer" | "normal" | undefined;
516
568
  hostPlays?: boolean | undefined;
517
569
  players?: string[] | undefined;
518
570
  bot?: {
@@ -726,8 +778,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
726
778
  color: z.ZodString;
727
779
  width: z.ZodNumber;
728
780
  }, "strip", z.ZodTypeAny, {
729
- color: string;
730
781
  t: "strokeBatch";
782
+ color: string;
731
783
  strokeId: string;
732
784
  seq: number;
733
785
  points: {
@@ -736,8 +788,8 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
736
788
  }[];
737
789
  width: number;
738
790
  }, {
739
- color: string;
740
791
  t: "strokeBatch";
792
+ color: string;
741
793
  strokeId: string;
742
794
  seq: number;
743
795
  points: {
@@ -896,7 +948,7 @@ interface PackView {
896
948
  hasImage?: boolean;
897
949
  }
898
950
  type GameStatus = 'setup' | 'categorySelection' | 'inProgress' | 'completed';
899
- type GameplayFlow = 'classic' | 'openJudged' | 'noHost' | 'buzzer';
951
+ type GameplayFlow = 'classic' | 'noHost' | 'buzzer';
900
952
  type BuzzerGameMode = 'off' | 'solo' | 'race' | 'controller';
901
953
  /** The help/lifeline ids (same set as the inbound `HelpType` zod enum above). */
902
954
  type HelpName = z.infer<typeof HelpType>;
@@ -912,6 +964,7 @@ interface TeamView {
912
964
  helps: {
913
965
  type: HelpName;
914
966
  isUsed: boolean;
967
+ unusable?: true;
915
968
  }[];
916
969
  /** The team's players in rotation order (`connected` = that player's socket is online right now).
917
970
  * Empty for host-run single-device teams. */
@@ -1251,6 +1304,7 @@ type ServerMsg = {
1251
1304
  hostFrame?: string;
1252
1305
  hostPlaying?: boolean;
1253
1306
  hostPinned?: boolean;
1307
+ hostTeamIndex?: number | null;
1254
1308
  players: LobbyPlayer[];
1255
1309
  categories: CategoryView[];
1256
1310
  packs?: PackView[];
@@ -1362,4 +1416,4 @@ interface MyQuestionRow {
1362
1416
  };
1363
1417
  }
1364
1418
 
1365
- export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, COMEBACK_BONUS_MIN_PV, COMEBACK_EFFECTS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, 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, type MyQuestionRow, PROTOCOL_VERSION, type PackView, type QuestionView, REJECT_REASONS, ROOM_ALPHABET, type RejectReason, type RosterEntry, SUBMISSION_KINDS, SUBMISSION_STATUSES, type SebaqStandingRow, type SebaqStateView, type ServerMsg, type SoundName, type StrokePointView, type SubmissionKind, type SubmissionStatus, type TeamSlotView, type TeamView, type YouContext, effectsForPv, gameIdForPv, gamesForPv, hostActivityForPv, modesForGame, normalizeWesternDigits, sanitizeName };
1419
+ export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, COMEBACK_BONUS_MIN_PV, COMEBACK_EFFECTS, type CategoryView, type CharacterId, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, EFFECT_MIN_PV, EMOTE_IDS, type Effect, type EmoteId, type ErsimhaStateView, 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, type MyQuestionRow, PROTOCOL_VERSION, type PackView, type QuestionView, REJECT_REASONS, ROOM_ALPHABET, type RejectReason, type RosterEntry, SEEDHA_MODE_IDS, SNAG_MODES_MIN_PV, SNAG_MODE_IDS, SUBMISSION_KINDS, SUBMISSION_STATUSES, type SebaqStandingRow, type SebaqStateView, type SeedhaModeId, type ServerMsg, type SnagModeId, type SoundName, type StrokePointView, type SubmissionKind, type SubmissionStatus, type TeamSlotView, type TeamView, type YouContext, effectsForPv, gameIdForPv, gamesForPv, hostActivityForPv, internalGameOf, modesForGame, modesOf, normalizeWesternDigits, sanitizeName, wireShapeOf };
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var LIMITS = {
9
9
  * Clients batch pen points to stay under this — ~150ms batching lands at ~7/sec. */
10
10
  STROKE_BATCH_PER_SEC: 20
11
11
  };
12
- var PROTOCOL_VERSION = 7;
12
+ var PROTOCOL_VERSION = 8;
13
13
  var MIN_SUPPORTED_PV = 1;
14
14
  var GAME_IDS = ["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"];
15
15
  var GAMES = [
@@ -41,21 +41,55 @@ var GAMES = [
41
41
  ];
42
42
  var hostActivityForPv = (activity, pv) => activity === "options" && (pv ?? MIN_SUPPORTED_PV) < 6 ? "configuring" : activity;
43
43
  var COMEBACK_BONUS_MIN_PV = 7;
44
+ var EFFECT_MIN_PV = {
45
+ comebackOffer: COMEBACK_BONUS_MIN_PV,
46
+ comebackBonus: COMEBACK_BONUS_MIN_PV,
47
+ comebackDeclined: COMEBACK_BONUS_MIN_PV
48
+ };
44
49
  var COMEBACK_EFFECTS = ["comebackOffer", "comebackBonus", "comebackDeclined"];
45
- var COMEBACK_EFFECT_SET = new Set(COMEBACK_EFFECTS);
46
- var effectsForPv = (effects, pv) => (pv ?? MIN_SUPPORTED_PV) >= COMEBACK_BONUS_MIN_PV ? effects : effects.filter((e) => !COMEBACK_EFFECT_SET.has(e.t));
50
+ var effectsForPv = (effects, pv) => {
51
+ const have = pv ?? MIN_SUPPORTED_PV;
52
+ return effects.filter((e) => (EFFECT_MIN_PV[e.t] ?? MIN_SUPPORTED_PV) <= have);
53
+ };
47
54
  var gamesForPv = (pv) => GAMES.filter((g) => !g.deprecated && g.minPv <= pv);
48
55
  var gameIdForPv = (gameId, pv) => {
49
56
  const g = GAMES.find((x) => x.id === gameId);
50
57
  return g && !g.deprecated && (pv ?? MIN_SUPPORTED_PV) >= g.minPv ? gameId : "trivia";
51
58
  };
52
- var MODE_IDS = ["solo", "oneDevice", "snag", "multiplayer"];
59
+ var SEEDHA_MODE_IDS = ["solo", "oneDevice", "multiplayer"];
60
+ var SNAG_MODE_IDS = ["normal", "sebaq"];
61
+ var MODE_IDS = [...SEEDHA_MODE_IDS, ...SNAG_MODE_IDS, "snag"];
62
+ var SNAG_MODES_MIN_PV = 8;
63
+ function modesOf(gameId) {
64
+ return MODES.filter((m) => m.gameId === gameId && !m.legacyAlias);
65
+ }
53
66
  var MODES = [
54
67
  { id: "solo", gameId: "seedha", label: "Training", onlineEligible: false },
55
68
  { id: "oneDevice", gameId: "seedha", label: "One Device", onlineEligible: false },
56
69
  { id: "multiplayer", gameId: "seedha", label: "Multiplayer", onlineEligible: true },
57
- { id: "snag", gameId: "snag", label: "Snag", onlineEligible: true }
70
+ // D-102: today's buzz Snag, by its real name. OFF the boards (owner 2026-08-29): the host awards 1-answer
71
+ // questions and the Comeback +100, so its score is not the relay's alone.
72
+ { id: "normal", gameId: "snag", label: "Normal", onlineEligible: false },
73
+ { id: "sebaq", gameId: "snag", label: "Sebaq", onlineEligible: true },
74
+ // D-102: the race — relay-scored end to end; THE Snag board
75
+ // NOT a mode — the legacy spelling of «snag / normal» (see MODE_IDS). Kept so old clients' `MODES.find` still maps it.
76
+ { id: "snag", gameId: "snag", label: "Snag", onlineEligible: false, legacyAlias: true }
58
77
  ];
78
+ function internalGameOf(game, modeId) {
79
+ if (modeId === "sebaq" || game === "sebaq") return { gameId: "sebaq", modeId: void 0 };
80
+ if (modeId === "snag") return { gameId: "snag", modeId: "normal" };
81
+ if (modeId === "normal") return { gameId: "snag", modeId: "normal" };
82
+ return { gameId: game, modeId };
83
+ }
84
+ function wireShapeOf(gameId, modeId, pv) {
85
+ const v = pv ?? MIN_SUPPORTED_PV;
86
+ if (gameId === "sebaq") return v >= SNAG_MODES_MIN_PV ? { gameId: "snag", modeId: "sebaq" } : { gameId: gameIdForPv("sebaq", pv), modeId: null };
87
+ if (gameId === "snag") {
88
+ const m = modeId === "normal" || modeId === "snag" ? v >= SNAG_MODES_MIN_PV ? "normal" : "snag" : modeId;
89
+ return { gameId: gameIdForPv("snag", pv), modeId: m };
90
+ }
91
+ return { gameId: gameIdForPv(gameId, pv), modeId };
92
+ }
59
93
  var modesForGame = (gameId) => MODES.filter((m) => m.gameId === gameId);
60
94
  var EMOTE_IDS = ["laugh", "wow", "think", "gg"];
61
95
  var CHARACTER_IDS = ["m", "f"];
@@ -319,6 +353,7 @@ export {
319
353
  COMEBACK_BONUS_MIN_PV,
320
354
  COMEBACK_EFFECTS,
321
355
  ClientMsg,
356
+ EFFECT_MIN_PV,
322
357
  EMOTE_IDS,
323
358
  GAMES,
324
359
  GAME_IDS,
@@ -329,13 +364,19 @@ export {
329
364
  PROTOCOL_VERSION,
330
365
  REJECT_REASONS,
331
366
  ROOM_ALPHABET,
367
+ SEEDHA_MODE_IDS,
368
+ SNAG_MODES_MIN_PV,
369
+ SNAG_MODE_IDS,
332
370
  SUBMISSION_KINDS,
333
371
  SUBMISSION_STATUSES,
334
372
  effectsForPv,
335
373
  gameIdForPv,
336
374
  gamesForPv,
337
375
  hostActivityForPv,
376
+ internalGameOf,
338
377
  modesForGame,
378
+ modesOf,
339
379
  normalizeWesternDigits,
340
- sanitizeName
380
+ sanitizeName,
381
+ wireShapeOf
341
382
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegeem/protocol",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
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",