@orakl/protocol 0.13.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
@@ -10,7 +10,7 @@ export * from "./pages/common.schema.ts";
10
10
  export * from "./pages/history.schema.ts";
11
11
  export * from "./pages/quiz.schema.ts";
12
12
  export type { PlayerSafeQuestion } from "./player-safe-question.ts";
13
- export { ATTRIBUTION_LICENCES, CREDIT_LICENCE_LABEL, CREDIT_LICENCES, type CreditLicence, CreditLicenceSchema, DISTRIBUTABLE_LICENCES, DifficultySchema, type ImageChoiceQuestion, type ImageMatchingQuestion, isHttpsUrl, type Media, type MediaCredit, MediaCreditSchema, MediaSchema, type MediaType, MediaTypeSchema, PostAnswerNoteSchema, type Question, QuestionSchema, type QuestionSource, QuestionSourceSchema, type QuestionType, QuestionTypeSchema, type Source, SourceSchema, type TextChoiceQuestion, type TrueFalseQuestion, } from "./question.schema.ts";
13
+ export { ATTRIBUTION_LICENCES, CREDIT_LICENCE_LABEL, CREDIT_LICENCES, type CreditLicence, CreditLicenceSchema, DISTRIBUTABLE_LICENCES, DifficultySchema, type ImageChoiceQuestion, type ImageMatchingQuestion, isHttpsUrl, type Media, type MediaCredit, MediaCreditSchema, MediaSchema, type MediaType, MediaTypeSchema, POST_ANSWER_NOTE_MAX_LEN, PostAnswerNoteSchema, type Question, QuestionSchema, type QuestionSource, QuestionSourceSchema, type QuestionType, QuestionTypeSchema, SOURCE_LABEL_MAX_LEN, SOURCE_URL_MAX_LEN, type Source, SourceSchema, type TextChoiceQuestion, type TrueFalseQuestion, } from "./question.schema.ts";
14
14
  export { hasMedia, isImageMatching, isTextChoice, isTrueFalse, } from "./question.ts";
15
15
  export type { QuestionRating } from "./question-rating.ts";
16
16
  export { OptionalScoringModeSchema, ScoringModeSchema, } from "./scoring.schema.ts";
@@ -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";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export * from "./pages/auth.schema.js";
5
5
  export * from "./pages/common.schema.js";
6
6
  export * from "./pages/history.schema.js";
7
7
  export * from "./pages/quiz.schema.js";
8
- export { ATTRIBUTION_LICENCES, CREDIT_LICENCE_LABEL, CREDIT_LICENCES, CreditLicenceSchema, DISTRIBUTABLE_LICENCES, DifficultySchema, isHttpsUrl, MediaCreditSchema, MediaSchema, MediaTypeSchema, PostAnswerNoteSchema, QuestionSchema, QuestionSourceSchema, QuestionTypeSchema, SourceSchema, } from "./question.schema.js";
8
+ export { ATTRIBUTION_LICENCES, CREDIT_LICENCE_LABEL, CREDIT_LICENCES, CreditLicenceSchema, DISTRIBUTABLE_LICENCES, DifficultySchema, isHttpsUrl, MediaCreditSchema, MediaSchema, MediaTypeSchema, POST_ANSWER_NOTE_MAX_LEN, PostAnswerNoteSchema, QuestionSchema, QuestionSourceSchema, QuestionTypeSchema, SOURCE_LABEL_MAX_LEN, SOURCE_URL_MAX_LEN, SourceSchema, } from "./question.schema.js";
9
9
  export { hasMedia, isImageMatching, isTextChoice, isTrueFalse, } from "./question.js";
10
10
  export { OptionalScoringModeSchema, ScoringModeSchema, } from "./scoring.schema.js";
11
11
  export { soloError, } from "./solo-ws.js";
@@ -288,6 +288,47 @@ export declare const ManageQuestionRatingsPageSchema: Schema.Struct<{
288
288
  initialHasMore: typeof Schema.Boolean;
289
289
  page: typeof Schema.Number;
290
290
  }>;
