@orakl/protocol 0.1.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.
Files changed (50) hide show
  1. package/LICENSE +662 -0
  2. package/dist/badges.d.ts +19 -0
  3. package/dist/badges.js +1 -0
  4. package/dist/emotes.d.ts +5 -0
  5. package/dist/emotes.js +62 -0
  6. package/dist/error-codes.d.ts +1 -0
  7. package/dist/error-codes.js +1 -0
  8. package/dist/game-answer-record.d.ts +31 -0
  9. package/dist/game-answer-record.js +1 -0
  10. package/dist/game.d.ts +83 -0
  11. package/dist/game.js +1 -0
  12. package/dist/game.settings.d.ts +91 -0
  13. package/dist/game.settings.js +61 -0
  14. package/dist/index.d.ts +23 -0
  15. package/dist/index.js +13 -0
  16. package/dist/pages/app.schema.d.ts +544 -0
  17. package/dist/pages/app.schema.js +336 -0
  18. package/dist/pages/auth.schema.d.ts +25 -0
  19. package/dist/pages/auth.schema.js +23 -0
  20. package/dist/pages/common.schema.d.ts +27 -0
  21. package/dist/pages/common.schema.js +28 -0
  22. package/dist/pages/history.schema.d.ts +273 -0
  23. package/dist/pages/history.schema.js +210 -0
  24. package/dist/pages/quiz.schema.d.ts +150 -0
  25. package/dist/pages/quiz.schema.js +132 -0
  26. package/dist/player-safe-question.d.ts +13 -0
  27. package/dist/player-safe-question.js +1 -0
  28. package/dist/question.d.ts +6 -0
  29. package/dist/question.js +12 -0
  30. package/dist/question.schema.d.ts +494 -0
  31. package/dist/question.schema.js +132 -0
  32. package/dist/scoring.schema.d.ts +7 -0
  33. package/dist/scoring.schema.js +8 -0
  34. package/dist/solo-ws.d.ts +202 -0
  35. package/dist/solo-ws.js +4 -0
  36. package/dist/streak-flourish.d.ts +6 -0
  37. package/dist/streak-flourish.js +1 -0
  38. package/dist/wire/leaderboard.d.ts +6 -0
  39. package/dist/wire/leaderboard.js +1 -0
  40. package/dist/wire/question-bank.d.ts +23 -0
  41. package/dist/wire/question-bank.js +1 -0
  42. package/dist/wire/question-flags.d.ts +40 -0
  43. package/dist/wire/question-flags.js +1 -0
  44. package/dist/ws-game.schema.d.ts +75 -0
  45. package/dist/ws-game.schema.js +131 -0
  46. package/dist/ws-solo.schema.d.ts +31 -0
  47. package/dist/ws-solo.schema.js +57 -0
  48. package/dist/ws.d.ts +281 -0
  49. package/dist/ws.js +1 -0
  50. package/package.json +39 -0
