/*

Test State: a live readout of the running game, plus a clock you can rewind.
@description Show the test-state snapshot fields, then reset ticks and watch the clock drop.

Part of the BLIT386 demo series.

Prerequisites:
  - Basics: https://demos.blit386.dev/basics
  - Animation: https://demos.blit386.dev/animation

Live version: https://demos.blit386.dev/test-state

Guides:
  - Game loop: https://blit386.dev/docs/guides/game-loop#seeking-and-stepping

Reference:
  - Core: https://blit386.dev/docs/api/core#test-state-for-agent-play-testing
  - Game loop: https://blit386.dev/docs/api/game-loop

---

WHAT YOU WILL SEE
-----------------

Four UI kit panels in two columns. On the left, "Clock" prints BT.ticks and
BT.timeSeconds, and "Snapshot" under it prints the fields of one BT.testState() call. On
the right, "BT.ticks, drawn" shows an amber square circling inside a hollow square frame,
with a small green dot marking its home position on the frame's right side, and "Try it"
under it holds the controls. The square's place on that circle is computed from the
engine clock (BT.ticks), so it is a picture of the tick counter.

WHAT YOU WILL LEARN
-------------------

- BT.testState() returns a TestStateSnapshot: ticks, backend, state, and, only on a failed
  read, error. It is how a script or an agent reads the game as JSON.
- The optional testState() method on the demo class is the game's half of that snapshot.
  The engine never calls it on its own.
- The snapshot is a deep copy made through JSON.stringify and JSON.parse. A Vector2i
  comes back as a plain { x, y }, and changing the copy cannot move the real square.
- BT.ticksReset() sets BT.ticks (and BT.timeSeconds) back to zero. Game fields such as
  the score stay where they were. The square jumps home because its position is computed
  from the clock, not stored.
- BT.testState() never throws. A throwing hook logs once and comes back as state null
  with the message in error.

TRY THIS
--------

- Press A (or tap "A Add point") and watch score climb in the snapshot.
- Press R (or tap "R Reset ticks"). The tick numbers drop to zero and the square snaps
  back to the right side of the frame. The score does not change.
- Press F (or tap "F Fail hook") to make testState() throw. The snapshot then shows
  state null and a short error, and the panel holds that one failed copy so the console is
  not flooded. Press F again to resume live reads.

*/

