Skip to content

The simulator

The simulator runs in the browser, and it plays: it writes nothing. A game is written in the App's workshop, which runs it with the same engine. The simulator 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, the space bar is the A button (jump, fire), X is B. Z also works as A, for the fingers that learned it first.

A published game

The simulator plays the games published in the App — there is no local folder to open. The list on the left gives every published game (anyone's latest version; the gallery examples live there). A published game has its own address, …/sim/?jeu=<author>/<project>, which the browser's bar shows as soon as it is opened: that is the one to share.

Clicking a .lua shows its code, read-only; clicking a PNG shows a preview and flags a non-indexed format. An error takes you to its line.

To modify a game, clone it in the workshop: Public library, Games tab, Clone into my projects. Signed in, the simulator gives the link (clone it in the workshop ↗). The clone is yours, with the game's levels, their maps, models and sprites, ready to be modified and recompiled.

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.

Run

  • Ctrl+Enter (or ⌘+Enter) runs the game again.
  • The dividers can be dragged: between the screen and the code (the screen grows in whole steps, ×1, ×2…), under the game and file lists, between those two lists, and above the errors. Sizes are remembered by the browser; double-clicking a divider restores its original size.

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.