@thegeem/protocol 0.1.24 → 0.1.26

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
@@ -20,7 +20,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ CHARACTER_IDS: () => CHARACTER_IDS,
23
24
  ClientMsg: () => ClientMsg,
25
+ EMOTE_IDS: () => EMOTE_IDS,
24
26
  GAMES: () => GAMES,
25
27
  GAME_IDS: () => GAME_IDS,
26
28
  LIMITS: () => LIMITS,
@@ -69,6 +71,8 @@ var MODES = [
69
71
  { id: "snag", gameId: "snag", label: "Snag", onlineEligible: true }
70
72
  ];
71
73
  var modesForGame = (gameId) => MODES.filter((m) => m.gameId === gameId);
74
+ var EMOTE_IDS = ["laugh", "wow", "think", "gg"];
75
+ var CHARACTER_IDS = ["m", "f"];
72
76
  var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
73
77
  var UNSAFE_CHARS = /[\p{Cc}<>]/gu;
74
78
  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");
@@ -90,9 +94,10 @@ var Avatar = import_zod.z.object({
90
94
  emoji: import_zod.z.string().min(1).max(16).transform((s) => s.replace(UNSAFE_CHARS, "")).refine((s) => s.length >= 1, "empty emoji"),
91
95
  color: import_zod.z.string().regex(/^#[0-9a-fA-F]{6}$/, "color must be #rrggbb")
92
96
  });
97
+ var Character = import_zod.z.enum(CHARACTER_IDS);
93
98
  var ClientMsg = import_zod.z.discriminatedUnion("t", [
94
99
  // room / lobby
95
- import_zod.z.object({ t: import_zod.z.literal("createRoom"), name: DisplayName.optional(), avatar: Avatar.optional(), game: import_zod.z.enum(GAME_IDS).optional(), pv: Pv, idToken: IdToken, platform: Platform }),
100
+ import_zod.z.object({ t: import_zod.z.literal("createRoom"), name: DisplayName.optional(), avatar: Avatar.optional(), game: import_zod.z.enum(GAME_IDS).optional(), character: Character.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
96
101
  // Host sets/changes their display name AFTER createRoom (for anonymous one-tap
97
102
  // hosts who started nameless). Host-only; the relay re-broadcasts the lobby.
98
103
  // SUPERSEDED by setProfile (kept working for shipped builds).
@@ -105,9 +110,9 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
105
110
  // Note: mid-game propagation depends on what the running game's state carries — trivia's
106
111
  // view carries names+avatars so it updates live; dama/ersimha views are id-keyed (clients
107
112
  // map ids→names from the lobby roster), so a change there lands on the next lobby.
108
- import_zod.z.object({ t: import_zod.z.literal("setProfile"), name: DisplayName.optional(), avatar: Avatar.optional() }),
109
- import_zod.z.object({ t: import_zod.z.literal("joinRoom"), code: RoomCode, name: DisplayName, avatar: Avatar.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
110
- 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 }),
113
+ import_zod.z.object({ t: import_zod.z.literal("setProfile"), name: DisplayName.optional(), avatar: Avatar.optional(), character: Character.optional() }),
114
+ import_zod.z.object({ t: import_zod.z.literal("joinRoom"), code: RoomCode, name: DisplayName, avatar: Avatar.optional(), character: Character.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
115
+ import_zod.z.object({ t: import_zod.z.literal("resume"), code: RoomCode, token: import_zod.z.string().max(120), avatar: Avatar.optional(), character: Character.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
111
116
  import_zod.z.object({ t: import_zod.z.literal("leaveRoom") }),
112
117
  import_zod.z.object({ t: import_zod.z.literal("approve"), playerId: PlayerId }),
113
118
  import_zod.z.object({ t: import_zod.z.literal("reject"), playerId: PlayerId }),
@@ -150,6 +155,12 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
150
155
  // false = the first two approved joiners take the board and the host WATCHES
151
156
  // (yourColor:null) while keeping every host power. Needs 2 approved joiners.
152
157
  hostPlays: import_zod.z.boolean().optional(),
158
+ // Name the seats explicitly instead of taking them by join order (gs#47 Ask 2, layered on per
159
+ // D-037). Exactly two DISTINCT ids, both approved members of the room (the host may be one):
160
+ // `players[0]` plays black and moves first, `players[1]` plays white; everyone else in the room
161
+ // spectates. When present this OVERRIDES `hostPlays` — an explicit seating beats an implicit one.
162
+ // Violations are rejected with `bad_players`. Consumed by دامة (the only fixed-seat game today).
163
+ players: import_zod.z.array(PlayerId).length(2).optional(),
153
164
  // PREFERRED: pick a mode from the registry (see MODES). The server maps it to the engine
154
165
  // rules. Send this and omit buzzerMode/noHost. Falls back to the legacy flags below if omitted.
155
166
  modeId: import_zod.z.enum(MODE_IDS).optional(),
@@ -173,6 +184,11 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
173
184
  import_zod.z.object({ t: import_zod.z.literal("noHostNobody") }),
174
185
  // player actions
175
186
  import_zod.z.object({ t: import_zod.z.literal("buzz") }),
187
+ // «طقطقة» (D-038): send one of the four closed emotes. Server-enforced 5s per-player throttle on
188
+ // its OWN rate lane — a client-side throttle is trivially bypassed, and this is a family app, so
189
+ // Shape B applies exactly as it does to buzz arrival order. Over-rate emotes are dropped silently.
190
+ // Ephemeral: relayed, never stored. Allowed only while a game whose driver permits emotes is running.
191
+ import_zod.z.object({ t: import_zod.z.literal("sendEmote"), emoteId: import_zod.z.enum(EMOTE_IDS) }),
176
192
  import_zod.z.object({ t: import_zod.z.literal("answer"), index: import_zod.z.number().int().min(0).max(3) }),
177
193
  // trivia is 4-option (0–3)
178
194
  import_zod.z.object({ t: import_zod.z.literal("useHelp"), helpType: HelpType }),
@@ -223,7 +239,9 @@ var ClientMsg = import_zod.z.discriminatedUnion("t", [
223
239
  ]);
224
240
  // Annotate the CommonJS export names for ESM import in node:
225
241
  0 && (module.exports = {
242
+ CHARACTER_IDS,
226
243
  ClientMsg,
244
+ EMOTE_IDS,
227
245
  GAMES,
228
246
  GAME_IDS,
229
247
  LIMITS,
package/dist/index.d.cts CHANGED
@@ -83,6 +83,24 @@ interface ModeInfo {
83
83
  declare const MODES: readonly ModeInfo[];
84
84
  /** The modes available for a given game (for a client's mode picker). */
85
85
  declare const modesForGame: (gameId: GameId) => ModeInfo[];
86
+ /**
87
+ * «طقطقة» (tagtaga) emotes — D-038 (filed as D-036 in gs#50), design `geem-design DESIGN.md § tagtaga`.
88
+ * A CLOSED, POSITIVE-ONLY set of exactly four: ههههه · يا سلام · قاعد أفكر · لعب زين. The ids match the
89
+ * art masters (`art/tagtaga/Tagtaga-<emoteId>-<character>.png`), so the wire addresses the sprite directly.
90
+ * ⚠ Never add hurry-up / gloat / crying / angry — that's a RULING, not an oversight: «think» exists so a
91
+ * slow player can speak about THEMSELVES rather than anyone pressuring anyone. A closed enum with no
92
+ * free-form payload is also what keeps emotes off the UGC/moderation surface entirely.
93
+ */
94
+ declare const EMOTE_IDS: readonly ["laugh", "wow", "think", "gg"];
95
+ type EmoteId = (typeof EMOTE_IDS)[number];
96
+ /**
97
+ * Which character sprite a player is drawn as: `m` = ghutra, `f` = hijab (the `-m`/`-f` art suffix).
98
+ * Both carry the identical emote set. It's a display preference, not identity: set it on room entry
99
+ * or change it any time via `setProfile`, and the server stamps it on that player's outgoing emotes
100
+ * so receivers can pick the right sprite without a roster lookup.
101
+ */
102
+ declare const CHARACTER_IDS: readonly ["m", "f"];
103
+ type CharacterId = (typeof CHARACTER_IDS)[number];
86
104
  /** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
87
105
  declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
88
106
  declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
@@ -100,6 +118,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
100
118
  color: string;
101
119
  }>>;
102
120
  game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha"]>>;
121
+ character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
103
122
  pv: z.ZodOptional<z.ZodNumber>;
104
123
  idToken: z.ZodOptional<z.ZodString>;
105
124
  platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
@@ -111,6 +130,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
111
130
  color: string;
112
131
  } | undefined;
113
132
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
133
+ character?: "m" | "f" | undefined;
114
134
  pv?: number | undefined;
115
135
  idToken?: string | undefined;
116
136
  platform?: "web" | "ios" | "android" | undefined;
@@ -122,6 +142,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
122
142
  color: string;
123
143
  } | undefined;
124
144
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
145
+ character?: "m" | "f" | undefined;
125
146
  pv?: number | undefined;
126
147
  idToken?: string | undefined;
127
148
  platform?: "web" | "ios" | "android" | undefined;
@@ -147,6 +168,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
147
168
  emoji: string;
148
169
  color: string;
149
170
  }>>;
171
+ character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
150
172
  }, "strip", z.ZodTypeAny, {
151
173
  t: "setProfile";
152
174
  name?: string | undefined;
@@ -154,6 +176,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
154
176
  emoji: string;
155
177
  color: string;
156
178
  } | undefined;
179
+ character?: "m" | "f" | undefined;
157
180
  }, {
158
181
  t: "setProfile";
159
182
  name?: string | undefined;
@@ -161,6 +184,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
161
184
  emoji: string;
162
185
  color: string;
163
186
  } | undefined;
187
+ character?: "m" | "f" | undefined;
164
188
  }>, z.ZodObject<{
165
189
  t: z.ZodLiteral<"joinRoom">;
166
190
  code: z.ZodString;
@@ -175,6 +199,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
175
199
  emoji: string;
176
200
  color: string;
177
201
  }>>;
202
+ character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
178
203
  pv: z.ZodOptional<z.ZodNumber>;
179
204
  idToken: z.ZodOptional<z.ZodString>;
180
205
  platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
@@ -186,6 +211,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
186
211
  emoji: string;
187
212
  color: string;
188
213
  } | undefined;
214
+ character?: "m" | "f" | undefined;
189
215
  pv?: number | undefined;
190
216
  idToken?: string | undefined;
191
217
  platform?: "web" | "ios" | "android" | undefined;
@@ -197,6 +223,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
197
223
  emoji: string;
198
224
  color: string;
199
225
  } | undefined;
226
+ character?: "m" | "f" | undefined;
200
227
  pv?: number | undefined;
201
228
  idToken?: string | undefined;
202
229
  platform?: "web" | "ios" | "android" | undefined;
@@ -214,6 +241,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
214
241
  emoji: string;
215
242
  color: string;
216
243
  }>>;
244
+ character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
217
245
  pv: z.ZodOptional<z.ZodNumber>;
218
246
  idToken: z.ZodOptional<z.ZodString>;
219
247
  platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
@@ -225,6 +253,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
225
253
  emoji: string;
226
254
  color: string;
227
255
  } | undefined;
256
+ character?: "m" | "f" | undefined;
228
257
  pv?: number | undefined;
229
258
  idToken?: string | undefined;
230
259
  platform?: "web" | "ios" | "android" | undefined;
@@ -236,6 +265,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
236
265
  emoji: string;
237
266
  color: string;
238
267
  } | undefined;
268
+ character?: "m" | "f" | undefined;
239
269
  pv?: number | undefined;
240
270
  idToken?: string | undefined;
241
271
  platform?: "web" | "ios" | "android" | undefined;
@@ -326,6 +356,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
326
356
  totalRounds: z.ZodNumber;
327
357
  categoryIds: z.ZodArray<z.ZodString, "many">;
328
358
  hostPlays: z.ZodOptional<z.ZodBoolean>;
359
+ players: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
329
360
  modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
330
361
  buzzerMode: z.ZodOptional<z.ZodEnum<["off", "solo", "race", "controller"]>>;
331
362
  noHost: z.ZodOptional<z.ZodBoolean>;
@@ -336,6 +367,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
336
367
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
337
368
  modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
338
369
  hostPlays?: boolean | undefined;
370
+ players?: string[] | undefined;
339
371
  buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
340
372
  noHost?: boolean | undefined;
341
373
  }, {
@@ -345,6 +377,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
345
377
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
346
378
  modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
347
379
  hostPlays?: boolean | undefined;
380
+ players?: string[] | undefined;
348
381
  buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
349
382
  noHost?: boolean | undefined;
350
383
  }>, z.ZodObject<{
@@ -407,6 +440,15 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
407
440
  t: "buzz";
408
441
  }, {
409
442
  t: "buzz";
443
+ }>, z.ZodObject<{
444
+ t: z.ZodLiteral<"sendEmote">;
445
+ emoteId: z.ZodEnum<["laugh", "wow", "think", "gg"]>;
446
+ }, "strip", z.ZodTypeAny, {
447
+ t: "sendEmote";
448
+ emoteId: "laugh" | "wow" | "think" | "gg";
449
+ }, {
450
+ t: "sendEmote";
451
+ emoteId: "laugh" | "wow" | "think" | "gg";
410
452
  }>, z.ZodObject<{
411
453
  t: z.ZodLiteral<"answer">;
412
454
  index: z.ZodNumber;
@@ -827,6 +869,11 @@ type ServerMsg = {
827
869
  } | {
828
870
  t: 'roomClosed';
829
871
  reason: string;
872
+ } | {
873
+ t: 'emote';
874
+ from: string;
875
+ emoteId: EmoteId;
876
+ character: CharacterId;
830
877
  } | {
831
878
  t: 'lobby';
832
879
  code: string;
@@ -891,4 +938,4 @@ type ServerMsg = {
891
938
  message: string;
892
939
  };
893
940
 
894
- export { type AvatarView, type BuzzerGameMode, type CategoryView, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, type Effect, 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, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type ServerMsg, type SoundName, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
941
+ export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, 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, 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
@@ -83,6 +83,24 @@ interface ModeInfo {
83
83
  declare const MODES: readonly ModeInfo[];
84
84
  /** The modes available for a given game (for a client's mode picker). */
85
85
  declare const modesForGame: (gameId: GameId) => ModeInfo[];
86
+ /**
87
+ * «طقطقة» (tagtaga) emotes — D-038 (filed as D-036 in gs#50), design `geem-design DESIGN.md § tagtaga`.
88
+ * A CLOSED, POSITIVE-ONLY set of exactly four: ههههه · يا سلام · قاعد أفكر · لعب زين. The ids match the
89
+ * art masters (`art/tagtaga/Tagtaga-<emoteId>-<character>.png`), so the wire addresses the sprite directly.
90
+ * ⚠ Never add hurry-up / gloat / crying / angry — that's a RULING, not an oversight: «think» exists so a
91
+ * slow player can speak about THEMSELVES rather than anyone pressuring anyone. A closed enum with no
92
+ * free-form payload is also what keeps emotes off the UGC/moderation surface entirely.
93
+ */
94
+ declare const EMOTE_IDS: readonly ["laugh", "wow", "think", "gg"];
95
+ type EmoteId = (typeof EMOTE_IDS)[number];
96
+ /**
97
+ * Which character sprite a player is drawn as: `m` = ghutra, `f` = hijab (the `-m`/`-f` art suffix).
98
+ * Both carry the identical emote set. It's a display preference, not identity: set it on room entry
99
+ * or change it any time via `setProfile`, and the server stamps it on that player's outgoing emotes
100
+ * so receivers can pick the right sprite without a roster lookup.
101
+ */
102
+ declare const CHARACTER_IDS: readonly ["m", "f"];
103
+ type CharacterId = (typeof CHARACTER_IDS)[number];
86
104
  /** Unambiguous room-code alphabet — no 0/O, 1/I/L. */
87
105
  declare const ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
88
106
  declare const HelpType: z.ZodEnum<["removeTwoAnswers", "changeQuestion", "extraTime", "doublePoints", "stealPoints", "restPlayer", "tripLevel"]>;
@@ -100,6 +118,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
100
118
  color: string;
101
119
  }>>;
102
120
  game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha"]>>;
121
+ character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
103
122
  pv: z.ZodOptional<z.ZodNumber>;
104
123
  idToken: z.ZodOptional<z.ZodString>;
105
124
  platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
@@ -111,6 +130,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
111
130
  color: string;
112
131
  } | undefined;
113
132
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
133
+ character?: "m" | "f" | undefined;
114
134
  pv?: number | undefined;
115
135
  idToken?: string | undefined;
116
136
  platform?: "web" | "ios" | "android" | undefined;
@@ -122,6 +142,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
122
142
  color: string;
123
143
  } | undefined;
124
144
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
145
+ character?: "m" | "f" | undefined;
125
146
  pv?: number | undefined;
126
147
  idToken?: string | undefined;
127
148
  platform?: "web" | "ios" | "android" | undefined;
@@ -147,6 +168,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
147
168
  emoji: string;
148
169
  color: string;
149
170
  }>>;
171
+ character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
150
172
  }, "strip", z.ZodTypeAny, {
151
173
  t: "setProfile";
152
174
  name?: string | undefined;
@@ -154,6 +176,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
154
176
  emoji: string;
155
177
  color: string;
156
178
  } | undefined;
179
+ character?: "m" | "f" | undefined;
157
180
  }, {
158
181
  t: "setProfile";
159
182
  name?: string | undefined;
@@ -161,6 +184,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
161
184
  emoji: string;
162
185
  color: string;
163
186
  } | undefined;
187
+ character?: "m" | "f" | undefined;
164
188
  }>, z.ZodObject<{
165
189
  t: z.ZodLiteral<"joinRoom">;
166
190
  code: z.ZodString;
@@ -175,6 +199,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
175
199
  emoji: string;
176
200
  color: string;
177
201
  }>>;
202
+ character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
178
203
  pv: z.ZodOptional<z.ZodNumber>;
179
204
  idToken: z.ZodOptional<z.ZodString>;
180
205
  platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
@@ -186,6 +211,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
186
211
  emoji: string;
187
212
  color: string;
188
213
  } | undefined;
214
+ character?: "m" | "f" | undefined;
189
215
  pv?: number | undefined;
190
216
  idToken?: string | undefined;
191
217
  platform?: "web" | "ios" | "android" | undefined;
@@ -197,6 +223,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
197
223
  emoji: string;
198
224
  color: string;
199
225
  } | undefined;
226
+ character?: "m" | "f" | undefined;
200
227
  pv?: number | undefined;
201
228
  idToken?: string | undefined;
202
229
  platform?: "web" | "ios" | "android" | undefined;
@@ -214,6 +241,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
214
241
  emoji: string;
215
242
  color: string;
216
243
  }>>;
244
+ character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
217
245
  pv: z.ZodOptional<z.ZodNumber>;
218
246
  idToken: z.ZodOptional<z.ZodString>;
219
247
  platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android"]>>;
@@ -225,6 +253,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
225
253
  emoji: string;
226
254
  color: string;
227
255
  } | undefined;
256
+ character?: "m" | "f" | undefined;
228
257
  pv?: number | undefined;
229
258
  idToken?: string | undefined;
230
259
  platform?: "web" | "ios" | "android" | undefined;
@@ -236,6 +265,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
236
265
  emoji: string;
237
266
  color: string;
238
267
  } | undefined;
