@moku-labs/game 0.14.11 → 0.14.13

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 (56) hide show
  1. package/README.md +13 -12
  2. package/dist/app/page.d.mts +8 -3
  3. package/dist/app/page.mjs +57 -12
  4. package/dist/app/system.d.mts +5 -3
  5. package/dist/app/system.mjs +2 -0
  6. package/dist/app.d.mts +15 -7
  7. package/dist/app.mjs +131 -27
  8. package/dist/{assets-BrKSg1O6.mjs → assets-CEMfBogh.mjs} +471 -120
  9. package/dist/assets.d.mts +30 -13
  10. package/dist/assets.mjs +1 -1
  11. package/dist/cli.d.mts +105 -8
  12. package/dist/cli.mjs +1036 -128
  13. package/dist/{commands-d5Zlh9hf.d.mts → commands-DAETmK86.d.mts} +43 -7
  14. package/dist/{config-CdARrrhU.mjs → config-B87aFTW-.mjs} +25 -4
  15. package/dist/control.d.mts +130 -17
  16. package/dist/control.mjs +4 -4
  17. package/dist/{define-BN15lAWb.d.mts → define-BASd9ZUh.d.mts} +32 -4
  18. package/dist/dev-DbHgx-R-.mjs +89 -0
  19. package/dist/{fake-MpzwZhIZ.mjs → fake-6mazmHK3.mjs} +20 -2
  20. package/dist/features-DowEilZm.mjs +266 -0
  21. package/dist/{headless-CrhefcLu.d.mts → headless-cFveciuY.d.mts} +1 -1
  22. package/dist/hot.d.mts +4 -2
  23. package/dist/hot.mjs +7 -4
  24. package/dist/{index-0GIP3tgm.d.mts → index-DedbED3T.d.mts} +317 -69
  25. package/dist/index.d.mts +5 -6
  26. package/dist/index.mjs +3 -3
  27. package/dist/inspect.d.mts +12 -7
  28. package/dist/inspect.mjs +2 -2
  29. package/dist/jsx-dev-runtime.d.mts +1 -1
  30. package/dist/jsx-runtime.d.mts +1 -1
  31. package/dist/lint.d.mts +8 -3
  32. package/dist/lint.mjs +432 -20
  33. package/dist/project.d.mts +15 -1
  34. package/dist/project.mjs +1 -1
  35. package/dist/{run-Bf2hKyjZ.mjs → run-BbipFRc6.mjs} +250 -51
  36. package/dist/{runtime-aLkoeoGj.d.mts → runtime-DoSlRx8X.d.mts} +1 -1
  37. package/dist/{session-DgukhR_7.mjs → session-CeKgO2hy.mjs} +30 -2
  38. package/dist/{sources-Duzj6lhV.mjs → sources-Cdvb0rOS.mjs} +33 -6
  39. package/dist/{src-Do697seR.mjs → src-BEbJ_sSx.mjs} +29154 -11121
  40. package/dist/testing.d.mts +119 -14
  41. package/dist/testing.mjs +97 -7
  42. package/dist/{types-BZa187kz.d.mts → types-7GXcXxxv.d.mts} +196 -242
  43. package/dist/{types-BT9S_Lo2.d.mts → types-BGcda5Le.d.mts} +4464 -735
  44. package/dist/{types-yiHtzg1Y.d.mts → types-C73hbmxi.d.mts} +129 -17
  45. package/dist/{types-Bc9AvZVO.d.mts → types-DngFQJro.d.mts} +43 -5
  46. package/dist/{types-kNiV7iFj.d.mts → types-GHE4J5Uf.d.mts} +1027 -192
  47. package/dist/{visual-dMW8iqAE.mjs → visual-2m4wJL_z.mjs} +111 -7
  48. package/dist/visual-_XhoGeIC.d.mts +345 -0
  49. package/dist/visual.d.mts +1 -332
  50. package/dist/visual.mjs +130 -20
  51. package/dist/{watch-CWI_on4T.mjs → watch-BEdrp80i.mjs} +149 -30
  52. package/llms.txt +103 -51
  53. package/package.json +8 -6
  54. package/dist/dev-Cc50Dk7H.mjs +0 -47
  55. package/dist/memory-DR1ibF_s.d.mts +0 -50
  56. package/dist/types-DMoi447V.d.mts +0 -656
