js-game-zero 0.0.0-stage → 0.4.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 (53) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +138 -2
  3. package/dist/active-service.d.ts +21 -0
  4. package/dist/actor.d.ts +182 -0
  5. package/dist/animation.d.ts +61 -0
  6. package/dist/assets.d.ts +148 -0
  7. package/dist/audio.d.ts +150 -0
  8. package/dist/browser-asset-backends.d.ts +8 -0
  9. package/dist/browser.d.ts +10 -0
  10. package/dist/chunk-q9m20c2h.js +7972 -0
  11. package/dist/clock.d.ts +29 -0
  12. package/dist/color.d.ts +15 -0
  13. package/dist/events.d.ts +145 -0
  14. package/dist/friendly-names.d.ts +43 -0
  15. package/dist/geometry.d.ts +125 -0
  16. package/dist/helpers.d.ts +28 -0
  17. package/dist/host.d.ts +58 -0
  18. package/dist/host.js +435 -0
  19. package/dist/index.d.ts +340 -0
  20. package/dist/index.js +258 -0
  21. package/dist/input.d.ts +200 -0
  22. package/dist/inspection.d.ts +58 -0
  23. package/dist/jgz.js +8545 -0
  24. package/dist/node.d.ts +14 -0
  25. package/dist/node.js +145 -0
  26. package/dist/pixel-density.d.ts +23 -0
  27. package/dist/random.d.ts +31 -0
  28. package/dist/reference/actor.md +197 -0
  29. package/dist/reference/audio.md +128 -0
  30. package/dist/reference/clock.md +150 -0
  31. package/dist/reference/helpers.md +46 -0
  32. package/dist/reference/hooks.md +379 -0
  33. package/dist/reference/index.d.ts +2 -0
  34. package/dist/reference/index.js +39 -0
  35. package/dist/reference/loaders.md +164 -0
  36. package/dist/reference/rect.md +129 -0
  37. package/dist/reference/runtime.md +478 -0
  38. package/dist/reference/screen.md +392 -0
  39. package/dist/reference/storage.md +90 -0
  40. package/dist/rendering.d.ts +216 -0
  41. package/dist/runtime-contracts.d.ts +117 -0
  42. package/dist/runtime-diagnostics.d.ts +37 -0
  43. package/dist/runtime-persistence.d.ts +32 -0
  44. package/dist/runtime-source.d.ts +82 -0
  45. package/dist/runtime.d.ts +81 -0
  46. package/dist/source-scan.d.ts +49 -0
  47. package/dist/storage-path.d.ts +3 -0
  48. package/dist/storage.d.ts +88 -0
  49. package/dist/tone.d.ts +81 -0
  50. package/dist/touch-controls.d.ts +24 -0
  51. package/dist/touch-pointer.d.ts +11 -0
  52. package/dist/util.d.ts +62 -0
  53. package/package.json +69 -4
