Skip to content

The simulator

The simulator runs in the browser, and it is also the editor. It preceded the board, and it reproduces the board's measured contract: the cost of the blitter, the pool, the tile background, the slowness of the text mode. Everything that defines the behaviour of the console is the same code as the firmware; what the simulator adds is only the plumbing of the browser. That is why its rasteriser is software rather than WebGL: it must exhaust the same budget per line as the silicon and fail the same way, a line too busy shows stale, a sprite not yet in the pool misses a frame. A backend made of textured quads would give "it works in the simulator, it flickers on the board".

Keys: the arrows are the D-pad, Z is the A button (fire in the examples), X is B.

A game is a project

The simulator lives in the App, and a game is a project in the cloud — there is no local folder to open. The tree on the left lists two kinds of projects: published games (anyone's latest published version — the gallery examples live there), read-only and clonable, and your drafts, editable and saved in the cloud. To modify a published game, clone it: the clone is yours.

Clicking a .lua opens it in the editor; clicking a PNG shows a preview and flags a non-indexed format.

Only indexed-colour PNGs are accepted, the native output of Aseprite. On the board the art is pre-quantised in NOR flash; quantising on the fly in the browser would reintroduce a divergence on the indices. Index 0 is transparent for sprites, and it is also the hole of the background: a sprite drawn "behind the background" appears only there.

Lua modules go through the same channel: require("enemies") asks the game folder for the asset enemies.lua. No package.path, no path, a flat name resolved there and nowhere else. On the board it is the micro-SD card or the NOR flash behind the same mechanism.

The game folder carries its palette groups (game.pal, game.atlas, the PNGs the atlas names), its variants (game.nuit.pal) and its tile backgrounds (game-niveau1.map + game-niveau1-tiles.png); gfx.bundle() installs everything. See your first game.

Edit and run

  • Ctrl+Enter (or ⌘+Enter) runs the game again, Ctrl+S (⌘+S) saves the file — to the cloud, in your draft.
  • A published game is frozen: saving is refused, clone it first.

The status bar

The status bar gives the one number that matters: the load of the busiest scanline. The budget is per line, not per screen; thirty sprites lined up do not pass where two hundred spread out do. Late lines, shown stale on the board, are tinted red; the sprites that are missing (not in the pool in time) are counted.

Gamepads

The Gamepad API requires a secure context, a gesture in the page and a press on the gamepad. On macOS, a Nintendo gamepad paired over Bluetooth is seen only by Safari; Chromium does not see it, and it is not a setting.

Determinism and traces

The logical frame is the call to _update(), not the video scan. The console slows down, it never skips. No catching up with an accumulator: the number of calls must not depend on the real time elapsed, otherwise two runs of the same game diverge.

What goes with it: a budget in VM instructions rather than microseconds, the input latched at the start of the frame, a PRNG seeded explicitly (math.randomseed() without an argument is refused), and _draw() which does not modify the state.

A trace is the sequence of the inputs of a game — about 2.5 KB per minute of real play, since nothing is written as long as nothing changes. A game announces its score with sys.score(n). That is what the leaderboard server does: it does not take a score on trust, it replays the trace and reproduces it.