@@ -124,6 +124,20 @@ function system(definition) {
124
124
  return definition;
125
125
  }
126
126
  /**
127
+ * Binds `system` to one game's player and session, so `run` reads a typed snapshot. Type-only:
128
+ * the same function.
129
+ *
130
+ * @returns `{ system }` whose `run` reads the game's state types.
131
+ * @example
132
+ * ```ts
133
+ * const { system: gameSystem } = systemsFor<{ board: { cells: number[] } }, {}>();
134
+ * gameSystem({ name: "looks", phase: "animate", query: [Transform], run: (rows, { snapshot }) => void snapshot.player.board.cells });
135
+ * ```
136
+ */
137
+ function systemsFor() {
138
+ return { system };
139
+ }
140
+ /**
127
141
  * The layer a view is drawn in. Written only by the projection; a `view` that returns it is
128
142
  * logged and the value ignored.
129
143
  */
@@ -146,13 +160,48 @@ const Tree = /*#__PURE__*/ component("Tree", { node: {
146
160
  props: {},
147
161
  children: []
148
162
  } });
163
+ /**
164
+ * The moment of the frame on the game's clock: `clock.now()`, read by `world` once at the start of
165
+ * every frame (before the systems of `input`) and once when the world starts, headless too. It is
166
+ * the moment a node gets as `now`, so a fake clock makes it exact in tests. A system that shows
167
+ * time reads it and writes its component only when its picture changes; with `renderer.onDemand`
168
+ * that is when a frame is drawn. A write by a system is overwritten at the next frame. `0` only
169
+ * before the world started, and after `ecs.clear()` until the next frame. `"Now"` is an engine
170
+ * resource name: a game resource of that name is refused.
171
+ *
172
+ * @example
173
+ * ```ts
174
+ * // A toast shows the seconds left of its energy timer: it is written once a second, not every frame.
175
+ * const toastTime = system({
176
+ * name: "shared.toastTime",
177
+ * phase: "animate",
178
+ * query: [ToastTime],
179
+ * run: (rows, { world, res }) => {
180
+ * const { now } = res(Now); // 1_790_000_016_000: the fake clock of the test stands there
181
+ * for (const [entity, time] of rows) {
182
+ * const left = Math.max(0, time.until - now);
183
+ * if (Math.ceil(left / 1000) !== Math.ceil(time.left / 1000)) world.set(entity, ToastTime, { left });
184
+ * }
185
+ * }
186
+ * });
187
+ * app.world.ecs.resource(Now).now; // 1_790_000_016_000
188
+ * ```
189
+ */
190
+ const Now = /*#__PURE__*/ resource("Now", { now: 0 });
149
191
  //#endregion
150
192
  //#region src/plugins/renderer/components.ts
151
193
  /**
152
- * @file renderer plugin — the five components a game writes and the `sprite()` bundle helper.
153
- * Pure: no ctx, no state, no Pixi. Made with the `component()` helper of `world`, so nothing has
194
+ * @file renderer plugin — the components a game writes (the five of a view, the overlay marker and the HUD
195
+ * part), the engine's own overlay level, and the `sprite()` bundle helper. Pure: no ctx, no state, no Pixi. Made with the `component()` helper of `world`, so nothing has
154
196
  * to be registered.
155
197
  */
198
+ const overlayDefaults = {
199
+ dim: 0,
200
+ dimColor: 0,
201
+ cover: "none",
202
+ hud: "all",
203
+ fade: 250
204
+ };
156
205
  const displayDefaults = { object: void 0 };
