Development

npm run build      # Compile TypeScript to out/
npm run start      # Run the compiled game
npm run dev        # Build and run in one step
npm run debug      # Build and run in debug mode
npm run watch      # Watch mode for development
npm test           # Build, then run the test suite
npm run probe      # Build, then run a measurement probe

npm run probe with no arguments lists what there is to measure. Each probe takes a grid and a build:

npm run probe -- seen-versus-kill
npm run probe -- column --grid 205x50 --build browser
npm run probe -- free-flight --passes 2

Line Endings

Everything in the repository is LF, pinned by .gitattributes rather than left to whichever editor writes a file last. Git normalizes on the way into the index, so an editor that writes CRLF still commits LF and a diff stays the size of the change rather than the size of the file. The file is tracked, so a clone needs nothing configured.

Adding a new dotfile is the one thing that can need a local step. A global excludes file of the .* kind will hide it, and how you find out depends on how you staged it: git add <file> names the path, so git refuses it out loud and points at -f, while a bulk git add . or git add -A passes over it without a word. The quiet case is the one to watch for - if a dotfile you thought you staged never appears in git status, check it with git check-ignore -v --no-index <file> and stage it with git add -f <file>.

Tests

The suite uses the Node built-in test runner and adds no dependencies. It covers both builds and the agreement between them.

The terminal tests import the compiled output from out/, which is why npm test builds first. The browser build is a single self-contained index.html with no module boundary to import, so those tests read the file and evaluate its inline script up to the point where it starts touching the DOM. Everything above that line is game logic and rendering, which is what gets exercised.

The pulse cannon checks run at three grid sizes rather than one: the 80x24 a terminal opens at, the 60x20 floor a small browser window is clamped to, and the 205x50 a full-screen window gives at the default font. The hit test is not size-neutral - a row is worth about two world units of height at 80x24 and about one at 205x50 - so a check pinned at one size can pass while the game a player sees misses.

Probes

A probe reports; it never asserts. The figures quoted in comments, tests and the changelog come from test/probes/, so a number can be rebuilt from the repository rather than taken on trust, and a number moving is something you read rather than a build that fails. Each probe prints its own method - the grid, the placement walk, the ship heights, the band and the frame rate - above its table.

Probes and tests fly the same engagement, out of test/engagement.mjs, and both run against the two real engines. A figure taken off a scratch copy of the engine can be right about the copy and wrong about the game.

Most of them stage that engagement - one target parked in an emptied run, the ship steered onto it - which isolates the shot, and is not the shape a fault turns up in. free-flight is the other end: a run the engine opens for itself, sixty obstacles in the tunnel and volleys overlapping, watched frame by frame, with the walk over the firing columns no flown engagement ever reaches printed underneath it.

Every walk here gives the same numbers every time. A staged walk always did, because it places what it flies; the run the engine opens for itself did not, because it drew its sixty obstacles from an unseeded Math.random. Quoting one of those passes put figures in this repository that the next run fell outside of, and restating them as spreads did not help - two ten-pass runs of the same walk disagreed with each other. So the engine takes a seed: seedRng pins every draw either build makes, both builds draw the same sequence from it, and free-flight flies a fixed set of seeded worlds. --passes N narrows it to the first N while you iterate; a figure quoted anywhere here is taken over the whole set, or at the first seed alone where the suite pins it, and each table says which. The game itself stays unseeded - a run nobody can predict is the point.