@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # @joystyk/launcher
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - bb095c7: Add shared real-time input, pause, pace records and ghost races, plus a WebGL 2 renderer
8
+ lifecycle with context recovery and adaptive quality. Launch daily fixed-step simulations
9
+ from published configs and retain their puzzle identity through saved games.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [bb095c7]
14
+ - Updated dependencies [bb095c7]
15
+ - @joystyk/contracts@0.2.0
16
+ - @joystyk/storage@0.2.0
17
+ - @joystyk/session@0.2.0
18
+ - @joystyk/daily@0.2.0
19
+ - @joystyk/identity@0.2.0
20
+ - @joystyk/kit@0.2.0
21
+ - @joystyk/ui@0.2.0
22
+
23
+ ## 0.1.0
24
+
25
+ ### Minor Changes
26
+
27
+ - Prepare the first compiled library release: ESM and API declarations for every public entry point, runtime assets, and checks against installed npm tarballs. Games and applications remain private.
28
+
29
+ ### Patch Changes
30
+
31
+ - Updated dependencies
32
+ - @joystyk/contracts@0.1.0
33
+ - @joystyk/kit@0.1.0
34
+ - @joystyk/storage@0.1.0
35
+ - @joystyk/identity@0.1.0
36
+ - @joystyk/session@0.1.0
37
+ - @joystyk/daily@0.1.0
38
+ - @joystyk/ui@0.1.0
package/README.md CHANGED
@@ -1,5 +1,248 @@
1
1
  # @joystyk/launcher
2
2
 
3
- The first Joystyk library release is not yet available. This placeholder reserves the package name.
3
+ The frame around a game (host policy). `<jy-launcher>` lets the player choose how to play, starts a
4
+ local session or opens a server room, mounts the game's canvas, and draws the platform's own
5
+ screens around it: the resume offer, the pass-and-play hand-off (host policy), the room lobby, the
6
+ pause overlay with preferences, and the results with their ad slot. It talks to games only through the contracts in
7
+ game-contract.md (Joystyk host reference) and never imports a game.
4
8
 
