/**
* Snake - grid snake with walls, food, keyboard steering, and PipBoy CRT post-processing.
* @description Grid snake with walls, food, keyboard, D-pad, and swipe steering, plus PipBoy CRT post-processing.
*
* Part of the BLIT386 demo series.
* Prerequisites:
* Basics https://demos.blit386.dev/basics
* PipBoy CRT https://demos.blit386.dev/crt-pipboy
* Keyboard Input https://demos.blit386.dev/keyboard-input
*
* Live version: https://demos.blit386.dev/snake-game
*
* Move with WASD or the arrow keys (both are mapped to player 0 face buttons). On a phone
* or tablet, steer with the on-screen D-pad in the bottom-right corner (it appears at the
* first touch) or simply swipe anywhere in the direction you want to go - both come from
* the shared UI kit in src/shared/ui.js. Each food dot grows the snake and makes it
* one step faster. Hitting the boundary wall or your own body ends the run; the game
* restarts after two seconds.
* Gameplay uses rectangles; a short systemPrint note appears in software mode.
*
* Post-processing matches the PipBoy CRT demo when WebGPU is active. In software fallback mode the
* snake game still runs; CRT effects are skipped and a short note is shown on canvas.
*
* WebGPU path: PixelGlitch on the logical index buffer, then palette resolve + upscale,
* then display-tier barrel distortion, chromatic aberration, interference, rolling scan
* line, scanlines, RGB mask, vignette, noise, flicker, bloom, and the glitch state machine.
*
* Eating food and dying both play a synthesized sound effect (built with AudioClip.synth(),
* the same technique Synth Toy explores in depth). An upbeat music loop plays in the
* background; each food multiplies its playback rate so the beat climbs with snake length.
*/
import {
class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip,
class BarrelDistortionBarrel distortion that warps UVs outward from the screen center.
Display-tier: operates on the upscaled output. Applying this in the pixel
tier (logical 320x240) discretizes the curve onto the source texel grid,
which CSS upscale then magnifies into visible step artifacts. At output
resolution the curve has enough resolution to express smoothly.
The math comes from Timothy Lottes's public-domain `crt-lottes.glsl`:
`warp(uv) = uv + delta * d2 * curvature` where `delta = uv - 0.5` and
`d2 = dot(delta, delta)`.BarrelDistortion,
class BloomSingle-pass box-blur bloom.
Samples a 5x5 neighborhood (25 taps) around each fragment, averages, then
mixes with the original color by
{@link
glow
}
.
{@link
spread
}
scales the
texel offset so the bloom radius can be tuned independently of the source
resolution.
Display-tier: bloom mixes neighboring pixels into intermediate hues that
are not in the active palette. Running it in pixel space would violate the
palette-pixel aesthetic; running it on the upscaled output reads as the
warm phosphor glow of an old monitor instead.
The implementation matches the original PipBoy bloom shader. A future
optimization would be a two-pass separable Gaussian (5 + 5 = 10 taps); add
it once a GPU perf test demands it.Bloom,
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT,
class ChromaticAberrationRGB channel offset that simulates lens chromatic aberration: red samples
left of the fragment, blue samples right, green stays centered.
Display-tier: spreads color along the lens axis. At logical resolution the
single-pixel offset is too coarse and reads as a glitch instead of a soft
fringe.ChromaticAberration,
class Color32Mutable 32-bit RGBA color value with 8-bit channels.Color32,
class FlickerBrightness multiplier - the simplest CRT animation knob.
Demos drive
{@link
amount
}
per frame to simulate flicker (e.g. with
`0.95 + sin(t) * 0.05`). The effect is intentionally trivial so the demo
controls the pattern; for procedural noise-driven flicker, combine with
the
{@link
Noise
}
effect.
Display-tier.Flicker,
class InterferencePer-row horizontal jitter that simulates analog signal interference.
Each output row gets a deterministic random horizontal offset seeded by
row index and time. Row offsets are stable for one frame and re-seed every
frame, producing a buzzing-noise feel.
Display-tier. Drives jitter from
{@link
time
}
; demos typically pass
`BT.ticks / BT.targetFPS`.Interference,
class NoiseAdditive per-pixel pseudo-random noise. Reseeds each frame from
{@link
time
}
so the noise pattern animates.
Display-tier.Noise,
class PixelGlitchChunky pixel-aligned horizontal glitch: every Nth row of source pixels gets
a random horizontal shift. Shifts snap to integer source-pixel offsets so
palette indices move whole-texel (no RGB resampling).
Pixel-tier: runs on the logical `r8uint` framebuffer (palette indices).PixelGlitch,
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 RGBMaskCRT shadow mask: per-pixel R/G/B vertical-stripe pattern with darkened
cell borders, simulating the phosphor grille of an aperture-grille CRT.
Display-tier: at output resolution there are enough output pixels per
mask cell to read as colored stripes. The cell pitch (in output pixels) is
the
{@link
size
}
parameter.
Math is a direct WGSL port of the libretro `crt-lottes.glsl` mask code.RGBMask,
class RollLineSlowly scrolling vertical interference band that brightens a horizontal
stripe of the image. Combination of three cosines + smoothstep gives the
stripe a soft top/bottom edge.
Display-tier. Demo drives
{@link
time
}
(typically `BT.ticks / BT.targetFPS`).RollLine,
class ScanlinesCRT scanlines: alternating bright/dark horizontal bands aligned to the
source vertical resolution.
Display-tier: at output resolution there is enough vertical pixels for
scanlines to read as alternating bright/dark bands. At logical 320x240 the
Gaussian weight quantizes to one of two values per source row and you lose
the smooth fade.Scanlines,
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,
class VignetteEdge-darkening vignette: smooth radial fade from full brightness at the
center to black at the corners.
Display-tier: applies to the whole simulated screen, not the underlying
pixel art.Vignette,
} from 'blit386';
import {
import ABERRATION_BASEABERRATION_BASE,
import applyGlitchUniformsapplyGlitchUniforms,
import FLICKER_BASEFLICKER_BASE,
import GLITCH_ACTIVE_MAXGLITCH_ACTIVE_MAX,
import GLITCH_ACTIVE_MINGLITCH_ACTIVE_MIN,
import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX,
import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN,
import GLITCH_INTENSITY_MAXGLITCH_INTENSITY_MAX,
import GLITCH_INTENSITY_MINGLITCH_INTENSITY_MIN,
import GLITCH_TYPES_CHROMAGLITCH_TYPES_CHROMA,
import NOISE_BASENOISE_BASE,
import PIXEL_GLITCH_BAND_HEIGHTPIXEL_GLITCH_BAND_HEIGHT,
import resetGlitchUniformsresetGlitchUniforms,
} from './shared/crt-glitch.js';
import { import isAvailableisAvailable, import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE } from './shared/post-process-backend.js';
import { import applyThemeapplyTheme, import uiui, import UI_ANCHORSUI_ANCHORS } from './shared/ui.js';
/** @typedef {import('blit386').IBTDemo} IBTDemo */
/** @typedef {import('blit386').HardwareSettings} HardwareSettings */
/** @typedef {import('blit386').Palette} Palette */
// Palette indices (slot 0 reserved).
const const C_BG: 1C_BG = 1;
const const C_WALL: 2C_WALL = 2;
const const C_SNAKE: 3C_SNAKE = 3;
const const C_FOOD: 4C_FOOD = 4;
const const C_FOOTER_DIM: 5C_FOOTER_DIM = 5;
const const C_FOOTER_WHITE: 6C_FOOTER_WHITE = 6;
// Logical resolution: small playfield as requested.
const const DISPLAY_W: 160DISPLAY_W = 160;
const const DISPLAY_H: 120DISPLAY_H = 120;
// Canvas output size: 4x logical resolution so display-tier CRT effects have enough pixels.
const const OUTPUT_W: 640OUTPUT_W = 640;
const const OUTPUT_H: 480OUTPUT_H = 480;
const const TARGET_FPS: 60TARGET_FPS = 60;
// Wall thickness in pixels (also one grid cell tall/wide).
const const WALL: 8WALL = 8;
// Snake and food are drawn on a coarse grid so movement stays chunky and readable.
const const CELL: 8CELL = 8;
// Inner playable area after subtracting the wall strip from each side.
const const INNER_X0: 8INNER_X0 = const WALL: 8WALL;
const const INNER_Y0: 8INNER_Y0 = const WALL: 8WALL;
const const INNER_W: numberINNER_W = const DISPLAY_W: 160DISPLAY_W - 2 * const WALL: 8WALL;
const const INNER_H: numberINNER_H = const DISPLAY_H: 120DISPLAY_H - 2 * const WALL: 8WALL;
// How many cells fit inside the inner rectangle (should divide evenly).
const const CELLS_X: numberCELLS_X = const INNER_W: numberINNER_W / const CELL: 8CELL;
const const CELLS_Y: numberCELLS_Y = const INNER_H: numberINNER_H / const CELL: 8CELL;
// Ticks between snake steps (lower = faster). A new round starts slow, then each food
// shortens the wait by MOVE_INTERVAL_STEP until MOVE_INTERVAL_MIN. At 60 FPS, 20 ticks
// is three steps per second; 4 ticks is fifteen steps per second.
const const MOVE_INTERVAL_START: 20MOVE_INTERVAL_START = 20;
const const MOVE_INTERVAL_MIN: 4MOVE_INTERVAL_MIN = 4;
const const MOVE_INTERVAL_STEP: 1MOVE_INTERVAL_STEP = 1;
// Background-loop playback rate (Web Audio pitch). 1.0 is the file's natural tempo;
// higher values play faster and higher, like speeding up a cassette.
// MUSIC_PITCH_AT_START is the rate on a fresh round; each food multiplies that rate by
// MUSIC_PITCH_SCALE_PER_GROWTH (for example 0.57, then 0.57 * 1.03, then 0.57 * 1.03^2, ...).
const const MUSIC_PITCH_AT_START: 0.57MUSIC_PITCH_AT_START = 0.57;
const const MUSIC_PITCH_SCALE_PER_GROWTH: 1.03MUSIC_PITCH_SCALE_PER_GROWTH = 1.03;
// How many growths it takes for moveInterval to reach MOVE_INTERVAL_MIN. Music pitch uses
// the same ceiling so the loop stops speeding up once the snake is already at top speed.
const const MUSIC_PITCH_GROWTH_CAP: numberMUSIC_PITCH_GROWTH_CAP = (const MOVE_INTERVAL_START: 20MOVE_INTERVAL_START - const MOVE_INTERVAL_MIN: 4MOVE_INTERVAL_MIN) / const MOVE_INTERVAL_STEP: 1MOVE_INTERVAL_STEP;
// High enough that short eat / death blips never steal the looping music voice from the
// SFX pool (BT.soundPlay uses priority stealing when every voice is busy).
const const MUSIC_VOICE_PRIORITY: 100MUSIC_VOICE_PRIORITY = 100;
// Soft glide when the loop speeds up or slows down after a food eat or a new round.
const const MUSIC_PITCH_FADE_MS: 120MUSIC_PITCH_FADE_MS = 120;
// Two seconds at 60 ticks per second before a new round starts.
const const RESTART_DELAY_TICKS: 120RESTART_DELAY_TICKS = 120;
/**
* Minimal snake with PipBoy CRT post-processing from crt-pipboy demo.
*
* @implements {IBTDemo}
*/
class class DemoMinimal snake with PipBoy CRT post-processing from crt-pipboy demo.Demo {
/** @type {Palette | null} */
Demo.palette: Palette | nullpalette = null;
/** @type {AudioClip | null} Sound played when the snake eats food. */
Demo.eatClip: AudioClip | nulleatClip = null;
/** @type {AudioClip | null} Sound played when the snake dies. */
Demo.gameOverClip: AudioClip | nullgameOverClip = null;
/** @type {AudioClip | null} Looping background music. */
Demo.musicClip: AudioClip | nullmusicClip = null;
/**
* Live handle for the looping music voice, or null before unlock / if music failed to
* load. We use BT.soundPlay (not BT.musicPlay) so we can change pitch each time the
* snake speeds up - the music player has volume and crossfade controls, but no pitch.
*
* @type {import('blit386').SoundRef | null}
*/
Demo.musicRef: SoundRef | nullLive handle for the looping music voice, or null before unlock / if music failed to
load. We use BT.soundPlay (not BT.musicPlay) so we can change pitch each time the
snake speeds up - the music player has volume and crossfade controls, but no pitch.musicRef = null;
/** @type {{ x: number; y: number }[]} Head first, tail last (grid coords). */
Demo.snake: {}snake = [];
/** @type {{ x: number; y: number }} Food cell in grid coords. */
Demo.food: {
x: number;
y: number;
}
food = { x: numberx: 0, y: numbery: 0 };
/** Current step direction (grid units per move). */
Demo.dx: numberCurrent step direction (grid units per move).dx = 1;
Demo.dy: numberdy = 0;
/** Next direction chosen by the player (applied when the snake steps). */
Demo.pendingDx: numberNext direction chosen by the player (applied when the snake steps).pendingDx = 1;
Demo.pendingDy: numberpendingDy = 0;
/** Counts ticks until the next snake step while the game is running. */
Demo.moveCooldown: numberCounts ticks until the next snake step while the game is running.moveCooldown = 0;
/**
* Current ticks between steps. Starts at MOVE_INTERVAL_START each round and drops
* toward MOVE_INTERVAL_MIN every time the snake eats food.
*/
Demo.moveInterval: numberCurrent ticks between steps. Starts at MOVE_INTERVAL_START each round and drops
toward MOVE_INTERVAL_MIN every time the snake eats food.moveInterval = const MOVE_INTERVAL_START: 20MOVE_INTERVAL_START;
/**
* How many food dots the snake has eaten this round. Music pitch uses this count
* capped at MUSIC_PITCH_GROWTH_CAP so tempo plateaus with move speed.
*/
Demo.growthCount: numberHow many food dots the snake has eaten this round. Music pitch uses this count
capped at MUSIC_PITCH_GROWTH_CAP so tempo plateaus with move speed.growthCount = 0;
/** When true, the snake does not move until restart. */
Demo.gameOver: booleanWhen true, the snake does not move until restart.gameOver = false;
/** @type {number | null} Tick index when the snake died (`BT.ticks`); null while playing. */
Demo.deathTick: number | nulldeathTick = null;
/** True when the WebGPU backend is active, so the CRT effect chain can run (set in init()). */
Demo.effectsAvailable: booleanTrue when the WebGPU backend is active, so the CRT effect chain can run (set in init()).effectsAvailable = false;
// CRT effects (initialized in setupCrtEffects(), called from init())
/** @type {PixelGlitch} */
Demo.pixelGlitch: PixelGlitchpixelGlitch;
/** @type {BarrelDistortion} */
Demo.barrel: BarrelDistortionbarrel;
/** @type {ChromaticAberration} */
Demo.aberration: ChromaticAberrationaberration;
/** @type {Interference} */
Demo.interference: Interferenceinterference;
/** @type {RollLine} */
Demo.rollLine: RollLinerollLine;
/** @type {Scanlines} */
Demo.scanlines: Scanlinesscanlines;
/** @type {RGBMask} */
Demo.mask: RGBMaskmask;
/** @type {Vignette} */
Demo.vignette: Vignettevignette;
/** @type {Noise} */
Demo.noise: Noisenoise;
/** @type {Flicker} */
Demo.flicker: Flickerflicker;
/** @type {Bloom} */
Demo.bloom: Bloombloom;
Demo.glitchCooldown: numberglitchCooldown = 0;
/** How many ticks remain in the current glitch burst; 0 means no glitch is running. */
Demo.glitchTicksLeft: numberHow many ticks remain in the current glitch burst; 0 means no glitch is running.glitchTicksLeft = 0;
Demo.glitchDuration: numberglitchDuration = 0;
/** @type {string} */
Demo.glitchType: stringglitchType = 'none';
Demo.glitchPeak: numberglitchPeak = 0;
/**
* 160x120 framebuffer, 4x upscale for CRT chain, 60 fixed updates per second.
*
* @returns {Partial<HardwareSettings>}
*/
Demo.configure(): Partial<HardwareSettings>160x120 framebuffer, 4x upscale for CRT chain, 60 fixed updates per second.configure() {
return {
displaySize: Vector2idisplaySize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const DISPLAY_W: 160DISPLAY_W, const DISPLAY_H: 120DISPLAY_H),
drawingBufferSize: Vector2idrawingBufferSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const OUTPUT_W: 640OUTPUT_W, const OUTPUT_H: 480OUTPUT_H),
maxCanvasSize: Vector2imaxCanvasSize: new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const OUTPUT_W: 640OUTPUT_W, const OUTPUT_H: 480OUTPUT_H),
outputUpscaleFilter: stringoutputUpscaleFilter: 'nearest',
targetFPS: numbertargetFPS: const TARGET_FPS: 60TARGET_FPS,
// Phones and tablets dim, then lock, the screen after 30-60 seconds without a touch -
// annoying mid-game when you are only pressing arrow keys or swiping every few seconds.
// This asks the browser to keep the screen on while you are playing. Browsers that do
// not support the request just ignore it, so this is safe everywhere.
isWakeLockEnabled: booleanisWakeLockEnabled: true,
// Arrow keys (and Space) normally scroll the page. This demo maps those keys to move
// the snake, so opt in so the browser does not scroll the demo page while you play.
isCapturingKeyboardScroll: booleanisCapturingKeyboardScroll: true,
// Hide the small "~" toggle hint in the bottom-left corner so the game
// board stays clean. Players who want the stats overlay can still press
// the Backquote key (`) to show it and press ` again to hide it - hiding
// the hint does not turn the overlay off.
isOverlayToggleHintVisible: booleanisOverlayToggleHintVisible: false,
overlayStyle: {
barPaletteIndex: number;
textPaletteIndex: number;
gapPaletteIndex: number;
}
overlayStyle: {
barPaletteIndex: numberbarPaletteIndex: const C_BG: 1C_BG,
textPaletteIndex: numbertextPaletteIndex: const C_FOOTER_WHITE: 6C_FOOTER_WHITE,
gapPaletteIndex: numbergapPaletteIndex: const C_BG: 1C_BG,
},
isOverlayTimingChartEnabled: booleanisOverlayTimingChartEnabled: true,
overlayTimingChartStyle: {
updateBarPaletteIndex: number;
renderBarPaletteIndex: number;
warningPaletteIndex: number;
errorPaletteIndex: number;
tagPaletteIndex: number;
}
overlayTimingChartStyle: {
updateBarPaletteIndex: numberupdateBarPaletteIndex: const C_SNAKE: 3C_SNAKE,
renderBarPaletteIndex: numberrenderBarPaletteIndex: const C_FOOD: 4C_FOOD,
warningPaletteIndex: numberwarningPaletteIndex: const C_FOOTER_DIM: 5C_FOOTER_DIM,
errorPaletteIndex: numbererrorPaletteIndex: const C_WALL: 2C_WALL,
tagPaletteIndex: numbertagPaletteIndex: const C_FOOTER_WHITE: 6C_FOOTER_WHITE,
},
};
}
/**
* Palette, PipBoy CRT stack (crt-pipboy), glitch machine, then first round.
*
* @returns {Promise<boolean>}
*/
async Demo.init(): Promise<boolean>Palette, PipBoy CRT stack (crt-pipboy), glitch machine, then first round.init() {
// Start from default keyboard maps so this demo stays independent from remapping demos.
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.inputMapReset: () => voidRestores built-in default keyboard maps for players `0` and `1`.
Same tables as `BT.DEFAULT_KEYBOARD_PLAYER1` and `BT.DEFAULT_KEYBOARD_PLAYER2`.inputMapReset();
// Engine defaults put WASD on player 0 and arrows on player 1. This is a single-player
// game, so fold both layouts into player 0: either set of keys steers the same snake.
// BT.inputMap replaces the whole key list for that button; listing both codes means
// either key counts (logical OR), the same pattern input-map-remapping demo shows with Q|E for LEFT.
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.inputMap: (player: number, button: number, ...keys: string[]) => voidAssigns one or more `KeyboardEvent.code` values to a face button for a keyboard player.
Logical button state is the OR of all listed keys. Only players `0` and `1`
support keyboard; other indices no-op. `button` must be one face-button
bit flag (`BT.BTN_UP` … `BT.BTN_SELECT`). Pass an empty key list
to clear keyboard bindings for that button until remapped again.inputMap(0, 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_UP: numberUp button bit flag.BTN_UP, 'KeyW', 'ArrowUp');
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.inputMap: (player: number, button: number, ...keys: string[]) => voidAssigns one or more `KeyboardEvent.code` values to a face button for a keyboard player.
Logical button state is the OR of all listed keys. Only players `0` and `1`
support keyboard; other indices no-op. `button` must be one face-button
bit flag (`BT.BTN_UP` … `BT.BTN_SELECT`). Pass an empty key list
to clear keyboard bindings for that button until remapped again.inputMap(0, 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_DOWN: numberDown button bit flag.BTN_DOWN, 'KeyS', 'ArrowDown');
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.inputMap: (player: number, button: number, ...keys: string[]) => voidAssigns one or more `KeyboardEvent.code` values to a face button for a keyboard player.
Logical button state is the OR of all listed keys. Only players `0` and `1`
support keyboard; other indices no-op. `button` must be one face-button
bit flag (`BT.BTN_UP` … `BT.BTN_SELECT`). Pass an empty key list
to clear keyboard bindings for that button until remapped again.inputMap(0, 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_LEFT: numberLeft button bit flag.BTN_LEFT, 'KeyA', 'ArrowLeft');
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.inputMap: (player: number, button: number, ...keys: string[]) => voidAssigns one or more `KeyboardEvent.code` values to a face button for a keyboard player.
Logical button state is the OR of all listed keys. Only players `0` and `1`
support keyboard; other indices no-op. `button` must be one face-button
bit flag (`BT.BTN_UP` … `BT.BTN_SELECT`). Pass an empty key list
to clear keyboard bindings for that button until remapped again.inputMap(0, 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_RIGHT: numberRight button bit flag.BTN_RIGHT, 'KeyD', 'ArrowRight');
// BT.synthPreset bundles ready-tuned sound recipes (Synth Toy explores all six).
// Rendering them once here means eating and dying play back with zero delay later.
this.Demo.eatClip: AudioClip | nulleatClip = await class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip.AudioClip.synth(params: SynthParams): Promise<AudioClip>Synthesizes a clip from deterministic procedural parameters - no source file, no
`OfflineAudioContext`, and no audio graph involved.
Rendering happens entirely on the CPU via the pure
{@link
renderSynthSamples
}
function
against an `AudioBuffer` allocated from the registered decode context. The returned clip
flows through the same
{@link
buffer
}
getter and playback path as a loaded clip, but uses
a synthetic, non-cached identifier (`synth:<waveform>`) - it is never added to the
URL-keyed resolved cache and never deduplicated, so identical `params` still render a
fresh, independent `AudioBuffer` on every call. See
{@link
SynthParams
}
for the full
parameter set.synth(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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.synthPreset: {
jump: (seed?: number) => SynthParams;
pickup: (seed?: number) => SynthParams;
explosion: (seed?: number) => SynthParams;
laser: (seed?: number) => SynthParams;
hit: (seed?: number) => SynthParams;
blip: (seed?: number) => SynthParams;
}
Pre-configured `SynthParams` presets for common sound effects ("jump", "pickup",
"explosion", "laser", "hit", "blip").
Each function returns a fresh
{@link
SynthParams
}
object; pass it to
{@link
AudioClip.synth
}
to render a clip, then play the result via
{@link
BT.soundPlay
}
.
An optional `seed` argument applies small, bounded, deterministic jitter to a few
hand-picked fields per preset, so repeated plays vary without losing reproducibility -
the same seed always renders the exact same variant.synthPreset.pickup: (seed?: number) => SynthParamsItem pickup / coin: a short, bright square-wave blip that sweeps upward an octave.
`seed` jitters the base frequency (+/-8%) and duration (+/-12%). Omit `seed` for a fixed
baseline variant.pickup());
this.Demo.gameOverClip: AudioClip | nullgameOverClip = await class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip.AudioClip.synth(params: SynthParams): Promise<AudioClip>Synthesizes a clip from deterministic procedural parameters - no source file, no
`OfflineAudioContext`, and no audio graph involved.
Rendering happens entirely on the CPU via the pure
{@link
renderSynthSamples
}
function
against an `AudioBuffer` allocated from the registered decode context. The returned clip
flows through the same
{@link
buffer
}
getter and playback path as a loaded clip, but uses
a synthetic, non-cached identifier (`synth:<waveform>`) - it is never added to the
URL-keyed resolved cache and never deduplicated, so identical `params` still render a
fresh, independent `AudioBuffer` on every call. See
{@link
SynthParams
}
for the full
parameter set.synth(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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.synthPreset: {
jump: (seed?: number) => SynthParams;
pickup: (seed?: number) => SynthParams;
explosion: (seed?: number) => SynthParams;
laser: (seed?: number) => SynthParams;
hit: (seed?: number) => SynthParams;
blip: (seed?: number) => SynthParams;
}
Pre-configured `SynthParams` presets for common sound effects ("jump", "pickup",
"explosion", "laser", "hit", "blip").
Each function returns a fresh
{@link
SynthParams
}
object; pass it to
{@link
AudioClip.synth
}
to render a clip, then play the result via
{@link
BT.soundPlay
}
.
An optional `seed` argument applies small, bounded, deterministic jitter to a few
hand-picked fields per preset, so repeated plays vary without losing reproducibility -
the same seed always renders the exact same variant.synthPreset.explosion: (seed?: number) => SynthParamsExplosion: a low sawtooth rumble mixed heavily with noise, with a slow decay and release for
a boom that lingers.
`seed` jitters the base frequency (+/-8%), duration (+/-12%), and `noiseMix` (+/-10%,
clamped to `[0, 1]`) - the mix jitter alone gives every explosion a distinct noisy texture.
Omit `seed` for a fixed baseline variant.explosion());
// The background music file is loaded separately from the sound effects above, and
// wrapped in its own try/catch. Unlike the effects (built on the spot by the synth
// engine, so they cannot fail), this loads an actual audio file over the network -
// if it is missing or the browser cannot decode it, we do not want that one file to
// stop the whole game from starting. Catching the error here means Snake stays fully
// playable with sound effects but no music, instead of getting stuck on a blank
// screen.
//
// We only load here - we do not call BT.soundPlay yet. Browsers keep audio locked
// until the first click or key press, and unlike BT.musicPlay (which remembers a
// request while locked), BT.soundPlay would be dropped. update() starts the loop
// the moment BT.isAudioUnlocked flips true.
try {
this.Demo.musicClip: AudioClip | nullmusicClip = await class AudioClipDecoded audio asset with its winning source URL and buffer-derived metadata.
Construct instances with
{@link
AudioClip.load
}
,
{@link
AudioClip.loadAll
}
, or
{@link
AudioClip.synth
}
; there is no public constructor.AudioClip.AudioClip.load(url: string | string[], options?: AudioClipLoadOptions): Promise<AudioClip>Loads an audio clip from a single URL, or from an ordered list of
candidate URLs.
A single URL runs the download+decode pipeline directly, sharing the
per-URL cache and in-flight dedup described on
{@link
AudioClip
}
. A URL
array tries each candidate in order and resolves with the first one
that downloads and decodes successfully - useful for offering a
browser-friendly fallback (for example `['music.ogg', 'music.mp3']`)
when a container or codec isn't universally supported. If every
candidate fails, the error from the last candidate is thrown.load('/audio/music-upbeat.wav');
} catch (function (local var) error: unknownerror) {
console.warn('Snake Game: failed to load background music, continuing without it.', function (local var) error: unknownerror);
}
this.Demo.palette: Palette | nullpalette = 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.paletteCreate: (size?: number) => PaletteCreates a standalone palette instance.paletteCreate(256);
this.Demo.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(25, 35, 45));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_WALL: 2C_WALL, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(180, 170, 140));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_SNAKE: 3C_SNAKE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(90, 220, 120));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_FOOD: 4C_FOOD, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(240, 90, 70));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_FOOTER_DIM: 5C_FOOTER_DIM, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(120, 130, 150));
this.Demo.palette: Palettepalette.Palette.set(index: number, color: Color32): voidWrites a color into a palette slot.set(const C_FOOTER_WHITE: 6C_FOOTER_WHITE, new new Color32(r?: number, g?: number, b?: number, a?: number): Color32Creates a clamped 8-bit RGBA color.Color32(220, 230, 240));
// The shared UI kit draws the touch D-pad, and it needs its theme colors in the
// palette. applyTheme() writes them into high slots (240 and up), far away from the
// six scene colors above, so the game's look does not change at all.
import applyThemeapplyTheme(this.Demo.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;
... 106 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(this.Demo.palette: Palettepalette);
// CRT post-processing needs WebGPU. In software fallback mode we skip the whole
// effect chain - the snake game itself still runs fine without it.
this.Demo.effectsAvailable: booleanTrue when the WebGPU backend is active, so the CRT effect chain can run (set in init()).effectsAvailable = import isAvailableisAvailable();
if (this.Demo.effectsAvailable: booleanTrue when the WebGPU backend is active, so the CRT effect chain can run (set in init()).effectsAvailable) {
this.Demo.setupCrtEffects(): voidBuilds the WebGPU CRT effect chain (same stack and tuning as crt-pipboy demo) and seeds
the glitch state machine. Called from init() only when WebGPU is active.setupCrtEffects();
}
this.Demo.startRound(): voidPlaces a short snake in the middle, resets speed to the slow start, and spawns
the first food dot.startRound();
return true;
}
/**
* Drive CRT animation and glitch machine every tick; run snake logic when alive.
*/
Demo.update(): voidDrive CRT animation and glitch machine every tick; run snake logic when alive.update() {
// The UI kit's once-per-tick housekeeping: it tracks touch contacts for the D-pad
// and watches for swipe gestures. Always the first line of update().
import uiui.tick();
// Start (or restart) the tempo-linked music loop once audio is unlocked.
this.Demo.ensureBackgroundMusic(): voidStarts the looping music voice once audio is unlocked, or restarts it if the voice
was somehow lost. Safe to call every tick - it no-ops while already playing.
Why BT.soundPlay instead of BT.musicPlay: only the SFX path exposes pitch (playback
rate), which is how we keep the beat tied to snake growth. The tradeoff is that
soundPlay is dropped before unlock, so we wait for BT.isAudioUnlocked here.ensureBackgroundMusic();
// Did a swipe finish on this tick? ui.swipe() answers with a direction name
// ('up', 'down', 'left', 'right') or null. We read it once and hand it to the
// steering code below, next to the keyboard and D-pad checks.
const const swipe: anyswipe = import uiui.swipe();
if (this.Demo.effectsAvailable: booleanTrue when the WebGPU backend is active, so the CRT effect chain can run (set in init()).effectsAvailable) {
this.Demo.tickCrtClock(): voidUpdates time-driven uniforms for rolling noise, interference, and roll line.tickCrtClock();
this.Demo.tickGlitchMachine(): voidSame state machine as crt-pipboy demo: cooldown, burst envelope, random glitch type.tickGlitchMachine();
}
if (this.Demo.tickRestartAfterDeath(): booleanWhile game over, waits for the restart delay then starts a new round.tickRestartAfterDeath()) {
return;
}
this.Demo.pollDirectionInput(swipe: "up" | "down" | "left" | "right" | null): voidReads all three steering inputs and updates pending direction (no instant reverse):
player 0 face buttons (WASD, arrows, or gamepad), the on-screen touch D-pad, and swipes.
We use BT.isPressed (edge: up -> down this tick), not BT.isDown (held every tick),
and the D-pad's matching ui.dpad.isPressed. That way one tap per direction per move
interval feels like a classic D-pad. The `this.dy !== 1` checks block a 180-degree
turn: you cannot go straight back into your body on the next step.pollDirectionInput(const swipe: anyswipe);
this.Demo.moveCooldown: numberCounts ticks until the next snake step while the game is running.moveCooldown -= 1;
if (this.Demo.moveCooldown: numberCounts ticks until the next snake step while the game is running.moveCooldown > 0) {
return;
}
// Use the live interval so food-driven speed-ups take effect on the next step.
this.Demo.moveCooldown: numberCounts ticks until the next snake step while the game is running.moveCooldown = this.Demo.moveInterval: numberCurrent ticks between steps. Starts at MOVE_INTERVAL_START each round and drops
toward MOVE_INTERVAL_MIN every time the snake eats food.moveInterval;
if (!(this.Demo.pendingDx: numberNext direction chosen by the player (applied when the snake steps).pendingDx === -this.Demo.dx: numberCurrent step direction (grid units per move).dx && this.Demo.pendingDy: numberpendingDy === -this.Demo.dy: numberdy)) {
this.Demo.dx: numberCurrent step direction (grid units per move).dx = this.Demo.pendingDx: numberNext direction chosen by the player (applied when the snake steps).pendingDx;
this.Demo.dy: numberdy = this.Demo.pendingDy: numberpendingDy;
}
this.Demo.step(): voidOne grid step: wall check, self check, grow or shift tail.step();
}
/**
* Clear to background, draw walls, food, snake segments.
*/
Demo.render(): voidClear to background, draw walls, food, snake segments.render() {
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;
... 106 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);
this.Demo.renderWalls(): voidFour filled bars form the boundary the snake must not cross.renderWalls();
if (this.Demo.food: {
x: number;
y: number;
}
food.x: numberx >= 0 && this.Demo.food: {
x: number;
y: number;
}
food.y: numbery >= 0) {
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.gridRect(gx: number, gy: number): Rect2iPixel rectangle for one grid cell at (gx, gy), inside the inner playfield.gridRect(this.Demo.food: {
x: number;
y: number;
}
food.x: numberx, this.Demo.food: {
x: number;
y: number;
}
food.y: numbery), const C_FOOD: 4C_FOOD);
}
for (let let i: numberi = 0; let i: numberi < this.Demo.snake: {}snake.length; let i: numberi++) {
const const seg: anyseg = this.Demo.snake: {}snake[let i: numberi];
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(this.Demo.gridRect(gx: number, gy: number): Rect2iPixel rectangle for one grid cell at (gx, gy), inside the inner playfield.gridRect(const seg: anyseg.x, const seg: anyseg.y), const C_SNAKE: 3C_SNAKE);
}
if (!this.Demo.effectsAvailable: booleanTrue when the WebGPU backend is active, so the CRT effect chain can run (set in init()).effectsAvailable) {
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.systemPrint: (pos: Vector2i, paletteIndex: number, text: string) => voidDraws text using the built-in 6x14 system font.
The system font covers printable ASCII (characters 32-126). For custom
bitmap fonts with proportional glyphs, use
{@link
BT.printFont
}
instead.systemPrint(new new Vector2i(x?: number, y?: number): Vector2iCreates an integer 2D vector, truncating inputs toward zero.Vector2i(const INNER_X0: 8INNER_X0, const DISPLAY_H: 120DISPLAY_H - 16), const C_FOOTER_DIM: 5C_FOOTER_DIM, import SOFTWARE_FALLBACK_NOTESOFTWARE_FALLBACK_NOTE);
}
// Browsers keep all sound muted until the player clicks or presses a key. This
// shared row shows a warm "enable sound" hint only while audio is still locked,
// then disappears on its own the moment a first move unlocks it - which is also
// the same gesture that starts the snake moving, so the hint is usually only
// visible for a single frame. The default sentence is too long for this 160-wide
// playfield, so we pass a short override.
import uiui.begin(import UI_ANCHORSUI_ANCHORS.TOP_LEFT, { margin: numbermargin: 3 });
import uiui.audioUnlockHint({ text: stringtext: 'Click for sound' });
import uiui.end();
// The touch D-pad, drawn over the playfield in the bottom-right corner. It stays
// invisible until the first touch contact, so mouse-and-keyboard players never see
// it. This playfield is only 160x120 logical pixels, so the keys are scaled down
// from the kit's phone-sized default.
import uiui.dpadWidget({ size: numbersize: 22, gap: numbergap: 3, margin: numbermargin: 4 });
}
/**
* Builds the WebGPU CRT effect chain (same stack and tuning as crt-pipboy demo) and seeds
* the glitch state machine. Called from init() only when WebGPU is active.
*/
Demo.setupCrtEffects(): voidBuilds the WebGPU CRT effect chain (same stack and tuning as crt-pipboy demo) and seeds
the glitch state machine. Called from init() only when WebGPU is active.setupCrtEffects() {
// Pixel-tier glitch (same role as crt-pipboy demo).
this.Demo.pixelGlitch: PixelGlitchpixelGlitch = new new PixelGlitch(): PixelGlitchChunky pixel-aligned horizontal glitch: every Nth row of source pixels gets
a random horizontal shift. Shifts snap to integer source-pixel offsets so
palette indices move whole-texel (no RGB resampling).
Pixel-tier: runs on the logical `r8uint` framebuffer (palette indices).PixelGlitch();
this.Demo.pixelGlitch: PixelGlitchpixelGlitch.PixelGlitch.bandHeight: numberHeight of each glitch band in source pixels. Each band gets a single
shift value, so larger bands produce chunkier glitches.bandHeight = import PIXEL_GLITCH_BAND_HEIGHTPIXEL_GLITCH_BAND_HEIGHT;
this.Demo.pixelGlitch: PixelGlitchpixelGlitch.PixelGlitch.intensity: numberGlitch strength in `[0, 1]`. Scales the per-band horizontal shift
magnitude. Bands are shifted when their hash exceeds ~0.85 (~15% of
bands). `0` disables.intensity = 0;
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.effectAdd: (effect: Effect) => voidAppends a fullscreen post-processing effect to whichever chain matches
its declared
{@link
Effect.tier
}
.
- `tier='pixel'` -> pixel chain (logical resolution).
- `tier='display'` -> display chain (output resolution); requires
`drawingBufferSize` in effective hardware settings (`configure()` or
`defaultConfig()`).
Effects run in registration order within each tier. The pixel chain runs
first, followed by the upscale pass, followed by the display chain. Each
{@link
Effect
}
instance owns its own GPU resources and may be mutated
each frame from demo code.effectAdd(this.Demo.pixelGlitch: PixelGlitchpixelGlitch);
this.Demo.barrel: BarrelDistortionbarrel = new new BarrelDistortion(): BarrelDistortionBarrel distortion that warps UVs outward from the screen center.
Display-tier: operates on the upscaled output. Applying this in the pixel
tier (logical 320x240) discretizes the curve onto the source texel grid,
which CSS upscale then magnifies into visible step artifacts. At output
resolution the curve has enough resolution to express smoothly.
The math comes from Timothy Lottes's public-domain `crt-lottes.glsl`:
`warp(uv) = uv + delta * d2 * curvature` where `delta = uv - 0.5` and
`d2 = dot(delta, delta)`.BarrelDistortion();
this.Demo.barrel: BarrelDistortionbarrel.BarrelDistortion.curvature: numberCurvature strength. Typical values:
- `0.02` - subtle, like a flat-screen monitor.
- `0.05` - moderate desktop CRT.
- `0.10` - heavy small CRT or pocket TV.curvature = 0.2;
this.Demo.aberration: ChromaticAberrationaberration = new new ChromaticAberration(): ChromaticAberrationRGB channel offset that simulates lens chromatic aberration: red samples
left of the fragment, blue samples right, green stays centered.
Display-tier: spreads color along the lens axis. At logical resolution the
single-pixel offset is too coarse and reads as a glitch instead of a soft
fringe.ChromaticAberration();
this.Demo.aberration: ChromaticAberrationaberration.ChromaticAberration.aberration: numberChannel offset in display-chain (output) pixels. Reasonable values are `0.5` to `3.0`.
Set to `0` to disable.aberration = import ABERRATION_BASEABERRATION_BASE;
this.Demo.interference: Interferenceinterference = new new Interference(): InterferencePer-row horizontal jitter that simulates analog signal interference.
Each output row gets a deterministic random horizontal offset seeded by
row index and time. Row offsets are stable for one frame and re-seed every
frame, producing a buzzing-noise feel.
Display-tier. Drives jitter from
{@link
time
}
; demos typically pass
`BT.ticks / BT.targetFPS`.Interference();
this.Demo.interference: Interferenceinterference.Interference.amount: numberMaximum horizontal offset as a UV fraction (e.g. `0.06` shifts the row
by up to ~6% of the image width). Set to `0` to disable.amount = 0;
this.Demo.rollLine: RollLinerollLine = new new RollLine(): RollLineSlowly scrolling vertical interference band that brightens a horizontal
stripe of the image. Combination of three cosines + smoothstep gives the
stripe a soft top/bottom edge.
Display-tier. Demo drives
{@link
time
}
(typically `BT.ticks / BT.targetFPS`).RollLine();
this.Demo.rollLine: RollLinerollLine.RollLine.amount: numberRoll line amplitude (mix factor onto a brightness boost).amount = 0.1;
this.Demo.rollLine: RollLinerollLine.RollLine.speed: numberScroll speed multiplier; final scroll velocity = `time * speed`.speed = 1.0;
this.Demo.scanlines: Scanlinesscanlines = new new Scanlines(): ScanlinesCRT scanlines: alternating bright/dark horizontal bands aligned to the
source vertical resolution.
Display-tier: at output resolution there is enough vertical pixels for
scanlines to read as alternating bright/dark bands. At logical 320x240 the
Gaussian weight quantizes to one of two values per source row and you lose
the smooth fade.Scanlines();
this.Demo.scanlines: Scanlinesscanlines.Scanlines.amount: numberScanline mix amount in `[0, 1]`. 0 disables.amount = 0.55;
this.Demo.scanlines: Scanlinesscanlines.Scanlines.strength: numberNegative gaussian falloff parameter for scanline brightness. More
negative values produce sharper dark bands. PipBoy reference: `-8.0`.strength = -8;
this.Demo.scanlines: Scanlinesscanlines.Scanlines.density: numberNumber of scanline cycles vertically. Should match the demo's logical
source vertical resolution so each "source pixel row" maps to one
scanline cycle. Defaults to `240`, the most common pixel-art height.
Set to e.g. `200` for VGA-style 320x200 games or `144` for Game Boy
resolution.density = const DISPLAY_H: 120DISPLAY_H;
this.Demo.mask: RGBMaskmask = new new RGBMask(): RGBMaskCRT shadow mask: per-pixel R/G/B vertical-stripe pattern with darkened
cell borders, simulating the phosphor grille of an aperture-grille CRT.
Display-tier: at output resolution there are enough output pixels per
mask cell to read as colored stripes. The cell pitch (in output pixels) is
the
{@link
size
}
parameter.
Math is a direct WGSL port of the libretro `crt-lottes.glsl` mask code.RGBMask();
this.Demo.mask: RGBMaskmask.RGBMask.intensity: numberMask brightness mix amount in `[0, 1]`. 0 hides the mask.intensity = 0.18;
this.Demo.mask: RGBMaskmask.RGBMask.size: numberMask cell pitch in output (display-chain) pixels. Smaller = denser mask.size = 6;
this.Demo.mask: RGBMaskmask.RGBMask.border: numberBorder darkening within each mask cell. 0 disables, 1 strong.border = 0.5;
this.Demo.vignette: Vignettevignette = new new Vignette(): VignetteEdge-darkening vignette: smooth radial fade from full brightness at the
center to black at the corners.
Display-tier: applies to the whole simulated screen, not the underlying
pixel art.Vignette();
this.Demo.vignette: Vignettevignette.Vignette.amount: numberVignette darkening exponent. Higher values produce a stronger vignette
with a sharper falloff. PipBoy reference: `0.2`. Set to `0` to disable.amount = 0.35;
this.Demo.noise: Noisenoise = new new Noise(): NoiseAdditive per-pixel pseudo-random noise. Reseeds each frame from
{@link
time
}
so the noise pattern animates.
Display-tier.Noise();
this.Demo.noise: Noisenoise.Noise.amount: numberNoise amplitude as a `[-amount, +amount]` additive perturbation on each
channel. Reasonable values are `0.005` to `0.05`. Set to `0` to disable.amount = import NOISE_BASENOISE_BASE;
this.Demo.flicker: Flickerflicker = new new Flicker(): FlickerBrightness multiplier - the simplest CRT animation knob.
Demos drive
{@link
amount
}
per frame to simulate flicker (e.g. with
`0.95 + sin(t) * 0.05`). The effect is intentionally trivial so the demo
controls the pattern; for procedural noise-driven flicker, combine with
the
{@link
Noise
}
effect.
Display-tier.Flicker();
this.Demo.flicker: Flickerflicker.Flicker.amount: numberBrightness multiplier. `1` is unmodulated; values below `1` darken the
frame. The demo typically updates this each frame from a sin wave or
random source.amount = import FLICKER_BASEFLICKER_BASE;
this.Demo.bloom: Bloombloom = new new Bloom(): BloomSingle-pass box-blur bloom.
Samples a 5x5 neighborhood (25 taps) around each fragment, averages, then
mixes with the original color by
{@link
glow
}
.
{@link
spread
}
scales the
texel offset so the bloom radius can be tuned independently of the source
resolution.
Display-tier: bloom mixes neighboring pixels into intermediate hues that
are not in the active palette. Running it in pixel space would violate the
palette-pixel aesthetic; running it on the upscaled output reads as the
warm phosphor glow of an old monitor instead.
The implementation matches the original PipBoy bloom shader. A future
optimization would be a two-pass separable Gaussian (5 + 5 = 10 taps); add
it once a GPU perf test demands it.Bloom();
this.Demo.bloom: Bloombloom.Bloom.spread: numberTexel offset multiplier for the box-blur kernel.spread = 3.0;
this.Demo.bloom: Bloombloom.Bloom.glow: numberMix factor between the original sample and the blurred neighborhood.glow = 0.18;
for (const const fx: anyfx of [
this.Demo.barrel: BarrelDistortionbarrel,
this.Demo.aberration: ChromaticAberrationaberration,
this.Demo.interference: Interferenceinterference,
this.Demo.rollLine: RollLinerollLine,
this.Demo.scanlines: Scanlinesscanlines,
this.Demo.mask: RGBMaskmask,
this.Demo.vignette: Vignettevignette,
this.Demo.noise: Noisenoise,
this.Demo.flicker: Flickerflicker,
this.Demo.bloom: Bloombloom,
]) {
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.effectAdd: (effect: Effect) => voidAppends a fullscreen post-processing effect to whichever chain matches
its declared
{@link
Effect.tier
}
.
- `tier='pixel'` -> pixel chain (logical resolution).
- `tier='display'` -> display chain (output resolution); requires
`drawingBufferSize` in effective hardware settings (`configure()` or
`defaultConfig()`).
Effects run in registration order within each tier. The pixel chain runs
first, followed by the upscale pass, followed by the display chain. Each
{@link
Effect
}
instance owns its own GPU resources and may be mutated
each frame from demo code.effectAdd(const fx: anyfx);
}
// BT.random is the engine's shared random number generator.
// Its int() method returns a whole number from the first value up to (but not including) the second,
// so this picks how many ticks to wait before the first glitch burst.
this.Demo.glitchCooldown: numberglitchCooldown = 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX);
this.Demo.glitchTicksLeft: numberHow many ticks remain in the current glitch burst; 0 means no glitch is running.glitchTicksLeft = 0;
this.Demo.glitchDuration: numberglitchDuration = 0;
this.Demo.glitchType: stringglitchType = 'none';
this.Demo.glitchPeak: numberglitchPeak = 0;
}
/**
* While game over, waits for the restart delay then starts a new round.
*
* @returns {boolean} True when the snake should not move this tick.
*/
Demo.tickRestartAfterDeath(): booleanWhile game over, waits for the restart delay then starts a new round.tickRestartAfterDeath() {
if (!this.Demo.gameOver: booleanWhen true, the snake does not move until restart.gameOver) {
return false;
}
const const tick: numbertick = 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;
... 106 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;
if (this.Demo.deathTick: number | nulldeathTick !== null && const tick: numbertick - this.Demo.deathTick: numberdeathTick >= const RESTART_DELAY_TICKS: 120RESTART_DELAY_TICKS) {
this.Demo.startRound(): voidPlaces a short snake in the middle, resets speed to the slow start, and spawns
the first food dot.startRound();
}
return true;
}
/**
* Reads all three steering inputs and updates pending direction (no instant reverse):
* player 0 face buttons (WASD, arrows, or gamepad), the on-screen touch D-pad, and swipes.
*
* We use BT.isPressed (edge: up -> down this tick), not BT.isDown (held every tick),
* and the D-pad's matching ui.dpad.isPressed. That way one tap per direction per move
* interval feels like a classic D-pad. The `this.dy !== 1` checks block a 180-degree
* turn: you cannot go straight back into your body on the next step.
*
* @param {'up' | 'down' | 'left' | 'right' | null} swipe - The swipe finished this
* tick, if any (from ui.swipe() in update()).
*/
Demo.pollDirectionInput(swipe: "up" | "down" | "left" | "right" | null): voidReads all three steering inputs and updates pending direction (no instant reverse):
player 0 face buttons (WASD, arrows, or gamepad), the on-screen touch D-pad, and swipes.
We use BT.isPressed (edge: up -> down this tick), not BT.isDown (held every tick),
and the D-pad's matching ui.dpad.isPressed. That way one tap per direction per move
interval feels like a classic D-pad. The `this.dy !== 1` checks block a 180-degree
turn: you cannot go straight back into your body on the next step.pollDirectionInput(swipe: "up" | "down" | "left" | "right" | null- The swipe finished this
tick, if any (from ui.swipe() in update()).swipe) {
// Up: only if we are not currently moving down (would reverse into the tail).
if (this.Demo.isSteerPressed(button: number, dir: "up" | "down" | "left" | "right", swipe: "up" | "down" | "left" | "right" | null): booleanCombines the three ways to steer in one direction: the engine face button (keyboard
or gamepad), the on-screen touch D-pad key, and a swipe that way.isSteerPressed(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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_UP: numberUp button bit flag.BTN_UP, 'up', swipe: "up" | "down" | "left" | "right" | null- The swipe finished this
tick, if any (from ui.swipe() in update()).swipe) && this.Demo.dy: numberdy !== 1) {
this.Demo.pendingDx: numberNext direction chosen by the player (applied when the snake steps).pendingDx = 0;
this.Demo.pendingDy: numberpendingDy = -1;
}
// Down: only if we are not currently moving up.
if (this.Demo.isSteerPressed(button: number, dir: "up" | "down" | "left" | "right", swipe: "up" | "down" | "left" | "right" | null): booleanCombines the three ways to steer in one direction: the engine face button (keyboard
or gamepad), the on-screen touch D-pad key, and a swipe that way.isSteerPressed(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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_DOWN: numberDown button bit flag.BTN_DOWN, 'down', swipe: "up" | "down" | "left" | "right" | null- The swipe finished this
tick, if any (from ui.swipe() in update()).swipe) && this.Demo.dy: numberdy !== -1) {
this.Demo.pendingDx: numberNext direction chosen by the player (applied when the snake steps).pendingDx = 0;
this.Demo.pendingDy: numberpendingDy = 1;
}
// Left: only if we are not currently moving right.
if (this.Demo.isSteerPressed(button: number, dir: "up" | "down" | "left" | "right", swipe: "up" | "down" | "left" | "right" | null): booleanCombines the three ways to steer in one direction: the engine face button (keyboard
or gamepad), the on-screen touch D-pad key, and a swipe that way.isSteerPressed(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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_LEFT: numberLeft button bit flag.BTN_LEFT, 'left', swipe: "up" | "down" | "left" | "right" | null- The swipe finished this
tick, if any (from ui.swipe() in update()).swipe) && this.Demo.dx: numberCurrent step direction (grid units per move).dx !== 1) {
this.Demo.pendingDx: numberNext direction chosen by the player (applied when the snake steps).pendingDx = -1;
this.Demo.pendingDy: numberpendingDy = 0;
}
// Right: only if we are not currently moving left.
if (this.Demo.isSteerPressed(button: number, dir: "up" | "down" | "left" | "right", swipe: "up" | "down" | "left" | "right" | null): booleanCombines the three ways to steer in one direction: the engine face button (keyboard
or gamepad), the on-screen touch D-pad key, and a swipe that way.isSteerPressed(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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.type BTN_RIGHT: numberRight button bit flag.BTN_RIGHT, 'right', swipe: "up" | "down" | "left" | "right" | null- The swipe finished this
tick, if any (from ui.swipe() in update()).swipe) && this.Demo.dx: numberCurrent step direction (grid units per move).dx !== -1) {
this.Demo.pendingDx: numberNext direction chosen by the player (applied when the snake steps).pendingDx = 1;
this.Demo.pendingDy: numberpendingDy = 0;
}
}
/**
* Combines the three ways to steer in one direction: the engine face button (keyboard
* or gamepad), the on-screen touch D-pad key, and a swipe that way.
*
* @param {number} button - The engine face button mask (BT.BTN_UP and friends).
* @param {'up' | 'down' | 'left' | 'right'} dir - The D-pad/swipe direction name.
* @param {'up' | 'down' | 'left' | 'right' | null} swipe - The swipe finished this tick.
* @returns {boolean} True when any of the three pressed that direction this tick.
*/
Demo.isSteerPressed(button: number, dir: "up" | "down" | "left" | "right", swipe: "up" | "down" | "left" | "right" | null): booleanCombines the three ways to steer in one direction: the engine face button (keyboard
or gamepad), the on-screen touch D-pad key, and a swipe that way.isSteerPressed(button: number- The engine face button mask (BT.BTN_UP and friends).button, dir: "up" | "down" | "left" | "right"- The D-pad/swipe direction name.dir, swipe: "up" | "down" | "left" | "right" | null- The swipe finished this tick.swipe) {
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isPressed: (button: number, player?: number, repeatRate?: number) => booleanChecks whether a button was pressed on the current frame.
Same parameter semantics as
{@link
isDown
}
; returns `true` only on
the frame the button transitions from up to down.
Call from `update()`, not `render()`, for reliable detection: for keyboard-mapped
face buttons (players 0 and 1), the press edge clears once per fixed-update tick,
which always runs before that frame's `render()`.isPressed(button: number- The engine face button mask (BT.BTN_UP and friends).button, 0) || import uiui.dpad.isPressed(dir: "up" | "down" | "left" | "right"- The D-pad/swipe direction name.dir) || swipe: "up" | "down" | "left" | "right" | null- The swipe finished this tick.swipe === dir: "up" | "down" | "left" | "right"- The D-pad/swipe direction name.dir;
}
/**
* Updates time-driven uniforms for rolling noise, interference, and roll line.
*/
Demo.tickCrtClock(): voidUpdates time-driven uniforms for rolling noise, interference, and roll line.tickCrtClock() {
const const seconds: numberseconds = 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;
... 106 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 TARGET_FPS: 60TARGET_FPS;
this.Demo.rollLine: RollLinerollLine.RollLine.time: numberWall-clock seconds; demos typically drive this each frame.time = const seconds: numberseconds;
this.Demo.noise: Noisenoise.Noise.time: numberWall-clock seconds; reseeds the noise each frame.time = const seconds: numberseconds;
this.Demo.interference: Interferenceinterference.Interference.time: numberWall-clock seconds; reseeds the row offsets each frame.time = const seconds: numberseconds;
}
/**
* Same state machine as crt-pipboy demo: cooldown, burst envelope, random glitch type.
*/
Demo.tickGlitchMachine(): voidSame state machine as crt-pipboy demo: cooldown, burst envelope, random glitch type.tickGlitchMachine() {
// A burst is running while there are ticks left on its countdown.
if (this.Demo.glitchTicksLeft: numberHow many ticks remain in the current glitch burst; 0 means no glitch is running.glitchTicksLeft > 0) {
const const t: numbert = 1 - this.Demo.glitchTicksLeft: numberHow many ticks remain in the current glitch burst; 0 means no glitch is running.glitchTicksLeft / this.Demo.glitchDuration: numberglitchDuration;
const const envelope: anyenvelope = Math.sin(const t: numbert * Math.PI);
import applyGlitchUniformsapplyGlitchUniforms(this, const envelope: anyenvelope);
this.Demo.glitchTicksLeft: numberHow many ticks remain in the current glitch burst; 0 means no glitch is running.glitchTicksLeft--;
// Countdown just hit zero: the burst is over, so calm the effects down
// and roll a fresh cooldown until the next burst.
if (this.Demo.glitchTicksLeft: numberHow many ticks remain in the current glitch burst; 0 means no glitch is running.glitchTicksLeft === 0) {
import resetGlitchUniformsresetGlitchUniforms(this);
this.Demo.glitchCooldown: numberglitchCooldown = 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(import GLITCH_COOLDOWN_MINGLITCH_COOLDOWN_MIN, import GLITCH_COOLDOWN_MAXGLITCH_COOLDOWN_MAX);
}
return;
}
this.Demo.glitchCooldown: numberglitchCooldown--;
if (this.Demo.glitchCooldown: numberglitchCooldown <= 0) {
// pick() draws one item out of a list, like taking a card off the top of a shuffled deck.
// float() is the decimal cousin of int(), for values that are not whole numbers.
this.Demo.glitchType: stringglitchType = 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.random.Random.pick<string>(arr: readonly string[]): stringReturns one element chosen uniformly from a non-empty array.pick(import GLITCH_TYPES_CHROMAGLITCH_TYPES_CHROMA);
this.Demo.glitchDuration: numberglitchDuration = 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(import GLITCH_ACTIVE_MINGLITCH_ACTIVE_MIN, import GLITCH_ACTIVE_MAXGLITCH_ACTIVE_MAX);
this.Demo.glitchTicksLeft: numberHow many ticks remain in the current glitch burst; 0 means no glitch is running.glitchTicksLeft = this.Demo.glitchDuration: numberglitchDuration;
this.Demo.glitchPeak: numberglitchPeak = 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(import GLITCH_INTENSITY_MINGLITCH_INTENSITY_MIN, import GLITCH_INTENSITY_MAXGLITCH_INTENSITY_MAX);
this.Demo.pixelGlitch: PixelGlitchpixelGlitch.PixelGlitch.seed: numberPer-glitch random seed. Change between glitches to vary the band noise
pattern.seed = 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.random.Random.float(min: number, max: number): numberReturns the next pseudo-random float in [min, max).float(0, 1000);
}
}
/**
* Pixel rectangle for one grid cell at (gx, gy), inside the inner playfield.
*
* @param {number} gx
* @param {number} gy
* @returns {Rect2i}
*/
Demo.gridRect(gx: number, gy: number): Rect2iPixel rectangle for one grid cell at (gx, gy), inside the inner playfield.gridRect(gx: numbergx, gy: numbergy) {
const const px: numberpx = const INNER_X0: 8INNER_X0 + gx: numbergx * const CELL: 8CELL;
const const py: numberpy = const INNER_Y0: 8INNER_Y0 + gy: numbergy * const CELL: 8CELL;
return new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const px: numberpx, const py: numberpy, const CELL: 8CELL, const CELL: 8CELL);
}
/**
* Four filled bars form the boundary the snake must not cross.
*/
Demo.renderWalls(): voidFour filled bars form the boundary the snake must not cross.renderWalls() {
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, 0, const DISPLAY_W: 160DISPLAY_W, const WALL: 8WALL), const C_WALL: 2C_WALL);
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, const DISPLAY_H: 120DISPLAY_H - const WALL: 8WALL, const DISPLAY_W: 160DISPLAY_W, const WALL: 8WALL), const C_WALL: 2C_WALL);
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(0, const WALL: 8WALL, const WALL: 8WALL, const DISPLAY_H: 120DISPLAY_H - 2 * const WALL: 8WALL), const C_WALL: 2C_WALL);
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.drawRectFill: (rect: Rect2i, paletteIndex: number) => voidDraws a filled rectangle.drawRectFill(new new Rect2i(x?: number, y?: number, width?: number, height?: number): Rect2iCreates an integer rectangle, truncating all inputs toward zero.Rect2i(const DISPLAY_W: 160DISPLAY_W - const WALL: 8WALL, const WALL: 8WALL, const WALL: 8WALL, const DISPLAY_H: 120DISPLAY_H - 2 * const WALL: 8WALL), const C_WALL: 2C_WALL);
}
/**
* Playback rate for the background loop from the starting pitch and how many times
* the snake has grown this round. One food is start * scale, two foods is
* start * scale * scale, and so on; zero growths leaves the rate at the start pitch.
* Growth above MUSIC_PITCH_GROWTH_CAP no longer raises the rate (matches top speed).
*
* @returns {number} Playback rate to pass to BT.soundPlay / BT.soundPitchSet.
*/
Demo.currentMusicPitch(): numberPlayback rate for the background loop from the starting pitch and how many times
the snake has grown this round. One food is start * scale, two foods is
start * scale * scale, and so on; zero growths leaves the rate at the start pitch.
Growth above MUSIC_PITCH_GROWTH_CAP no longer raises the rate (matches top speed).currentMusicPitch() {
// The ** operator is "to the power of": a ** b means a multiplied by itself b times.
// growthCount 0 gives 1, so the rate stays at MUSIC_PITCH_AT_START on a fresh round.
// Cap at MUSIC_PITCH_GROWTH_CAP so pitch plateaus with moveInterval at top speed.
const const growthForPitch: anygrowthForPitch = Math.min(this.Demo.growthCount: numberHow many food dots the snake has eaten this round. Music pitch uses this count
capped at MUSIC_PITCH_GROWTH_CAP so tempo plateaus with move speed.growthCount, const MUSIC_PITCH_GROWTH_CAP: numberMUSIC_PITCH_GROWTH_CAP);
return const MUSIC_PITCH_AT_START: 0.57MUSIC_PITCH_AT_START * const MUSIC_PITCH_SCALE_PER_GROWTH: 1.03MUSIC_PITCH_SCALE_PER_GROWTH ** const growthForPitch: anygrowthForPitch;
}
/**
* Starts the looping music voice once audio is unlocked, or restarts it if the voice
* was somehow lost. Safe to call every tick - it no-ops while already playing.
*
* Why BT.soundPlay instead of BT.musicPlay: only the SFX path exposes pitch (playback
* rate), which is how we keep the beat tied to snake growth. The tradeoff is that
* soundPlay is dropped before unlock, so we wait for BT.isAudioUnlocked here.
*/
Demo.ensureBackgroundMusic(): voidStarts the looping music voice once audio is unlocked, or restarts it if the voice
was somehow lost. Safe to call every tick - it no-ops while already playing.
Why BT.soundPlay instead of BT.musicPlay: only the SFX path exposes pitch (playback
rate), which is how we keep the beat tied to snake growth. The tradeoff is that
soundPlay is dropped before unlock, so we wait for BT.isAudioUnlocked here.ensureBackgroundMusic() {
if (this.Demo.musicClip: AudioClip | nullmusicClip === null || !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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isAudioUnlocked: booleanWhether the audio context has been unlocked by a user gesture.
Browsers require a user gesture (pointer, key, or touch press) before
allowing audio playback. Starts `false`; flips to `true` for the rest of
the session after the first gesture successfully resumes the audio context.isAudioUnlocked) {
return;
}
// Already have a live looping voice - nothing to do.
if (this.Demo.musicRef: SoundRef | nullLive handle for the looping music voice, or null before unlock / if music failed to
load. We use BT.soundPlay (not BT.musicPlay) so we can change pitch each time the
snake speeds up - the music player has volume and crossfade controls, but no pitch.musicRef !== null && 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isSoundPlaying: (ref: SoundRef) => booleanReports whether a sound is still playing.isSoundPlaying(this.Demo.musicRef: SoundRefLive handle for the looping music voice, or null before unlock / if music failed to
load. We use BT.soundPlay (not BT.musicPlay) so we can change pitch each time the
snake speeds up - the music player has volume and crossfade controls, but no pitch.musicRef)) {
return;
}
// Start (or restart) at the pitch for the current growth count.
this.Demo.musicRef: SoundRef | nullLive handle for the looping music voice, or null before unlock / if music failed to
load. We use BT.soundPlay (not BT.musicPlay) so we can change pitch each time the
snake speeds up - the music player has volume and crossfade controls, but no pitch.musicRef = 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.soundPlay: (clip: AudioClip, options?: SoundPlayOptions) => SoundRefPlays a loaded audio clip through the SFX voice pool.
Returns an inert
{@link
SoundRef
}
without allocating a voice when the clip hasn't finished
loading yet (or was already unloaded), when the pool has no free or stealable voice at this
priority, or before the engine has unlocked audio playback.soundPlay(this.Demo.musicClip: AudioClipmusicClip, {
VoicePlayOptions.loop?: boolean | undefinedWhether the buffer loops. Defaults to `false`.loop: true,
VoicePlayOptions.volume?: number | undefinedInitial gain in `[0, 1]` (unclamped). Defaults to `1`.volume: 0.65,
VoicePlayOptions.pitch?: number | undefinedInitial `playbackRate`. Defaults to `1`.pitch: this.Demo.currentMusicPitch(): numberPlayback rate for the background loop from the starting pitch and how many times
the snake has grown this round. One food is start * scale, two foods is
start * scale * scale, and so on; zero growths leaves the rate at the start pitch.
Growth above MUSIC_PITCH_GROWTH_CAP no longer raises the rate (matches top speed).currentMusicPitch(),
VoicePlayOptions.priority?: number | undefinedAllocation priority; higher survives stealing longer. Defaults to `0`.priority: const MUSIC_VOICE_PRIORITY: 100MUSIC_VOICE_PRIORITY,
});
}
/**
* Glides the live music loop to the playback rate for the current growth count.
* No-op before the loop has started (still locked, or music failed to load).
*/
Demo.syncMusicTempo(): voidGlides the live music loop to the playback rate for the current growth count.
No-op before the loop has started (still locked, or music failed to load).syncMusicTempo() {
if (this.Demo.musicRef: SoundRef | nullLive handle for the looping music voice, or null before unlock / if music failed to
load. We use BT.soundPlay (not BT.musicPlay) so we can change pitch each time the
snake speeds up - the music player has volume and crossfade controls, but no pitch.musicRef === null || !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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.isSoundPlaying: (ref: SoundRef) => booleanReports whether a sound is still playing.isSoundPlaying(this.Demo.musicRef: SoundRefLive handle for the looping music voice, or null before unlock / if music failed to
load. We use BT.soundPlay (not BT.musicPlay) so we can change pitch each time the
snake speeds up - the music player has volume and crossfade controls, but no pitch.musicRef)) {
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.soundPitchSet: (ref: SoundRef, value: number, options?: SoundParamSetOptions) => voidSets a sound's playback rate, optionally fading to it.soundPitchSet(this.Demo.musicRef: SoundRefLive handle for the looping music voice, or null before unlock / if music failed to
load. We use BT.soundPlay (not BT.musicPlay) so we can change pitch each time the
snake speeds up - the music player has volume and crossfade controls, but no pitch.musicRef, this.Demo.currentMusicPitch(): numberPlayback rate for the background loop from the starting pitch and how many times
the snake has grown this round. One food is start * scale, two foods is
start * scale * scale, and so on; zero growths leaves the rate at the start pitch.
Growth above MUSIC_PITCH_GROWTH_CAP no longer raises the rate (matches top speed).currentMusicPitch(), {
SoundParamSetOptions.fadeMs?: number | undefinedFade duration in milliseconds. Omit for an immediate change.fadeMs: const MUSIC_PITCH_FADE_MS: 120MUSIC_PITCH_FADE_MS,
});
}
/**
* Places a short snake in the middle, resets speed to the slow start, and spawns
* the first food dot.
*/
Demo.startRound(): voidPlaces a short snake in the middle, resets speed to the slow start, and spawns
the first food dot.startRound() {
this.Demo.gameOver: booleanWhen true, the snake does not move until restart.gameOver = false;
this.Demo.deathTick: number | nulldeathTick = null;
// Every new round begins at the slow pace; eating food will speed things up again.
this.Demo.moveInterval: numberCurrent ticks between steps. Starts at MOVE_INTERVAL_START each round and drops
toward MOVE_INTERVAL_MIN every time the snake eats food.moveInterval = const MOVE_INTERVAL_START: 20MOVE_INTERVAL_START;
this.Demo.moveCooldown: numberCounts ticks until the next snake step while the game is running.moveCooldown = this.Demo.moveInterval: numberCurrent ticks between steps. Starts at MOVE_INTERVAL_START each round and drops
toward MOVE_INTERVAL_MIN every time the snake eats food.moveInterval;
// Zero foods eaten - music returns to MUSIC_PITCH_AT_START for the new round.
this.Demo.growthCount: numberHow many food dots the snake has eaten this round. Music pitch uses this count
capped at MUSIC_PITCH_GROWTH_CAP so tempo plateaus with move speed.growthCount = 0;
this.Demo.syncMusicTempo(): voidGlides the live music loop to the playback rate for the current growth count.
No-op before the loop has started (still locked, or music failed to load).syncMusicTempo();
const const midX: anymidX = Math.floor(const CELLS_X: numberCELLS_X / 2);
const const midY: anymidY = Math.floor(const CELLS_Y: numberCELLS_Y / 2);
this.Demo.snake: {}snake = [
{ x: anyx: const midX: anymidX, y: anyy: const midY: anymidY },
{ x: numberx: const midX: anymidX - 1, y: anyy: const midY: anymidY },
{ x: numberx: const midX: anymidX - 2, y: anyy: const midY: anymidY },
];
this.Demo.dx: numberCurrent step direction (grid units per move).dx = 1;
this.Demo.dy: numberdy = 0;
this.Demo.pendingDx: numberNext direction chosen by the player (applied when the snake steps).pendingDx = 1;
this.Demo.pendingDy: numberpendingDy = 0;
this.Demo.placeFood(): voidPicks a random empty grid cell for food.placeFood();
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('Round start');
}
/**
* Picks a random empty grid cell for food.
*/
Demo.placeFood(): voidPicks a random empty grid cell for food.placeFood() {
const const occupied: anyoccupied = new Set();
for (let let i: numberi = 0; let i: numberi < this.Demo.snake: {}snake.length; let i: numberi++) {
const const s: anys = this.Demo.snake: {}snake[let i: numberi];
const occupied: anyoccupied.add(`${const s: anys.x},${const s: anys.y}`);
}
for (let let attempt: numberattempt = 0; let attempt: numberattempt < 4000; let attempt: numberattempt++) {
// int() with a single argument counts from 0, so int(CELLS_X) lands on any column from 0 to CELLS_X - 1
// - exactly the valid grid positions.
const const x: numberx = 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(const CELLS_X: numberCELLS_X);
const const y: numbery = 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.random: RandomDefault engine PRNG (live reference - not a copy).
Time-seeded when the engine singleton is created. Call
{@link
BT.randomSeed
}
for a reproducible run. Mutating the instance (for example `BT.random.int(10)`)
advances the shared stream.random.Random.int(minOrMaxExclusive: number, maxExclusive?: number): numberReturns a pseudo-random integer in [0, maxExclusive) or [min, maxExclusive).int(const CELLS_Y: numberCELLS_Y);
const const key: stringkey = `${const x: numberx},${const y: numbery}`;
if (!const occupied: anyoccupied.has(const key: stringkey)) {
this.Demo.food: {
x: number;
y: number;
}
food = { x: numberx, y: numbery };
return;
}
}
// Board full - no free cell found; (-1, -1) is a sentinel that render() sees and skips drawing the food dot.
this.Demo.food: {
x: number;
y: number;
}
food = { x: numberx: -1, y: numbery: -1 };
}
/**
* One grid step: wall check, self check, grow or shift tail.
*/
Demo.step(): voidOne grid step: wall check, self check, grow or shift tail.step() {
const const head: anyhead = this.Demo.snake: {}snake[0];
const const nx: anynx = const head: anyhead.x + this.Demo.dx: numberCurrent step direction (grid units per move).dx;
const const ny: anyny = const head: anyhead.y + this.Demo.dy: numberdy;
if (const nx: anynx < 0 || const nx: anynx >= const CELLS_X: numberCELLS_X || const ny: anyny < 0 || const ny: anyny >= const CELLS_Y: numberCELLS_Y) {
this.Demo.endGame(): voidFreeze movement and remember when to restart.endGame();
return;
}
const const eating: booleaneating = const nx: anynx === this.Demo.food: {
x: number;
y: number;
}
food.x: numberx && const ny: anyny === this.Demo.food: {
x: number;
y: number;
}
food.y: numbery;
const const limit: anylimit = const eating: booleaneating ? this.Demo.snake: {}snake.length : this.Demo.snake: {}snake.length - 1;
for (let let i: numberi = 0; let i: numberi < const limit: anylimit; let i: numberi++) {
const const s: anys = this.Demo.snake: {}snake[let i: numberi];
if (const s: anys.x === const nx: anynx && const s: anys.y === const ny: anyny) {
this.Demo.endGame(): voidFreeze movement and remember when to restart.endGame();
return;
}
}
this.Demo.snake: {}snake.unshift({ x: anyx: const nx: anynx, y: anyy: const ny: anyny });
if (const eating: booleaneating) {
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.soundPlay: (clip: AudioClip, options?: SoundPlayOptions) => SoundRefPlays a loaded audio clip through the SFX voice pool.
Returns an inert
{@link
SoundRef
}
without allocating a voice when the clip hasn't finished
loading yet (or was already unloaded), when the pool has no free or stealable voice at this
priority, or before the engine has unlocked audio playback.soundPlay(this.Demo.eatClip: AudioClip | nulleatClip);
// Shorter wait between steps = faster snake. Clamp so it never goes below
// MOVE_INTERVAL_MIN; Math.max picks the larger of the floor and the stepped-down
// value, which is how we stop the interval from dropping into the negatives.
this.Demo.moveInterval: numberCurrent ticks between steps. Starts at MOVE_INTERVAL_START each round and drops
toward MOVE_INTERVAL_MIN every time the snake eats food.moveInterval = Math.max(const MOVE_INTERVAL_MIN: 4MOVE_INTERVAL_MIN, this.Demo.moveInterval: numberCurrent ticks between steps. Starts at MOVE_INTERVAL_START each round and drops
toward MOVE_INTERVAL_MIN every time the snake eats food.moveInterval - const MOVE_INTERVAL_STEP: 1MOVE_INTERVAL_STEP);
// Keep counting every food for length / UI; currentMusicPitch() caps the exponent
// at MUSIC_PITCH_GROWTH_CAP so tempo stops climbing once speed has already maxed out.
this.Demo.growthCount: numberHow many food dots the snake has eaten this round. Music pitch uses this count
capped at MUSIC_PITCH_GROWTH_CAP so tempo plateaus with move speed.growthCount += 1;
this.Demo.syncMusicTempo(): voidGlides the live music loop to the playback rate for the current growth count.
No-op before the loop has started (still locked, or music failed to load).syncMusicTempo();
this.Demo.placeFood(): voidPicks a random empty grid cell for food.placeFood();
} else {
this.Demo.snake: {}snake.pop();
}
}
/**
* Freeze movement and remember when to restart.
*/
Demo.endGame(): voidFreeze movement and remember when to restart.endGame() {
this.Demo.gameOver: booleanWhen true, the snake does not move until restart.gameOver = true;
this.Demo.deathTick: number | nulldeathTick = 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;
... 106 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 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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.soundPlay: (clip: AudioClip, options?: SoundPlayOptions) => SoundRefPlays a loaded audio clip through the SFX voice pool.
Returns an inert
{@link
SoundRef
}
without allocating a voice when the clip hasn't finished
loading yet (or was already unloaded), when the pool has no free or stealable voice at this
priority, or before the engine has unlocked audio playback.soundPlay(this.Demo.gameOverClip: AudioClip | nullgameOverClip);
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;
... 106 more ...;
spritesRefresh: () => void;
}
Main BLIT386 API namespace used by runtime demos.BT.assignTag: (label?: string) => voidPlaces a labeled marker on the overlay timing chart at the current tick.
Requires `isOverlayTimingChartEnabled: true` in `configure()`. Tags scroll with the chart
history and are pruned when they leave the visible window. Empty labels become
`"Untitled"`. Chart width resets add an automatic `"Start"` tag.assignTag('Game over');
}
}
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 DemoMinimal snake with PipBoy CRT post-processing from crt-pipboy demo.Demo);