@orakl/protocol 0.14.0 → 0.15.0

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.
@@ -1 +1 @@
1
- export type ErrorCode = "no_active_lobby" | "no_active_game" | "device_blocked" | "nickname_taken" | "nickname_locked" | "device_in_lobby" | "player_not_found" | "player_not_pending" | "intermission_invalid_phase" | "not_in_intermission" | "category_name_exists" | "category_not_found" | "category_has_questions" | "tag_name_exists" | "tag_not_found" | "tag_has_questions" | "tag_in_season" | "category_in_season" | "season_name_exists" | "season_not_found" | "user_not_found" | "curation_request_pending" | "curator_self_join" | "already_flagged" | "question_not_found" | "invalid_answer" | "game_in_progress" | "already_answered" | "observer_forbidden" | "no_questions" | "season_out_of_season" | "no_categories" | "too_few_players" | "hosting_closed" | "not_answered" | "emote_not_in_set" | "flag_not_toggleable" | "solo_invalid_phase" | "solo_invalid_answer" | "solo_nonce_mismatch" | "solo_answer_expired" | "solo_guest_limit" | "solo_no_questions" | "solo_season_out_of_season" | "solo_resume_denied" | "solo_auth_failed";
1
+ export type ErrorCode = "no_active_lobby" | "no_active_game" | "device_blocked" | "nickname_taken" | "nickname_locked" | "device_in_lobby" | "player_not_found" | "player_not_pending" | "intermission_invalid_phase" | "not_in_intermission" | "category_name_exists" | "category_not_found" | "category_has_questions" | "tag_name_exists" | "tag_not_found" | "tag_has_questions" | "tag_in_season" | "category_in_season" | "season_name_exists" | "season_not_found" | "user_not_found" | "curation_request_pending" | "curator_self_join" | "already_flagged" | "question_not_found" | "invalid_answer" | "game_in_progress" | "already_answered" | "observer_forbidden" | "no_questions" | "season_out_of_season" | "no_categories" | "too_few_players" | "hosting_closed" | "not_answered" | "emote_not_in_set" | "flag_not_toggleable" | "undo_disabled" | "undo_exhausted" | "solo_invalid_phase" | "solo_invalid_answer" | "solo_nonce_mismatch" | "solo_answer_expired" | "solo_guest_limit" | "solo_no_questions" | "solo_season_out_of_season" | "solo_resume_denied" | "solo_auth_failed";
@@ -79,6 +79,11 @@ export declare const GAME: {
79
79
  readonly player: {
80
80
  readonly maxNicknameLength: 20;
81
81
  };
82
+ readonly undo: {
83
+ readonly budgetFraction: 0.1;
84
+ readonly minBudget: 1;
85
+ readonly lastCallSeconds: 5;
86
+ };
82
87
  readonly sse: {
83
88
  readonly keepaliveIntervalMs: 15000;
84
89
  };
@@ -55,6 +55,14 @@ export const GAME = {
55
55
  player: {
56
56
  maxNicknameLength: 20,
57
57
  },
58
+ undo: {
59
+ // Per-player budget = max(minBudget, ceil(totalQuestions * budgetFraction)).
60
+ budgetFraction: 0.1,
61
+ minBudget: 1,
62
+ // Grace window once every hero has answered, during which an undo may
63
+ // still cancel the round close.
64
+ lastCallSeconds: 5,
65
+ },
58
66
  sse: {
59
67
  keepaliveIntervalMs: 15000,
60
68
  },
package/dist/index.d.ts CHANGED
@@ -19,6 +19,6 @@ export type { StreakFlourish } from "./streak-flourish.ts";
19
19
  export type { LeaderboardBoard, LeaderboardDifficulty, LeaderboardScope, } from "./wire/leaderboard.ts";
20
20
  export type { OperatorQuestionRevision, PublishManyResult, QuestionRevision, } from "./wire/question-bank.ts";
21
21
  export type { FlagDetail, FlagReporter, FlagStatus, QuestionFlagAnswerSnapshot, QuestionSnapshot, } from "./wire/question-flags.ts";
22
- export type { ClientMessage, CuratorAdvanceMessage, CuratorIntermissionMessage, CuratorPlayerActionMessage, CuratorResetMessage, CuratorStartMessage, CuratorStartRoleSelectionMessage, CuratorStopMessage, GameErrorMessage, GameFinalScoresMessage, GameIntermissionMessage, GamePingMessage, GameQuestionMessage, GameResultsReadyMessage, GameResumeMessage, GameRoundResultMessage, GameStartMessage, GameTickMessage, JoinAcceptedMessage, JoinRejectedMessage, LobbyEndedMessage, LobbyResetMessage, LobbyUpdateMessage, PlayerAnsweredMessage, PlayerAnswerMessage, PlayerEmoteBroadcastMessage, PlayerEmoteMessage, PlayerLeaveMessage, PlayerNicknameChangedMessage, PlayerQuestionAnswer, PlayerRateAckMessage, PlayerRateMessage, PlayerRemovedMessage, PlayerRoleMessage, RateAckReason, RoleSelectionLockedMessage, RoleSelectionStartMessage, RoleSelectionUpdateMessage, ServerMessage, WsAuthMessage, WsPongMessage, } from "./ws.ts";
22
+ export type { ClientMessage, CuratorAdvanceMessage, CuratorIntermissionMessage, CuratorPlayerActionMessage, CuratorResetMessage, CuratorStartMessage, CuratorStartRoleSelectionMessage, CuratorStopMessage, GameErrorMessage, GameFinalScoresMessage, GameIntermissionMessage, GameLastCallCancelledMessage, GameLastCallMessage, GamePingMessage, GameQuestionMessage, GameResultsReadyMessage, GameResumeMessage, GameRoundResultMessage, GameStartMessage, GameTickMessage, JoinAcceptedMessage, JoinRejectedMessage, LobbyEndedMessage, LobbyResetMessage, LobbyUpdateMessage, PlayerAnsweredMessage, PlayerAnswerMessage, PlayerEmoteBroadcastMessage, PlayerEmoteMessage, PlayerLeaveMessage, PlayerNicknameChangedMessage, PlayerQuestionAnswer, PlayerRateAckMessage, PlayerRateMessage, PlayerRemovedMessage, PlayerRoleMessage, PlayerUnansweredMessage, PlayerUndoAckMessage, PlayerUndoMessage, RateAckReason, RoleSelectionLockedMessage, RoleSelectionStartMessage, RoleSelectionUpdateMessage, ServerMessage, WsAuthMessage, WsPongMessage, } from "./ws.ts";
23
23
  export { ClientMessageSchema } from "./ws-game.schema.ts";
24
24
  export { SoloClientMessageSchema } from "./ws-solo.schema.ts";
@@ -431,6 +431,7 @@ declare const QuizPresetSchema: Schema.Struct<{
431
431
  scoring_mode: Schema.NullOr<typeof Schema.String>;
432
432
  access_mode: typeof Schema.String;
433
433
  max_players: Schema.NullOr<typeof Schema.Number>;
434
+ undo_enabled: typeof Schema.Number;
434
435
  created_at: typeof Schema.String;
435
436
  }>;
436
437
  export type QuizPreset = typeof QuizPresetSchema.Type;
@@ -498,6 +499,7 @@ export declare const CuratorCreatePageSchema: Schema.Struct<{
498
499
  readonly scoring_mode: string | null;
499
500
  readonly access_mode: string;
500
501
  readonly max_players: number | null;
502
+ readonly undo_enabled: number;
501
503
  }, {
502
504
  readonly name: string;
503
505
  readonly id: string;
@@ -513,6 +515,7 @@ export declare const CuratorCreatePageSchema: Schema.Struct<{
513
515
  readonly scoring_mode: string | null;
514
516
  readonly access_mode: string;
515
517
  readonly max_players: number | null;
518
+ readonly undo_enabled: number;
516
519
  }, never>>>;
517
520
  presetMarks: Schema.Record$<typeof Schema.String, Schema.Struct<{
518
521
  closedSeasons: Schema.mutable<Schema.Array$<Schema.Schema<{
@@ -282,6 +282,7 @@ const QuizPresetSchema = Schema.Struct({
282
282
  scoring_mode: Schema.NullOr(Schema.String),
283
283
  access_mode: Schema.String,
284
284
  max_players: Schema.NullOr(Schema.Number),
285
+ undo_enabled: Schema.Number,
285
286
  created_at: Schema.String,
286
287
  });
287
288
  /** What stopped a saved Quiz's members from drawing: closed or gone Seasons, gone or hidden categories. */
@@ -69,6 +69,9 @@ export type PlayerJourney = typeof PlayerJourneySchema.Type;
69
69
  /** Body of `GET /api/quiz/journey`: a `PlayerJourney` plus the registry-miss diagnosis. */
70
70
  export declare const QuizJourneyPageSchema: Schema.Struct<{
71
71
  miss: Schema.NullOr<Schema.Literal<["lobby_lost"]>>;
72
+ /** The caller's token names a lobby that is gone for good (closed or
73
+ * expired): the client can say so instead of showing a bare code entry. */
74
+ lobbyGone: Schema.optional<typeof Schema.Boolean>;
72
75
  membership: Schema.Literal<["none", "pending", "active"]>;
73
76
  phase: Schema.NullOr<Schema.Literal<["lobby", "playing", "round_results", "final_scores", "role_selection", "intermission"]>>;
74
77
  state: Schema.Literal<["code-entry", "setup", "pending", "lobby", "playing", "role_selection", "final_scores"]>;
@@ -70,6 +70,9 @@ const PlayerJourneySchema = Schema.Struct({
70
70
  export const QuizJourneyPageSchema = Schema.Struct({
71
71
  ...PlayerJourneySchema.fields,
72
72
  miss: Schema.NullOr(Schema.Literal("lobby_lost")),
73
+ /** The caller's token names a lobby that is gone for good (closed or
74
+ * expired): the client can say so instead of showing a bare code entry. */
75
+ lobbyGone: Schema.optional(Schema.Boolean),
73
76
  });
74
77
  // ---- quiz context (the `/quiz/*` layout's session seed) ----
75
78
  const DifficultyFilterSchema = Schema.Literal(...GAME.roundConfig.difficultyOptions);
@@ -16,6 +16,8 @@ export declare const ClientMessageSchema: Schema.Union<[Schema.Struct<{
16
16
  role: Schema.Literal<["player", "observer"]>;
17
17
  }>, Schema.Struct<{
18
18
  type: Schema.Literal<["player:leave"]>;
19
+ }>, Schema.Struct<{
20
+ type: Schema.Literal<["player:undo"]>;
19
21
  }>, Schema.Struct<{
20
22
  type: Schema.Literal<["player:rate"]>;
21
23
  questionId: Schema.filter<typeof Schema.NonEmptyString>;
@@ -29,6 +31,7 @@ export declare const ClientMessageSchema: Schema.Union<[Schema.Struct<{
29
31
  difficulty: Schema.optional<Schema.Literal<["all", "easy", "medium", "hard"]>>;
30
32
  advanceMode: Schema.optional<Schema.Literal<["auto_5", "auto_10", "manual"]>>;
31
33
  scoringMode: Schema.optional<Schema.Literal<["classic", "time-based"]>>;
34
+ undoEnabled: Schema.optional<typeof Schema.Boolean>;
32
35
  preloadedQuestions: Schema.optional<Schema.filter<Schema.Array$<Schema.Union<[Schema.Struct<{
33
36
  type: Schema.Literal<["text_choice", "image_choice", "true_false", "finish_line", "audio_identify", "sequence_order", "estimate_range", "fill_blank", "matching_pairs", "progressive_reveal"]>;
34
37
  id: Schema.filter<typeof Schema.NonEmptyString>;
@@ -46,6 +46,9 @@ const PlayerRoleMessageSchema = Schema.Struct({
46
46
  const PlayerLeaveMessageSchema = Schema.Struct({
47
47
  type: Schema.Literal("player:leave"),
48
48
  });
49
+ const PlayerUndoMessageSchema = Schema.Struct({
50
+ type: Schema.Literal("player:undo"),
51
+ });
49
52
  const PlayerRateMessageSchema = Schema.Struct({
50
53
  type: Schema.Literal("player:rate"),
51
54
  questionId: BoundedIdSchema,
@@ -104,6 +107,7 @@ const CuratorStartMessageSchema = Schema.Struct({
104
107
  difficulty: Schema.optional(DifficultyFilterSchema),
105
108
  advanceMode: Schema.optional(Schema.Literal(...GAME.roundConfig.advanceModeOptions)),
106
109
  scoringMode: OptionalScoringModeSchema,
110
+ undoEnabled: Schema.optional(Schema.Boolean),
107
111
  // Generous headroom above any real round size (max 100) so a legitimate
108
112
  // curator import is never rejected; still bounded against a DoS payload.
109
113
  preloadedQuestions: Schema.optional(Schema.Array(PreloadedQuestionSchema).pipe(Schema.maxItems(500))),
@@ -135,5 +139,5 @@ const WsPongMessageSchema = Schema.Struct({
135
139
  });
136
140
  // Discriminant: Effect Schema detects the shared `type: Schema.Literal(...)`
137
141
  // field and short-circuits matching — every member needs a unique `type`.
138
- export const ClientMessageSchema = Schema.Union(WsAuthMessageSchema, PlayerAnswerMessageSchema, PlayerEmoteMessageSchema, PlayerRoleMessageSchema, PlayerLeaveMessageSchema, PlayerRateMessageSchema, CuratorStartMessageSchema, CuratorAdvanceMessageSchema, CuratorStartRoleSelectionMessageSchema, CuratorIntermissionMessageSchema, CuratorPlayerActionMessageSchema, CuratorStopMessageSchema, CuratorResetMessageSchema, WsPongMessageSchema);
142
+ export const ClientMessageSchema = Schema.Union(WsAuthMessageSchema, PlayerAnswerMessageSchema, PlayerEmoteMessageSchema, PlayerRoleMessageSchema, PlayerLeaveMessageSchema, PlayerUndoMessageSchema, PlayerRateMessageSchema, CuratorStartMessageSchema, CuratorAdvanceMessageSchema, CuratorStartRoleSelectionMessageSchema, CuratorIntermissionMessageSchema, CuratorPlayerActionMessageSchema, CuratorStopMessageSchema, CuratorResetMessageSchema, WsPongMessageSchema);
139
143
  const _assertNoClientMessageDrift = null;
package/dist/ws.d.ts CHANGED
@@ -6,7 +6,7 @@ import type { GameAnswerRecord } from "./game-answer-record.ts";
6
6
  import type { MediaCredit, MediaType, QuestionType } from "./question.schema.ts";
7
7
  import type { QuestionRating } from "./question-rating.ts";
8
8
  import type { StreakFlourish } from "./streak-flourish.ts";
9
- export type ClientMessage = WsAuthMessage | PlayerAnswerMessage | PlayerEmoteMessage | PlayerRoleMessage | PlayerLeaveMessage | PlayerRateMessage | CuratorStartMessage | CuratorAdvanceMessage | CuratorStartRoleSelectionMessage | CuratorIntermissionMessage | CuratorPlayerActionMessage | CuratorStopMessage | CuratorResetMessage | WsPongMessage;
9
+ export type ClientMessage = WsAuthMessage | PlayerAnswerMessage | PlayerEmoteMessage | PlayerRoleMessage | PlayerLeaveMessage | PlayerUndoMessage | PlayerRateMessage | CuratorStartMessage | CuratorAdvanceMessage | CuratorStartRoleSelectionMessage | CuratorIntermissionMessage | CuratorPlayerActionMessage | CuratorStopMessage | CuratorResetMessage | WsPongMessage;
10
10
  /** First frame for native/display (cross-origin, no cookie): authenticates the
11
11
  * socket with the bearer game token (ADR 0014). Web rides the cookie instead. */
12
12
  export interface WsAuthMessage {
@@ -35,6 +35,12 @@ export interface PlayerRoleMessage {
35
35
  export interface PlayerLeaveMessage {
36
36
  type: "player:leave";
37
37
  }
38
+ /** Player withdraws their answer to the open question and may answer again.
39
+ * Needs the curator's undo flag and remaining budget; the server replies with
40
+ * `player:undo-ack`. */
41
+ export interface PlayerUndoMessage {
42
+ type: "player:undo";
43
+ }
38
44
  /** Rate a question revealed in this game — from a signed-in player who played
39
45
  * its round, or the curator. The server answers with `player:rate-ack`
40
46
  * echoing `questionId`; `reason: "disabled"` when the rating flag is off. */
@@ -57,6 +63,8 @@ export interface CuratorStartMessage {
57
63
  difficulty?: DifficultyFilter;
58
64
  advanceMode?: AdvanceMode;
59
65
  scoringMode?: ScoringMode;
66
+ /** Players may withdraw and redo answers, within a per-player budget. */
67
+ undoEnabled?: boolean;
60
68
  preloadedQuestions?: QuizQuestion[];
61
69
  }
62
70
  export interface CuratorAdvanceMessage {
@@ -89,7 +97,7 @@ export interface CuratorPlayerActionMessage {
89
97
  export interface WsPongMessage {
90
98
  type: "pong";
91
99
  }
92
- export type ServerMessage = JoinAcceptedMessage | JoinRejectedMessage | LobbyUpdateMessage | LobbyEndedMessage | LobbyResetMessage | PlayerNicknameChangedMessage | GameStartMessage | GameQuestionMessage | GameTickMessage | PlayerAnsweredMessage | GameRoundResultMessage | GameFinalScoresMessage | GameResultsReadyMessage | RoleSelectionStartMessage | RoleSelectionUpdateMessage | RoleSelectionLockedMessage | PlayerRemovedMessage | GameIntermissionMessage | GameResumeMessage | PlayerEmoteBroadcastMessage | GameErrorMessage | GamePingMessage | PlayerRateAckMessage;
100
+ export type ServerMessage = JoinAcceptedMessage | JoinRejectedMessage | LobbyUpdateMessage | LobbyEndedMessage | LobbyResetMessage | PlayerNicknameChangedMessage | GameStartMessage | GameQuestionMessage | GameTickMessage | PlayerAnsweredMessage | GameRoundResultMessage | GameFinalScoresMessage | GameResultsReadyMessage | RoleSelectionStartMessage | RoleSelectionUpdateMessage | RoleSelectionLockedMessage | PlayerRemovedMessage | GameIntermissionMessage | GameResumeMessage | PlayerEmoteBroadcastMessage | GameErrorMessage | GamePingMessage | PlayerRateAckMessage | PlayerUndoAckMessage | PlayerUnansweredMessage | GameLastCallMessage | GameLastCallCancelledMessage;
93
101
  export interface JoinAcceptedMessage {
94
102
  type: "join:accepted";
95
103
  playerId: string;
@@ -139,6 +147,8 @@ export interface GameStartMessage {
139
147
  totalQuestions: number;
140
148
  timerDuration: TimerDuration;
141
149
  advanceMode: AdvanceMode;
150
+ /** Undos each player may use this game; absent or 0 = undo is off. */
151
+ undoBudget?: number;
142
152
  }
143
153
  /** Question sent to players — isCorrect is omitted to prevent cheating */
144
154
  export interface PlayerQuestionAnswer {
@@ -177,6 +187,10 @@ export interface GameQuestionMessage {
177
187
  * resets to 0 until the next player:answered frame). Omitted on broadcast. */
178
188
  answeredCount?: number;
179
189
  answeredTotal?: number;
190
+ /** This recipient's undos left, set only on a snapshot replay. */
191
+ undoRemaining?: number;
192
+ /** Seconds left of an active last call, set only on a snapshot replay. */
193
+ lastCallSeconds?: number;
180
194
  }
181
195
  export interface GameTickMessage {
182
196
  type: "game:tick";
@@ -194,6 +208,32 @@ export interface PlayerAnsweredMessage {
194
208
  answeredCount: number;
195
209
  totalCount: number;
196
210
  }
211
+ /** A player withdrew their answer. Curator/display only. */
212
+ export interface PlayerUnansweredMessage {
213
+ type: "player:unanswered";
214
+ playerId: string;
215
+ answeredCount: number;
216
+ totalCount: number;
217
+ }
218
+ /** Reply to `player:undo`, sent to the acting socket only. */
219
+ export interface PlayerUndoAckMessage {
220
+ type: "player:undo-ack";
221
+ ok: boolean;
222
+ /** Undos the player has left after this request. */
223
+ remaining: number;
224
+ /** Why the undo was refused; set when `ok` is false. */
225
+ reason?: ErrorCode;
226
+ }
227
+ /** Every active hero has answered with undo on: the round closes after
228
+ * `seconds` unless an undo cancels the last call. */
229
+ export interface GameLastCallMessage {
230
+ type: "game:last-call";
231
+ seconds: number;
232
+ }
233
+ /** An undo during the last call cancelled it; the round resumes its timer. */
234
+ export interface GameLastCallCancelledMessage {
235
+ type: "game:last-call-cancelled";
236
+ }
197
237
  export interface GameRoundResultMessage {
198
238
  type: "game:round-result";
199
239
  correctAnswerId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orakl/protocol",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Orakl wire contract: WebSocket frames and API response schemas.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {