@joystyk/launcher 0.0.0 → 0.2.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 (55) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +245 -2
  3. package/dist/browser/daily-prefetch.d.ts +8 -0
  4. package/dist/browser/daily-prefetch.js +32 -0
  5. package/dist/browser/daily-prefetch.js.map +1 -0
  6. package/dist/browser/index.d.ts +3 -0
  7. package/dist/browser/index.js +4 -0
  8. package/dist/browser/rooms.d.ts +2 -0
  9. package/dist/browser/rooms.js +49 -0
  10. package/dist/browser/rooms.js.map +1 -0
  11. package/dist/browser/services.d.ts +127 -0
  12. package/dist/browser/services.js +95 -0
  13. package/dist/browser/services.js.map +1 -0
  14. package/dist/browser/stage.d.ts +28 -0
  15. package/dist/browser/stage.js +1195 -0
  16. package/dist/browser/stage.js.map +1 -0
  17. package/dist/fixture/engine.d.ts +60 -0
  18. package/dist/fixture/engine.js +164 -0
  19. package/dist/fixture/engine.js.map +1 -0
  20. package/dist/fixture/index.d.ts +52 -0
  21. package/dist/fixture/index.js +63 -0
  22. package/dist/fixture/index.js.map +1 -0
  23. package/dist/fixture/module.d.ts +2 -0
  24. package/dist/fixture/module.js +135 -0
  25. package/dist/fixture/module.js.map +1 -0
  26. package/dist/fixture/probe.d.ts +10 -0
  27. package/dist/fixture/probe.js +19 -0
  28. package/dist/fixture/probe.js.map +1 -0
  29. package/dist/index.d.ts +3 -0
  30. package/dist/index.js +4 -0
  31. package/dist/launcher.d.ts +190 -0
  32. package/dist/launcher.js +4614 -0
  33. package/dist/launcher.js.map +1 -0
  34. package/dist/model.d.ts +431 -0
  35. package/dist/model.js +566 -0
  36. package/dist/model.js.map +1 -0
  37. package/dist/report-problem.d.ts +39 -0
  38. package/dist/report-problem.js +359 -0
  39. package/dist/report-problem.js.map +1 -0
  40. package/dist/report.d.ts +18 -0
  41. package/dist/report.js +32 -0
  42. package/dist/report.js.map +1 -0
  43. package/dist/room-drivers.d.ts +26 -0
  44. package/dist/room-drivers.js +92 -0
  45. package/dist/room-drivers.js.map +1 -0
  46. package/dist/safe-point.d.ts +33 -0
  47. package/dist/safe-point.js +44 -0
  48. package/dist/safe-point.js.map +1 -0
  49. package/dist/save-slots.d.ts +6 -0
  50. package/dist/save-slots.js +18 -0
  51. package/dist/save-slots.js.map +1 -0
  52. package/dist/theme.d.ts +35 -0
  53. package/dist/theme.js +111 -0
  54. package/dist/theme.js.map +1 -0
  55. package/package.json +67 -11