268
+ character?: "m" | "f" | undefined;
239
269
  pv?: number | undefined;
240
270
  idToken?: string | undefined;
241
271
  platform?: "web" | "ios" | "android" | undefined;
@@ -326,6 +356,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
326
356
  totalRounds: z.ZodNumber;
327
357
  categoryIds: z.ZodArray<z.ZodString, "many">;
328
358
  hostPlays: z.ZodOptional<z.ZodBoolean>;
359
+ players: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
329
360
  modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
330
361
  buzzerMode: z.ZodOptional<z.ZodEnum<["off", "solo", "race", "controller"]>>;
331
362
  noHost: z.ZodOptional<z.ZodBoolean>;
@@ -336,6 +367,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
336
367
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
337
368
  modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
338
369
  hostPlays?: boolean | undefined;
370
+ players?: string[] | undefined;
339
371
  buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
340
372
  noHost?: boolean | undefined;
341
373
  }, {
@@ -345,6 +377,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
345
377
  game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
346
378
  modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
347
379
  hostPlays?: boolean | undefined;
380
+ players?: string[] | undefined;
348
381
  buzzerMode?: "solo" | "off" | "race" | "controller" | undefined;
349
382
  noHost?: boolean | undefined;
350
383
  }>, z.ZodObject<{
@@ -407,6 +440,15 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
407
440
  t: "buzz";
408
441
  }, {
409
442
  t: "buzz";
443
+ }>, z.ZodObject<{
444
+ t: z.ZodLiteral<"sendEmote">;
445
+ emoteId: z.ZodEnum<["laugh", "wow", "think", "gg"]>;
446
+ }, "strip", z.ZodTypeAny, {
447
+ t: "sendEmote";
448
+ emoteId: "laugh" | "wow" | "think" | "gg";
449
+ }, {
450
+ t: "sendEmote";
451
+ emoteId: "laugh" | "wow" | "think" | "gg";
410
452
  }>, z.ZodObject<{
411
453
  t: z.ZodLiteral<"answer">;
412
454
  index: z.ZodNumber;
@@ -827,6 +869,11 @@ type ServerMsg = {
827
869
  } | {
828
870
  t: 'roomClosed';
829
871
  reason: string;
872
+ } | {
873
+ t: 'emote';
874
+ from: string;
875
+ emoteId: EmoteId;
876
+ character: CharacterId;
830
877
  } | {
831
878
  t: 'lobby';
832
879
  code: string;
@@ -891,4 +938,4 @@ type ServerMsg = {
891
938
  message: string;
892
939
  };
893
940
 
894
- export { type AvatarView, type BuzzerGameMode, type CategoryView, ClientMsg, type DamaMoveView, type DamaPieceView, type DamaStateView, type Effect, 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, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type ServerMsg, type SoundName, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
941
+ export { type AvatarView, type BuzzerGameMode, CHARACTER_IDS, 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, PROTOCOL_VERSION, type QuestionView, ROOM_ALPHABET, type ServerMsg, type SoundName, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame };
package/dist/index.js CHANGED
@@ -34,6 +34,8 @@ var MODES = [
34
34
  { id: "snag", gameId: "snag", label: "Snag", onlineEligible: true }
35
35
  ];
36
36
  var modesForGame = (gameId) => MODES.filter((m) => m.gameId === gameId);
37
+ var EMOTE_IDS = ["laugh", "wow", "think", "gg"];
38
+ var CHARACTER_IDS = ["m", "f"];
37
39
  var ROOM_ALPHABET = "ACDEFGHJKMNPQRTUVWXY2346789";
38
40
  var UNSAFE_CHARS = /[\p{Cc}<>]/gu;
39
41
  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");
@@ -55,9 +57,10 @@ var Avatar = z.object({
55
57
  emoji: z.string().min(1).max(16).transform((s) => s.replace(UNSAFE_CHARS, "")).refine((s) => s.length >= 1, "empty emoji"),
56
58
  color: z.string().regex(/^#[0-9a-fA-F]{6}$/, "color must be #rrggbb")
57
59
  });
60
+ var Character = z.enum(CHARACTER_IDS);
58
61
  var ClientMsg = z.discriminatedUnion("t", [
59
62
  // room / lobby
60
- z.object({ t: z.literal("createRoom"), name: DisplayName.optional(), avatar: Avatar.optional(), game: z.enum(GAME_IDS).optional(), pv: Pv, idToken: IdToken, platform: Platform }),
63
+ z.object({ t: z.literal("createRoom"), name: DisplayName.optional(), avatar: Avatar.optional(), game: z.enum(GAME_IDS).optional(), character: Character.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
61
64
  // Host sets/changes their display name AFTER createRoom (for anonymous one-tap
62
65
  // hosts who started nameless). Host-only; the relay re-broadcasts the lobby.
63
66
  // SUPERSEDED by setProfile (kept working for shipped builds).
@@ -70,9 +73,9 @@ var ClientMsg = z.discriminatedUnion("t", [
70
73
  // Note: mid-game propagation depends on what the running game's state carries — trivia's
71
74
  // view carries names+avatars so it updates live; dama/ersimha views are id-keyed (clients
72
75
  // map ids→names from the lobby roster), so a change there lands on the next lobby.
73
- z.object({ t: z.literal("setProfile"), name: DisplayName.optional(), avatar: Avatar.optional() }),
74
- z.object({ t: z.literal("joinRoom"), code: RoomCode, name: DisplayName, avatar: Avatar.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
75
- z.object({ t: z.literal("resume"), code: RoomCode, token: z.string().max(120), avatar: Avatar.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
76
+ z.object({ t: z.literal("setProfile"), name: DisplayName.optional(), avatar: Avatar.optional(), character: Character.optional() }),
77
+ z.object({ t: z.literal("joinRoom"), code: RoomCode, name: DisplayName, avatar: Avatar.optional(), character: Character.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
78
+ z.object({ t: z.literal("resume"), code: RoomCode, token: z.string().max(120), avatar: Avatar.optional(), character: Character.optional(), pv: Pv, idToken: IdToken, platform: Platform }),
76
79
  z.object({ t: z.literal("leaveRoom") }),
77
80
  z.object({ t: z.literal("approve"), playerId: PlayerId }),
78
81
  z.object({ t: z.literal("reject"), playerId: PlayerId }),
@@ -115,6 +118,12 @@ var ClientMsg = z.discriminatedUnion("t", [
115
118
  // false = the first two approved joiners take the board and the host WATCHES
116
119
  // (yourColor:null) while keeping every host power. Needs 2 approved joiners.
117
120
  hostPlays: z.boolean().optional(),
121
+ // Name the seats explicitly instead of taking them by join order (gs#47 Ask 2, layered on per
122
+ // D-037). Exactly two DISTINCT ids, both approved members of the room (the host may be one):
123
+ // `players[0]` plays black and moves first, `players[1]` plays white; everyone else in the room
124
+ // spectates. When present this OVERRIDES `hostPlays` — an explicit seating beats an implicit one.
125
+ // Violations are rejected with `bad_players`. Consumed by دامة (the only fixed-seat game today).
126
+ players: z.array(PlayerId).length(2).optional(),
118
127
  // PREFERRED: pick a mode from the registry (see MODES). The server maps it to the engine
119
128
  // rules. Send this and omit buzzerMode/noHost. Falls back to the legacy flags below if omitted.
120
129
  modeId: z.enum(MODE_IDS).optional(),
@@ -138,6 +147,11 @@ var ClientMsg = z.discriminatedUnion("t", [
138
147
  z.object({ t: z.literal("noHostNobody") }),
139
148
  // player actions
140
149
  z.object({ t: z.literal("buzz") }),
150
+ // «طقطقة» (D-038): send one of the four closed emotes. Server-enforced 5s per-player throttle on
151
+ // its OWN rate lane — a client-side throttle is trivially bypassed, and this is a family app, so
152
+ // Shape B applies exactly as it does to buzz arrival order. Over-rate emotes are dropped silently.
153
+ // Ephemeral: relayed, never stored. Allowed only while a game whose driver permits emotes is running.
154
+ z.object({ t: z.literal("sendEmote"), emoteId: z.enum(EMOTE_IDS) }),
141
155
  z.object({ t: z.literal("answer"), index: z.number().int().min(0).max(3) }),
142
156
  // trivia is 4-option (0–3)
143
157
  z.object({ t: z.literal("useHelp"), helpType: HelpType }),
@@ -187,7 +201,9 @@ var ClientMsg = z.discriminatedUnion("t", [
187
201
  z.object({ t: z.literal("skipWord") })
188
202
  ]);
189
203
  export {
204
+ CHARACTER_IDS,
190
205
  ClientMsg,
206
+ EMOTE_IDS,
191
207
  GAMES,
192
208
  GAME_IDS,
193
209
  LIMITS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thegeem/protocol",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
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",