/*
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.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 Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32, class Rect2iInteger 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.Rect2i, class Vector2iInteger 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.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 | nullfrozenSnapshot = 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): Vector2iCreates an integer 2D vector, truncating inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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): Rect2iCreates an integer rectangle, truncating all inputs toward zero.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) => PaletteCreates a standalone palette instance.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): voidWrites a color into a palette slot.set(const C_BG: 1C_BG, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(12, 16, 28));
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_RING: 2C_RING, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(70, 84, 110));
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_HOME: 3C_HOME, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(90, 160, 110));
const palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_MARKER: 4C_MARKER, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.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) => voidStores 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.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) => voidSets 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.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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: () => voidResets the fixed-update tick counter back to zero.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.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: numberFixed-step elapsed time in seconds (`BT.ticks * BT.deltaSeconds`).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: unknownDeep 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: numberCurrent fixed-update tick, same as
{@link
BT.ticks
}
.ticks);
import uiui.pip('Matches clock', const snap: TestStateSnapshotsnap.TestStateSnapshot.ticks: numberCurrent 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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks);
// 'webgpu' or 'software' after init. ui.kv prints null as the word "null".
import uiui.kv('backend', const snap: TestStateSnapshotsnap.TestStateSnapshot.backend: Backend | nullBackend 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 | undefinedPresent 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 | nullfrozenSnapshot === null) {
this.Demo.frozenSnapshot: TestStateSnapshot | nullfrozenSnapshot = 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: () => TestStateSnapshotJSON 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.testState();
}
return this.Demo.frozenSnapshot: TestStateSnapshotfrozenSnapshot;
}
// Healthy again: drop the held failure so the next read is live.
this.Demo.frozenSnapshot: TestStateSnapshot | nullfrozenSnapshot = 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: () => TestStateSnapshotJSON 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.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: numberCurrent fixed-update tick counter.
Increments once per engine update. Reset via
{@link
BT.ticksReset
}
.ticks / const ORBIT_TICKS_PER_RADIAN: 16ORBIT_TICKS_PER_RADIAN;
this.Demo.marker: Vector2imarker.Vector2i.x: numberHorizontal 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: numberVertical 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: numberLeft edge X coordinate (defaults to 0).x = this.Demo.marker: Vector2imarker.Vector2i.x: numberHorizontal component (defaults to 0).x - (const MARKER_SIZE: 7MARKER_SIZE >> 1);
this.Demo.markerRect: Rect2imarkerRect.Rect2i.y: numberTop edge Y coordinate (defaults to 0).y = this.Demo.marker: Vector2imarker.Vector2i.y: numberVertical 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.bootstrap(class DemoDemo);