@@ -0,0 +1,431 @@
1
+ import { type GameOption, type GameManifest, type Json, type Mode, type Outcome, type OutcomeResult, type SeatInfo } from '@joystyk/contracts';
2
+ import { type DailyWordStats } from '@joystyk/daily';
3
+ import type { LocalMode } from '@joystyk/session';
4
+ import type { StoredPreferences } from '@joystyk/storage';
5
+ export type GameStatus = 'available' | 'coming-soon';
6
+ /** What the launcher needs from the page: manifest facts, plus whether the game exists yet. */
7
+ export type LauncherGame = {
8
+ readonly id: string;
9
+ readonly name: string;
10
+ readonly modes: readonly Mode[];
11
+ /** `default`: the table size the player-count picker starts at against the computer. */
12
+ readonly players: {
13
+ readonly min: number;
14
+ readonly max: number;
15
+ readonly default?: number | undefined;
16
+ };
17
+ readonly aiLevels: readonly string[];
18
+ readonly status: GameStatus;
19
+ /** The game plays AI-generated content: it stays locked until the player accepts the notice (D-44). */
20
+ readonly aiContent?: boolean;
21
+ readonly options?: readonly GameOption[];
22
+ };
23
+ export declare const MODES: readonly Mode[];
24
+ export declare const isLauncherGame: (value: unknown) => value is LauncherGame;
25
+ /** The launcher's view of a manifest, for pages that have the manifest itself. */
26
+ export declare const launcherGame: (manifest: GameManifest, status?: GameStatus) => LauncherGame;
27
+ /** The AI-content notice (D-44; wording approved as D-52). */
28
+ export declare const AI_NOTICE: {
29
+ readonly title: "This game uses AI-made content";
30
+ readonly body: "Some puzzles and clues here are written by an AI model. Each one passes automatic safety checks before it appears, but no person reads every one first, so now and then something may be odd or wrong. If you spot a problem, use Report a problem.";
31
+ readonly scope: "Accepting unlocks all AI-content games on this device. You can change this anytime in Preferences.";
32
+ };
33
+ /** True when an AI-content game must show the notice before it can start. */
34
+ export declare const needsAiNotice: (game: LauncherGame, preferences: StoredPreferences) => boolean;
35
+ /**
36
+ * The access gate (monetization.md): where premium games and modes will ask for an
37
+ * entitlement in v2. Every game is free in v1, so it is always open.
38
+ */
39
+ export declare const accessGate: (access: GameManifest["access"]) => "open";
40
+ export declare const MODE_LABELS: Readonly<Record<Mode, string>>;
41
+ /** The modes the launcher runs on a local session. The daily puzzle is a local session on today's published config. */
42
+ export declare const toLocalMode: (mode: Mode) => LocalMode | null;
43
+ /** What the stage runs: a local session, or a server room (D-48). P2P rooms arrive later. */
44
+ export type StageMode = LocalMode | 'server';
45
+ export declare const toStageMode: (mode: Mode) => StageMode | null;
46
+ export type ModeOption = {
47
+ readonly mode: Mode;
48
+ readonly label: string;
49
+ readonly playable: boolean;
50
+ /** Why it can't be picked, or null. */
51
+ readonly note: string | null;
52
+ };
53
+ /**
54
+ * What the launcher knows about the room server (`GET /api/rooms/games`): still asking, the
55
+ * games it hosts, or no usable answer. A manifest may list `server` before the room server
56
+ * hosts the game; only a hosted game offers Online with friends.
57
+ */
58
+ export type OnlineRooms = {
59
+ readonly _tag: 'Checking';
60
+ } | {
61
+ readonly _tag: 'Hosted';
62
+ readonly games: readonly string[];
63
+ readonly protocol: number;
64
+ } | {
65
+ readonly _tag: 'Unreachable';
66
+ };
67
+ export declare const ONLINE_CHECKING: OnlineRooms;
68
+ export declare const ONLINE_UNREACHABLE: OnlineRooms;
69
+ /** The room server's answer, checked at the boundary; anything else (an error page, a proxy's 502 body) is undefined. */
70
+ export declare const parseHostedGames: (value: unknown) => OnlineRooms | undefined;
71
+ /** True when the room server opens rooms for this game. */
72
+ export declare const hostedOnline: (game: LauncherGame, online: OnlineRooms) => boolean;
73
+ /**
74
+ * One option per mode in the manifest, in manifest order. Online with friends is playable only
75
+ * once the room server says it hosts the game; until it answers, and when it can't be reached,
76
+ * the local modes are unaffected.
77
+ */
78
+ export declare const modeOptions: (game: LauncherGame, online?: OnlineRooms) => readonly ModeOption[];
79
+ /**
80
+ * The player counts a mode allows: solo is one; local multiplayer takes two or more; a room
81
+ * sizes itself. In pass-and-play the count is people: a game with AI and more seats than
82
+ * people (Hearts, Spades) lets two or more share the device while the computer fills the rest.
83
+ */
84
+ export declare const playerCounts: (game: LauncherGame, mode: Mode | null) => readonly number[];
85
+ /**
86
+ * The count the picker starts at for a mode: against the computer, the game's declared
87
+ * default when the mode offers it (Shift 23 seats five, D-58); otherwise the fewest. Other
88
+ * modes count people, and a room sizes itself, so the default never applies to them.
89
+ */
90
+ export declare const startingCount: (game: LauncherGame, mode: Mode | null) => number;
91
+ /** Pass-and-play with fewer people than the game's seats: the computer plays the rest. */
92
+ export declare const computerSeatsFor: (game: LauncherGame, setup: Setup) => number;
93
+ export type Setup = {
94
+ readonly mode: Mode | null;
95
+ readonly aiLevel: string | null;
96
+ readonly players: number;
97
+ /** Pass-and-play names as typed, by seat. */
98
+ readonly names: readonly string[];
99
+ /** A problem per name, by seat, or null. */
100
+ readonly errors: readonly (string | null)[];
101
+ /** Online with friends: the invite code (or link) as typed into the join form. */
102
+ readonly code: string;
103
+ /** Join to watch, even if a seat is free (D-33). */
104
+ readonly watch: boolean;
105
+ /** Why the code can't be used, or null. */
106
+ readonly codeError: string | null;
107
+ readonly options?: Readonly<Record<string, string | number | boolean>>;
108
+ };
109
+ export declare const initialSetup: (game: LauncherGame, online?: OnlineRooms) => Setup;
110
+ export declare const isRoomCode: (value: unknown) => value is string;
111
+ /**
112
+ * The room code in what a player typed or pasted: a code in any case, with spaces or dashes
113
+ * ("k7m-2qx"), or a whole invite link (`…?room=K7M2QX`). Null when there is none.
114
+ */
115
+ export declare const parseRoomCode: (input: string) => string | null;
116
+ /** "K7M 2QX": a code grouped for reading aloud and copying by eye. */
117
+ export declare const spacedCode: (code: string) => string;
118
+ /** How the stage opens its room. */
119
+ export type RoomPlan = {
120
+ readonly action: 'create';
121
+ } | {
122
+ readonly action: 'join';
123
+ readonly code: string;
124
+ readonly watch: boolean;
125
+ };
126
+ /** A clock choice in the lobby (D-34, D-42): a per-move clock, a time bank, or Off. */
127
+ export type ClockSetting = {
128
+ readonly kind: 'move';
129
+ readonly ms: number;
130
+ } | {
131
+ readonly kind: 'bank';
132
+ readonly initialMs: number;
133
+ readonly incrementMs: number;
134
+ } | {
135
+ readonly kind: 'off';
136
+ };
137
+ /** A select option for a clock setting; `value` round-trips through `parseClockValue`. */
138
+ export type ClockOption = {
139
+ readonly value: string;
140
+ readonly label: string;
141
+ };
142
+ export declare const clockValue: (setting: ClockSetting) => string;
143
+ export declare const parseClockValue: (value: string | undefined) => ClockSetting | undefined;
144
+ /** "30 seconds a move", "5 minutes, plus 2 seconds a move", "Off". */
145
+ export declare const clockLabel: (setting: ClockSetting) => string;
146
+ /**
147
+ * The lobby's clock choices for a manifest clock: its presets with the default marked, then
148
+ * Off, which private rooms always offer and never default to (D-42). None for a game without
149
+ * a clock.
150
+ */
151
+ export declare const clockOptions: (clock: GameManifest["clock"]) => readonly ClockOption[];
152
+ /** A seat in the lobby, as the stage reports it. */
153
+ export type RoomSeat = {
154
+ readonly id: number;
155
+ readonly name: string;
156
+ readonly controller: 'human' | 'ai';
157
+ readonly aiLevel: string | null;
158
+ /** A player holds the seat (also while the computer covers for them). */
159
+ readonly claimed: boolean;
160
+ readonly ready: boolean;
161
+ readonly connected: boolean;
162
+ };
163
+ /** The current decision clock, for the bar; `paused` while nobody is at the table (D-58). */
164
+ export type RoomTimer = {
165
+ readonly seat: number;
166
+ readonly name: string;
167
+ readonly seconds: number;
168
+ readonly paused: boolean;
169
+ };
170
+ /** A rematch or end-match offer as the results and the bar show it (D-61). */
171
+ export type RoomOfferFrame = {
172
+ /** Names the offer in answers. */
173
+ readonly id: number;
174
+ readonly kind: 'rematch' | 'end';
175
+ readonly by: number;
176
+ readonly byName: string;
177
+ readonly status: 'open' | 'declined' | 'withdrawn';
178
+ /** Seats that have accepted (the proposer, and computer seats for a rematch). */
179
+ readonly accepted: readonly number[];
180
+ /** The seat that declined, if one did, and its player's name. */
181
+ readonly declinedBy: number | null;
182
+ readonly declinedName: string | null;
183
+ /** Players whose answer an open offer still waits for. */
184
+ readonly waiting: readonly string[];
185
+ /** An end offer: whole seconds until the match ends unless someone declines. */
186
+ readonly seconds: number | null;
187
+ };
188
+ /** What the stage reports about its room after every change: plain data for the lobby and the bar. */
189
+ export type RoomFrame = {
190
+ readonly code: string;
191
+ /** The invite link: this game page with `?room=CODE`. */
192
+ readonly link: string;
193
+ /** `connecting` until the first snapshot arrives. */
194
+ readonly phase: 'connecting' | 'lobby' | 'playing' | 'finished' | 'closed';
195
+ readonly connection: 'connecting' | 'online' | 'reconnecting' | 'closed';
196
+ /** The viewer's seat; null for a spectator. */
197
+ readonly seat: number | null;
198
+ readonly host: number | null;
199
+ readonly seats: readonly RoomSeat[];
200
+ readonly minSeats: number;
201
+ readonly clock: ClockSetting;
202
+ readonly clockOptions: readonly ClockOption[];
203
+ readonly aiLevel: string | null;
204
+ readonly aiLevels: readonly string[];
205
+ readonly spectators: number;
206
+ readonly timer: RoomTimer | null;
207
+ /** The latest rejection or connection problem, worded for players, and its code. */
208
+ readonly notice: string | null;
209
+ readonly noticeCode: string | null;
210
+ /** Counts matches in this room; a rematch raises it. */
211
+ readonly match: number;
212
+ /** The latest rematch or end-match offer. */
213
+ readonly offer: RoomOfferFrame | null;
214
+ };
215
+ /** What the lobby asks of the room. The stage turns each into a room control. */
216
+ export type RoomRequest = {
217
+ readonly kind: 'ready';
218
+ readonly ready: boolean;
219
+ } | {
220
+ readonly kind: 'clock';
221
+ readonly setting: ClockSetting;
222
+ } | {
223
+ readonly kind: 'ai-level';
224
+ readonly level: string;
225
+ } | {
226
+ readonly kind: 'fill-with-ai';
227
+ readonly seat: number;
228
+ } | {
229
+ readonly kind: 'remove-seat';
230
+ readonly seat: number;
231
+ } | {
232
+ readonly kind: 'start';
233
+ }
234
+ /** Offer a rematch, or to end the match early (D-61); offering what is already offered accepts it. */
235
+ | {
236
+ readonly kind: 'offer';
237
+ readonly offer: 'rematch' | 'end';
238
+ }
239
+ /** Answer the offer with this id. */
240
+ | {
241
+ readonly kind: 'answer';
242
+ readonly offer: number;
243
+ readonly answer: 'accept' | 'decline' | 'withdraw';
244
+ } | {
245
+ readonly kind: 'leave';
246
+ }
247
+ /** Take the seat back after another tab replaced this one. */
248
+ | {
249
+ readonly kind: 'reconnect';
250
+ };
251
+ /**
252
+ * What an offer's state means to the viewer (`me`, null for a spectator), in words. An open
253
+ * end offer says how long is left unless `countdown` is false (live-region announcements).
254
+ */
255
+ export declare const offerText: (offer: RoomOfferFrame, me: number | null, countdown?: boolean) => string;
256
+ /**
257
+ * What to announce (politely) when the room's offer changed: a new offer, an answer, or
258
+ * nothing. `seats` names the players who accepted.
259
+ */
260
+ export declare const offerAnnouncement: (before: RoomOfferFrame | null, after: RoomOfferFrame | null, me: number | null, seats: readonly RoomSeat[]) => string | null;
261
+ /** Seats a player or the computer fills. */
262
+ export declare const filledSeats: (room: RoomFrame) => readonly RoomSeat[];
263
+ /** Why the host can't start yet, or null when the match can start (the room checks again). */
264
+ export declare const startProblem: (room: RoomFrame) => string | null;
265
+ /**
266
+ * A new mode keeps the AI level, and fits the player count to the mode: a count the player
267
+ * chose carries over when the new mode allows it. In a game that declares a default, a count
268
+ * still at the old mode's starting count was never chosen, so the new mode starts at its own.
269
+ */
270
+ export declare const pickMode: (game: LauncherGame, setup: Setup, mode: Mode) => Setup;
271
+ /** The name a seat gets when its field is left empty. */
272
+ export declare const defaultName: (seat: number) => string;
273
+ /** An empty name on a human seat means "the guest's own display name"; the stage fills it in. */
274
+ export declare const GUEST_NAME = "";
275
+ /**
276
+ * The seats for a setup, numbered from 0, or a problem per pass-and-play name. Solo and
277
+ * vs-AI play seat the guest first; AI seats are named for the computer.
278
+ */
279
+ export declare const seatsFor: (game: LauncherGame, setup: Setup) => {
280
+ readonly ok: true;
281
+ readonly seats: readonly SeatInfo[];
282
+ } | {
283
+ readonly ok: false;
284
+ readonly errors: readonly (string | null)[];
285
+ };
286
+ /** How to start: a new game, the saved one, or not yet (only load and look for a save). */
287
+ export type Begin = 'new' | 'resume' | null;
288
+ /**
289
+ * What the launcher's view hands the stage element that runs the game. Plain data: the
290
+ * stage acts on changes (begin, pause, a new reveal, new preferences).
291
+ */
292
+ export type StageControl = {
293
+ readonly gameId: string;
294
+ readonly mode: StageMode;
295
+ readonly seats: readonly SeatInfo[];
296
+ readonly begin: Begin;
297
+ /** True while the platform overlay is open. */
298
+ readonly paused: boolean;
299
+ /** How many hand-offs the players have revealed. Each increase reveals one. */
300
+ readonly reveals: number;
301
+ /** The player's choices, or null until the stage has read them. */
302
+ readonly preferences: StoredPreferences | null;
303
+ /** A rematch's config (the engine's `rematch`), or null for the game's own setup. */
304
+ readonly config: Json | null;
305
+ /** Online with friends: how to open the room. Null for local play. */
306
+ readonly room: RoomPlan | null;
307
+ /** A validated choice from the resume offer, when this game keeps several saves. */
308
+ readonly resumeSlot?: string;
309
+ readonly setup?: Json;
310
+ };
311
+ /** A saved game, as the resume offer describes it. */
312
+ export type SaveSummary = {
313
+ readonly savedAt: number;
314
+ readonly elapsedMs: number;
315
+ readonly players: readonly string[];
316
+ readonly slot?: string;
317
+ readonly choices?: readonly {
318
+ readonly slot: string;
319
+ readonly label: string;
320
+ readonly savedAt: number;
321
+ readonly elapsedMs: number;
322
+ readonly players: readonly string[];
323
+ }[];
324
+ };
325
+ /** What the frame needs from each snapshot; the stage reports it only when it changes. */
326
+ export type FrameState = {
327
+ readonly seats: readonly SeatInfo[];
328
+ /** Pass-and-play, D-31: the seat the device goes to next, behind the hand-off screen. */
329
+ readonly handoff: {
330
+ readonly seat: number;
331
+ readonly name: string;
332
+ } | null;
333
+ readonly outcome: Outcome | null;
334
+ readonly safePoint: boolean;
335
+ /** The latest rejection or problem, worded for players. */
336
+ readonly notice: string | null;
337
+ };
338
+ /**
339
+ * The daily puzzle's results, as the stage reports them after recording the result
340
+ * (content-pipeline.md, "Results and streaks" and "Share text"). Plain data.
341
+ */
342
+ export type DailyFrame = {
343
+ readonly statistics?: DailyWordStats;
344
+ /** The puzzle's UTC date, `yyyy-mm-dd`. */
345
+ readonly date: string;
346
+ /** The share text in both forms: emoji rows, and the rows in words for *Copy as text*. */
347
+ readonly share: string;
348
+ readonly shareWords: string;
349
+ /** Days finished in a row, and the best run. */
350
+ readonly streak: number;
351
+ readonly bestStreak: number;
352
+ /** Days won in a row, for a daily that can be lost; null otherwise. */
353
+ readonly winStreak: number | null;
354
+ readonly bestWinStreak: number | null;
355
+ /** "New puzzle at 8:00 PM (in 6 hours)", in the player's own time. */
356
+ readonly next: string;
357
+ /** Set when today's puzzle was already finished before this visit: its result and summary. */
358
+ readonly earlier: {
359
+ readonly result: OutcomeResult;
360
+ readonly summary: string;
361
+ } | null;
362
+ };
363
+ export type StageOutput = {
364
+ readonly _tag: 'Prepared';
365
+ readonly save: SaveSummary | null;
366
+ readonly preferences: StoredPreferences;
367
+ }
368
+ /** The player changed a preference in another tab of the site (gp-d4v). */
369
+ | {
370
+ readonly _tag: 'Preferences';
371
+ readonly preferences: StoredPreferences;
372
+ } | {
373
+ readonly _tag: 'Frame';
374
+ readonly frame: FrameState;
375
+ } | {
376
+ readonly _tag: 'ExitRequested';
377
+ } | {
378
+ readonly _tag: 'Announce';
379
+ readonly message: string;
380
+ readonly politeness: 'polite' | 'assertive';
381
+ } | {
382
+ readonly _tag: 'Notice';
383
+ readonly message: string;
384
+ }
385
+ /** At the outcome, when the engine names a rematch config: what "Play again" starts with. */
386
+ | {
387
+ readonly _tag: 'Rematch';
388
+ readonly config: Json;
389
+ }
390
+ /** Online with friends: the room after every change (and each second while a clock runs). */
391
+ | {
392
+ readonly _tag: 'Room';
393
+ readonly room: RoomFrame;
394
+ }
395
+ /** The daily puzzle: its recorded result, streaks and share text, or today's earlier result. */
396
+ | {
397
+ readonly _tag: 'Daily';
398
+ readonly daily: DailyFrame;
399
+ }
400
+ /** The puzzle the game plays, when the stage knows its ID (a daily puzzle): for "Report a problem". */
401
+ | {
402
+ readonly _tag: 'Puzzle';
403
+ readonly puzzleId: string;
404
+ } | {
405
+ readonly _tag: 'Failed';
406
+ readonly message: string;
407
+ };
408
+ export declare const isStoredPreferences: (value: unknown) => value is StoredPreferences;
409
+ /** Checks a stage report at the boundary; anything unexpected is dropped. */
410
+ export declare const parseStageOutput: (value: unknown) => StageOutput | undefined;
411
+ export type ResultView = {
412
+ readonly result: OutcomeResult;
413
+ readonly headline: string;
414
+ readonly summary: string;
415
+ readonly elapsedMs: number | null;
416
+ /** "Assisted: 2 undos, hints", or null for an unassisted game. */
417
+ readonly assisted: string | null;
418
+ };
419
+ /** "0:42", "12:05", "1:02:03". */
420
+ export declare const formatElapsed: (ms: number) => string;
421
+ /** An ISO 8601 duration for `<time datetime>`: "PT1M5S". */
422
+ export declare const isoDuration: (ms: number) => string;
423
+ /** The assistance line for an outcome, from what the runner recorded, or null when there was none. */
424
+ export declare const assistLine: (outcome: Outcome) => string | null;
425
+ /**
426
+ * The results screen's words for an outcome. In a room the headline depends on who is
427
+ * looking: `viewer` is the player's seat, or null for a spectator, who hears who won.
428
+ */
429
+ export declare const resultView: (outcome: Outcome, seats: readonly SeatInfo[], mode: StageMode, viewer?: number | null) => ResultView;
430
+ /** One preference control's change, parsed from its name and value, or undefined if invalid. */
431
+ export declare const parsePreferenceChange: (name: string, value: string | undefined, checked: boolean | undefined) => Partial<StoredPreferences> | undefined;