How It Works
Terminal Version
The game uses a custom double-buffered screen renderer built on raw ANSI escape codes. Each frame, the screen buffer is populated with characters and colors, then flushed to stdout as a single optimized write. Input is handled via Node.js raw stdin mode. Terminals only provide key-press events, not key-release, so a key counts as held until its characters stop arriving, and the decay window is checked once per frame. Keys that act on the press rather than the hold get a longer window than the movement keys, so that the first character of an OS auto-repeat is not read as a second press. That single write is synchronous, so a terminal falling behind blocks it and the loop with it; the input layer tracks how long each frame took and discounts the time it was blocked, since a loop that was not reading is no evidence a key went quiet.
Browser Version
The browser version (index.html) is a self-contained HTML file that faithfully reproduces the terminal game as a canvas-based character grid. Each character cell is drawn to an HTML5 Canvas using a monospace font, matching the exact same rendering pipeline: screen buffer, perspective projection, tunnel drawing, entity rendering, HUD, and menus. The grid dimensions adapt dynamically to the browser window size: the font is drawn at its full size wherever the window has room for at least the 60x20 the game is laid out against, and shrinks toward that floor in a window that does not, so every cell the buffer holds has somewhere on the canvas to be drawn. A cell is a whole number of pixels wide, because a grid laid out on fractional columns loses its last column off the edge, so each one carries the difference between its width and the width the font actually draws at; the glyph is centred in that difference rather than packed against the cell’s left edge, which lifts the seam between two of the box characters the tunnel walls are built from most at the font sizes where that difference is widest, which are the sizes where the seam read worst packed against the edge, and which does nothing, or a little harm, where the difference is narrow, and the canvas is asked for exact glyph positions rather than hinted ones, which browsers measured so far honour either way, so the request insures the centring rather than being what carries it. At a comfortable font it is a fifth of a pixel nobody sees; at the smallest the fitting will settle on it is a real share of a cell. Keyboard input maps directly to the same control scheme. All game logic — collision detection, entity spawning, difficulty scaling, scoring, and debug modes — is identical to the CLI version.
Eight things differ, each because the medium allows or demands it. High score persistence uses localStorage, which the terminal has no equivalent for, so the CLI version keeps a best score for the session only; the top-ten table rides the same storage and is read back through the same guards, with one difference that follows from its shape - the stored value is JSON rather than a number, so a row that is not an entry is dropped on its own rather than condemning the nine good scores beside it. The damage screen shake offsets the canvas by a few pixels in the browser, while the terminal has no subpixel positioning and jolts the play area by a whole character column instead. Sound is synthesized with the Web Audio API: the engine names the events either way, queueing a cue for the frame it has just simulated, and only the browser turns those names into tones. A cue is played only when the audio context is actually running: a browser that blocks sound until the page has been touched hands back a suspended one, and the cues raised meanwhile are dropped rather than queued, so a run opened straight from a ?mode= link is silent until the first key and then plays from that moment on rather than releasing everything it missed. A browser that permits the sound outright — a returning player's high media engagement, or the site given a sound permission — hands back one already running, and the same link plays from its first frame. The CRT overlay is CSS laid over the canvas rather than anything drawn into the character grid, so C toggles it in the browser and it does not exist in the terminal. The colour scheme follows the system's own: a light desktop re-inks the palette the grid resolves through and repaints the page behind it, which a terminal has no say in, since there the colours are the terminal's. And a touchscreen gets a thumbstick and two buttons laid over the canvas, feeding the same two key maps the keyboard does — revealed on the first touch rather than on a capability check, because a laptop with a touchscreen is nearly always being driven by its keyboard, and taken down again on a window the fitter refuses, because the notice asking for the window back is the whole of what that screen has to say. A touch is remembered rather than spent, so the controls come back on the first window that holds the grid. A standard-layout gamepad is the seventh, and the only one with no events to listen for — the Gamepad API hands out a snapshot when asked and nothing else, so the page reads it once a frame and feeds the same two maps the keyboard and the thumbstick do. Its stick goes through the thumbstick’s own reading at full throw, so one dead zone and one set of eight sectors govern both. It releases only the keys it pressed, because a controller reports everything it is not holding as up and a pad left on a desk would otherwise clear the keyboard’s keys sixty times a second.
The eighth is the split screen, and it is the only one of the eight that is a way to play rather than a way to drive or draw a run: a browser window is wide enough to seat two pilots where a terminal, laid out against a 60-column floor, is not. It is built as a composition rather than as a second engine. A round holds two ordinary Game instances, each drawn by the same renderGame a whole screen is drawn by into a buffer half the grid wide, and the two buffers are laid into the full grid with a divider between them - so the rules a pilot flies under are the solo game's rules and not a second copy of them that could drift. Three seams are all the shell adds. The keyboard is split into two maps onto the names the engine already reads, so neither run knows it is one of a pair. The one global source of randomness is pointed at a stream of that pilot's own for the span of their step and put back afterwards, because two runs stepped off it in turn would each be taking the next number out of the other's sequence; both streams come from one seed drawn when the round opens, which is what makes the opening field the same in both halves. And the round reads a pilot's own mode for whether they are still flying, which is also why a round never reaches the table: a qualifying score leaves the engine on the name entry screen, the round reads that as out exactly as it reads a plain death, and nothing is ever routed there.