Determinism & testing
The seedable RNG, the headless surface, and the test suite that plays whole tours without a browser.
The game is one client-side file, yet it has a fast, deterministic test suite with zero dependencies — only Node's built-in test runner. The whole thing rests on two mechanisms you've already met: the HAS_DOM seam and a swappable RNG.
Determinism: one swappable rand
Every random draw in the codebase goes through helpers (rnd, chance, pick, shuffle) that read a single module-global rand. The test surface can swap it for a seeded generator:
3909 setSeed: function (n) { rand = (n === null || n === undefined) ? Math.random : mulberry32(n); },
With a seed set, a whole playthrough is byte-for-byte reproducible. This is the reason the "route new randomness through rand" rule (from data-driven design) is non-negotiable — a stray Math.random() would make seeded tests flap.
The headless surface
When HAS_DOM is false, the IIFE exposes globalThis.TerrapinTrail (game.js:3752) — the catalogs, the lifecycle functions the UI calls, getState(), setSeed(), and viewHtml(). The test harness imports the game (triggering that surface) and offers three helpers:
freshGame(role, seed, year)— seed, pick a tour, start a run.step()— advance one interactive transition (it drives the real state machine:chooseEvent,continueResult,finishDrive,lockMini, …).settle()— run transitions until things stabilize.
Because step() calls the same entry points the browser's handle() dispatcher does, a test plays the game exactly as a human would, minus the pixels.
The suites
| File | Covers |
|---|---|
test/rules.test.js |
economy, mods, van upgrades, the Peak, catalogs, crew/perks, achievements, save/resume, and a "every screen renders non-empty with no stray undefined" sweep |
test/playthrough.test.js |
full seeded tours, start to finish |
test/hall_validate.test.js |
the leaderboard validator (imports only _validate.js, staying dependency-free) |
test/sizes.js |
the gzipped-size ratchet |
The render sweep is worth calling out: it renders every viewHtml case headlessly and asserts the markup is non-empty and undefined-free — a cheap guard that catches a whole class of "I added a screen and forgot a field" bugs. The full run takes well under a second, which makes it the cheap inner loop; a real browser is only needed to eyeball visuals and animation.
This version's tests
The table above describes the original standalone project's Node suite. In this workspace the game's tests are Python (test_terrapin_trail.py, run with pytest): they boot the whole server over seeded archive caches with no network, exercise every route (bands, years, tagged tours, rules codes, run tickets, score checks, name filtering, share counts, Tour of the Day, the rule gallery), and drive the real game in a browser from a tagged tour down to its first lot. A guide test checks that every excerpt, Q&A answer and function-map entry still points at exactly one line of the live code, so this guide can't silently drift.
- Determinism comes from one swappable
rand+setSeed; that's why all randomness must route through the helpers. - Tests drive the real state machine through the same entry points the UI uses — no logic is duplicated.
- A headless render sweep over every screen catches missing-field bugs; the whole suite runs in under a second.