import { function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
,
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
, class Color32
Mutable 32-bit RGBA color value with 8-bit channels.
@since0.1.0
Color32
, class Rect2i
Integer rectangle for pixel-perfect bounds and regions. Used throughout the engine for sprite regions, display-space bounds, and zero-allocation geometry helpers. Both convenience getters and allocation-free `*To()` helpers are provided so callers can choose between readability and hot-path efficiency.
@since0.1.0
Rect2i
, class Vector2i
Integer 2D vector for pixel-perfect positioning. Used for points, sizes, directions, and camera offsets throughout the engine. The API includes both allocation-free `*To()` / `*InPlace()` variants and convenience methods that return new vectors.
@since0.1.0
Vector2i
} from 'blit386';
import { import applyThemeapplyTheme, import THEME_OVERLAY_STYLETHEME_OVERLAY_STYLE, import uiui } from './shared/ui.js'; /** @typedef {import('blit386').TestStateSnapshot} TestStateSnapshot */ // Scene colors live in low palette slots. The shared UI kit owns slots 240-251 (see // applyTheme in init()), so these never collide with panel colors. const const C_BG: 1C_BG = 1; // near-black blue: the page behind the ring const const C_RING: 2C_RING = 2; // dim slate: the box the square orbits inside const const C_HOME: 3C_HOME = 3; // quiet green: the spot the square sits on at tick 0 const const C_MARKER: 4C_MARKER = 4; // bright amber: the square itself // Screen layout: two columns, MARGIN pixels in from the screen edges and PANEL_GAP apart. // The left column holds the clock with the snapshot under it; the right column holds the // orbit, drawn inside its own panel, with the controls under it. In each column the two // panels together fill the height, so both columns start and end on the same rows. const const DISPLAY_W: 320DISPLAY_W = 320; const const DISPLAY_H: 240DISPLAY_H = 240; const const MARGIN: 8MARGIN = 8; const const PANEL_GAP: 6PANEL_GAP = 6; const const LEFT_W: 150LEFT_W = 150; const const RIGHT_X: numberRIGHT_X = const MARGIN: 8MARGIN + const LEFT_W: 150LEFT_W + const PANEL_GAP: 6PANEL_GAP; const const RIGHT_W: numberRIGHT_W = const DISPLAY_W: 320DISPLAY_W - const MARGIN: 8MARGIN - const RIGHT_X: numberRIGHT_X; const const CLOCK_H: 77CLOCK_H = 77; const const SNAPSHOT_Y: numberSNAPSHOT_Y = const MARGIN: 8MARGIN + const CLOCK_H: 77CLOCK_H + const PANEL_GAP: 6PANEL_GAP; const const CONTROLS_H: 97CONTROLS_H = 97; const const CONTROLS_Y: numberCONTROLS_Y = const DISPLAY_H: 240DISPLAY_H - const MARGIN: 8MARGIN - const CONTROLS_H: 97CONTROLS_H; const const ORBIT_PANEL_H: numberORBIT_PANEL_H = const CONTROLS_Y: numberCONTROLS_Y - const PANEL_GAP: 6PANEL_GAP - const MARGIN: 8MARGIN; // Widgets inside a panel are 20 pixels narrower than the panel itself; the first row // starts 22 pixels down, under the title, and the panel keeps 11 pixels below its last. const const PANEL_CONTENT_SHRINK: 20PANEL_CONTENT_SHRINK = 20; const const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP = 22; const const PANEL_BOTTOM_PAD: 11PANEL_BOTTOM_PAD = 11; // The snapshot panel's rows fall short of the column's height, so this much empty space // before its closing note makes it end on the same row as the controls. A failed read // shows one line where a healthy one shows three, so it gets two rows' worth more. const const SNAPSHOT_FILLER: 14SNAPSHOT_FILLER = 14; const const ROW_H: 10ROW_H = 10; // The ring sits in the middle of the orbit panel's content area. const const ORBIT_CENTER_X: anyORBIT_CENTER_X = const RIGHT_X: numberRIGHT_X + Math.floor(const RIGHT_W: numberRIGHT_W / 2); const const ORBIT_CENTER_Y: anyORBIT_CENTER_Y = const MARGIN: 8MARGIN + const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP + Math.floor((const ORBIT_PANEL_H: numberORBIT_PANEL_H - const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP - const PANEL_BOTTOM_PAD: 11PANEL_BOTTOM_PAD) / 2); const const ORBIT_RADIUS: 34ORBIT_RADIUS = 34; // Tick 0 puts the square on the right edge of the ring (cos 0 is 1, sin 0 is 0). That dot // stays put so a reset has an obvious home to snap back to. const const HOME_X: anyHOME_X = const ORBIT_CENTER_X: anyORBIT_CENTER_X + const ORBIT_RADIUS: 34ORBIT_RADIUS; const const HOME_Y: anyHOME_Y = const ORBIT_CENTER_Y: anyORBIT_CENTER_Y; // Side length in pixels of the square and of the home dot. const const MARKER_SIZE: 7MARKER_SIZE = 7; const const HOME_SIZE: 3HOME_SIZE = 3; // Math.cos and Math.sin take an angle in radians, and a full circle is about 6.28 of // those. Dividing the tick count by this number makes one lap take about 6.28 * 16 = 100 // ticks, a bit over a second and a half at 60 updates per second. const const ORBIT_TICKS_PER_RADIAN: 16ORBIT_TICKS_PER_RADIAN = 16; // Shows BT.testState() and what BT.ticksReset() does to the clock. class class DemoDemo { // Points the player has added. This is game state, not the engine clock, so a tick // reset must leave it alone. Demo.score: numberscore = 0; // True while the readout is deliberately broken, so the error field has something to // show. Demo.failReadout: booleanfailReadout = false; // The failed snapshot, kept on screen until the hook is healthy again. Calling // BT.testState() every frame while the hook throws would log a fresh error sixty // times a second. /** @type {TestStateSnapshot | null} */ Demo.frozenSnapshot: TestStateSnapshot | null
@type{TestStateSnapshot | null}
frozenSnapshot
= null;
// Where the square is this frame, in whole pixels. testState() hands this object to // the engine, which copies it into plain JSON. Demo.marker: Vector2imarker = new new Vector2i(x?: number, y?: number): Vector2i
Creates an integer 2D vector, truncating inputs toward zero.
@paramx - Horizontal component (defaults to 0).@paramy - Vertical component (defaults to 0).
Vector2i
(const HOME_X: anyHOME_X, const HOME_Y: anyHOME_Y);
// Reused rectangles so drawing the ring does not allocate a new one every frame. Demo.ringRect: Rect2iringRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(
const ORBIT_CENTER_X: anyORBIT_CENTER_X - const ORBIT_RADIUS: 34ORBIT_RADIUS, const ORBIT_CENTER_Y: anyORBIT_CENTER_Y - const ORBIT_RADIUS: 34ORBIT_RADIUS, const ORBIT_RADIUS: 34ORBIT_RADIUS * 2, const ORBIT_RADIUS: 34ORBIT_RADIUS * 2, ); Demo.homeRect: Rect2ihomeRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(const HOME_X: anyHOME_X - (const HOME_SIZE: 3HOME_SIZE >> 1), const HOME_Y: anyHOME_Y - (const HOME_SIZE: 3HOME_SIZE >> 1), const HOME_SIZE: 3HOME_SIZE, const HOME_SIZE: 3HOME_SIZE);
Demo.markerRect: Rect2imarkerRect = new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2i
Creates an integer rectangle, truncating all inputs toward zero.
@paramx - Left-edge X coordinate (defaults to 0).@paramy - Top-edge Y coordinate (defaults to 0).@paramwidth - Width in pixels (defaults to 0).@paramheight - Height in pixels (defaults to 0).
Rect2i
(0, 0, const MARKER_SIZE: 7MARKER_SIZE, const MARKER_SIZE: 7MARKER_SIZE);
// Text for the clock panel: the tick count just before the latest reset. Demo.resetNote: stringresetNote = 'No reset yet'; // Colors the engine overlay (toggled with the backquote key) from the shared UI // theme. Its default colors live in palette slots 1 and 2, which here are the page // and the ring - dark bars with dark text.
Demo.configure(): {
    overlayStyle: any;
}
configure
() {
return { overlayStyle: anyoverlayStyle: import THEME_OVERLAY_STYLETHEME_OVERLAY_STYLE, }; } async Demo.init(): unknowninit() { // The palette is local because no later frame changes a slot. applyTheme() still // has to run before BT.paletteSet(): it paints the shared UI colors into slots // 240-251, which every panel draws with. const const palette: Palettepalette =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteCreate: (size?: number) => Palette
Creates a standalone palette instance.
@since1.0.3@paramsize - Palette size. Defaults to 256 colors.@returnsNew mutable palette.
paletteCreate
(256);
import applyThemeapplyTheme(const palette: Palettepalette); // Color32(red, green, blue) stores one color. Each channel is 0 (none) through // 255 (full). palette.set() drops that color into a numbered slot. const palette: Palettepalette.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_BG: 1C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(12, 16, 28));
const palette: Palettepalette.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_RING: 2C_RING, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(70, 84, 110));
const palette: Palettepalette.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_HOME: 3C_HOME, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(90, 160, 110));
const palette: Palettepalette.Palette.set(index: number, color: Color32): void
Writes a color into a palette slot.
@paramindex - Palette index to overwrite.@paramcolor - Color to store.@throwsError if the index is invalid or if index `0` is set opaque.
set
(const C_MARKER: 4C_MARKER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32
Creates a clamped 8-bit RGBA color.
@paramr - Red channel (0-255, defaults to 255).@paramg - Green channel (0-255, defaults to 255).@paramb - Blue channel (0-255, defaults to 255).@parama - Alpha channel (0-255, defaults to 255 = opaque).
Color32
(240, 176, 48));
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.paletteSet: (palette: Palette) => void
Stores the active engine palette. Use this to swap the **entire palette** (e.g. switch between a day and night theme). After this call the renderer uploads the new palette uniform on the next frame. **Palette-value swap (change what a slot looks like):** mutate the live {@link BT.palette } in place with `palette.set(slot, newColor)`. The renderer uploads dirty slots on the next frame; no `paletteSet()` or {@link BT.spritesRefresh } needed. **Palette-layout swap (same colors, different slot positions):** build a new palette with the same colors at new indices, call `paletteSet()`, then call {@link BT.spritesRefresh } so every sprite sheet re-maps its original RGBA pixels against the new slot layout.
@since1.0.3@parampalette - Palette to make active.
paletteSet
(const palette: Palettepalette);
return true; } Demo.update(): voidupdate() { // First line: lets the kit latch key presses and taps for the buttons below. This // demo does not move anything here. The square's position is a picture of // BT.ticks, worked out in render() after a reset has had a chance to run. import uiui.tick(); } Demo.render(): voidrender() {
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.clear: (paletteIndex: number) => void
Sets the frame clear color using a palette index. The renderer uses this color when clearing the full display at the start of the next frame.
@since0.1.0@parampaletteIndex - Palette index for the full-screen clear pass.
clear
(const C_BG: 1C_BG);
// Buttons first, so a reset or a new point is already applied before the panels // and the square read the clock and the score. this.Demo.drawControls(): voiddrawControls(); this.Demo.placeMarker(): voidplaceMarker(); this.Demo.drawClock(): voiddrawClock(); this.Demo.drawSnapshot(): voiddrawSnapshot(); // The orbit last: its panel, then the ring, the home dot, and the square on top, // following a reset that happened earlier in this same frame. this.Demo.drawOrbit(): voiddrawOrbit(); } // The game's half of the snapshot. BT.testState() calls this, then wraps the result // with the tick count and the backend. Must return plain JSON: numbers, strings, // booleans, null, arrays, and plain objects. A Vector2i is a class instance; the // engine copies it down to { x, y }. Throws when the fail switch is on.
Demo.testState(): {
    score: number;
    marker: Vector2i;
}
testState
() {
if (this.Demo.failReadout: booleanfailReadout) { // The engine catches this. The snapshot comes back with state null and the // message in error, and the game keeps running. throw new Error('hook failed'); } return { score: numberscore: this.Demo.score: numberscore, marker: Vector2imarker: this.Demo.marker: Vector2imarker, }; } // Bottom-right: the three actions, each with a key and a tap target. Demo.drawControls(): voiddrawControls() { const const buttonW: numberbuttonW = const RIGHT_W: numberRIGHT_W - const PANEL_CONTENT_SHRINK: 20PANEL_CONTENT_SHRINK; import uiui.begin('topLeft', { x: numberx: const RIGHT_X: numberRIGHT_X, y: numbery: const CONTROLS_Y: numberCONTROLS_Y, width: numberwidth: const RIGHT_W: numberRIGHT_W }); import uiui.panel('Try it'); if (import uiui.button('A Add point', { key: stringkey: 'KeyA', width: numberwidth: const buttonW: numberbuttonW })) { // One point per press. Held keys do not repeat, because the button listens // for the press edge the kit latched in update(). this.Demo.score: numberscore += 1; } if (import uiui.button('R Reset ticks', { key: stringkey: 'KeyR', width: numberwidth: const buttonW: numberbuttonW })) { // Remember the old count first. ticksReset() does not return it. this.Demo.resetNote: stringresetNote = `Was ${
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
}`;
// Sends the engine clock back to zero. BT.ticks and BT.timeSeconds both // follow that counter. The score field above is not touched.
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticksReset: () => void
Resets the fixed-update tick counter back to zero.
@since0.1.0
ticksReset
();
} // A checkbox, not a button: the broken hook stays broken until you turn it off, // which is what makes the error line readable. this.Demo.failReadout: booleanfailReadout = import uiui.checkbox('F Fail hook', this.Demo.failReadout: booleanfailReadout, { key: stringkey: 'KeyF' }); import uiui.end(); } // Top-left: the live clock, and the tick count from just before the last reset. Demo.drawClock(): voiddrawClock() { import uiui.begin('topLeft', { x: numberx: const MARGIN: 8MARGIN, y: numbery: const MARGIN: 8MARGIN, width: numberwidth: const LEFT_W: 150LEFT_W }); import uiui.panel('Clock'); // BT.ticks counts finished update() steps. BT.ticksReset() sets it to 0, and the // next update() climbs from there. The counter advances after update() returns, // so a reset made in render() shows 0 here for the rest of this frame and usually // 1 on the next one. import uiui.kv('ticks',
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
);
// timeSeconds is ticks times the length of one tick. At 60 updates per second // that length is 1/60, so resetting ticks also resets this to 0. toFixed(2) // prints two digits after the decimal point. import uiui.kv('seconds',
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.timeSeconds: number
Fixed-step elapsed time in seconds (`BT.ticks * BT.deltaSeconds`).
@since1.0.4@returnsElapsed fixed-step time in seconds since initialization.
timeSeconds
.toFixed(2));
import uiui.spacer(4); import uiui.label(this.Demo.resetNote: stringresetNote, { color: stringcolor: 'dim' }); import uiui.label('Press R to reset it.', { color: stringcolor: 'dim' }); import uiui.end(); } // Bottom-left: one TestStateSnapshot, field by field. Demo.drawSnapshot(): voiddrawSnapshot() { const const snap: TestStateSnapshotsnap = this.Demo.readSnapshot(): TestStateSnapshotreadSnapshot(); // null means the hook failed (or a game has no hook). Anything else is the copied // score and marker; other values are not reshaped into null. const const copied: unknowncopied = const snap: TestStateSnapshotsnap.TestStateSnapshot.state: unknown
Deep copy of the game's {@link IBTDemo.testState } result, or `null` when the game has no hook, the hook threw, or its result could not be serialized.
state
;
import uiui.begin('topLeft', { x: numberx: const MARGIN: 8MARGIN, y: numbery: const SNAPSHOT_Y: numberSNAPSHOT_Y, width: numberwidth: const LEFT_W: 150LEFT_W }); import uiui.panel('Snapshot'); // Same number as BT.ticks, copied at the moment BT.testState() ran. import uiui.kv('ticks', const snap: TestStateSnapshotsnap.TestStateSnapshot.ticks: number
Current fixed-update tick, same as {@link BT.ticks } .
ticks
);
import uiui.pip('Matches clock', const snap: TestStateSnapshotsnap.TestStateSnapshot.ticks: number
Current fixed-update tick, same as {@link BT.ticks } .
ticks
===
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
);
// 'webgpu' or 'software' after init. ui.kv prints null as the word "null". import uiui.kv('backend', const snap: TestStateSnapshotsnap.TestStateSnapshot.backend: Backend | null
Backend that actually started, same as {@link BT.activeBackend } .
backend
);
import uiui.kv('state', const copied: unknowncopied === null ? 'null' : 'copied'); // error is missing on a healthy read. Present means the hook threw, or its return // value could not be turned into JSON (a loop of objects pointing at each other, // or a BigInt). import uiui.kv('error', const snap: TestStateSnapshotsnap.TestStateSnapshot.error?: string | undefined
Present only when {@link state } is `null` because the hook threw or its result could not be serialized (a cycle or a `BigInt`); holds the error message.
error
?? '(none)');
import uiui.spacer(4); if (const copied: unknowncopied !== null) { // These numbers are the copy, not the live fields. score stays put across a // tick reset; x and y jump because placeMarker() follows the clock. import uiui.kv('score', const copied: {} | undefinedcopied.score); import uiui.kv('x', const copied: {} | undefinedcopied.marker.x); import uiui.kv('y', const copied: {} | undefinedcopied.marker.y); } else { import uiui.label('No copied state', { color: stringcolor: 'warm' }); } import uiui.spacer(const copied: unknowncopied === null ? const SNAPSHOT_FILLER: 14SNAPSHOT_FILLER + 2 * const ROW_H: 10ROW_H : const SNAPSHOT_FILLER: 14SNAPSHOT_FILLER); import uiui.label('Copied as plain JSON.', { color: stringcolor: 'dim' }); import uiui.end(); } // Reads BT.testState() once per frame while the hook works. While it is set to fail, // keeps the first failed snapshot so the error line stays readable. Returns the // envelope the panels print. Demo.readSnapshot(): TestStateSnapshotreadSnapshot() { if (this.Demo.failReadout: booleanfailReadout) { if (this.Demo.frozenSnapshot: TestStateSnapshot | null
@type{TestStateSnapshot | null}
frozenSnapshot
=== null) {
this.Demo.frozenSnapshot: TestStateSnapshot | null
@type{TestStateSnapshot | null}
frozenSnapshot
=
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.testState: () => TestStateSnapshot
JSON readout of the running game for automated play-testing. Returns the current tick and active backend plus a deep copy of the game's optional {@link IBTDemo.testState } result (`null` when the game has no hook). In dev builds `BT` is exposed as `window.BT`, so a browser-driving agent or a CDP script reads it with `window.BT.testState()` - no per-game global needed. The hook is looked up on the active demo instance on every call, so it keeps working after a `'reinit'` hot reload replaces that instance. The result is snapshotted through `JSON.parse(JSON.stringify(...))`: callers get plain data they cannot use to mutate live game objects. Never throws - a throwing hook or an unserializable result (a cycle or a `BigInt`) is logged as `[BT] testState() threw:` and reported as `state: null` with the message in `error`. Available in release builds too; it only reads state and does nothing until called.
@since1.8.0@returns`{ ticks, backend, state }`, plus `error` when the readout failed.
testState
();
} return this.Demo.frozenSnapshot: TestStateSnapshot
@type{TestStateSnapshot | null}
frozenSnapshot
;
} // Healthy again: drop the held failure so the next read is live. this.Demo.frozenSnapshot: TestStateSnapshot | null
@type{TestStateSnapshot | null}
frozenSnapshot
= null;
return
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.testState: () => TestStateSnapshot
JSON readout of the running game for automated play-testing. Returns the current tick and active backend plus a deep copy of the game's optional {@link IBTDemo.testState } result (`null` when the game has no hook). In dev builds `BT` is exposed as `window.BT`, so a browser-driving agent or a CDP script reads it with `window.BT.testState()` - no per-game global needed. The hook is looked up on the active demo instance on every call, so it keeps working after a `'reinit'` hot reload replaces that instance. The result is snapshotted through `JSON.parse(JSON.stringify(...))`: callers get plain data they cannot use to mutate live game objects. Never throws - a throwing hook or an unserializable result (a cycle or a `BigInt`) is logged as `[BT] testState() threw:` and reported as `state: null` with the message in `error`. Available in release builds too; it only reads state and does nothing until called.
@since1.8.0@returns`{ ticks, backend, state }`, plus `error` when the readout failed.
testState
();
} // Parks the square on the circle for the current tick. Math.cos and Math.sin both // return a fraction from -1 to 1. Multiply by the radius and round: the engine draws // on whole pixels only. Demo.placeMarker(): voidplaceMarker() { const const angle: numberangle =
const BT: {
    FLIP_H: number;
    FLIP_V: number;
    ROT_90_CW: number;
    ROT_180_CW: number;
    ROT_270_CW: number;
    BTN_UP: number;
    BTN_DOWN: number;
    BTN_LEFT: number;
    BTN_RIGHT: number;
    BTN_A: number;
    BTN_B: number;
    BTN_X: number;
    BTN_Y: number;
    BTN_L: number;
    BTN_R: number;
    BTN_START: number;
    BTN_SELECT: number;
    BTN_POINTER_A: number;
    BTN_POINTER_B: number;
    BTN_POINTER_C: number;
    BTN_POINTER_D: number;
    PLAYER_ONE: number;
    PLAYER_TWO: number;
    PLAYER_THREE: number;
    PLAYER_FOUR: number;
    AXIS_LEFT_X: number;
    AXIS_LEFT_Y: number;
    AXIS_RIGHT_X: number;
    AXIS_RIGHT_Y: number;
    AXIS_TRIGGER_L: number;
    ... 124 more ...;
    spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.
BT
.ticks: number
Current fixed-update tick counter. Increments once per engine update. Reset via {@link BT.ticksReset } .
@since1.0.4@returnsCurrent tick count since initialization or last reset.
ticks
/ const ORBIT_TICKS_PER_RADIAN: 16ORBIT_TICKS_PER_RADIAN;
this.Demo.marker: Vector2imarker.Vector2i.x: number
Horizontal component (defaults to 0).
x
= const ORBIT_CENTER_X: anyORBIT_CENTER_X + Math.round(Math.cos(const angle: numberangle) * const ORBIT_RADIUS: 34ORBIT_RADIUS);
this.Demo.marker: Vector2imarker.Vector2i.y: number
Vertical component (defaults to 0).
y
= const ORBIT_CENTER_Y: anyORBIT_CENTER_Y + Math.round(Math.sin(const angle: numberangle) * const ORBIT_RADIUS: 34ORBIT_RADIUS);
// >> 1 is integer divide by 2, so the square is centered on that pixel. this.Demo.markerRect: Rect2imarkerRect.Rect2i.x: number
Left edge X coordinate (defaults to 0).
x
= this.Demo.marker: Vector2imarker.Vector2i.x: number
Horizontal component (defaults to 0).
x
- (const MARKER_SIZE: 7MARKER_SIZE >> 1);
this.Demo.markerRect: Rect2imarkerRect.Rect2i.y: number
Top edge Y coordinate (defaults to 0).
y
= this.Demo.marker: Vector2imarker.Vector2i.y: number
Vertical component (defaults to 0).
y
- (const MARKER_SIZE: 7MARKER_SIZE >> 1);
} // Top-right: a panel with the ring, the home dot, and the square inside it. The kit's // ui.drawRect() and ui.drawRectFill() draw as sprites in call order, so they land on // top of the panel; plain BT.drawRect() would be hidden under it. Demo.drawOrbit(): voiddrawOrbit() { import uiui.begin('topLeft', { x: numberx: const RIGHT_X: numberRIGHT_X, y: numbery: const MARGIN: 8MARGIN, width: numberwidth: const RIGHT_W: numberRIGHT_W }); import uiui.panel('BT.ticks, drawn'); import uiui.spacer(const ORBIT_PANEL_H: numberORBIT_PANEL_H - const PANEL_CONTENT_TOP: 22PANEL_CONTENT_TOP - const PANEL_BOTTOM_PAD: 11PANEL_BOTTOM_PAD); import uiui.end(); // Hollow box: the square's path touches the middle of each side. import uiui.drawRect(this.Demo.ringRect: Rect2iringRect, const C_RING: 2C_RING); import uiui.drawRectFill(this.Demo.homeRect: Rect2ihomeRect, const C_HOME: 3C_HOME); import uiui.drawRectFill(this.Demo.markerRect: Rect2imarkerRect, const C_MARKER: 4C_MARKER); } } function bootstrap(DemoClass: DemoConstructor, options?: BootstrapOptions): Promise<boolean>
One-liner bootstrap function for BLIT386 demos. Handles canvas retrieval and engine initialization. Backend selection (WebGPU or software fallback) is managed internally by BTAPI. This function provides a streamlined way to start a demo with sensible defaults while allowing customization through options.
@since0.2.0@changed1.4.0 Calling `bootstrap()` again while already initialized now routes to a hot swap (via {@link registerHotReload}) when a Vite HMR context is registered, or logs a double-bootstrap guard and returns `false` otherwise - previously it silently started a second, unstoppable `GameLoop`.@changed1.7.0 Exposes `BT` on `window.BT` after bootstrap finishes, gated by {@link BootstrapOptions.exposeGlobal} (default: {@link BT.isDevMode}).@paramDemoClass - Demo class constructor implementing `IBTDemo` (optional `configure()` for hardware settings).@paramoptions - Optional configuration for IDs and callbacks.@returns`true` when the demo boots successfully; otherwise `false`.@example// Simplest usage - uses default IDs. bootstrap(MyDemo);@example// With custom options. bootstrap(MyDemo, { canvasID: 'custom-canvas', containerID: 'custom-container', onSuccess: () => console.log('Demo started!'), onError: (err) => analytics.trackError(err), });@example// Await the result. const success = await bootstrap(MyDemo); if (success) { console.log('Demo is running'); }
bootstrap
(class DemoDemo);