import { type EventsObservation } from './events.js'; import type { MobileParityGap } from './mobile-parity.js'; import type { VerifyPlatform } from './platform.js'; export interface CanvasObservation { /** CSS pixel size from getBoundingClientRect() — whether the canvas is visible/laid out. */ width: number; height: number; /** * canvas.width / canvas.height — the drawing-buffer *attributes*, not CSS size. A `` * nobody has touched keeps the HTML defaults (300x150); a renderer that actually claims the * canvas resizes these to the real framebuffer dimensions. This is deliberately not a * getContext() probe: getContext() itself creates a context — even 'webgpu' on a page with no * navigator.gpu — whether or not anything ever renders into it, which is why that used to be * checked and always passed. */ bufferWidth: number; bufferHeight: number; } /** What the canvas area of the captured screenshot contains. */ export interface CanvasFrameObservation { /** Fraction of sampled pixels that are not black. */ litFraction: number; /** * How many distinct colours the sampled pixels fall into, quantised to 5 bits per channel. * * The load-bearing number, and the reason this is not just a brightness check: a canvas that * rendered nothing composites as a FLAT area — black on a black page, but whatever the page's * background happens to be otherwise. Flatness catches that in every case; darkness only catches * it when the backdrop is black. A real rendered frame from a 3D scene has thousands. */ distinctColors: number; } /** Which renderer the run asked for and what actually drove the canvas. */ export interface RendererObservation { /** The `?renderer=` value verify booted the game with. */ requested: 'webgpu' | 'webgl'; /** * `engine.getRendererType()` — which renderer CLASS the engine constructed (⇒ TSL vs GLSL * material pipeline). Null when the engine or the getter could not be reached (older engine, * probe failed): "not observed", never a failure by itself. */ rendererType: string | null; /** * `engine.getActiveBackend()` — what is actually driving the canvas. The load-bearing one: * a WebGPURenderer whose adapter never came up silently runs `'webgl2'`, and only this getter * can tell. Null means "not observed", same contract as `rendererType`. */ backend: string | null; } /** * A snapshot of live engine state at the end of the run, read through `window.gameTemplate`. * * Every field is null when it could not be observed (older engine, probe failure) and classifies * as "not observed", never as a failure — same contract as `RendererObservation`. What this buys * over the console log: the console only says what the engine chose to print; this says what the * running game actually IS. */ export interface StateSnapshot { /** `gameStateManager.getCurrentState()` — lowercase engine values ('playing', 'menu', …). */ gameState: string | null; hasStarted: boolean | null; /** Engine clock seconds since boot. */ elapsedTimeSeconds: number | null; /** Rendered frames per second across the final settle polls — page rAF rate, so it proves the * page's loop is alive rather than measuring render quality. */ fps: number | null; player: { x: number; y: number; z: number; isFalling: boolean; } | null; physics: { rigidBodyCount: number; colliderCount: number; } | null; scene: { objects: number; meshes: number; lights: number; } | null; npcCount: number | null; } /** * What the engine's MobileControls actually rendered, counted by the engine's own class names. * * Only taken on a phone-emulated run — on desktop MobileControls is never built, so zero there * is the correct state rather than a finding. Null means the probe could not be taken. */ export interface TouchControlsObservation { joysticks: number; buttons: number; /** * The engine's own answer to whether this game has a player for a joystick to drive, read at * the end of the run. False is a POINTER-DRIVEN game — a board game, a city builder, a * strategy game played by tapping the scene — in either of its two shapes: the engine has no * player controller at all (the `no-character` template), or its controller reports * `getMobileMovementControlsAvailable() === false` (a headless player declared with * `hasPlayerCharacter: false`), the same predicate the joystick itself obeys. For such a game * no touch controls is the declared state. Null (or absent, on records written before the * field existed) means the controller predates the getter or the probe failed: "not * observed", which classifies exactly as it did before the engine could say. */ movementControlsAvailable?: boolean | null; } /** How the settle ended: early because the game was provably stable, or at the `--timeout` cap. */ export interface SettleObservation { durationMs: number; reason: 'stable' | 'timeout'; } /** What the start interaction (waiting for and clicking the engine Play button) observed. */ export interface StartObservations { playButtonSeen: boolean; playButtonClicked: boolean; /** Pre-play selection steps (level chooser, difficulty, …) the run clicked through before * looking for Play. */ selectionsPicked: number; /** The engine's game-start console line appeared at any point during the run. */ gameStartDetected: boolean; /** The start menu overlay was still displayed after settling. */ menuVisibleAfterSettle: boolean; } export interface VerifyObservations { /** Console lines, each prefixed with its type, e.g. `error: …`. */ consoleLines: string[]; pageErrors: string[]; canvas: CanvasObservation | null; /** Byte length of the PNG screenshot taken after settling, or null when the run was asked not * to capture one (`--fast`) — "not observed", never "blank". */ screenshotBytes: number | null; /** * The page fired `webglcontextlost`. The renderer is dead from that moment: rAF keeps firing, * the engine logs its usual lines, the DOM HUD still paints, and nothing else gives it away. */ webglContextLost: boolean; /** * What the captured screenshot actually contains INSIDE the canvas rect, or null when it could * not be measured (no canvas, decode failed). * * Neither the console nor the page can answer "did a frame actually come out": what matters is * what the COMPOSITOR captured, because that image is also the game's published thumbnail when * the project has no cover art. See `BLACK_FRAME_MAX_LIT_FRACTION` and `MIN_DISTINCT_COLORS`. */ canvasFrame: CanvasFrameObservation | null; /** Only present when the run was asked to perform the start interaction (`bitmagic verify`); * `publish`'s smoke check never sets it and classifies exactly as before. */ start?: StartObservations; /** Only present when the run was told which renderer it booted (`bitmagic verify` passes its * `--renderer` choice); `publish`'s smoke check never sets it and classifies exactly as before. */ renderer?: RendererObservation; /** End-of-run engine state. Present on `bitmagic verify` runs against a reachable page; null * when the probe failed outright, absent on the smoke check. */ snapshot?: StateSnapshot | null; /** How the settle ended. Only present when the run settled adaptively (`bitmagic verify`). */ settle?: SettleObservation; /** The semantic gameplay-event trace (`?eventlog=1` + `window.__bmDebug`). Null when the * engine predates the namespace ("not observed"), absent on the smoke check. */ events?: EventsObservation | null; /** Touch affordances counted on a phone-emulated run. Absent on a desktop run, where the * engine never builds MobileControls; null when the probe could not be taken. */ touchControls?: TouchControlsObservation | null; /** The context viewport this run used, in CSS pixels. The screenshot-size floor is a function * of how many pixels the frame has; absent keeps the desktop-calibrated constant. */ viewport?: { width: number; height: number; }; /** The engine's mobile-parity verdict, read from the console it logs on every game start. * Null when it reported no gap — a clean game, or a genre without the check. */ mobileParity?: MobileParityGap | null; } /** * What the run was, as far as the classifier needs to know. * * Optional in full so `publish`'s smoke check keeps calling `classifyVerifyRun` with one argument * and classifying exactly as it always has. */ export interface ClassifyContext { /** Which platform this run booted. */ platform?: VerifyPlatform; /** `game.json` declares `primaryPlatform: "mobile"`, which escalates a parity gap to a failure. */ mobilePrimary?: boolean; } export interface VerifyResult { ok: boolean; failures: string[]; warnings: string[]; } /** * The engine's line when the player has fallen out of the world and is being put back. * * A game the player cannot stand up in is not verified, however cleanly it renders. This was not * theoretical: a forged level whose terrain never loaded rendered a perfect frame, threw nothing, * and reported PASSED, while the player fell and respawned 14 times in a 45s settle. */ export declare const RESPAWN_LINE = "Fell below -100"; export declare function classifyVerifyRun(observations: VerifyObservations, context?: ClassifyContext): VerifyResult;