5
- Source: https://github.com/joystyk-games/game-libraries
9
+ ## npm package
10
+
11
+ The first `0.1.0` release is being prepared; it has not been published. Once available:
12
+
13
+ ```sh
14
+ npm install @joystyk/launcher
15
+ ```
16
+
17
+ Node consumers require Node 24.15 or later. Browser entry points require a modern
18
+ browser and should be loaded in the browser. Tested with Gyral 0.3.1.
19
+
20
+ | Import | Format |
21
+ | --- | --- |
22
+ | `@joystyk/launcher` | ES module and TypeScript declarations |
23
+ | `@joystyk/launcher/browser` | ES module and TypeScript declarations |
24
+ | `@joystyk/launcher/fixture` | ES module and TypeScript declarations |
25
+ | `@joystyk/launcher/report` | ES module and TypeScript declarations |
26
+
27
+ Only the entries listed above are public API. Installed packages contain compiled ESM,
28
+ type declarations and runtime assets. Repository-relative links describe the Joystyk
29
+ workspace and are not needed to use the npm package.
30
+
31
+ ## Entry points
32
+
33
+ | Import | Runs in | Contents |
34
+ | --- | --- | --- |
35
+ | `@joystyk/launcher` | Server and browser | `<jy-launcher>` (server-rendered), its pure model, the safe-point event |
36
+ | `@joystyk/launcher/browser` | Browser | `createLauncherServices`, `provideLauncherServices`, `watchSafePoints`, `requestSave` |
37
+ | `@joystyk/launcher/fixture` | Tests | Tally, a tiny test game, and `tallyProbe` |
38
+
39
+ The stage element (`<jy-game-stage>`), the session runners, storage and identity load on
40
+ the first Start, so a game page that nobody plays never downloads them.
41
+
42
+ ## Use
43
+
44
+ A page renders the launcher with what its game page knows about the game:
45
+
46
+ ```ts
47
+ html`<jy-launcher .game=${{ id, name, modes, players, aiLevels, status, aiContent }}></jy-launcher>`;
48
+ ```
49
+
50
+ `players` is `{ min, max }`, plus the manifest's optional `default`: against the computer
51
+ the Players picker starts at that count, and otherwise at the fewest.
52
+
53
+ The app provides the games it can load, once per page (platform packages never import a
54
+ game, so the list comes from the app; `apps/site/src/game-loaders.ts` builds it):
55
+
56
+ ```ts
57
+ import { createLauncherServices, provideLauncherServices } from '@joystyk/launcher/browser';
58
+
59
+ provideLauncherServices(document, createLauncherServices({
60
+ games: { nim: { entry: () => import('@joystyk/game-nim').then((m) => m.entry) } },
61
+ }));
62
+ ```
63
+
64
+ A loader may also give `aiWorker: () => new Worker(…)` for a manifest with `ai.heavy`; the
65
+ local runner then chooses AI moves in that worker (`workerAiRunner`). A game whose engine
66
+ requests jobs that local play may run gives `jobWorker: (kind) => new Worker(…)`; the runner
67
+ then performs each job in a fresh worker (`workerJobRunner`). Greed's dice throws work this
68
+ way, with the factory from `@joystyk/game-greed/jobs`.
69
+
70
+ Without JavaScript the setup form still renders, with a disabled Start button and the line
71
+ "JavaScript is needed to play".
72
+
73
+ ## How a game starts
74
+
75
+ 1. **Setup.** The modes come from the manifest. Solo, vs AI (with a level), pass-and-play
76
+ (with seat names, checked by `@joystyk/identity`'s name filter) and, for a manifest that
77
+ lists `daily`, the daily puzzle are playable; P2P rooms show as coming soon (host policy). Solo,
78
+ vs-AI and daily play seat the guest under their display name. In pass-and-play the count is
79
+ people: a game with AI and more seats than people (Hearts and Spades seat four) lets two or
80
+ more share the device, and the computer plays the other seats at the chosen level
81
+ (`computerSeatsFor`). Online with friends is
82
+ playable only for a game the room server hosts: once live, a launcher whose manifest lists
83
+ `server` asks `GET /api/rooms/games` (the `launcher-online-rooms` driver) and reads an
84
+ invite after the answer. A game the server doesn't host shows online play as coming soon
85
+ (host policy); while it asks, the option waits without a note; and when the server can't be
86
+ reached (an error, a page that isn't the answer, or 10 seconds of silence) it shows as
87
+ "Unavailable right now". The local modes never wait for it. The answer holds for the page,
88
+ so client navigation to another game reuses it.
89
+ 2. **Loading.** `<jy-game-stage>` loads the entry through the app's loader, checks that the
90
+ manifest allows the mode and that the access gate is open (always, until premium access
91
+ in v2), loads the module, opens the player's storage and reads their preferences.
92
+ 3. **AI-made content (host policy).** A manifest with `aiContent: true` stays locked until the
93
+ player accepts a short notice. The answer is a preference (`aiContentAccepted`), stored
94
+ once per device and changeable in the overlay. Such a game's results and pause menu carry
95
+ `<jy-report-problem>` (*Report a problem*, host policy): a reason from a fixed list and an
96
+ optional note, posted to the site's `POST /api/reports` with the game and, for a daily
97
+ puzzle, its ID (the stage's `Puzzle` output). `@joystyk/launcher/report` holds the wire
98
+ constants the site shares.
99
+ 4. **Resume or new.** Each local mode has its own save slot. A save in progress is offered
100
+ for resuming; a finished one, one from older rules, or one the engine's `worthSaving`
101
+ rejects (an untouched game), is cleared. The slot's port lets the runner clear it too.
102
+ 5. **Playing.** The stage builds the session (`createLocalSession` for a Layer 2 engine,
103
+ `createFixedStepRunner` for a simulation, or the game's own `createSession`) and calls
104
+ `mount(canvas, session, platform)`.
105
+ 6. **Results.** At the outcome the stage clears the save slot, records stats and, if the
106
+ engine has a `rematch` hook, the config for "Play again". The launcher shows the outcome,
107
+ play time, any assistance (`Outcome.undosUsed` and `assists`), Play again, Change mode,
108
+ Back to all games, and the `results` ad slot, over the final board (see below). The
109
+ session is disposed only when the player picks Play again or Change mode, the game asks
110
+ to exit, or the page goes away.
111
+
112
+ ## Online with friends
113
+
114
+ Server rooms (host policy), for any game whose manifest lists `server` and that the room server
115
+ hosts:
116
+
117
+ 1. **Create or join.** "Create a room" asks the room server for a room (`POST /api/rooms`)
118
+ with the guest's display name; the creator holds seat 0 and hosts. Below it, a join form
119
+ takes a code or a pasted invite link, and "Just watch" joins as a spectator (host policy). A page
120
+ opened from an invite (`?room=CODE`) picks the mode and fills in the code; the player still
121
+ presses Join. A malformed code is refused in the form; an unknown one, or one for another
122
+ game, gets the stage's message.
123
+ 2. **Lobby.** The invite link (in a read-only field, with Copy link and Copy code) and the
124
+ code; the seats, each open, the computer at the room's level, or a player marked host, you,
125
+ ready or away; the spectator count; the turn clock. Everyone with a seat presses Ready
126
+ (`aria-pressed`). The host picks the clock, from the manifest's presets (the usual one
127
+ marked) or Off, which is never the default (host policy, host policy), and the computer's level; adds the
128
+ computer to an open seat or removes a seat; and starts once every seat is filled and
129
+ ready. The hint under Start says what it waits for. Leave room gives the seat up.
130
+ 3. **Play.** When the match starts the stage mounts the game on the remote session. The bar
131
+ shows the room code, the connection ("Connection lost. Reconnecting…"), and the running
132
+ turn clock (`role="timer"`). The menu doesn't pause a room ("The match goes on while this
133
+ menu is open") and offers Leave room. A newer tab taking the seat shows "Use this tab
134
+ instead". Hosts can't remove a player mid-match (host policy).
135
+ 4. **Results** show over the final board as for local play, worded for the viewer ("You
136
+ won!", "Ben wins", or "Ben wins!" for a spectator). The host's Rematch starts a new match
137
+ in the same room, on a fresh canvas; Leave room goes back to the setup.
138
+
139
+ The stage reports the room after every change as a `Room` output (`RoomFrame`: plain data,
140
+ checked at the boundary) and performs the lobby's requests (`StageControl.roomRequests`) in
141
+ order. The room's code goes in the address bar while it is open (`history.replaceState`),
142
+ so a reload or a shared address finds it again. `LauncherServices.rooms` (`RoomServices`) is
143
+ how the stage reaches the room server; the default (`browser/rooms.ts`) uses the page's own
144
+ origin, where Traefik routes `/api/rooms` and `/ws`, and tests pass a loopback.
145
+
146
+ ## The daily puzzle
147
+
148
+ For a manifest that lists `daily` (host policy, content-pipeline.md (Joystyk host reference)):
149
+
150
+ 1. **After the notice.** The stage downloads nothing until the game may start: an AI-content
151
+ game's notice comes first (host policy).
152
+ 2. **Today's puzzle.** It asks `@joystyk/daily/browser`'s content client for today's UTC date
153
+ (`LauncherServices.daily`: the base URL, default `/daily`, a `fetch` and the wall clock).
154
+ The client revalidates the index, uses or refreshes the cached file and decodes `config`
155
+ with the engine's `schemas.config`; offline, a cached copy plays. With nothing to play, the
156
+ stage fails with the client's plain message ("This puzzle needs a connection…").
157
+ 3. **Played once.** If the results store already holds today's result, the launcher shows it
158
+ ("You've played today's puzzle") with the streak and share text, and mounts nothing.
159
+ Otherwise the game starts on the day's config (a resumed save finishes the puzzle it holds,
160
+ even after the next release, and its result counts for that puzzle's date).
161
+ 4. **Results.** At the outcome the stage records the `DailyResult` (with the engine's `share`
162
+ rows in both forms), reads the streaks and reports a `Daily` frame. The results show the
163
+ streak, the win streak for a daily that can be lost, "New puzzle at 8:00 PM (in 6 hours)"
164
+ in local time, the share preview, *Share* (Web Share, else the clipboard) and *Copy as
165
+ text*. There is no Play again; *Change mode* appears when the game has other modes.
166
+ 5. **Prefetch.** `dailyPrefetchers(services, gameIds)` gives the PWA's `registerPwa` one
167
+ prefetcher per daily game. It prefetches today and tomorrow through the same client and
168
+ storage, and nothing for an AI-content game until the notice is accepted.
169
+
170
+ ## What the game gets
171
+
172
+ `mount` receives the canvas, a `<div>` in the stage's own shadow root (the frame's styles
173
+ never reach it), and a `PlatformApi`:
174
+
175
+ | Field | Source |
176
+ | --- | --- |
177
+ | `storage` | `@joystyk/storage`'s game storage, scoped to the game id and the player |
178
+ | `preferences` | The stored preferences resolved against the system (reduced motion, sound and volume, single-key shortcuts, color scheme, locale), updated live when the player changes them in the overlay or the system setting changes |
179
+ | `announce` | The launcher's polite and assertive live regions |
180
+ | `requestExit` | Back to the setup, saving the game |
181
+
182
+ **Theme handoff.** The canvas carries the manifest's theme as `--game-accent`,
183
+ `--game-surface` and `--game-ink`, and the player's color scheme, which resolves any
184
+ `light-dark()` pair; the frame keeps Joystyk's tokens. It paints the surface and sets its
185
+ text color to the ink. A surface and its ink always follow the same scheme: a single surface
186
+ without an ink gets near-black or near-white, whichever reads better (`readableInk`), and a
187
+ game without a surface gets the frame's own surface and text. `canvasTheme` in `src/theme.ts`
188
+ computes the tokens; the rules are in the
189
+ game contract (Joystyk host reference) ("Theme").
190
+
191
+ ## The frame's screens
192
+
193
+ - **Hand-off (host policy).** When the session publishes a `handoff`, the canvas is hidden and the
194
+ frame shows "Pass to <name>, then tap to reveal". Focus moves to the reveal button.
195
+ - **Results over the final board.** The results open as a non-modal `<dialog>` laid over the
196
+ canvas, out of the flow, so neither the board nor the page moves; a board shorter than the
197
+ results scrolls them. The canvas stays mounted, dimmed and `inert` behind them. Focus moves
198
+ to the headline, which names the dialog. Escape or "Review the board" closes the results
199
+ and moves focus to the board, which can be read (and heard) again but not played: the
200
+ session refuses moves once the game is over. "Show results" in the bar reopens them, with
201
+ the same ad slot. The dialog is not modal because a modal one would make the page inert,
202
+ and the results are where the PWA may offer an update.
203
+ - **Overlay.** The Menu button opens a modal `<dialog>`: Resume, Save and exit, and the
204
+ preferences. The dialog traps focus and closes on Escape; the session is paused while it
205
+ is open. Focus returns to the Menu button.
206
+ - **Reduced motion.** The frame's own transitions stop when the system asks for reduced
207
+ motion, unless the player chose full motion, and always when the player chose to reduce
208
+ it. Games receive the resolved value and remove only decorative motion.
209
+
210
+ ## Safe points
211
+
212
+ The launcher reports every change with a `joystyk:safe-point` event on the document
213
+ (`{ safe, gameId }`). The page is safe outside play and on the results screen, open or closed; during play
214
+ it is safe only when the session says so (`meta.safePoint`). The PWA listens with
215
+ `watchSafePoints` and offers updates only when `safe` is true
216
+ (offline-and-updates.md (Joystyk host reference), Rule 2).
217
+
218
+ A forced security update can't wait for one, so it asks: `requestSave()` dispatches a
219
+ `joystyk:save-request` event, and each game stage stops play (pausing a real-time game) and
220
+ writes the current position through its save slot. The promise settles once every write has
221
+ landed or failed, and at once when no game is in play. Rooms and Layer 1 games add nothing:
222
+ the room server keeps a room's state, and those games save themselves.
223
+
224
+ ## Tests
225
+
226
+ - `model.test.ts` and `launcher.test.ts`: the pure helpers and the component's model, with
227
+ `@gyral/testing`, in Node.
228
+ - `theme.test.ts` and `theme.browser.test.ts`: the canvas theme tokens, the picked ink and
229
+ its CSS fallback.
230
+ - `fixture/engine.test.ts`: the Tally fixture's rules.
231
+ - `launcher.browser.test.ts`: the real stage, runner and storage (memory backend) in
232
+ Chromium: modes from the keyboard, mounting and disposal, theme tokens and readable canvas
233
+ text in light and dark, resume, the hand-off screen, rematch, the AI-content notice, the
234
+ overlay's focus trap and Escape, preferences and reduced motion, safe points, results over
235
+ the inert final board (Escape, Show results, no layout shift, disposal on Play again) and a
236
+ Layer 1 game.
237
+ - `launcher.daily.browser.test.ts`: the daily mode with a fake `fetch` and a fixed clock:
238
+ nothing downloads before the notice, the published config plays, the results show the
239
+ streak and share text, a second visit shows today's result, offline with nothing cached says
240
+ so, and the prefetcher waits for the notice.
241
+ - `launcher.rooms.browser.test.ts`: Online with friends in Chromium against the real room
242
+ core running in the page (`__tests__/loopback-room.ts`): create, invite, the computer in a
243
+ seat, ready, start, results, rematch and leave; a guest joining from an invite with a
244
+ spectator watching; malformed and unknown codes.
245
+ - `apps/site`'s end-to-end tests play Tally and Nim through the launcher on the real server,
246
+ where no room server answers, so online play shows as unavailable; `rooms.e2e.test.ts`
247
+ plays Nim, Greed and Shift 23 rooms on the real site and room servers, and checks that a
248
+ game the room server doesn't host shows online play as coming soon.
@@ -0,0 +1,8 @@
1
+ import type { LauncherServices } from './services.js';
2
+ /** What `registerPwa` takes for each daily game. */
3
+ export type DailyPrefetch = {
4
+ readonly gameId: string;
5
+ prefetch(today: string): Promise<unknown>;
6
+ };
7
+ /** Prefetchers for the daily games among `gameIds` (games whose manifest lists `daily`). */
8
+ export declare const dailyPrefetchers: (services: LauncherServices, gameIds: readonly string[]) => readonly DailyPrefetch[];
@@ -0,0 +1,32 @@
1
+ //#region src/browser/daily-prefetch.ts
2
+ /** Prefetchers for the daily games among `gameIds` (games whose manifest lists `daily`). */
3
+ var dailyPrefetchers = (services, gameIds) => gameIds.map((gameId) => ({
4
+ gameId,
5
+ prefetch: async (today) => {
6
+ const loader = services.games[gameId];
7
+ if (loader === void 0) return null;
8
+ const entry = await loader.entry();
9
+ if (!entry.manifest.modes.includes("daily")) return null;
10
+ const player = await services.openPlayer();
11
+ if (player.device === void 0) return null;
12
+ if (entry.manifest.aiContent === true) {
13
+ const preferences = await player.storage.preferences.get();
14
+ if (!preferences.ok || preferences.value.aiContentAccepted !== true) return null;
15
+ }
16
+ const module = await entry.load();
17
+ const rules = module.engine ?? module.simulation;
18
+ if (rules === void 0) return null;
19
+ const { createDailyClient } = await import("@joystyk/daily/browser");
20
+ return createDailyClient({
21
+ baseUrl: services.daily?.baseUrl ?? "/daily",
22
+ gameId,
23
+ config: rules.schemas.config,
24
+ storage: player.device,
25
+ ...services.daily?.fetch === void 0 ? {} : { fetch: services.daily.fetch }
26
+ }).prefetch(today);
27
+ }
28
+ }));
29
+ //#endregion
30
+ export { dailyPrefetchers };
31
+
32
+ //# sourceMappingURL=daily-prefetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daily-prefetch.js","names":[],"sources":["../../src/browser/daily-prefetch.ts"],"sourcesContent":["// Daily prefetching for the PWA (D-46, content-pipeline.md, \"Prefetching for offline play\"):\n// one prefetcher per daily game, which the app hands to `registerPwa`. The PWA decides when\n// (at a safe point, while idle and online) and for whom (an installed app, or a pinned game);\n// this decides how: through the same storage, player and content client the daily mode plays\n// from, so a prefetched day plays offline. An AI-content game prefetches nothing until the\n// player has accepted the notice (D-44). Everything loads lazily, on the first prefetch.\nimport type { Validator } from '@joystyk/contracts';\nimport type { LauncherServices } from './services.ts';\n\n/** What `registerPwa` takes for each daily game. */\nexport type DailyPrefetch = {\n readonly gameId: string;\n prefetch(today: string): Promise<unknown>;\n};\n\n/** Prefetchers for the daily games among `gameIds` (games whose manifest lists `daily`). */\nexport const dailyPrefetchers = (services: LauncherServices, gameIds: readonly string[]): readonly DailyPrefetch[] =>\n gameIds.map((gameId) => ({\n gameId,\n prefetch: async (today: string) => {\n const loader = services.games[gameId];\n if (loader === undefined) return null;\n const entry = await loader.entry();\n if (!entry.manifest.modes.includes('daily')) return null;\n const player = await services.openPlayer();\n if (player.device === undefined) return null;\n if (entry.manifest.aiContent === true) {\n const preferences = await player.storage.preferences.get();\n if (!preferences.ok || preferences.value.aiContentAccepted !== true) return null;\n }\n const module = await entry.load();\n const rules = module.engine ?? module.simulation;\n if (rules === undefined) return null;\n const { createDailyClient } = await import('@joystyk/daily/browser');\n const client = createDailyClient({\n baseUrl: services.daily?.baseUrl ?? '/daily',\n gameId,\n config: rules.schemas.config as Validator<unknown>,\n storage: player.device,\n ...(services.daily?.fetch === undefined ? {} : { fetch: services.daily.fetch }),\n });\n return client.prefetch(today);\n },\n }));\n"],"mappings":";;AAgBA,IAAa,oBAAoB,UAA4B,YAC3D,QAAQ,KAAK,YAAY;CACvB;CACA,UAAU,OAAO,UAAkB;EACjC,MAAM,SAAS,SAAS,MAAM;EAC9B,IAAI,WAAW,KAAA,GAAW,OAAO;EACjC,MAAM,QAAQ,MAAM,OAAO,MAAM;EACjC,IAAI,CAAC,MAAM,SAAS,MAAM,SAAS,OAAO,GAAG,OAAO;EACpD,MAAM,SAAS,MAAM,SAAS,WAAW;EACzC,IAAI,OAAO,WAAW,KAAA,GAAW,OAAO;EACxC,IAAI,MAAM,SAAS,cAAc,MAAM;GACrC,MAAM,cAAc,MAAM,OAAO,QAAQ,YAAY,IAAI;GACzD,IAAI,CAAC,YAAY,MAAM,YAAY,MAAM,sBAAsB,MAAM,OAAO;EAC9E;EACA,MAAM,SAAS,MAAM,MAAM,KAAK;EAChC,MAAM,QAAQ,OAAO,UAAU,OAAO;EACtC,IAAI,UAAU,KAAA,GAAW,OAAO;EAChC,MAAM,EAAE,sBAAsB,MAAM,OAAO;EAQ3C,OAPe,kBAAkB;GAC/B,SAAS,SAAS,OAAO,WAAW;GACpC;GACA,QAAQ,MAAM,QAAQ;GACtB,SAAS,OAAO;GAChB,GAAI,SAAS,OAAO,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,SAAS,MAAM,MAAM;EAC/E,CACO,CAAA,CAAO,SAAS,KAAK;CAC9B;AACF,EAAE"}
@@ -0,0 +1,3 @@
1
+ export { createLauncherServices, cryptoSeed, findLauncherServices, mediaSystem, provideLauncherServices, type DailyServices, type GameLoader, type GameLoaders, type LauncherServiceOptions, type LauncherServices, type PlayerContext, type RoomAnswer, type RoomServices, type RunnerPorts, type SystemSource, } from './services.js';
2
+ export { dailyPrefetchers, type DailyPrefetch } from './daily-prefetch.js';
3
+ export { requestSave, SAFE_POINT_EVENT, SAVE_REQUEST_EVENT, watchSafePoints, type SafePointDetail, type SaveRequestDetail, } from '../safe-point.js';
@@ -0,0 +1,4 @@
1
+ import { dailyPrefetchers } from "./daily-prefetch.js";
2
+ import { createLauncherServices, cryptoSeed, findLauncherServices, mediaSystem, provideLauncherServices } from "./services.js";
3
+ import { SAFE_POINT_EVENT, SAVE_REQUEST_EVENT, requestSave, watchSafePoints } from "../safe-point.js";
4
+ export { SAFE_POINT_EVENT, SAVE_REQUEST_EVENT, createLauncherServices, cryptoSeed, dailyPrefetchers, findLauncherServices, mediaSystem, provideLauncherServices, requestSave, watchSafePoints };
@@ -0,0 +1,2 @@
1
+ import type { RoomServices } from './services.js';
2
+ export declare const pageRooms: (page?: Location) => RoomServices;
@@ -0,0 +1,49 @@
1
+ import { browserSocket, createRoom, fetchRoomInfo, randomActionId, roomsSocketUrl, webStorageTokens } from "@joystyk/session/browser/rooms";
2
+ //#region src/browser/rooms.ts
3
+ var pageRooms = (page = location) => {
4
+ const origin = page.origin;
5
+ return {
6
+ create: async (gameId, name) => {
7
+ const created = await createRoom(origin, {
8
+ game: gameId,
9
+ name
10
+ });
11
+ if (!created.ok) return {
12
+ ok: false,
13
+ message: created.error.message
14
+ };
15
+ return {
16
+ ok: true,
17
+ value: {
18
+ room: created.value.room,
19
+ token: created.value.token,
20
+ notice: created.value.notice?.message ?? null
21
+ }
22
+ };
23
+ },
24
+ find: async (code) => {
25
+ const found = await fetchRoomInfo(origin, code);
26
+ return found.ok ? {
27
+ ok: true,
28
+ value: { gameId: found.value.game.id }
29
+ } : {
30
+ ok: false,
31
+ message: found.error.message
32
+ };
33
+ },
34
+ connect: browserSocket(roomsSocketUrl(origin)),
35
+ tokens: webStorageTokens(),
36
+ actionId: randomActionId,
37
+ inviteLink: (code) => {
38
+ const url = new URL(page.href);
39
+ url.hash = "";
40
+ url.search = "";
41
+ url.searchParams.set("room", code);
42
+ return url.href;
43
+ }
44
+ };
45
+ };
46
+ //#endregion
47
+ export { pageRooms };
48
+
49
+ //# sourceMappingURL=rooms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rooms.js","names":[],"sources":["../../src/browser/rooms.ts"],"sourcesContent":["// The default room services: the room server on this page's own origin. In production\n// Traefik routes `/api/rooms` and `/ws` on the site's host to the room service (DEPLOY.md);\n// in development the site's dev server forwards them to a local apps/rooms. Loaded with the\n// stage, so a page nobody plays never fetches the protocol's schemas.\nimport {\n browserSocket,\n createRoom,\n fetchRoomInfo,\n randomActionId,\n roomsSocketUrl,\n webStorageTokens,\n} from '@joystyk/session/browser/rooms';\nimport type { RoomServices } from './services.ts';\n\nexport const pageRooms = (page: Location = location): RoomServices => {\n const origin = page.origin;\n return {\n create: async (gameId, name) => {\n const created = await createRoom(origin, { game: gameId, name });\n if (!created.ok) return { ok: false, message: created.error.message };\n return {\n ok: true,\n value: { room: created.value.room, token: created.value.token, notice: created.value.notice?.message ?? null },\n };\n },\n find: async (code) => {\n const found = await fetchRoomInfo(origin, code);\n return found.ok\n ? { ok: true, value: { gameId: found.value.game.id } }\n : { ok: false, message: found.error.message };\n },\n connect: browserSocket(roomsSocketUrl(origin)),\n tokens: webStorageTokens(),\n actionId: randomActionId,\n inviteLink: (code) => {\n const url = new URL(page.href);\n url.hash = '';\n url.search = '';\n url.searchParams.set('room', code);\n return url.href;\n },\n };\n};\n"],"mappings":";;AAcA,IAAa,aAAa,OAAiB,aAA2B;CACpE,MAAM,SAAS,KAAK;CACpB,OAAO;EACL,QAAQ,OAAO,QAAQ,SAAS;GAC9B,MAAM,UAAU,MAAM,WAAW,QAAQ;IAAE,MAAM;IAAQ;GAAK,CAAC;GAC/D,IAAI,CAAC,QAAQ,IAAI,OAAO;IAAE,IAAI;IAAO,SAAS,QAAQ,MAAM;GAAQ;GACpE,OAAO;IACL,IAAI;IACJ,OAAO;KAAE,MAAM,QAAQ,MAAM;KAAM,OAAO,QAAQ,MAAM;KAAO,QAAQ,QAAQ,MAAM,QAAQ,WAAW;IAAK;GAC/G;EACF;EACA,MAAM,OAAO,SAAS;GACpB,MAAM,QAAQ,MAAM,cAAc,QAAQ,IAAI;GAC9C,OAAO,MAAM,KACT;IAAE,IAAI;IAAM,OAAO,EAAE,QAAQ,MAAM,MAAM,KAAK,GAAG;GAAE,IACnD;IAAE,IAAI;IAAO,SAAS,MAAM,MAAM;GAAQ;EAChD;EACA,SAAS,cAAc,eAAe,MAAM,CAAC;EAC7C,QAAQ,iBAAiB;EACzB,UAAU;EACV,aAAa,SAAS;GACpB,MAAM,MAAM,IAAI,IAAI,KAAK,IAAI;GAC7B,IAAI,OAAO;GACX,IAAI,SAAS;GACb,IAAI,aAAa,IAAI,QAAQ,IAAI;GACjC,OAAO,IAAI;EACb;CACF;AACF"}
@@ -0,0 +1,127 @@
1
+ import type { GameEntry, RngState } from '@joystyk/contracts';
2
+ import type { Clock, FrameScheduler, Reporter, SocketFactory, TokenStore, Visibility } from '@joystyk/session';
3
+ import type { PlayerStorage, Storage, StorageNotice, SystemEnvironment } from '@joystyk/storage';
4
+ /** How the app loads one game: its entry, and a worker for heavy AI if the game has one. */
5
+ export type GameLoader = {
6
+ /** The game's `entry.ts` (manifest plus lazy loaders), usually a dynamic import. */
7
+ readonly entry: () => Promise<GameEntry<HTMLElement>>;
8
+ /**
9
+ * For a manifest with `ai.heavy`: a worker running the game's `jobs/ai.worker.ts`
10
+ * (`new Worker(new URL(…, import.meta.url), { type: 'module' })`). Without it the runner
11
+ * plays heavy AI inline and reports it.
12
+ */
13
+ readonly aiWorker?: () => Worker;
14
+ /**
15
+ * For an engine that requests jobs a local game may run (`where: 'either'`, such as Greed's
16
+ * throws): a fresh worker for one job of `kind`, running the game's `jobs/<kind>.worker.ts`.
17
+ * Without it the runner fails those jobs, and the engine decides what that means.
18
+ */
19
+ readonly jobWorker?: (kind: string) => Worker;
20
+ };
21
+ /** The games a page can launch, by id. */
22
+ export type GameLoaders = Readonly<Record<string, GameLoader>>;
23
+ /** The guest, with their storage. */
24
+ export type PlayerContext = {
25
+ readonly playerId: string;
26
+ readonly displayName: string;
27
+ readonly storage: PlayerStorage;
28
+ /** The device's storage, which the daily puzzle's content cache and results use (`@joystyk/daily/browser`). */
29
+ readonly device?: Storage;
30
+ };
31
+ /**
32
+ * Where the daily mode finds published puzzles, and the wall clock that says which UTC day it
33
+ * is (D-46). Tests pass a fake `fetch` and a fixed clock.
34
+ */
35
+ export type DailyServices = {
36
+ /** Default "/daily", the site's published-content path (content-pipeline.md, "Delivery"). */
37
+ readonly baseUrl?: string;
38
+ readonly fetch?: (url: string, init?: RequestInit) => Promise<Response>;
39
+ /** Epoch milliseconds. Default `Date.now`. */
40
+ readonly now?: () => number;
41
+ };
42
+ /** The system settings `resolvePreferences` falls back on, now and on change. */
43
+ export interface SystemSource {
44
+ get(): SystemEnvironment;
45
+ subscribe(listener: (environment: SystemEnvironment) => void): () => void;
46
+ }
47
+ /** A room server answer: the value, or why not, worded for players. */
48
+ export type RoomAnswer<T> = {
49
+ readonly ok: true;
50
+ readonly value: T;
51
+ } | {
52
+ readonly ok: false;
53
+ readonly message: string;
54
+ };
55
+ /**
56
+ * How the stage reaches the room server (D-48). The default (`browser/rooms.ts`, loaded with
57
+ * the stage) uses this page's origin, where Traefik routes `/api/rooms` and `/ws` to the room
58
+ * service; tests pass their own.
59
+ */
60
+ export interface RoomServices {
61
+ /** `POST /api/rooms`: a new room with the creator in seat 0, and a notice when the name was replaced. */
62
+ create(gameId: string, name: string): Promise<RoomAnswer<{
63
+ readonly room: string;
64
+ readonly token: string;
65
+ readonly notice: string | null;
66
+ }>>;
67
+ /** `GET /api/rooms/:code`: which game the room plays. */
68
+ find(code: string): Promise<RoomAnswer<{
69
+ readonly gameId: string;
70
+ }>>;
71
+ readonly connect: SocketFactory;
72
+ readonly tokens: TokenStore;
73
+ /** A fresh action ID: 8–40 URL-safe characters. */
74
+ actionId(): string;
75
+ /** The invite link: this game page with `?room=CODE`. */
76
+ inviteLink(code: string): string;
77
+ }
78
+ export type RunnerPorts = {
79
+ readonly clock: Clock;
80
+ readonly visibility: Visibility;
81
+ readonly frames: FrameScheduler;
82
+ readonly report: Reporter;
83
+ };
84
+ export interface LauncherServices {
85
+ readonly games: GameLoaders;
86
+ /** Opens storage and the guest identity; the same promise every time. */
87
+ openPlayer(): Promise<PlayerContext>;
88
+ readonly system: SystemSource;
89
+ readonly ports: RunnerPorts;
90
+ /** A fresh, unpredictable seed for a new game (and, separately, its AI). */
91
+ newSeed(): RngState;
92
+ /** Storage problems worded for players (a full disk, private browsing). Returns how to stop. */
93
+ onNotice(listener: (message: string) => void): () => void;
94
+ /** The pause before an inline AI moves, so players can follow. Default 500 ms. */
95
+ readonly aiDelayMs?: number;
96
+ /** Deep-freeze snapshots (development). Default true. */
97
+ readonly freeze?: boolean;
98
+ /** The room server for Online with friends. Default: this page's origin. */
99
+ readonly rooms?: RoomServices;
100
+ /** The daily puzzle's content and clock. Default: this page's `/daily/` and the device clock. */
101
+ readonly daily?: DailyServices;
102
+ }
103
+ /** Makes `services` available to every launcher below `target` (the document, or a test container). */
104
+ export declare const provideLauncherServices: (target: Node, services: LauncherServices) => (() => void);
105
+ /** The services of the nearest provider above `from`, crossing shadow roots, or null. */
106
+ export declare const findLauncherServices: (from: Node) => LauncherServices | null;
107
+ /** The system settings from media queries, following changes. */
108
+ export declare const mediaSystem: (view?: Window) => SystemSource;
109
+ /** A seed from 128 bits of the browser's cryptographic randomness. */
110
+ export declare const cryptoSeed: () => RngState;
111
+ export type LauncherServiceOptions = {
112
+ readonly games: GameLoaders;
113
+ /** Opens storage. Default: IndexedDB (`openBrowserStorage`), falling back to memory. */
114
+ readonly openStorage?: (onNotice: (notice: StorageNotice) => void) => Promise<Storage>;
115
+ readonly system?: SystemSource;
116
+ readonly ports?: Partial<RunnerPorts>;
117
+ readonly newSeed?: () => RngState;
118
+ readonly aiDelayMs?: number;
119
+ readonly freeze?: boolean;
120
+ readonly rooms?: RoomServices;
121
+ readonly daily?: DailyServices;
122
+ };
123
+ /**
124
+ * The page's services. Storage and identity open on the first game, not on page load, and
125
+ * their code loads then too.
126
+ */
127
+ export declare const createLauncherServices: (options: LauncherServiceOptions) => LauncherServices;
@@ -0,0 +1,95 @@
1
+ import { seedState } from "@joystyk/kit/rng";
2
+ import { browserPorts } from "@joystyk/session/browser";
3
+ //#region src/browser/services.ts
4
+ var provided = /* @__PURE__ */ new WeakMap();
5
+ /** Makes `services` available to every launcher below `target` (the document, or a test container). */
6
+ var provideLauncherServices = (target, services) => {
7
+ provided.set(target, services);
8
+ return () => {
9
+ if (provided.get(target) === services) provided.delete(target);
10
+ };
11
+ };
12
+ /** The services of the nearest provider above `from`, crossing shadow roots, or null. */
13
+ var findLauncherServices = (from) => {
14
+ let node = from;
15
+ while (node !== null) {
16
+ const found = provided.get(node);
17
+ if (found !== void 0) return found;
18
+ node = node.parentNode ?? (node instanceof ShadowRoot ? node.host : null);
19
+ }
20
+ return null;
21
+ };
22
+ /** The system settings from media queries, following changes. */
23
+ var mediaSystem = (view = window) => {
24
+ const motion = view.matchMedia("(prefers-reduced-motion: reduce)");
25
+ const dark = view.matchMedia("(prefers-color-scheme: dark)");
26
+ const get = () => ({
27
+ prefersReducedMotion: motion.matches,
28
+ prefersDark: dark.matches,
29
+ locale: view.navigator.language
30
+ });
31
+ return {
32
+ get,
33
+ subscribe: (listener) => {
34
+ const onChange = () => listener(get());
35
+ motion.addEventListener("change", onChange);
36
+ dark.addEventListener("change", onChange);
37
+ return () => {
38
+ motion.removeEventListener("change", onChange);
39
+ dark.removeEventListener("change", onChange);
40
+ };
41
+ }
42
+ };
43
+ };
44
+ /** A seed from 128 bits of the browser's cryptographic randomness. */
45
+ var cryptoSeed = () => seedState(crypto.randomUUID());
46
+ /**
47
+ * The page's services. Storage and identity open on the first game, not on page load, and
48
+ * their code loads then too.
49
+ */
50
+ var createLauncherServices = (options) => {
51
+ const listeners = /* @__PURE__ */ new Set();
52
+ const notify = (notice) => {
53
+ for (const listener of [...listeners]) listener(notice.message);
54
+ };
55
+ let player = null;
56
+ const openPlayer = async () => {
57
+ const storage = await (options.openStorage ?? (async (onNotice) => (await import("@joystyk/storage/browser")).openBrowserStorage({ onNotice })))(notify);
58
+ const { createBrowserIdentity } = await import("@joystyk/identity/browser");
59
+ const profile = await createBrowserIdentity(storage).current();
60
+ return {
61
+ playerId: profile.id,
62
+ displayName: profile.displayName,
63
+ storage: storage.player(profile.id),
64
+ device: storage
65
+ };
66
+ };
67
+ return {
68
+ games: options.games,
69
+ openPlayer: () => {
70
+ player ??= openPlayer().catch((error) => {
71
+ player = null;
72
+ throw error;
73
+ });
74
+ return player;
75
+ },
76
+ system: options.system ?? mediaSystem(),
77
+ ports: {
78
+ ...browserPorts(),
79
+ ...options.ports
80
+ },
81
+ newSeed: options.newSeed ?? cryptoSeed,
82
+ onNotice: (listener) => {
83
+ listeners.add(listener);
84
+ return () => listeners.delete(listener);
85
+ },
86
+ ...options.aiDelayMs === void 0 ? {} : { aiDelayMs: options.aiDelayMs },
87
+ ...options.freeze === void 0 ? {} : { freeze: options.freeze },
88
+ ...options.rooms === void 0 ? {} : { rooms: options.rooms },
89
+ ...options.daily === void 0 ? {} : { daily: options.daily }
90
+ };
91
+ };
92
+ //#endregion
93
+ export { createLauncherServices, cryptoSeed, findLauncherServices, mediaSystem, provideLauncherServices };
94
+
95
+ //# sourceMappingURL=services.js.map