157
206
  const shapeDefaults = {
158
207
  kind: "rect",
@@ -227,6 +276,36 @@ const Display = /*#__PURE__*/ component("Display", displayDefaults);
227
276
  */
228
277
  const Shape = /*#__PURE__*/ component("Shape", shapeDefaults);
229
278
  /**
279
+ * Marks the view an overlay is drawn from. Everything drawn before the root of that view is kept as one picture while
280
+ * the overlay is there, and drawn again in the same frame anything in it changes; `cover: "opaque"` draws nothing
281
+ * under it while it is fully shown. The engine draws its dim and fades the HUD parts it does not keep, with the root's
282
+ * own enter and exit motion (`fade` when it has none).
283
+ *
284
+ * @example
285
+ * ```tsx
286
+ * // The case file: opaque once it has faded in, the ward under it is not drawn until it starts to leave.
287
+ * <panel key="caseSheet" motion={sheetFade} components={[Overlay({ cover: "opaque" })]} />
288
+ * ```
289
+ */
290
+ const Overlay = /*#__PURE__*/ component("Overlay", overlayDefaults);
291
+ /**
292
+ * Tags a view of the HUD layer as a part an overlay may keep or let fade, by `key`.
293
+ *
294
+ * @example
295
+ * ```tsx
296
+ * <stack key="resEnergy" components={[HudPart({ key: "energy" })]} />
297
+ * ```
298
+ */
299
+ const HudPart = /*#__PURE__*/ component("HudPart", { key: "" });
300
+ /**
301
+ * The level and the landing of an overlay, on the entity that carries `Overlay`. Engine state: `ui` writes it, the
302
+ * renderer reads it; a game never writes it.
303
+ */
304
+ const OverlayLevel = /*#__PURE__*/ component("OverlayLevel", {
305
+ level: 0,
306
+ landed: false
307
+ });
308
+ /**
230
309
  * Bundles the two components every visual needs, so a projection `view` reads as one line.
231
310
  *
232
311
  * @param options - Texture key, position, and the four values that have defaults.
@@ -253,20 +332,24 @@ function sprite(options) {
253
332
  })];
254
333
  }
255
334
  /**
256
- * Binds `sprite`, `Sprite` and `NineSlice` to one game's asset keys. Type-only: the same objects.
335
+ * Binds `sprite`, `Sprite` and `NineSlice` to one game's asset keys, and `Overlay` and `HudPart` to its HUD part keys.
336
+ * Type-only: the same objects.
257
337
  *
258
- * @returns The three helpers with `texture` narrowed to `Asset`.
338
+ * @returns The five helpers with `texture` narrowed to `Asset` and the HUD keys to `HudKey`.
259
339
  * @example
260
340
  * ```ts
261
- * const { Sprite } = componentsFor<"board.cell" | "board.item-wood-1">();
341
+ * const { Sprite, HudPart } = componentsFor<"board.cell" | "board.item-wood-1", "energy">();
262
342
  * Sprite({ texture: "board.cell" }).value.texture; // "board.cell"
343
+ * HudPart({ key: "energy" }).value.key; // "energy"
263
344
  * ```
264
345
  */
265
346
  function componentsFor() {
266
347
  return {
267
348
  sprite,
268
349
  Sprite,
269
- NineSlice
350
+ NineSlice,
351
+ Overlay,
352
+ HudPart
270
353
  };
271
354
  }
272
355
  /**
@@ -790,5 +873,26 @@ function visualRectOf(element, lookup) {
790
873
  function samePose(first, second) {
791
874
  return first.x === second.x && first.y === second.y && first.rotation === second.rotation && first.scale === second.scale && first.pivot.x === second.pivot.x && first.pivot.y === second.pivot.y;
792
875
  }
876
+ /**
877
+ * Where a local box of a view lands in root space: moved by the pose, scaled around its pivot.
878
+ * A guide hole is axis-aligned, so the rotation is left out.
879
+ *
880
+ * @param pose - The root pose of the view, `rootPoseOf` of `renderer`.
881
+ * @param local - The box in the view's local units.
882
+ * @returns The rect in root coordinates.
883
+ * @example
884
+ * ```ts
885
+ * drawnRect({ x: 200, y: 200, rotation: 0, scale: 0.8, pivot: { x: 250, y: 250 } },
886
+ * { x: 50, y: 50, w: 100, h: 100 }); // { x: 40, y: 40, w: 80, h: 80 }
887
+ * ```
888
+ */
889
+ function drawnRect(pose, local) {
890
+ return {
891
+ x: pose.x + pose.scale * (local.x - pose.pivot.x),
892
+ y: pose.y + pose.scale * (local.y - pose.pivot.y),
893
+ w: local.w * pose.scale,
894
+ h: local.h * pose.scale
895
+ };
896
+ }
793
897
  //#endregion
794
- export { system as A, Exiting as C, component as D, Tree as E, mut as O, sprite as S, Order as T, Parent as _, visualOf as a, Transform as b, Text as c, defineTextStyles as d, label as f, NineSlice as g, Display as h, scaleByFits as i, tag as j, resource as k, bind as l, textFor as m, fitScaleOf as n, visualRectOf as o, readStyle as p, samePose as r, Countdown as s, CONTENT as t, builtInStyles as u, Shape as v, Layer as w, componentsFor as x, Sprite as y };
898
+ export { Order as A, Sprite as C, Exiting as D, sprite as E, system as F, systemsFor as I, tag as L, component as M, mut as N, Layer as O, resource as P, Shape as S, componentsFor as T, HudPart as _, scaleByFits as a, OverlayLevel as b, Countdown as c, builtInStyles as d, defineTextStyles as f, Display as g, textFor as h, samePose as i, Tree as j, Now as k, Text as l, readStyle as m, drawnRect as n, visualOf as o, label as p, fitScaleOf as r, visualRectOf as s, CONTENT as t, bind as u, NineSlice as v, Transform as w, Parent as x, Overlay as y };
@@ -0,0 +1,345 @@
1
+ import { T as Api, sr as Api$2, wr as Api$3, zt as Api$1 } from "./types-BGcda5Le.mjs";
2
+ import { P as AnimApi, r as UiApi } from "./types-GHE4J5Uf.mjs";
3
+ import { j as InputApi } from "./types-7GXcXxxv.mjs";
4
+ import { r as Repro } from "./headless-cFveciuY.mjs";
5
+ import { a as InputOf, r as ControlApp } from "./types-DngFQJro.mjs";
6
+ import { t as commands } from "./commands-DAETmK86.mjs";
7
+
8
+ //#region src/visual/types.d.ts
9
+ /** The base commands of the `/control` door, keyed by their short names. */
10
+ type Catalogue = typeof commands;
11
+ /**
12
+ * The commands a step may name: the catalogue of `/control` minus `bookmark` (it only reads),
13
+ * `capture` (the runner captures by itself) and `debug` (debug drawing would enter a baseline). A
14
+ * command the catalogue gains is a step with no change to the runner.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const name: StepCommand = "tap"; // also "answer", "walk", "step", "reducedMotion", ...
19
+ * ```
20
+ */
21
+ type StepCommand = Exclude<keyof Catalogue, "bookmark" | "capture" | "debug">;
22
+ /**
23
+ * Where a visual test starts: what `game.restore` loads, a repro without its route. The player
24
+ * is required; the session, the rng and the checkpoint are optional, as in a repro.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * // A save with a plank ready for the first order, entered at Home.
29
+ * const start: VisualStart = { player: { coins: 40, orders: [] }, checkpoint: "home" };
30
+ * ```
31
+ */
32
+ type VisualStart = Omit<Repro, "route">;
33
+ /**
34
+ * One step of a visual test: a `/control` command by its short name with that command's input,
35
+ * typed from the catalogue, or a checkpoint by its name.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * // Tap Play, answer the order on the board, then compare the screen with its baseline.
40
+ * const steps: VisualStep[] = [
41
+ * { tap: { key: "play" } },
42
+ * { answer: { intent: "deliver", payload: { orderId: "o1" } } },
43
+ * { checkpoint: "open" }
44
+ * ];
45
+ * ```
46
+ */
47
+ type VisualStep = {
48
+ checkpoint: string;
49
+ } | { [K in StepCommand]: { [P in K]: InputOf<Catalogue[K]["input"]> } }[StepCommand];
50
+ /**
51
+ * A visual test as `defineVisualTest` answers it: its name, where it starts, its steps, and
52
+ * `webgl`, true when it also runs in the WebGL leg. Frozen data; nothing runs at definition.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * const test: VisualTest = defineVisualTest("reward-popup", {
57
+ * start: { player: { coins: 40 }, checkpoint: "home" },
58
+ * steps: [{ tap: { key: "play" } }, { checkpoint: "board" }]
59
+ * });
60
+ * test.name; // "reward-popup"
61
+ * test.webgl; // false
62
+ * Object.isFrozen(test.steps); // true
63
+ * ```
64
+ */
65
+ type VisualTest = {
66
+ readonly name: string;
67
+ readonly start: Readonly<VisualStart>;
68
+ readonly steps: readonly VisualStep[];
69
+ readonly webgl: boolean;
70
+ };
71
+ /**
72
+ * What the runner needs of an app: the control app, the plugins the base commands act on and
73
+ * the plugins the readers read. Every app composed with the screen set fits.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * const app: VisualApp = createApp({ plugins: [...screen, boardFeature], pluginConfigs });
78
+ * ```
79
+ */
80
+ type VisualApp = ControlApp & {
81
+ readonly anim: AnimApi;
82
+ readonly ui: UiApi;
83
+ readonly renderer: Api;
84
+ readonly world: Api$1;
85
+ readonly model: Api$2;
86
+ readonly input: InputApi;
87
+ readonly lifecycle: Api$3;
88
+ };
89
+ /**
90
+ * The dev page the browser leg opens: its URL, the viewport width in CSS px (390 by default), the
91
+ * device scale (2 by default) and which Chrome to launch. The page height follows the inert frame
92
+ * of the renderer: 520 for a 390 wide portrait game at the default 4:3. Without `browser` the leg
93
+ * launches Playwright's own Chromium, and the system Chrome when that one is not installed.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * // The fixture's dev server, a 390 px wide phone at 2x, the system Chrome.
98
+ * const page: VisualPage = { url: "http://localhost:3000/", browser: { channel: "chrome" } };
99
+ * ```
100
+ */
101
+ type VisualPage = {
102
+ url: string;
103
+ width?: number;
104
+ deviceScaleFactor?: number;
105
+ browser?: {
106
+ channel?: string;
107
+ executablePath?: string;
108
+ };
109
+ };
110
+ /**
111
+ * How the runner builds a game: a fresh app per test, not started, with the screen set
112
+ * composed; the renderer stays inert in Bun. `page` is the dev page of the pixel leg.
113
+ *
114
+ * @example
115
+ * ```ts
116
+ * // tests/visual/run.ts of a game.
117
+ * const setup: VisualSetup = { app: () => createScreenGame().app, page: { url: "http://localhost:3000/" } };
118
+ * ```
119
+ */
120
+ type VisualSetup = {
121
+ app: () => VisualApp;
122
+ page?: VisualPage;
123
+ };
124
+ /**
125
+ * How far a pixel may differ: `threshold` is the largest channel delta that still counts as the
126
+ * same pixel, `ratio` the share of differing pixels a checkpoint tolerates.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * const tolerance: VisualTolerance = { ratio: 0.001, threshold: 24 }; // the defaults
131
+ * ```
132
+ */
133
+ type VisualTolerance = {
134
+ ratio: number;
135
+ threshold: number;
136
+ };
137
+ /**
138
+ * The backend the browser leg draws with: `"webgpu"`, or `"webgl"` for the WebGL leg.
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * const renderer: VisualRenderer = "webgl"; // what --webgl asks for
143
+ * ```
144
+ */
145
+ type VisualRenderer = "webgpu" | "webgl";
146
+ /**
147
+ * The options of a run. Each one left out comes from the command line (`argv`), then from its
148
+ * default: `dir` `"tests/visual"`, `update` false, `pixels` true with a `page` on a Mac,
149
+ * `renderer` `"webgpu"`, `settleFrames` 600, `tolerance` `{ ratio: 0.001, threshold: 24 }`,
150
+ * `argv` `process.argv.slice(2)`. An option given here wins over a flag. `renderer: "webgl"`
151
+ * runs only the tests with `webgl: true`, on the page with `?renderer=webgl`, and compares their
152
+ * pictures with `screen.webgl.webp`.
153
+ *
154
+ * @example
155
+ * ```ts
156
+ * // The same as `bun tests/visual/run.ts --only reward-popup --no-pixels`.
157
+ * const options: VisualOptions = { only: ["reward-popup"], pixels: false };
158
+ * // The same as `bun tests/visual/run.ts --webgl`.
159
+ * const webgl: VisualOptions = { renderer: "webgl" };
160
+ * // The same as `bun tests/visual/run.ts --no-verify-frames`: skipped frames are not checked.
161
+ * const unchecked: VisualOptions = { verifyFrames: false };
162
+ * ```
163
+ */
164
+ type VisualOptions = {
165
+ dir?: string;
166
+ update?: boolean;
167
+ pixels?: boolean;
168
+ /**
169
+ * The browser leg turns on `debug.verifyFrames` and fails a test on the first
170
+ * `renderer:missed-frame`, `renderer:missed-mark` or `renderer:stale-picture` of the page, and on
171
+ * the first `effects:broken`, `renderer:warm-failed` or `renderer: pixel read failed`, also one
172
+ * logged while the page loaded; `true` by default, `false` with `--no-verify-frames`.
173
+ */
174
+ verifyFrames?: boolean;
175
+ renderer?: VisualRenderer;
176
+ only?: readonly string[];
177
+ settleFrames?: number;
178
+ tolerance?: VisualTolerance;
179
+ argv?: readonly string[];
180
+ };
181
+ /**
182
+ * What a comparison with a baseline file found: the same content, a different one, or no
183
+ * baseline (or `--update`), so the file was written.
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * const outcome: Outcome = "written"; // the first run of a new checkpoint
188
+ * ```
189
+ */
190
+ type Outcome = "same" | "different" | "written";
191
+ /**
192
+ * The outcome of one checkpoint. `first` is the JSON path of the first difference, the state's
193
+ * before the describe's. `pixels` is `"skipped"` when the pixel leg did not run; `pixelRatio`
194
+ * and `verdict` come with a pixel difference.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * // The player's coins differ from the baseline; the layout is the same.
199
+ * const result: CheckpointResult = {
200
+ * name: "claimed", state: "different", describe: "same", pixels: "skipped", first: "player.coins"
201
+ * };
202
+ * ```
203
+ */
204
+ type CheckpointResult = {
205
+ name: string;
206
+ state: Outcome;
207
+ describe: Outcome;
208
+ pixels: Outcome | "skipped";
209
+ pixelRatio?: number;
210
+ first?: string;
211
+ verdict?: "rendering" | "behaviour";
212
+ };
213
+ /**
214
+ * The outcome of one visual test: every checkpoint it reached, and the error that ended it early.
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * const result: VisualTestResult = {
219
+ * name: "reward-popup",
220
+ * checkpoints: [{ name: "open", state: "same", describe: "same", pixels: "skipped" }]
221
+ * };
222
+ * ```
223
+ */
224
+ type VisualTestResult = {
225
+ name: string;
226
+ checkpoints: readonly CheckpointResult[];
227
+ error?: string;
228
+ };
229
+ /**
230
+ * What `runVisualTests` answers: `ok` is true when no test failed and no outcome is
231
+ * `"different"`.
232
+ *
233
+ * @example
234
+ * ```ts
235
+ * const report: VisualReport = await runVisualTests(setup, [rewardPopup], { argv: [] });
236
+ * process.exitCode = report.ok ? 0 : 1;
237
+ * ```
238
+ */
239
+ type VisualReport = {
240
+ ok: boolean;
241
+ tests: readonly VisualTestResult[];
242
+ };
243
+ /** What the command line asked for: the flags it carried, nothing else. */
244
+ type VisualFlags = {
245
+ dir?: string;
246
+ update?: boolean;
247
+ pixels?: boolean;
248
+ verifyFrames?: boolean;
249
+ renderer?: VisualRenderer;
250
+ only?: readonly string[];
251
+ };
252
+ //#endregion
253
+ //#region src/visual/define.d.ts
254
+ /**
255
+ * Defines a visual test: where it starts, what `game.restore` loads, and its steps. A step is a
256
+ * `/control` command by its short name with that command's input, or `{ checkpoint: name }`,
257
+ * where the runner saves `state.json` and `describe.json` next to the test. `walk` runs fast: a
258
+ * popup its last node opens never mounts, so the step before a checkpoint that shows a node's
259
+ * result is `answer` or `tap`. With `webgl: true` the test also runs in the WebGL leg
260
+ * (`--webgl`), whose pictures go to `screen.webgl.webp`: set it on a screen with a custom filter.
261
+ * The answer is a frozen copy; nothing runs here.
262
+ *
263
+ * @param name - The test name, also its folder: lowercase letters, digits and dashes.
264
+ * @param test - Where it starts, its steps and its WebGL leg.
265
+ * @param test.start - The player, and optionally the session, the rng and the checkpoint.
266
+ * @param test.steps - The commands and the checkpoints, in order.
267
+ * @param test.webgl - True to run the test in the WebGL leg too; false by default.
268
+ * @returns The test, frozen.
269
+ * @throws {Error} When the name or a checkpoint name is not a file name, a checkpoint name is
270
+ * used twice, or a step is not one step command.
271
+ * @example
272
+ * ```ts
273
+ * // tests/visual/home.visual.ts: Home with the glowing Play plank, in both legs.
274
+ * const home = defineVisualTest("home", { start: atHome, steps: [{ checkpoint: "rest" }], webgl: true });
275
+ * home.webgl; // true
276
+ * ```
277
+ */
278
+ declare function defineVisualTest(name: string, test: {
279
+ start: VisualStart;
280
+ steps: readonly VisualStep[];
281
+ webgl?: boolean;
282
+ }): VisualTest;
283
+ //#endregion
284
+ //#region src/visual/run.d.ts
285
+ /**
286
+ * Reads the flags of the visual tests from a command line: `--update`, `--no-pixels`, `--pixels`,
287
+ * `--webgl`, `--no-verify-frames`, `--only <name>` (repeatable) and `--dir <path>`. `--pixels`
288
+ * forces the pixel leg on off a Mac; the last of `--pixels` and `--no-pixels` wins.
289
+ * `--no-verify-frames` keeps the browser leg from checking the frames the renderer skips. An argument it does not know is left
290
+ * to the script that got it, so a test runner's own arguments pass through. `moku-game visual`
291
+ * reads its flags with it.
292
+ *
293
+ * @param argv - The arguments after the script name.
294
+ * @returns The flags that were given, and nothing else.
295
+ * @throws {Error} When `--only` or `--dir` has no value.
296
+ * @example
297
+ * ```ts
298
+ * // `bun tests/visual/run.ts --update --only reward-popup`
299
+ * parseVisualArgv(["--update", "--only", "reward-popup"]); // { update: true, only: ["reward-popup"] }
300
+ * parseVisualArgv(["--no-pixels", "--dir", "shots"]); // { pixels: false, dir: "shots" }
301
+ * parseVisualArgv(["--webgl"]); // { renderer: "webgl" }
302
+ * parseVisualArgv(["--pixels"]); // { pixels: true }
303
+ * parseVisualArgv(["--no-verify-frames"]); // { verifyFrames: false }
304
+ * ```
305
+ */
306
+ declare function parseVisualArgv(argv: readonly string[]): VisualFlags;
307
+ /**
308
+ * Runs visual tests: the headless leg over every selected test, then the browser leg over the
309
+ * same tests when `pixels` is on (by default only with a `page` on a Mac). With `renderer:
310
+ * "webgl"` (`--webgl`) only the tests with `webgl: true` run, the page opens with
311
+ * `?renderer=webgl` and its pictures go to `screen.webgl.webp`; `state.json` and `describe.json`
312
+ * are shared with the WebGPU leg and compared as always. A checkpoint saves
313
+ * `state.json` and `describe.json` in `<dir>/<test>/<checkpoint>/`, and the browser leg
314
+ * `screen.webp`, lossless: a missing file is written, `--update` rewrites every file of the tests
315
+ * run, a JSON file is compared exactly. The browser leg opens the dev page in Chrome with WebGPU
316
+ * through `playwright-core`, pauses its game, plays each test there on stepped frames of 1000/60
317
+ * ms, so two runs see the same game time, checks the page's state against `state.json` and
318
+ * compares the picture in the page: a pixel differs above `tolerance.threshold`, and the screen
319
+ * differs when more than `tolerance.ratio` of its pixels do; `screen.actual.webp` and
320
+ * `screen.diff.webp` are written beside the baseline then. A pixel difference over the same state
321
+ * and describe is a rendering regression (`verdict: "rendering"`). One line per checkpoint goes
322
+ * to the branded console; the script sets the exit code from `ok`.
323
+ *
324
+ * @param setup - How to build a game: a fresh, unstarted app per test, and the dev page.
325
+ * @param tests - Every visual test of the game.
326
+ * @param options - Overrides of the flags of `argv` and of the defaults.
327
+ * @returns The report: `ok` when no test failed and nothing is `"different"`.
328
+ * @throws {Error} When a test is not well formed, two tests share a name, `only` names no test,
329
+ * a flag misses its value, or the browser leg fails as a whole: no `page`, no
330
+ * `playwright-core`, no Chrome, or a page that does not open, expose `game` and `doors`, start,
331
+ * draw with the requested backend or stop loading. In the WebGL leg, also when `only` names a
332
+ * test without `webgl: true`.
333
+ * @example
334
+ * ```ts
335
+ * // tests/visual/run.ts of a game, `bun tests/visual/run.ts --update` on a Mac
336
+ * const report = await runVisualTests(
337
+ * { app: () => createScreenGame().app, page: { url: "http://localhost:3000/" } },
338
+ * [rewardPopup]
339
+ * );
340
+ * report.tests[0]?.checkpoints[0]; // { name: "open", state: "written", describe: "written", pixels: "written" }
341
+ * ```
342
+ */
343
+ declare function runVisualTests(setup: VisualSetup, tests: readonly VisualTest[], options?: VisualOptions): Promise<VisualReport>;
344
+ //#endregion
345
+ export { VisualApp as a, VisualRenderer as c, VisualStart as d, VisualStep as f, VisualTolerance as h, CheckpointResult as i, VisualReport as l, VisualTestResult as m, runVisualTests as n, VisualOptions as o, VisualTest as p, defineVisualTest as r, VisualPage as s, parseVisualArgv as t, VisualSetup as u };