@@ -0,0 +1,478 @@
1
+ # The `run()` Module API
2
+
3
+ Most learners never call `run()` directly — the sandbox and zero-boilerplate
4
+ examples call it for you, and you just write top-level `WIDTH`/`draw`/
5
+ `update`/hook definitions. This page is for import-mode code and for anyone
6
+ embedding the framework in a host page (a custom sandbox, a course platform,
7
+ an automated test).
8
+
9
+ ```ts
10
+ function run(
11
+ moduleOrSource: GameModule | DefinedGame | string,
12
+ options?: RunOptions,
13
+ ): GameHandle;
14
+ ```
15
+
16
+ ## Three entry styles
17
+
18
+ **Scoped import mode (recommended for applications)** — `defineGame()` gives
19
+ the game a typed set of services owned by that one run. Actors, assets,
20
+ storage, animation, input, and audio therefore cannot accidentally bind to a
21
+ different game on the same page:
22
+
23
+ ```ts
24
+ import { defineGame, run } from "js-game-zero";
25
+
26
+ run(
27
+ defineGame(({ Actor, screen, keyboard }) => {
28
+ const player = new Actor("player.svg");
29
+ return {
30
+ WIDTH: 320,
31
+ HEIGHT: 180,
32
+ update(dt) {
33
+ if (keyboard.right) player.x += 120 * dt;
34
+ },
35
+ draw() {
36
+ screen.clear();
37
+ player.draw();
38
+ },
39
+ onKeyDown(event) {
40
+ console.log(event.key);
41
+ },
42
+ };
43
+ }),
44
+ );
45
+ ```
46
+
47
+ The input hooks in a defined game receive one typed event object.
48
+ Zero-boilerplate and raw import hooks keep their Pygame Zero-style
49
+ parameter-name dispatch.
50
+
51
+ | Event | Fields |
52
+ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | `GameKeyEvent` (`onKeyDown`, `onKeyUp`) | `key`, `mod`, `unicode` and `repeat`, as [`hooks.md`](hooks.md#onkeydownkey-mod-unicode-repeat--onkeyupkey-mod-unicode-repeat) describes them. |
54
+ | `GameMouseEvent` (`onMouseDown`, `onMouseUp`, `onMouseMove`) | `pos`, `button` (the button pressed or released, or `null` for one with no `mouse` constant; `onMouseMove` reads `buttons` instead), `buttons` (a `Set` of held buttons) and `rel` (movement since the last event). |
55
+
56
+ The function given to `defineGame()` receives the run's own services:
57
+ `screen`, `clock`, `keyboard`, `mouse`, `images`, `sounds`, `music`, `fonts`,
58
+ `storage`, `tone`, `animate` and `exit`, the constants `keys` and `keymods`,
59
+ and the classes `Actor`, `Animation`, `Rect`, `ZRect`, `Screen` and
60
+ `Storage`. An `Actor` or `Animation` made from them belongs to that run.
61
+
62
+ Package-level `screen`, `clock`, `keyboard`, `images`, `sounds`, `music`, and
63
+ `storage` remain available for learner compatibility. They are convenience
64
+ proxies for a single active runtime; applications that can run more than one
65
+ game should prefer the services passed to `defineGame()`.
66
+
67
+ **Import mode** — pass a plain object of hooks and config:
68
+
69
+ ```js
70
+ import { run, screen, keyboard } from "js-game-zero";
71
+
72
+ run({
73
+ WIDTH: 320,
74
+ HEIGHT: 180,
75
+ draw() {
76
+ screen.fill("#10243d");
77
+ },
78
+ update(dt) {
79
+ /* ... */
80
+ },
81
+ onKeyDown(key) {
82
+ /* hooks receive values by parameter name */
83
+ },
84
+ });
85
+ ```
86
+
87
+ **Global mode** — pass a source-code string, which runs with the framework's
88
+ globals (`Actor`, `screen`, `keyboard`, `WIDTH`, `draw`, ...) already in
89
+ scope, exactly like a zero-boilerplate sandbox project. This is how the
90
+ sandbox's preview pane runs the code from your editor.
91
+
92
+ ```js
93
+ run(`
94
+ WIDTH = 320;
95
+ HEIGHT = 180;
96
+ function draw() { screen.fill("navy"); }
97
+ `);
98
+ ```
99
+
100
+ `handle.mode` tells you which one was used (`"import"` or `"global"`).
101
+
102
+ A global-mode game runs in its own scope rather than the page's. Reading a
103
+ name nothing defines throws a `ReferenceError` with a suggestion
104
+ (`scren is not defined. Did you mean screen?`); `typeof name` stays safe.
105
+ Browser APIs a game can't reach say what to use instead — for example,
106
+ `setTimeout` points at `clock.schedule(callback, seconds)`, and
107
+ `localStorage` at `storage`. Standard JavaScript built-ins such as `Math`,
108
+ `JSON`, `isNaN`, `Map` and `console` are available as usual.
109
+
110
+ Every library name has one spelling, in camelCase: `screen.draw.filledCircle`,
111
+ `actor.angleTo`, `rect.collidePoint`, `clock.scheduleInterval`, `onKeyDown`.
112
+ Constants stay upper case (`WIDTH`, `keys.SPACE`, `mouse.LEFT`). Another
113
+ spelling of a member, such as Pygame Zero's `actor.angle_to` or
114
+ `rect.colliderect`, throws where it's read, naming the real one
115
+ (`actor.angle_to doesn't exist. Did you mean actor.angleTo?`). In a
116
+ global-mode game, `screen`, `clock`, `music`, `tone`, `keys`, `keymods` and
117
+ `mouse` also throw with a suggestion for any member they don't have
118
+ (`screen.draw.cirlce doesn't exist. Did you mean screen.draw.circle?`), and
119
+ an undefined global suggests the real one
120
+ (`random_int is not defined. Did you mean randomInt?`).
121
+ The classes behind them (`Screen`, `Clock`, `Storage`) are globals too, so a
122
+ capital letter by mistake, as in `Screen.fill("navy")`, points at the service
123
+ instead (`Screen.fill doesn't exist. Did you mean screen.fill?`).
124
+
125
+ ## `RunOptions`
126
+
127
+ | Option | Purpose |
128
+ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
129
+ | `canvas` | Draw into an existing `<canvas>` instead of creating one. |
130
+ | `container` | Where to mount a created canvas (default the page's `<body>`). Called from a script in `<head>`, before there's a `<body>`, the game starts at once and its canvas joins the `<body>` when the page has been read. |
131
+ | `autostart` | Default `true`. Set `false` to skip the automatic frame loop and drive frames yourself via `handle.step(dt)` — useful for deterministic tests. |
132
+ | `projectId` | Scopes storage keys and asset URLs, so multiple games on one page don't collide. |
133
+ | `assetBaseUrl` | Where asset URLs are rooted (default `/assets`). |
134
+ | `assetUrls` | The game's assets by path, each with its URL: `{ "images/cat.svg": "data:image/svg+xml;base64,…" }`. When set, assets load only from here, never from `assetBaseUrl`. See [assets without a server](loaders.md#assets-without-a-server). |
135
+ | `preload` | An array of asset paths (e.g. `["images/player.svg"]`) to start loading immediately, before any is referenced by code. |
136
+ | `preloadedAssets` | Pre-built `{ images, sounds, music, fonts }` asset maps to seed the cache synchronously — used by hosts that already have the bytes. `loadImages(assetUrls)` makes the `images` map; see [assets without a server](loaders.md#assets-without-a-server). |
137
+ | `storage` | A `Storage` instance or a raw `StorageAdapter` (`{ path, load(), save(text) }`) to use instead of the default `localStorage`-backed one. |
138
+ | `loaders` | Override one or more of the `images`/`sounds`/`music`/`fonts` loader instances. |
139
+ | `keyboardTarget` | The `EventTarget` that receives keyboard events (default `window`). |
140
+ | `mouseReleaseTarget` | The `EventTarget` where mouse releases (`mouseup`) are heard (default `window`, so a drag released outside the canvas still ends). A host driving a game itself passes the canvas, so its releases reach no other game. |
141
+ | `seed` | A string or number that makes the game's randomness repeatable: `Math.random` as a zero-boilerplate game sees it, and `randomInt` and `randomChoice`, follow one sequence for this seed, the sequence `jgz test --seed` uses. See [seeded runs](#seeded-runs). |
142
+ | `TOUCH_CONTROLS` | `true` for the full touch-control preset, `false` to disable it, or a button mapping with optional `show: "auto"` / `"always"`. See [touch controls](hooks.md#touch-controls). |
143
+ | `PIXEL_DENSITY` | Canvas pixels per logical pixel. Defaults to the device's pixel ratio, from 1 to 3. See [pixel density](#pixel-density). |
144
+ | `scheduler` | Override `requestAnimationFrame`/`cancelAnimationFrame` (for tests). |
145
+ | `diagnostics` | `(diagnostic) => void` — called for every diagnostic (see below). Without it, they go to the browser console. |
146
+
147
+ Any hook or `WIDTH`/`HEIGHT`/`PIXEL_DENSITY`/`TITLE`/`ICON`/`TOUCH_CONTROLS`
148
+ config can also be passed directly on `RunOptions`; if the same one is also present on the module
149
+ object, the option wins and a diagnostic warns about the duplicate.
150
+
151
+ ## `GameHandle`
152
+
153
+ `run()` returns a handle for controlling the running game:
154
+
155
+ | Member | Purpose |
156
+ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
157
+ | `canvas`, `screen`, `clock`, `keyboard`, `loaders`, `storage` | The live instances this run is using. |
158
+ | `module` | The final, merged hook/config object. |
159
+ | `diagnostics` | The diagnostics collected so far. |
160
+ | `state`, `failure` | Lifecycle state and the first fatal runtime failure, if one occurred. |
161
+ | `ready: Promise<void>` | Resolves once the preload gate is satisfied (see below). |
162
+ | `done: Promise<void>` | Resolves after final storage flushing and resource cleanup complete. |
163
+ | `pause()` / `resume()` | Stop/restart the frame loop without tearing anything down. |
164
+ | `step(dt)` | Manually advance one frame by `dt` seconds — only meaningful with `autostart: false`. |
165
+ | `stop()` | Detaches input listeners, flushes any pending storage save, and cancels the frame loop. |
166
+ | `dispose()` | Awaited form of cleanup; resolves only after pending persistence work is finished. |
167
+ | `restart(nextSource?)` | Awaits cleanup, then starts a fresh run with the same options. Resolves to the new run's handle; this one stays stopped. |
168
+ | `reload(nextSource?)` | Compatibility alias that starts synchronously and returns the new run's handle; prefer `restart()` when persistence ordering matters. |
169
+
170
+ ```js
171
+ const handle = run(gameModule, { canvas });
172
+ // later
173
+ handle.stop();
174
+ ```
175
+
176
+ ## `exit()`
177
+
178
+ ```js
179
+ import { exit } from "js-game-zero";
180
+ exit(); // equivalent to calling .stop() on the currently active run() handle
181
+ ```
182
+
183
+ Also injected as a zero-boilerplate global, so game code can call `exit()`
184
+ directly to end itself.
185
+
186
+ ## Preload gate
187
+
188
+ Each run tracks its own required image and font loads (Actor constructions,
189
+ `images.player` touches, and `preload` entries) and holds frames, with no
190
+ `update` and no `draw`, until those loads and storage initialization settle.
191
+ An image or font first asked for later, in a hook (a new `actor.image`, a first
192
+ `screen.blit`), holds the frames after it the same way until it has loaded,
193
+ and `handle.step()` does nothing meanwhile. So a host that steps a game in a
194
+ synchronous loop should hand it its images decoded, with
195
+ [`loadImages`](loaders.md#assets-without-a-server). A required image/font
196
+ failure rejects `handle.ready`, fails the handle, and performs cleanup. Sound and music loads
197
+ are recoverable: they report a warning but never freeze another run or stop
198
+ rendering. `handle.ready` lets hosts await startup explicitly instead of
199
+ polling.
200
+
201
+ ## Pixel density
202
+
203
+ The canvas holds `WIDTH × HEIGHT × density` pixels, so a game is as sharp
204
+ as the screen it's on. Density is the device's `devicePixelRatio`, from 1
205
+ to 3, unless the game file or `RunOptions` sets `PIXEL_DENSITY`. A
206
+ `PIXEL_DENSITY` that isn't a positive number warns and uses the device's
207
+ ratio.
208
+
209
+ Game code works in logical pixels at any density; only reading the
210
+ canvas itself sees its real pixels:
211
+
212
+ | Logical pixels | Canvas pixels (logical × density) |
213
+ | -------------------------------------------------------- | --------------------------------- |
214
+ | `screen.width`, `screen.height`, `screen.bounds()` | `canvas.width`, `canvas.height` |
215
+ | Every drawing call, text, images and Actors | `getImageData()`, `toDataURL()` |
216
+ | Mouse and touch positions, Actor positions and hit tests | |
217
+
218
+ `screen.density` reports the density in use, so a host reading pixels back
219
+ multiplies logical coordinates by it.
220
+
221
+ **CSS size.** A denser canvas would show at its pixel size, so the runtime
222
+ gives it a CSS width of `WIDTH` px through a `:where(canvas[data-jgz-density])`
223
+ rule; its height follows the aspect ratio. The rule has no specificity, so
224
+ CSS the page sets on the canvas wins. A page that sizes the canvas should
225
+ set its width, or both width and height: a height alone keeps the rule's
226
+ width. The touch-control dock and the sandbox preview fit the canvas by its
227
+ logical size. At density 1 the canvas gets no rule and shows at its own
228
+ size.
229
+
230
+ **Changes.** Before each frame the runtime checks the canvas against the
231
+ screen:
232
+
233
+ - With no `PIXEL_DENSITY`, density follows `devicePixelRatio`, which
234
+ changes when the page is zoomed or moved to another display. The canvas
235
+ keeps what it shows, rescaled, for games that don't redraw everything.
236
+ - A host that sets `canvas.width` or `canvas.height` sets the logical size,
237
+ and the runtime multiplies it by the density.
238
+ - Setting a canvas's size resets its context, so the runtime restores the
239
+ density scale.
240
+
241
+ **Limit.** iOS Safari leaves a canvas over 16,777,216 pixels (4096²)
242
+ blank, so density comes down until the canvas fits, though never below 1
243
+ unless the game asks for less. A `PIXEL_DENSITY` that doesn't fit warns
244
+ and uses the most that does.
245
+
246
+ Raster images (PNG, JPEG) can't hold more detail than their own pixels. In
247
+ Chromium, SVG images draw from their vectors at the canvas's density, so an
248
+ SVG Actor stays sharp at any size.
249
+
250
+ ## Package boundaries
251
+
252
+ Games import learner-facing values from `js-game-zero`. Embedders should
253
+ import loader factories, browser adapters, scheduler/diagnostic types, and
254
+ other integration APIs from `js-game-zero/host`; Node filesystem storage
255
+ lives at `js-game-zero/node`. Browser-safe advanced root imports remain
256
+ as deprecated compatibility aliases through 0.2.
257
+
258
+ Host tooling can also import `getGameInspector(handle)` from
259
+ `js-game-zero/host`. It returns a live, read-only binding inspector for a
260
+ running zero-boilerplate source game, or `null` for import-mode and disposed
261
+ handles (a failed game keeps its inspector, so a host can read the state at the
262
+ crash). This is the inspection seam the browser-backed `jgz` verification CLI
263
+ uses; it does not add globals or learner-facing runtime methods.
264
+
265
+ | Member | Purpose |
266
+ | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
267
+ | `bindings()` | The captured non-function top-level identifiers, in source order. |
268
+ | `functions()` | The top-level functions. |
269
+ | `read(path)` | The live value at a binding or a [state path](#state-paths), as it is. |
270
+ | `inspect(path?)` | The value at a path as JSON, exactly as `jgz`'s `inspect` reports it (`serializeGameValue`); with no path, every binding's value by name. |
271
+
272
+ The same module exports `serializeGameValue(value)`, `parseStatePath(source)`,
273
+ `createSeededRandom(seed)` and `loadImages(assetUrls)` (see
274
+ [assets without a server](loaders.md#assets-without-a-server)).
275
+
276
+ The rest of `js-game-zero/host` is for building a host:
277
+
278
+ | Export | What it's for |
279
+ | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
280
+ | `createImageLoader`, `createSoundLoader`, `createMusicLoader`, `createFontLoader` and `createAssetLoaders` | Loaders for `RunOptions.loaders`, each taking a `projectId` and an `assetBaseUrl` or `assetUrls`. |
281
+ | `createStorage({ adapter?, projectId?, path? })` and `createLocalStorageAdapter({ projectId?, path? })` | A `Storage`, or the `localStorage` adapter behind the default one, for `RunOptions.storage`. |
282
+ | `collectRuntimeDiagnostics(game, options?)` | The static [diagnostics](#diagnostics) `run()` would report for a game's source or module, without running it. |
283
+ | `layoutText(context, text, pos, options?)` and `fitTextboxFontSize(context, text, rect, options?)` | Where `screen.draw.text` and `screen.draw.textbox` would put text, and at what size. |
284
+
285
+ Its other exports (the input dispatcher, the `map…Event` functions,
286
+ `dispatchHook`, the audio and tone classes, `getScreen`/`setScreen`) are the
287
+ runtime's own parts, exported for tools and tests, and may change.
288
+ `js-game-zero/node` exports `createSandboxFileStorageAdapter({
289
+ projectRoot, path? })`, a `StorageAdapter` that keeps a project's storage in
290
+ a JSON file under `projectRoot` (`storage.json` by default).
291
+
292
+ This reference ships with `js-game-zero`, for a host that hands it to a
293
+ learner, a tool or a model. `js-game-zero/reference` exports `reference`,
294
+ every page's Markdown by its file name (`reference["actor.md"]`), and each
295
+ page is also a file at `js-game-zero/reference/<page>.md`.
296
+
297
+ ### State paths
298
+
299
+ A path names a top-level binding, then `.name` for a property and `[n]` for an
300
+ index: `score`, `player.x`, `fish[0].y`. `parseStatePath("fish[0].y")` returns
301
+ `{ source: "fish[0].y", binding: "fish", segments: [0, "y"] }`, and throws a
302
+ `TypeError` for anything else, including `__proto__`, `prototype` and
303
+ `constructor`. `jgz`'s `inspect` takes the same paths.
304
+
305
+ ### What a frame drew
306
+
307
+ A path that starts with `$drawn` reads what the game's last `draw()` drew,
308
+ in order, rather than one of its bindings, through `read`, `inspect` and
309
+ `jgz`'s `inspect` alike. Each thing drawn has its `kind` and what it is and
310
+ where, as the game gave them:
311
+
312
+ | `kind` | Fields | From |
313
+ | --------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
314
+ | `fill` | `color`, and `to` for a gradient | `screen.fill` |
315
+ | `clear` | | `screen.clear` |
316
+ | `image` | `image` (its name), `x`, `y` (its top-left) | `screen.blit`, `screen.draw.image` |
317
+ | `actor` | `image`, `x`, `y` (its `pos`: its center, unless its `anchor` says otherwise) | an Actor's `draw()` |
318
+ | `circle` | `x`, `y`, `radius`, `color`, `filled` | `screen.circle`, `screen.draw.circle`, `filledCircle` |
319
+ | `rect` | `x`, `y`, `width`, `height`, `color`, `filled`, and `radius` when rounded | `screen.rect`, `screen.draw.rect`, `filledRect`, and the `roundedRect` forms |
320
+ | `polygon` | `x`, `y` (its first point), `points` (how many), `color`, `filled` | `screen.triangle`, `screen.draw.polygon`, `filledPolygon` |
321
+ | `line` | `x`, `y`, `x2`, `y2`, `color` | `screen.line`, `screen.draw.line` |
322
+ | `text` | `text`, `x`, `y`, `width`, `height` (its box), and `color` when it has one | `screen.text`, `screen.draw.text`, `screen.draw.textbox` |
323
+
324
+ `$drawn[0].kind`, `$drawn[2].image` and the like read single things. A frame
325
+ keeps its first 200 things; before the first frame `$drawn` is empty. So a
326
+ check that something is on the screen reads what the game drew rather than
327
+ its pixels: `{ kind: "actor", image: "robot.svg" }` is in `$drawn`.
328
+
329
+ ### Inspected values
330
+
331
+ `serializeGameValue` turns a value into JSON: numbers, strings, booleans,
332
+ arrays and plain objects as they are (object keys sorted); anything else tagged
333
+ with `$type`:
334
+
335
+ ```js
336
+ serializeGameValue(new Actor("cat", [40, 60]));
337
+ // { $type: "Actor", image: "cat", x: 40, y: 60, width, height, angle, …, properties: {} }
338
+ serializeGameValue(new Rect(1, 2, 3, 4));
339
+ // { $type: "Rect", left: 1, top: 2, right: 4, bottom: 6, width: 3, height: 4, center: [2.5, 4] }
340
+ serializeGameValue({ missing: undefined, big: Infinity });
341
+ // { big: { $type: "Number", value: "Infinity" }, missing: { $type: "Undefined" } }
342
+ ```
343
+
344
+ An Actor's `properties` are the fields the game gave it (`cat.speed = 40` is
345
+ `properties.speed`); a `read` gives the Actor itself, so `cat.speed` there.
346
+ Sets, maps, dates and errors are tagged too; values deeper than six levels,
347
+ lists past 100 items and circular references come back as `Truncated` or
348
+ `Circular`.
349
+
350
+ ### Seeded runs
351
+
352
+ `run(source, { seed })` gives the game a sequence of its own. A
353
+ zero-boilerplate game sees a `Math` whose `random` follows the seed (every
354
+ other member is the real `Math`'s), and `randomInt` and `randomChoice` draw
355
+ from it while the game runs, in its hooks and its top-level code alike. An
356
+ import-mode game's `randomInt` and `randomChoice` follow the seed in its hooks;
357
+ its `Math.random` is the page's. Nothing else changes: the page's own
358
+ `Math.random`, and any other game on the page, keep their randomness, and a
359
+ `restart()` starts the sequence again.
360
+
361
+ Up to nine plain digits are the number they spell (`42` and `"42"` are the same
362
+ seed); any other string is hashed (FNV-1a), and the sequence is Mulberry32 over
363
+ that 32-bit state. `jgz test --seed` passes its seed to the same option, so one
364
+ seed gives the same values in the CLI and in a host page;
365
+ `createSeededRandom(seed)` returns the sequence itself, for a host or a test to
366
+ predict. A seed that isn't a string or a finite number fails at startup.
367
+
368
+ ### Driving a game from a host page
369
+
370
+ The script-tag build is `dist/jgz.js` in `js-game-zero` (`js-game-zero/iife`),
371
+ which jsDelivr also serves, at
372
+ `https://cdn.jsdelivr.net/npm/js-game-zero@0.3/dist/jgz.js`.
373
+ Its `window.JGZ` carries `loadImages`, `getGameInspector`,
374
+ `serializeGameValue`, `parseStatePath` and `createSeededRandom` beside the
375
+ learner-facing API, so a page can run a game it drives itself, the way the CLI
376
+ does, without a bundler:
377
+
378
+ ```js
379
+ const canvas = document.createElement("canvas"); // never added to the page
380
+ const handle = JGZ.run(source, {
381
+ canvas,
382
+ autostart: false,
383
+ seed: "check-1",
384
+ keyboardTarget: canvas,
385
+ mouseReleaseTarget: canvas,
386
+ PIXEL_DENSITY: 1,
387
+ });
388
+ await handle.ready;
389
+ handle.step(0); // draws the first screen, as the CLI does
390
+ canvas.dispatchEvent(
391
+ new KeyboardEvent("keydown", { key: "ArrowRight", code: "ArrowRight" }),
392
+ );
393
+ for (let frame = 0; frame < 30; frame += 1) handle.step(1 / 60);
394
+ canvas.dispatchEvent(
395
+ new KeyboardEvent("keyup", { key: "ArrowRight", code: "ArrowRight" }),
396
+ );
397
+ canvas.dispatchEvent(
398
+ new MouseEvent("mousedown", {
399
+ clientX: 25,
400
+ clientY: 30,
401
+ button: 0,
402
+ buttons: 1,
403
+ }),
404
+ );
405
+ canvas.dispatchEvent(
406
+ new MouseEvent("mouseup", { clientX: 25, clientY: 30, button: 0 }),
407
+ );
408
+ JGZ.getGameInspector(handle).inspect("player.x");
409
+ ```
410
+
411
+ - **Frames.** With `autostart: false`, nothing runs until the page calls
412
+ `handle.step(dt)`; each call is one `update(dt)` and one `draw()`. The CLI
413
+ steps once with `dt` 0 after `ready`, then one sixtieth of a second per frame,
414
+ so a page that does the same sees what `jgz test play` sees.
415
+ - **Keys.** Keyboard events are heard on `keyboardTarget` (default `window`):
416
+ `keydown` and `keyup`, read by `code` first (`ArrowRight`, `Space`, `KeyA`)
417
+ and then `key`. Passing the canvas keeps them from reaching other games.
418
+ - **Mouse and taps.** `mousedown`, `mousemove` and `wheel` are heard on the
419
+ canvas, `mouseup` on `mouseReleaseTarget` (default `window`). A position is
420
+ mapped from the canvas's displayed box onto the logical screen:
421
+ `clientX = box.left + x / screen.width * box.width`, and the same for `y`
422
+ (`handle.canvas.getBoundingClientRect()`, `handle.screen`). A canvas that
423
+ isn't on the page has an empty box, so its client coordinates are the
424
+ logical ones, as above. Touch arrives as `pointerdown`, `pointermove` and
425
+ `pointerup` with `pointerType: "touch"`; the CLI uses the mouse events.
426
+
427
+ ## Diagnostics
428
+
429
+ `run()` performs lightweight static checks and reports them through the
430
+ `diagnostics` option (and collects them on `handle.diagnostics`) rather than
431
+ failing outright. A few mistakes stop the game before it starts instead:
432
+ `run()` reports a `runtime-error` in the `startup` phase and throws. The
433
+ main one is that **`draw()` must not declare any parameters** (a
434
+ `TypeError`, since mixing it up with `update(dt)` is a very common typo).
435
+ The others are a `TOUCH_CONTROLS` that isn't a valid mapping, a `seed` that
436
+ isn't a string or a finite number, an invalid `projectId` or `assetUrls`, and
437
+ a top-level statement that throws.
438
+
439
+ Everything else is a warning:
440
+
441
+ | `code` | Meaning |
442
+ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
443
+ | `hook-typo` | A top-level function is named nearly like a hook (e.g. `onKeyDwon`), or is a hook spelled another way (`on_key_down`), which is never called. |
444
+ | `constant-typo` | A top-level assignment is named nearly like a setting, such as `WIDTH` or `TITLE`. |
445
+ | `duplicate-config` | The same setting was given both on the module and in the run options. |
446
+ | `hook-arity` | `update` declares more than one parameter. |
447
+ | `hook-parameter` | An input hook parameter isn't one of its documented names; says what it receives. |
448
+ | `invalid-config` | `WIDTH`/`HEIGHT`/`PIXEL_DENSITY`/`ICON` isn't usable. |
449
+ | `asset-name` | A quoted string that looks like an asset filename doesn't match the naming grammar. |
450
+ | `runtime-error` | An uncaught error, or a failure loading/saving storage. |
451
+
452
+ The two typo checks only look at names the script _defines_ at top level
453
+ (`function drow()`, `const udpate = () => {}`, `WIDHT = 800`), never at names
454
+ it merely uses (`row`, `col`, `rows[y]`), and they stay quiet when the real
455
+ name is also defined. Short names get one edit of slack and longer ones two.
456
+
457
+ A `runtime-error` raised by the game's own code (a hook that threw, or a
458
+ top-level statement at startup) also carries `line`: the 1-based line in the
459
+ game source where the error was thrown, when the engine's stack trace names it.
460
+ A `hook-typo` or `constant-typo` warning carries the line that declares the
461
+ misspelled name, and `draw()` declaring parameters fails on the line that
462
+ declares it.
463
+ A syntax error carries one too: where the engine gives no position (V8 gives
464
+ none for compiled game code), it's the first line whose part of the source
465
+ fails the same way, so a missing `)` is placed on the line it's missing from,
466
+ and a block left open on the line it opens on. A name the game never defined
467
+ is placed the same way where the engine gives no position (JavaScriptCore): on
468
+ the one line that reads it, when only one does. `handle.failure.line` is the
469
+ same number. Errors thrown inside the library have no `line`.
470
+
471
+ The sandbox shows these under Messages, and marks a problem's line in the editor; import-mode hosts can pass
472
+ a `diagnostics` callback to log or display them however they like. Without
473
+ one, a run reports them to the browser console: a warning with
474
+ `console.warn` and an error with `console.error`, each starting
475
+ `js-game-zero:` and ending with its line, if it has one. A mistake that
476
+ makes `run()` itself throw isn't logged as well. A game that fails while
477
+ running removes a canvas it made, so on a page with no callback, the
478
+ console is where to find out why.