package/dist/badges.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare const EMOTE_SPRITES: Record<string, {
2
+ x: number;
3
+ y: number;
4
+ }>;
5
+ export declare const SELECTABLE_EMOTE_IDS: readonly ["alert", "anger", "bars", "cash", "circle", "cloud", "cross", "dots1", "dots2", "dots3", "drop", "drops", "exclamation", "exclamations", "faceAngry", "faceHappy", "faceSad", "heart", "heartBroken", "hearts", "idea", "laugh", "music", "question", "sleep", "sleeps", "star", "stars", "swirl"];
package/dist/emotes.js ADDED
@@ -0,0 +1,62 @@
1
+ export const EMOTE_SPRITES = {
2
+ alert: { x: 64, y: 64 },
3
+ anger: { x: 64, y: 48 },
4
+ bars: { x: 64, y: 32 },
5
+ cash: { x: 64, y: 16 },
6
+ circle: { x: 64, y: 0 },
7
+ cloud: { x: 48, y: 80 },
8
+ cross: { x: 48, y: 64 },
9
+ dots1: { x: 48, y: 48 },
10
+ dots2: { x: 48, y: 32 },
11
+ dots3: { x: 48, y: 16 },
12
+ drop: { x: 48, y: 0 },
13
+ drops: { x: 32, y: 80 },
14
+ exclamation: { x: 32, y: 64 },
15
+ exclamations: { x: 32, y: 48 },
16
+ faceAngry: { x: 64, y: 80 },
17
+ faceHappy: { x: 32, y: 16 },
18
+ faceSad: { x: 32, y: 0 },
19
+ heart: { x: 16, y: 80 },
20
+ heartBroken: { x: 16, y: 64 },
21
+ hearts: { x: 16, y: 48 },
22
+ idea: { x: 16, y: 32 },
23
+ laugh: { x: 16, y: 16 },
24
+ music: { x: 16, y: 0 },
25
+ question: { x: 0, y: 80 },
26
+ sleep: { x: 0, y: 64 },
27
+ sleeps: { x: 0, y: 48 },
28
+ star: { x: 0, y: 32 },
29
+ stars: { x: 0, y: 16 },
30
+ swirl: { x: 0, y: 0 },
31
+ };
32
+ export const SELECTABLE_EMOTE_IDS = [
33
+ "alert",
34
+ "anger",
35
+ "bars",
36
+ "cash",
37
+ "circle",
38
+ "cloud",
39
+ "cross",
40
+ "dots1",
41
+ "dots2",
42
+ "dots3",
43
+ "drop",
44
+ "drops",
45
+ "exclamation",
46
+ "exclamations",
47
+ "faceAngry",
48
+ "faceHappy",
49
+ "faceSad",
50
+ "heart",
51
+ "heartBroken",
52
+ "hearts",
53
+ "idea",
54
+ "laugh",
55
+ "music",
56
+ "question",
57
+ "sleep",
58
+ "sleeps",
59
+ "star",
60
+ "stars",
61
+ "swirl",
62
+ ];
@@ -0,0 +1 @@
1
+ export type ErrorCode = "no_active_lobby" | "no_active_game" | "device_blocked" | "nickname_taken" | "device_in_lobby" | "player_not_found" | "player_not_pending" | "intermission_invalid_phase" | "not_in_intermission" | "category_name_exists" | "category_not_found" | "category_has_questions" | "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" | "no_categories" | "too_few_players" | "hosting_closed" | "not_answered" | "flag_not_toggleable" | "solo_invalid_phase" | "solo_invalid_answer" | "solo_nonce_mismatch" | "solo_answer_expired" | "solo_guest_limit" | "solo_no_questions" | "solo_resume_denied" | "solo_auth_failed";
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import type { MediaType, QuestionSource } from "./question.schema.ts";
2
+ /**
3
+ * One round in a player's multiplayer answer log. Misses recorded during the
4
+ * player's hero tenure (timeout, disconnect, quit) appear with isCorrect=false
5
+ * and the full clock as timeToAnswerMs. Persisted as game_results.question_breakdown.
6
+ */
7
+ export interface GameAnswerRecord {
8
+ questionIndex: number;
9
+ categoryId: string;
10
+ difficulty?: "easy" | "medium" | "hard";
11
+ isCorrect: boolean;
12
+ timeToAnswerMs: number;
13
+ /** Full-fidelity fields (ADR 0020) — optional, absent on rows written before this. */
14
+ questionId?: string;
15
+ questionText?: string;
16
+ selectedAnswerId?: string;
17
+ selectedAnswerText?: string;
18
+ correctAnswerId?: string;
19
+ correctAnswerText?: string;
20
+ /** Snapshotted at play time — a later edit to the question does not
21
+ * change what a finished game shows (map #912). Absent on rows written
22
+ * before this. `mediaUrl` holds whatever form the question carried when
23
+ * the round was played: a signed `/media/<key>` path for a bucket object,
24
+ * since questions reach the wire already signed, or a curator's pasted
25
+ * external URL. Either way its signature has long expired by the time a
26
+ * game is reviewed, so a reader re-signs it (see `resignBreakdown`). */
27
+ mediaUrl?: string;
28
+ mediaType?: MediaType;
29
+ source?: QuestionSource;
30
+ postAnswerNote?: string;
31
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/game.d.ts ADDED
@@ -0,0 +1,83 @@
1
+ import type { AdvanceMode, Difficulty, TimerDuration } from "./game.settings.ts";
2
+ import type { MediaType, QuestionSource, QuestionType } from "./question.schema.ts";
3
+ export type { AdvanceMode, Difficulty, TimerDuration, } from "./game.settings.ts";
4
+ export type PlayerRole = "player" | "observer";
5
+ export type PlayerStatus = "pending" | "active" | "disconnected";
6
+ export type AccessMode = "open" | "invite-only";
7
+ export type GamePhase = "lobby" | "playing" | "round_results" | "final_scores" | "role_selection" | "intermission";
8
+ export type ActiveGamePhase = Exclude<GamePhase, "lobby">;
9
+ export interface QuizConfig {
10
+ name: string;
11
+ description?: string;
12
+ categoryIds: string[];
13
+ timerDuration: TimerDuration;
14
+ questionsPerRound: number;
15
+ difficulty?: Difficulty;
16
+ advanceMode?: AdvanceMode;
17
+ }
18
+ export interface Player {
19
+ id: string;
20
+ nickname: string;
21
+ avatar: string;
22
+ score: number;
23
+ ready: boolean;
24
+ status: PlayerStatus;
25
+ role: PlayerRole;
26
+ deviceId?: string;
27
+ /** True only for the curator's own player row (playing curator, ADR 0019).
28
+ * Fresh-UUID row id, never the curator's token/user id — see game-store.ts
29
+ * joinAsCurator. Omitted (not false) for every other player. */
30
+ isCurator?: true;
31
+ /**
32
+ * Real `users.id` this row is linked to, when known — the ONE mechanism a
33
+ * row ends up with a genuine account link (question-flagging precheck,
34
+ * ADR 0020 follow-up). Set at join time only when the browser held a
35
+ * genuine account session (captured in POST /api/lobby/join, before the
36
+ * response overwrites the cookie with a rowless per-lobby player token —
37
+ * see that route's doc comment), or for the curator's own row via
38
+ * joinAsCurator (mirrors `lobby.curatorUserId`). Never trust `role` alone
39
+ * for "has an account" — a joined player's token role is always "member"
40
+ * regardless of whether they have one. Omitted (not null) when absent —
41
+ * never sent over the wire (see serializers.ts's toPublicPlayer allowlist). */
42
+ accountUserId?: string;
43
+ }
44
+ export interface QuizAnswer {
45
+ id: string;
46
+ text: string;
47
+ isCorrect: boolean;
48
+ }
49
+ interface BaseQuizQuestion {
50
+ id: string;
51
+ text: string;
52
+ categoryId: string;
53
+ difficulty: Difficulty;
54
+ answers: QuizAnswer[];
55
+ mediaUrl?: string;
56
+ mediaType?: MediaType;
57
+ /** Carried only so `toPlayerSafeQuestion` can strip it — never sent on the
58
+ * wire during a game (map #912, decision 2). */
59
+ source?: QuestionSource;
60
+ /** Carried only so `toPlayerSafeQuestion` can strip it from `game:question`;
61
+ * reaches the client separately in `game:round-result` at reveal. */
62
+ postAnswerNote?: string;
63
+ }
64
+ export interface StandardQuizQuestion extends BaseQuizQuestion {
65
+ type: Exclude<QuestionType, "image_matching">;
66
+ }
67
+ export interface ImageMatchingQuizQuestion extends BaseQuizQuestion {
68
+ type: "image_matching";
69
+ matchItems: {
70
+ left: string[];
71
+ right: string[];
72
+ };
73
+ }
74
+ export type QuizQuestion = StandardQuizQuestion | ImageMatchingQuizQuestion;
75
+ export interface GameState {
76
+ phase: GamePhase;
77
+ config: QuizConfig;
78
+ players: Player[];
79
+ questions: QuizQuestion[];
80
+ currentQuestion: number;
81
+ totalQuestions: number;
82
+ timeRemaining: number;
83
+ }
package/dist/game.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Single source of truth for all game constants.
3
+ * Derive types from this file — do not duplicate values elsewhere.
4
+ */
5
+ export declare const GAME: {
6
+ readonly roundConfig: {
7
+ readonly timerOptions: readonly [15, 30, 45, 60];
8
+ readonly questionCounts: readonly [10, 20, 50, 100];
9
+ readonly difficultyOptions: readonly ["all", "easy", "medium", "hard"];
10
+ readonly difficulties: readonly ["easy", "medium", "hard"];
11
+ readonly autoAdvanceMs: 5000;
12
+ readonly advanceModeOptions: readonly ["auto_5", "auto_10", "manual"];
13
+ readonly defaultAdvanceMode: "auto_5";
14
+ readonly autoAdvanceMsMap: {
15
+ readonly auto_5: 5000;
16
+ readonly auto_10: 10000;
17
+ };
18
+ readonly scoringOptions: readonly ["classic", "time-based"];
19
+ readonly defaultScoring: "classic";
20
+ readonly defaultQuestions: 10;
21
+ readonly defaultTimer: 30;
22
+ };
23
+ readonly endless: {
24
+ readonly lives: 3;
25
+ };
26
+ readonly streak: {
27
+ readonly steps: readonly [{
28
+ readonly threshold: 3;
29
+ readonly label: "Kindled!";
30
+ readonly tone: "ember";
31
+ }, {
32
+ readonly threshold: 5;
33
+ readonly label: "Ablaze!";
34
+ readonly tone: "spark";
35
+ }, {
36
+ readonly threshold: 10;
37
+ readonly label: "Inferno!";
38
+ readonly tone: "blaze";
39
+ }, {
40
+ readonly threshold: 15;
41
+ readonly label: "Rampage!";
42
+ readonly tone: "conflagration";
43
+ }, {
44
+ readonly threshold: 20;
45
+ readonly label: "Legendary!";
46
+ readonly tone: "inferno";
47
+ }, {
48
+ readonly threshold: 25;
49
+ readonly label: "We ran out of medals...";
50
+ readonly tone: "inferno";
51
+ }];
52
+ readonly losses: readonly [{
53
+ readonly threshold: 3;
54
+ readonly label: "Flame quenched.";
55
+ }, {
56
+ readonly threshold: 5;
57
+ readonly label: "Embers scattered.";
58
+ }, {
59
+ readonly threshold: 10;
60
+ readonly label: "The inferno falls silent.";
61
+ }, {
62
+ readonly threshold: 15;
63
+ readonly label: "Rampage broken.";
64
+ }, {
65
+ readonly threshold: 20;
66
+ readonly label: "Legend undone.";
67
+ }, {
68
+ readonly threshold: 25;
69
+ readonly label: "The oracle mourns.";
70
+ }];
71
+ readonly visibleAt: 2;
72
+ };
73
+ readonly question: {
74
+ readonly sources: readonly ["opentdb", "custom", "the-trivia-api"];
75
+ };
76
+ readonly roleSelection: {
77
+ readonly durationSeconds: 60;
78
+ };
79
+ readonly player: {
80
+ readonly maxNicknameLength: 20;
81
+ };
82
+ readonly sse: {
83
+ readonly keepaliveIntervalMs: 15000;
84
+ };
85
+ };
86
+ export type TimerDuration = (typeof GAME.roundConfig.timerOptions)[number];
87
+ export type QuestionsPerRound = (typeof GAME.roundConfig.questionCounts)[number];
88
+ export type DifficultyFilter = (typeof GAME.roundConfig.difficultyOptions)[number];
89
+ export type Difficulty = Exclude<DifficultyFilter, "all">;
90
+ export type AdvanceMode = (typeof GAME.roundConfig.advanceModeOptions)[number];
91
+ export type ScoringMode = (typeof GAME.roundConfig.scoringOptions)[number];
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Single source of truth for all game constants.
3
+ * Derive types from this file — do not duplicate values elsewhere.
4
+ */
5
+ export const GAME = {
6
+ roundConfig: {
7
+ timerOptions: [15, 30, 45, 60],
8
+ questionCounts: [10, 20, 50, 100],
9
+ difficultyOptions: ["all", "easy", "medium", "hard"],
10
+ difficulties: ["easy", "medium", "hard"],
11
+ autoAdvanceMs: 5000,
12
+ advanceModeOptions: ["auto_5", "auto_10", "manual"],
13
+ defaultAdvanceMode: "auto_5",
14
+ autoAdvanceMsMap: { auto_5: 5000, auto_10: 10000 },
15
+ // Scoring: "classic" = 1 point per correct answer; "time-based" = base +
16
+ // speed bonus (mirrors solo, see src/lib/scoring.ts).
17
+ scoringOptions: ["classic", "time-based"],
18
+ defaultScoring: "classic",
19
+ defaultQuestions: 10,
20
+ defaultTimer: 30,
21
+ },
22
+ endless: {
23
+ lives: 3,
24
+ },
25
+ streak: {
26
+ // Milestone flourish shown mid-round — extend/reorder here, nowhere else.
27
+ // tone keys map to the app's flame/flame-dark/flame-light/flame-lighter palette.
28
+ steps: [
29
+ { threshold: 3, label: "Kindled!", tone: "ember" },
30
+ { threshold: 5, label: "Ablaze!", tone: "spark" },
31
+ { threshold: 10, label: "Inferno!", tone: "blaze" },
32
+ { threshold: 15, label: "Rampage!", tone: "conflagration" },
33
+ { threshold: 20, label: "Legendary!", tone: "inferno" },
34
+ { threshold: 25, label: "We ran out of medals...", tone: "inferno" },
35
+ ],
36
+ // Loss flourish shown when a streak at/above threshold breaks. Mirrors the
37
+ // gain thresholds so a run always mourns the tier it fell from.
38
+ losses: [
39
+ { threshold: 3, label: "Flame quenched." },
40
+ { threshold: 5, label: "Embers scattered." },
41
+ { threshold: 10, label: "The inferno falls silent." },
42
+ { threshold: 15, label: "Rampage broken." },
43
+ { threshold: 20, label: "Legend undone." },
44
+ { threshold: 25, label: "The oracle mourns." },
45
+ ],
46
+ // Streak counter only earns screen space once it's genuinely building.
47
+ visibleAt: 2,
48
+ },
49
+ question: {
50
+ sources: ["opentdb", "custom", "the-trivia-api"],
51
+ },
52
+ roleSelection: {
53
+ durationSeconds: 60,
54
+ },
55
+ player: {
56
+ maxNicknameLength: 20,
57
+ },
58
+ sse: {
59
+ keepaliveIntervalMs: 15000,
60
+ },
61
+ };
@@ -0,0 +1,23 @@
1
+ export type { BadgeAward, BadgeId, BadgeMode, BadgeRunParams, } from "./badges.ts";
2
+ export { EMOTE_SPRITES, SELECTABLE_EMOTE_IDS } from "./emotes.ts";
3
+ export type { ErrorCode } from "./error-codes.ts";
4
+ export { type AdvanceMode, type Difficulty, type DifficultyFilter, GAME, type QuestionsPerRound, type ScoringMode, type TimerDuration, } from "./game.settings.ts";
5
+ export type { AccessMode, ActiveGamePhase, GamePhase, GameState, ImageMatchingQuizQuestion, Player, PlayerRole, PlayerStatus, QuizAnswer, QuizConfig, QuizQuestion, StandardQuizQuestion, } from "./game.ts";
6
+ export type { GameAnswerRecord } from "./game-answer-record.ts";
7
+ export * from "./pages/app.schema.ts";
8
+ export * from "./pages/auth.schema.ts";
9
+ export * from "./pages/common.schema.ts";
10
+ export * from "./pages/history.schema.ts";
11
+ export * from "./pages/quiz.schema.ts";
12
+ export type { PlayerSafeQuestion } from "./player-safe-question.ts";
13
+ export { DifficultySchema, type ImageChoiceQuestion, type ImageMatchingQuestion, isHttpsUrl, type Media, 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";
14
+ export { hasMedia, isImageMatching, isTextChoice, isTrueFalse, } from "./question.ts";
15
+ export { OptionalScoringModeSchema, ScoringModeSchema, } from "./scoring.schema.ts";
16
+ export { type SoloAnswerChangeMessage, type SoloAnswerMessage, type SoloAuthMessage, type SoloClientMessage, type SoloErrorMessage, type SoloFinalMessage, type SoloGuestLimitMessage, type SoloLifeSpentMessage, type SoloNextMessage, type SoloPingMessage, type SoloPongMessage, type SoloQuestionMessage, type SoloReadyMessage, type SoloResumeMessage, type SoloRoundResultMessage, type SoloServerMessage, type SoloStartMessage, type SoloStreakBestMessage, type SoloStreakLostMessage, type SoloStreakMessage, type SoloStreakNearMissMessage, type SoloTabBlurMessage, type SoloTabFocusMessage, type SoloTickMessage, soloError, } from "./solo-ws.ts";
17
+ export type { StreakFlourish } from "./streak-flourish.ts";
18
+ export type { LeaderboardBoard, LeaderboardDifficulty, LeaderboardScope, } from "./wire/leaderboard.ts";
19
+ export type { OperatorQuestionRevision, PublishManyResult, QuestionRevision, } from "./wire/question-bank.ts";
20
+ export type { FlagDetail, FlagReporter, FlagStatus, QuestionFlagAnswerSnapshot, QuestionSnapshot, } from "./wire/question-flags.ts";
21
+ 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, PlayerRemovedMessage, PlayerRoleMessage, RoleSelectionLockedMessage, RoleSelectionStartMessage, RoleSelectionUpdateMessage, ServerMessage, WsAuthMessage, WsPongMessage, } from "./ws.ts";
22
+ export { ClientMessageSchema } from "./ws-game.schema.ts";
23
+ export { SoloClientMessageSchema } from "./ws-solo.schema.ts";
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export { EMOTE_SPRITES, SELECTABLE_EMOTE_IDS } from "./emotes.js";
2
+ export { GAME, } from "./game.settings.js";
3
+ export * from "./pages/app.schema.js";
4
+ export * from "./pages/auth.schema.js";
5
+ export * from "./pages/common.schema.js";
6
+ export * from "./pages/history.schema.js";
7
+ export * from "./pages/quiz.schema.js";
8
+ export { DifficultySchema, isHttpsUrl, MediaSchema, MediaTypeSchema, PostAnswerNoteSchema, QuestionSchema, QuestionSourceSchema, QuestionTypeSchema, SourceSchema, } from "./question.schema.js";
9
+ export { hasMedia, isImageMatching, isTextChoice, isTrueFalse, } from "./question.js";
10
+ export { OptionalScoringModeSchema, ScoringModeSchema, } from "./scoring.schema.js";
11
+ export { soloError, } from "./solo-ws.js";
12
+ export { ClientMessageSchema } from "./ws-game.schema.js";
13
+ export { SoloClientMessageSchema } from "./ws-solo.schema.js";