291
+ declare const BankQuestionRowSchema: Schema.Struct<{
292
+ id: typeof Schema.String;
293
+ excerpt: typeof Schema.String;
294
+ source: Schema.Literal<["built-in", "promoted"]>;
295
+ category: typeof Schema.String;
296
+ /** The stored question type, e.g. `text_choice`. */
297
+ type: typeof Schema.String;
298
+ difficulty: Schema.NullOr<Schema.Literal<["easy", "medium", "hard"]>>;
299
+ /** At least one revision exists. */
300
+ edited: typeof Schema.Boolean;
301
+ }>;
302
+ /** One question an operator may edit on the question bank page: built-in or promoted. */
303
+ export type BankQuestionRow = typeof BankQuestionRowSchema.Type;
304
+ /** Body of `GET /api/pages/manage/bank`. */
305
+ export declare const ManageBankPageSchema: Schema.Struct<{
306
+ initial: Schema.mutable<Schema.Array$<Schema.Schema<{
307
+ readonly type: string;
308
+ readonly source: "built-in" | "promoted";
309
+ readonly id: string;
310
+ readonly category: string;
311
+ readonly difficulty: "medium" | "easy" | "hard" | null;
312
+ readonly excerpt: string;
313
+ readonly edited: boolean;
314
+ }, {
315
+ readonly type: string;
316
+ readonly source: "built-in" | "promoted";
317
+ readonly id: string;
318
+ readonly category: string;
319
+ readonly difficulty: "medium" | "easy" | "hard" | null;
320
+ readonly excerpt: string;
321
+ readonly edited: boolean;
322
+ }, never>>>;
323
+ initialHasMore: typeof Schema.Boolean;
324
+ page: typeof Schema.Number;
325
+ /** The search term the page was loaded with; empty for none. */
326
+ q: typeof Schema.String;
327
+ /** The category name the page was filtered to; empty for all. */
328
+ category: typeof Schema.String;
329
+ /** Category names offered by the filter, A to Z. */
330
+ categories: Schema.mutable<Schema.Array$<Schema.Schema<string, string, never>>>;
331
+ }>;
291
332
  declare const AuthoredQuestionRowSchema: Schema.Struct<{
292
333
  id: typeof Schema.String;
293
334
  category: typeof Schema.String;
@@ -390,6 +431,7 @@ declare const QuizPresetSchema: Schema.Struct<{
390
431
  scoring_mode: Schema.NullOr<typeof Schema.String>;
391
432
  access_mode: typeof Schema.String;
392
433
  max_players: Schema.NullOr<typeof Schema.Number>;
434
+ undo_enabled: typeof Schema.Number;
393
435
  created_at: typeof Schema.String;
394
436
  }>;
395
437
  export type QuizPreset = typeof QuizPresetSchema.Type;
@@ -457,6 +499,7 @@ export declare const CuratorCreatePageSchema: Schema.Struct<{
457
499
  readonly scoring_mode: string | null;
458
500
  readonly access_mode: string;
459
501
  readonly max_players: number | null;
502
+ readonly undo_enabled: number;
460
503
  }, {
461
504
  readonly name: string;
462
505
  readonly id: string;
@@ -472,6 +515,7 @@ export declare const CuratorCreatePageSchema: Schema.Struct<{
472
515
  readonly scoring_mode: string | null;
473
516
  readonly access_mode: string;
474
517
  readonly max_players: number | null;
518
+ readonly undo_enabled: number;
475
519
  }, never>>>;
476
520
  presetMarks: Schema.Record$<typeof Schema.String, Schema.Struct<{
477
521
  closedSeasons: Schema.mutable<Schema.Array$<Schema.Schema<{
@@ -196,6 +196,29 @@ export const ManageQuestionRatingsPageSchema = Schema.Struct({
196
196
  initialHasMore: Schema.Boolean,
197
197
  page: Schema.Number,
198
198
  });
199
+ const BankQuestionRowSchema = Schema.Struct({
200
+ id: Schema.String,
201
+ excerpt: Schema.String,
202
+ source: Schema.Literal("built-in", "promoted"),
203
+ category: Schema.String,
204
+ /** The stored question type, e.g. `text_choice`. */
205
+ type: Schema.String,
206
+ difficulty: Schema.NullOr(Schema.Literal("easy", "medium", "hard")),
207
+ /** At least one revision exists. */
208
+ edited: Schema.Boolean,
209
+ });
210
+ /** Body of `GET /api/pages/manage/bank`. */
211
+ export const ManageBankPageSchema = Schema.Struct({
212
+ initial: Arr(BankQuestionRowSchema),
213
+ initialHasMore: Schema.Boolean,
214
+ page: Schema.Number,
215
+ /** The search term the page was loaded with; empty for none. */
216
+ q: Schema.String,
217
+ /** The category name the page was filtered to; empty for all. */
218
+ category: Schema.String,
219
+ /** Category names offered by the filter, A to Z. */
220
+ categories: Arr(Schema.String),
221
+ });
199
222
  const AuthoredQuestionRowSchema = Schema.Struct({
200
223
  id: Schema.String,
201
224
  category: Schema.String,
@@ -259,6 +282,7 @@ const QuizPresetSchema = Schema.Struct({
259
282
  scoring_mode: Schema.NullOr(Schema.String),
260
283
  access_mode: Schema.String,
261
284
  max_players: Schema.NullOr(Schema.Number),
285
+ undo_enabled: Schema.Number,
262
286
  created_at: Schema.String,
263
287
  });
264
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);
@@ -53,6 +53,12 @@ export declare const MediaSchema: Schema.Struct<{
53
53
  licenceUrl: Schema.optional<Schema.filter<Schema.filter<typeof Schema.String>>>;
54
54
  }>>>;
55
55
  }>;
56
+ /** Max characters in a question source URL. */
57
+ export declare const SOURCE_URL_MAX_LEN = 2048;
58
+ /** Max characters in a question source label. */
59
+ export declare const SOURCE_LABEL_MAX_LEN = 200;
60
+ /** Max characters in a post-answer note. */
61
+ export declare const POST_ANSWER_NOTE_MAX_LEN = 160;
56
62
  export declare const QuestionSourceSchema: Schema.Struct<{
57
63
  url: Schema.filter<Schema.filter<typeof Schema.String>>;
58
64
  label: Schema.optional<Schema.filter<typeof Schema.String>>;
@@ -77,13 +77,19 @@ export const MediaSchema = Schema.Struct({
77
77
  // Security: source.url is stored in the DB and read back on results/history
78
78
  // pages; cap to match imageUrl's bound (curator.schema.ts) since both are
79
79
  // URLs of the same practical size.
80
- const HttpsUrlSchema = Schema.String.pipe(Schema.maxLength(2048, {
81
- message: () => "Source URL must be 2048 characters or fewer",
80
+ /** Max characters in a question source URL. */
81
+ export const SOURCE_URL_MAX_LEN = 2048;
82
+ /** Max characters in a question source label. */
83
+ export const SOURCE_LABEL_MAX_LEN = 200;
84
+ /** Max characters in a post-answer note. */
85
+ export const POST_ANSWER_NOTE_MAX_LEN = 160;
86
+ const HttpsUrlSchema = Schema.String.pipe(Schema.maxLength(SOURCE_URL_MAX_LEN, {
87
+ message: () => `Source URL must be ${SOURCE_URL_MAX_LEN} characters or fewer`,
82
88
  }), Schema.filter(isHttpsUrl, { message: () => "Source URL must be https" }));
83
89
  export const QuestionSourceSchema = Schema.Struct({
84
90
  url: HttpsUrlSchema,
85
- label: Schema.optional(Schema.String.pipe(Schema.maxLength(200, {
86
- message: () => "Source label must be 200 characters or fewer",
91
+ label: Schema.optional(Schema.String.pipe(Schema.maxLength(SOURCE_LABEL_MAX_LEN, {
92
+ message: () => `Source label must be ${SOURCE_LABEL_MAX_LEN} characters or fewer`,
87
93
  }))),
88
94
  });
89
95
  const isQuestionSource = Schema.is(QuestionSourceSchema);
@@ -97,8 +103,8 @@ export const StoredQuestionSourceSchema = Schema.transform(Schema.Unknown, Schem
97
103
  decode: (source) => (isQuestionSource(source) ? source : undefined),
98
104
  encode: (source) => source,
99
105
  });
100
- export const PostAnswerNoteSchema = Schema.String.pipe(Schema.maxLength(160, {
101
- message: () => "Post-answer note must be 160 characters or fewer",
106
+ export const PostAnswerNoteSchema = Schema.String.pipe(Schema.maxLength(POST_ANSWER_NOTE_MAX_LEN, {
107
+ message: () => `Post-answer note must be ${POST_ANSWER_NOTE_MAX_LEN} characters or fewer`,
102
108
  }));
103
109
  const MetadataSchema = Schema.Struct({
104
110
  source: SourceSchema,
@@ -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.13.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": {