@thegeem/protocol 0.1.38 → 0.1.41
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/copy-rules.json +120 -0
- package/dist/index.cjs +96 -6
- package/dist/index.d.cts +213 -16
- package/dist/index.d.ts +213 -16
- package/dist/index.js +92 -6
- package/package.json +3 -2
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 =
|
|
39
|
+
declare const PROTOCOL_VERSION = 7;
|
|
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
|
|
@@ -45,7 +45,7 @@ declare const MIN_SUPPORTED_PV = 1;
|
|
|
45
45
|
* pre-pv2 clients (they never saw seedha/snag), and what old clients send on startGame. Every room
|
|
46
46
|
* runs one game, identified by `gameId`.
|
|
47
47
|
*/
|
|
48
|
-
declare const GAME_IDS: readonly ["seedha", "snag", "trivia", "dama", "ersimha"];
|
|
48
|
+
declare const GAME_IDS: readonly ["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"];
|
|
49
49
|
type GameId = (typeof GAME_IDS)[number];
|
|
50
50
|
/**
|
|
51
51
|
* The GAME REGISTRY — player-facing games + the version gating (D-024/D-025). A client renders a tile
|
|
@@ -60,6 +60,56 @@ interface GameInfo {
|
|
|
60
60
|
}
|
|
61
61
|
declare const GAMES: readonly GameInfo[];
|
|
62
62
|
/** Games to OFFER a client of version `pv` (tiles). Excludes the deprecated alias + anything too new. */
|
|
63
|
+
/**
|
|
64
|
+
* ⚠️ THE OUTBOUND ADAPTER FOR `hostActivity`, AND IT PREVENTS A FROZEN LOBBY — not a wrong banner.
|
|
65
|
+
*
|
|
66
|
+
* The generated Swift type is `enum Activity: String, Codable` — a STRICT enum. A build that predates
|
|
67
|
+
* `options` has only `configuring` and `idle` in it, so receiving `"options"` makes the decode THROW,
|
|
68
|
+
* and because `hostActivity` sits inside the lobby struct **the entire lobby frame is dropped**. Every
|
|
69
|
+
* old joiner in the room stops updating: no new players, no team changes, no category picks. Nothing
|
|
70
|
+
* errors on either side.
|
|
71
|
+
*
|
|
72
|
+
* That is not a hypothetical — it is the same «one unknown key drops the whole frame» failure the iOS
|
|
73
|
+
* desk described for `SebaqStateView`, arriving through an enum instead of a missing field.
|
|
74
|
+
*
|
|
75
|
+
* ⚠️ SO IT IS DOWN-MAPPED PER RECIPIENT, and the alternative was worse: without this, the Options page
|
|
76
|
+
* could not ship until an App Store release carrying a lenient decoder had reached every player —
|
|
77
|
+
* and frozen app-store builds never fully do. This is [[D-025]]'s adapt-within-a-window applied to a
|
|
78
|
+
* second field: the server speaks each client's dialect on the way out.
|
|
79
|
+
*
|
|
80
|
+
* `configuring` and not `idle`, deliberately: the host IS busy setting up. An old joiner reads
|
|
81
|
+
* «… يختار الفئات» a step early, which is slightly wrong and completely harmless — where `idle` would
|
|
82
|
+
* say «nothing is happening» while the host configures, which is the wrong sentence.
|
|
83
|
+
*/
|
|
84
|
+
declare const hostActivityForPv: (activity: "options" | "configuring" | "idle", pv?: number) => "options" | "configuring" | "idle";
|
|
85
|
+
/**
|
|
86
|
+
* The pv at which a client can decode the `comebackBonus` effect. Below this it is WITHHELD, never
|
|
87
|
+
* down-mapped: there is nothing older to translate it into, and the alternative is worse than useless.
|
|
88
|
+
*
|
|
89
|
+
* ⚠️ THE FAILURE IT PREVENTS IS NOT «the card does not show». quicktype emits `Effect` as a strict
|
|
90
|
+
* Swift enum, so a build that predates this variant throws on decode — and because effects travel as
|
|
91
|
+
* an ARRAY inside one `fx` frame, the whole frame is dropped. Sounds, reveals, celebrations and score
|
|
92
|
+
* pops in the same batch die with it, silently, for the rest of that game. **One new variant can stop
|
|
93
|
+
* an old client updating at all.**
|
|
94
|
+
*
|
|
95
|
+
* So an old build gets the +100 (the score is in the state it already renders) and no card. That is
|
|
96
|
+
* the honest degradation: the game stays correct, the animation is missing.
|
|
97
|
+
*/
|
|
98
|
+
declare const COMEBACK_BONUS_MIN_PV = 7;
|
|
99
|
+
/**
|
|
100
|
+
* The effects safe to send a client of version `pv`.
|
|
101
|
+
*
|
|
102
|
+
* ⚠️ WITHHOLDING IS THE ONLY OPTION FOR AN EFFECT, and it is why this is a filter rather than a
|
|
103
|
+
* mapper like `hostActivityForPv`. An unknown `hostActivity` string could be down-mapped to an older
|
|
104
|
+
* word that means nearly the same thing; an unknown effect VARIANT has no older equivalent, and
|
|
105
|
+
* inventing one (sending `scorePop` instead) would make the client animate a normal score change for
|
|
106
|
+
* an event that is not one.
|
|
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. */
|
|
111
|
+
declare const COMEBACK_EFFECTS: readonly ["comebackOffer", "comebackBonus", "comebackDeclined"];
|
|
112
|
+
declare const effectsForPv: (effects: Effect[], pv: number | undefined) => Effect[];
|
|
63
113
|
declare const gamesForPv: (pv: number) => GameInfo[];
|
|
64
114
|
/** The gameId to SEND a client of version `pv`: new games need pv >= their minPv; older clients see 'trivia'. */
|
|
65
115
|
declare const gameIdForPv: (gameId: GameId, pv: number | undefined) => GameId;
|
|
@@ -134,11 +184,12 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
134
184
|
emoji: string;
|
|
135
185
|
color: string;
|
|
136
186
|
}>>;
|
|
137
|
-
game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha"]>>;
|
|
187
|
+
game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>>;
|
|
138
188
|
character: z.ZodOptional<z.ZodEnum<["m", "f"]>>;
|
|
139
189
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
140
190
|
idToken: z.ZodOptional<z.ZodString>;
|
|
141
191
|
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
192
|
+
games: z.ZodOptional<z.ZodArray<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>, "many">>;
|
|
142
193
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
143
194
|
}, "strip", z.ZodTypeAny, {
|
|
144
195
|
t: "createRoom";
|
|
@@ -147,11 +198,12 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
147
198
|
emoji: string;
|
|
148
199
|
color: string;
|
|
149
200
|
} | undefined;
|
|
150
|
-
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
|
|
201
|
+
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
|
|
151
202
|
character?: "m" | "f" | undefined;
|
|
152
203
|
pv?: number | undefined;
|
|
153
204
|
idToken?: string | undefined;
|
|
154
205
|
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
206
|
+
games?: ("seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq")[] | undefined;
|
|
155
207
|
appVersion?: string | undefined;
|
|
156
208
|
}, {
|
|
157
209
|
t: "createRoom";
|
|
@@ -160,11 +212,12 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
160
212
|
emoji: string;
|
|
161
213
|
color: string;
|
|
162
214
|
} | undefined;
|
|
163
|
-
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
|
|
215
|
+
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
|
|
164
216
|
character?: "m" | "f" | undefined;
|
|
165
217
|
pv?: number | undefined;
|
|
166
218
|
idToken?: string | undefined;
|
|
167
219
|
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
220
|
+
games?: ("seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq")[] | undefined;
|
|
168
221
|
appVersion?: string | undefined;
|
|
169
222
|
}>, z.ZodObject<{
|
|
170
223
|
t: z.ZodLiteral<"renameHost">;
|
|
@@ -223,6 +276,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
223
276
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
224
277
|
idToken: z.ZodOptional<z.ZodString>;
|
|
225
278
|
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
279
|
+
games: z.ZodOptional<z.ZodArray<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>, "many">>;
|
|
226
280
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
227
281
|
}, "strip", z.ZodTypeAny, {
|
|
228
282
|
code: string;
|
|
@@ -236,6 +290,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
236
290
|
pv?: number | undefined;
|
|
237
291
|
idToken?: string | undefined;
|
|
238
292
|
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
293
|
+
games?: ("seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq")[] | undefined;
|
|
239
294
|
appVersion?: string | undefined;
|
|
240
295
|
}, {
|
|
241
296
|
code: string;
|
|
@@ -249,6 +304,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
249
304
|
pv?: number | undefined;
|
|
250
305
|
idToken?: string | undefined;
|
|
251
306
|
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
307
|
+
games?: ("seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq")[] | undefined;
|
|
252
308
|
appVersion?: string | undefined;
|
|
253
309
|
}>, z.ZodObject<{
|
|
254
310
|
t: z.ZodLiteral<"resume">;
|
|
@@ -268,6 +324,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
268
324
|
pv: z.ZodOptional<z.ZodNumber>;
|
|
269
325
|
idToken: z.ZodOptional<z.ZodString>;
|
|
270
326
|
platform: z.ZodOptional<z.ZodEnum<["web", "ios", "android", "tv"]>>;
|
|
327
|
+
games: z.ZodOptional<z.ZodArray<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>, "many">>;
|
|
271
328
|
appVersion: z.ZodOptional<z.ZodString>;
|
|
272
329
|
}, "strip", z.ZodTypeAny, {
|
|
273
330
|
code: string;
|
|
@@ -281,6 +338,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
281
338
|
pv?: number | undefined;
|
|
282
339
|
idToken?: string | undefined;
|
|
283
340
|
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
341
|
+
games?: ("seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq")[] | undefined;
|
|
284
342
|
appVersion?: string | undefined;
|
|
285
343
|
}, {
|
|
286
344
|
code: string;
|
|
@@ -294,6 +352,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
294
352
|
pv?: number | undefined;
|
|
295
353
|
idToken?: string | undefined;
|
|
296
354
|
platform?: "web" | "ios" | "android" | "tv" | undefined;
|
|
355
|
+
games?: ("seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq")[] | undefined;
|
|
297
356
|
appVersion?: string | undefined;
|
|
298
357
|
}>, z.ZodObject<{
|
|
299
358
|
t: z.ZodLiteral<"setPlaying">;
|
|
@@ -340,6 +399,15 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
340
399
|
}, {
|
|
341
400
|
t: "kick";
|
|
342
401
|
playerId: string;
|
|
402
|
+
}>, z.ZodObject<{
|
|
403
|
+
t: z.ZodLiteral<"comebackDecide">;
|
|
404
|
+
grant: z.ZodBoolean;
|
|
405
|
+
}, "strip", z.ZodTypeAny, {
|
|
406
|
+
t: "comebackDecide";
|
|
407
|
+
grant: boolean;
|
|
408
|
+
}, {
|
|
409
|
+
t: "comebackDecide";
|
|
410
|
+
grant: boolean;
|
|
343
411
|
}>, z.ZodObject<{
|
|
344
412
|
t: z.ZodLiteral<"setCategories">;
|
|
345
413
|
categoryIds: z.ZodArray<z.ZodString, "many">;
|
|
@@ -367,30 +435,51 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
367
435
|
}, {
|
|
368
436
|
t: "pickTeam";
|
|
369
437
|
teamIndex: number;
|
|
438
|
+
}>, z.ZodObject<{
|
|
439
|
+
t: z.ZodLiteral<"sebaqAnswer">;
|
|
440
|
+
optionIndex: z.ZodNumber;
|
|
441
|
+
}, "strip", z.ZodTypeAny, {
|
|
442
|
+
t: "sebaqAnswer";
|
|
443
|
+
optionIndex: number;
|
|
444
|
+
}, {
|
|
445
|
+
t: "sebaqAnswer";
|
|
446
|
+
optionIndex: number;
|
|
447
|
+
}>, z.ZodObject<{
|
|
448
|
+
t: z.ZodLiteral<"sebaqRetry">;
|
|
449
|
+
}, "strip", z.ZodTypeAny, {
|
|
450
|
+
t: "sebaqRetry";
|
|
451
|
+
}, {
|
|
452
|
+
t: "sebaqRetry";
|
|
453
|
+
}>, z.ZodObject<{
|
|
454
|
+
t: z.ZodLiteral<"sebaqSkip">;
|
|
455
|
+
}, "strip", z.ZodTypeAny, {
|
|
456
|
+
t: "sebaqSkip";
|
|
457
|
+
}, {
|
|
458
|
+
t: "sebaqSkip";
|
|
370
459
|
}>, z.ZodObject<{
|
|
371
460
|
t: z.ZodLiteral<"setMode">;
|
|
372
|
-
game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha"]>>;
|
|
461
|
+
game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>>;
|
|
373
462
|
modeId: z.ZodOptional<z.ZodEnum<["solo", "oneDevice", "snag", "multiplayer"]>>;
|
|
374
463
|
}, "strip", z.ZodTypeAny, {
|
|
375
464
|
t: "setMode";
|
|
376
|
-
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
|
|
465
|
+
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
|
|
377
466
|
modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
|
|
378
467
|
}, {
|
|
379
468
|
t: "setMode";
|
|
380
|
-
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
|
|
469
|
+
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
|
|
381
470
|
modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
|
|
382
471
|
}>, z.ZodObject<{
|
|
383
472
|
t: z.ZodLiteral<"hostActivity">;
|
|
384
|
-
activity: z.ZodEnum<["configuring", "idle"]>;
|
|
473
|
+
activity: z.ZodEnum<["options", "configuring", "idle"]>;
|
|
385
474
|
}, "strip", z.ZodTypeAny, {
|
|
386
475
|
t: "hostActivity";
|
|
387
|
-
activity: "configuring" | "idle";
|
|
476
|
+
activity: "options" | "configuring" | "idle";
|
|
388
477
|
}, {
|
|
389
478
|
t: "hostActivity";
|
|
390
|
-
activity: "configuring" | "idle";
|
|
479
|
+
activity: "options" | "configuring" | "idle";
|
|
391
480
|
}>, z.ZodObject<{
|
|
392
481
|
t: z.ZodLiteral<"startGame">;
|
|
393
|
-
game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha"]>>;
|
|
482
|
+
game: z.ZodOptional<z.ZodEnum<["seedha", "snag", "trivia", "dama", "ersimha", "sebaq"]>>;
|
|
394
483
|
totalRounds: z.ZodNumber;
|
|
395
484
|
categoryIds: z.ZodArray<z.ZodString, "many">;
|
|
396
485
|
hostPlays: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -409,7 +498,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
409
498
|
t: "startGame";
|
|
410
499
|
categoryIds: string[];
|
|
411
500
|
totalRounds: number;
|
|
412
|
-
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
|
|
501
|
+
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
|
|
413
502
|
modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
|
|
414
503
|
hostPlays?: boolean | undefined;
|
|
415
504
|
players?: string[] | undefined;
|
|
@@ -422,7 +511,7 @@ declare const ClientMsg: z.ZodDiscriminatedUnion<"t", [z.ZodObject<{
|
|
|
422
511
|
t: "startGame";
|
|
423
512
|
categoryIds: string[];
|
|
424
513
|
totalRounds: number;
|
|
425
|
-
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | undefined;
|
|
514
|
+
game?: "seedha" | "snag" | "trivia" | "dama" | "ersimha" | "sebaq" | undefined;
|
|
426
515
|
modeId?: "snag" | "solo" | "oneDevice" | "multiplayer" | undefined;
|
|
427
516
|
hostPlays?: boolean | undefined;
|
|
428
517
|
players?: string[] | undefined;
|
|
@@ -901,6 +990,26 @@ interface GameStateView {
|
|
|
901
990
|
* paused (e.g. steal intro), or after time-up. Present ⇒ animate a countdown from
|
|
902
991
|
* it; the server remains the authority on actual time-up. */
|
|
903
992
|
timerRemainingMs?: number;
|
|
993
|
+
/**
|
|
994
|
+
* A comeback bonus awaiting the host's verdict (gs#123), or ABSENT when there is none.
|
|
995
|
+
*
|
|
996
|
+
* ⚠️ IT IS STATE, NOT ONLY AN EFFECT, AND THE DIFFERENCE IS A WEDGE. `comebackOffer` arrives as an
|
|
997
|
+
* `fx` — and `fx` is transient and never replayed. So a host who reconnects between the offer and
|
|
998
|
+
* their verdict would come back to a state with no mention of it: no card, no buttons, and no way
|
|
999
|
+
* to resolve. The server would still hold the pending offer, and a pending offer BLOCKS the next
|
|
1000
|
+
* one, so the feature would be permanently dead for the rest of that game with nothing in any log.
|
|
1001
|
+
*
|
|
1002
|
+
* Found by the iOS desk asking «a resume snapshot after the accept carries the new score only,
|
|
1003
|
+
* yes?» — a question about the happy path, whose answer exposed the unhappy one.
|
|
1004
|
+
*
|
|
1005
|
+
* ABSENT rather than null when there is no offer: the card is either on screen or it is not, and an
|
|
1006
|
+
* optional key is the shape a client already has to handle for `timerRemainingMs`.
|
|
1007
|
+
*/
|
|
1008
|
+
comebackOffer?: {
|
|
1009
|
+
teamId: string;
|
|
1010
|
+
questionText: string;
|
|
1011
|
+
scoreGapBeforeBonus: number;
|
|
1012
|
+
};
|
|
904
1013
|
}
|
|
905
1014
|
/** Transient presentation effects the relay emits; clients play them once. */
|
|
906
1015
|
type Effect = {
|
|
@@ -931,6 +1040,21 @@ type Effect = {
|
|
|
931
1040
|
t: 'scorePop';
|
|
932
1041
|
amount: number;
|
|
933
1042
|
streakBonus: number;
|
|
1043
|
+
} | {
|
|
1044
|
+
t: 'comebackBonus';
|
|
1045
|
+
teamId: string;
|
|
1046
|
+
points: number;
|
|
1047
|
+
previousScore: number;
|
|
1048
|
+
newScore: number;
|
|
1049
|
+
scoreGapBeforeBonus: number;
|
|
1050
|
+
} | {
|
|
1051
|
+
t: 'comebackOffer';
|
|
1052
|
+
teamId: string;
|
|
1053
|
+
questionText: string;
|
|
1054
|
+
scoreGapBeforeBonus: number;
|
|
1055
|
+
} | {
|
|
1056
|
+
t: 'comebackDeclined';
|
|
1057
|
+
teamId: string;
|
|
934
1058
|
} | {
|
|
935
1059
|
t: 'chargeCredit';
|
|
936
1060
|
} | {
|
|
@@ -989,6 +1113,73 @@ interface DamaStateView {
|
|
|
989
1113
|
moveCount: number;
|
|
990
1114
|
black?: string;
|
|
991
1115
|
white?: string;
|
|
1116
|
+
/**
|
|
1117
|
+
* When the side to move runs out of time, as absolute epoch ms — render it as a countdown.
|
|
1118
|
+
*
|
|
1119
|
+
* دامة is strictly 1v1, so ONE absent player stops the board completely: every other engine bounded
|
|
1120
|
+
* its equivalent and this one did not, which left a stalled game sitting until the 30-minute room
|
|
1121
|
+
* reaper. On expiry the game ends with NO winner and NO rating change — the same ruling as a
|
|
1122
|
+
* mid-game kick (2026-08-08): counting an abandonment as a win would let someone farm ELO by
|
|
1123
|
+
* walking away from a losing board.
|
|
1124
|
+
*
|
|
1125
|
+
* ADDITIVE and optional: absent while a bot is to move, after the game is over, and from any older
|
|
1126
|
+
* relay. A client that ignores it is correct, just less kind — it simply sees the game end.
|
|
1127
|
+
*/
|
|
1128
|
+
moveDeadlineMs?: number;
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* سباق — the race. Everyone runs the SAME questions, in the SAME order, privately, at their own speed.
|
|
1132
|
+
*
|
|
1133
|
+
* ⚠️ WHY THIS IS A RELAY GAME AND NOT A LOCAL ONE, in one sentence from the iOS desk: **timing cannot
|
|
1134
|
+
* be client-reported or the player with the best connection wins.** Every advance is stamped by the
|
|
1135
|
+
* server, the question set is dealt by the server, and a client never asks for a question — that last
|
|
1136
|
+
* one is how two players would otherwise end up racing different sets.
|
|
1137
|
+
*
|
|
1138
|
+
* The rules, all the owner's:
|
|
1139
|
+
* 1. same questions, same order, for every player — it is what makes «who finished first» mean anything
|
|
1140
|
+
* 2. right on the first tap → FULL points
|
|
1141
|
+
* 3. wrong → the player CHOOSES: try again for HALF, or move on. The cost of a retry is TIME, which
|
|
1142
|
+
* is why it needs no extra penalty and why the choice changes depending on whether you are ahead
|
|
1143
|
+
* 4. wrong twice → 0 for that question, next question. ⚠️ TWO TIERS ONLY — full → half → gone is
|
|
1144
|
+
* something a child can hold; a third tier turns it into arithmetic. Do not add one.
|
|
1145
|
+
* 5. finishing faster → BONUS points
|
|
1146
|
+
* 6. 🔴 HIGHEST SCORE WINS. SPEED ADDS, IT NEVER OVERRIDES. A player who mashes through in 30
|
|
1147
|
+
* seconds finishes first and still loses — that is the intended outcome and it is load-bearing.
|
|
1148
|
+
* 7. live standings visible during play
|
|
1149
|
+
* 8. the host races too
|
|
1150
|
+
*
|
|
1151
|
+
* ⚠️ QUESTION COUNT IS NOT FIXED: it is the selected categories × 6 levels (owner, 2026-08-25 —
|
|
1152
|
+
* «Questions are as players selected categories»). «3 categories» was his example, not the rule.
|
|
1153
|
+
*/
|
|
1154
|
+
interface SebaqStandingRow {
|
|
1155
|
+
playerId: string;
|
|
1156
|
+
/** How many questions this player has FINISHED (not the one they are on). */
|
|
1157
|
+
done: number;
|
|
1158
|
+
score: number;
|
|
1159
|
+
/** Set only when they have finished the whole set — the bonus is not knowable before then. */
|
|
1160
|
+
speedBonus?: number;
|
|
1161
|
+
finished: boolean;
|
|
1162
|
+
}
|
|
1163
|
+
interface SebaqStateView {
|
|
1164
|
+
/** Total questions in this race = categories × 6. Every player runs all of them. */
|
|
1165
|
+
total: number;
|
|
1166
|
+
/** Live standings for everyone, always visible (rule 7). Ordered by the server, best first. */
|
|
1167
|
+
standings: SebaqStandingRow[];
|
|
1168
|
+
/** Set when every racer has finished. Ties on total score break on base score, then on time. */
|
|
1169
|
+
winnerId?: string | null;
|
|
1170
|
+
/** ⚠️ YOUR question only, and only the one you are ON. Never another player's, never the next one —
|
|
1171
|
+
* a client that could see ahead could pre-read while others cannot, which is the whole race. */
|
|
1172
|
+
you?: {
|
|
1173
|
+
index: number;
|
|
1174
|
+
/** ⚠️ The SAME anti-cheat boundary as trivia's: `correctAnswerIndex` is null while the question is
|
|
1175
|
+
* live and set only on reveal. In a race the temptation is stronger — every player holds a live
|
|
1176
|
+
* question at the same moment, so a leaked answer index is a leak to everyone at once. */
|
|
1177
|
+
question: QuestionView;
|
|
1178
|
+
/** true once you have answered wrong ONCE on this question: the half-or-skip choice is live. */
|
|
1179
|
+
retryOffered: boolean;
|
|
1180
|
+
score: number;
|
|
1181
|
+
finished: boolean;
|
|
1182
|
+
};
|
|
992
1183
|
}
|
|
993
1184
|
/**
|
|
994
1185
|
* The authoritative Ersimha round state. Anti-cheat boundary (D-030): `word` is set ONLY in the
|
|
@@ -1065,7 +1256,7 @@ type ServerMsg = {
|
|
|
1065
1256
|
packs?: PackView[];
|
|
1066
1257
|
selectedCategoryIds: string[];
|
|
1067
1258
|
teams: TeamSlotView[];
|
|
1068
|
-
hostActivity: 'configuring' | 'idle';
|
|
1259
|
+
hostActivity: 'options' | 'configuring' | 'idle';
|
|
1069
1260
|
tally?: {
|
|
1070
1261
|
[playerId: string]: number;
|
|
1071
1262
|
};
|
|
@@ -1087,6 +1278,12 @@ type ServerMsg = {
|
|
|
1087
1278
|
view: DamaStateView;
|
|
1088
1279
|
you: YouContext;
|
|
1089
1280
|
roster?: RosterEntry[];
|
|
1281
|
+
} | {
|
|
1282
|
+
t: 'sebaqState';
|
|
1283
|
+
gameId: 'sebaq';
|
|
1284
|
+
view: SebaqStateView;
|
|
1285
|
+
you: YouContext;
|
|
1286
|
+
roster?: RosterEntry[];
|
|
1090
1287
|
} | {
|
|
1091
1288
|
t: 'ersimhaState';
|
|
1092
1289
|
gameId: 'ersimha';
|
|
@@ -1165,4 +1362,4 @@ interface MyQuestionRow {
|
|
|
1165
1362
|
};
|
|
1166
1363
|
}
|
|
1167
1364
|
|
|
1168
|
-
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, type MyQuestionRow, PROTOCOL_VERSION, type PackView, type QuestionView, REJECT_REASONS, ROOM_ALPHABET, type RejectReason, type RosterEntry, SUBMISSION_KINDS, SUBMISSION_STATUSES, type ServerMsg, type SoundName, type StrokePointView, type SubmissionKind, type SubmissionStatus, type TeamSlotView, type TeamView, type YouContext, gameIdForPv, gamesForPv, modesForGame, normalizeWesternDigits, sanitizeName };
|
|
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 };
|