Capstone: The Interstellar Simulator
Four phases of skills, one game to bind them: a pocket galaxy that is pure at its core, scripted with effects, generated in parallel, and playable in a browser. This is the boss level — and you walk in carrying every weapon this course forged.
You will be able to…
- Scope a simulation game you can actually finish — and defend that scope
- Architect it across three seams: pure sim core, native tools, browser UI
- State a determinism contract and enforce it with a world-hash replay test
- Reconcile parallelism with determinism (split deterministically, merge in fixed order)
- Use AI as a boilerplate department — and only as that
The pitch
The Interstellar Simulator: a dozen star systems scattered on a starmap. Freighters haul ore and spice between them. Every tick, prices drift with supply and demand; the player draws trade routes and watches credits climb — or crash. Now and then a scripted event takes the stage for a few seconds: a comet flyby, a mining festival, a suspiciously cheap camel-shaped asteroid. That's it. That's the game.
Say the non-goals out loud, together, before writing a line: no combat, no diplomacy, no 3D, no networking, no infinite procedural universe. Twelve systems, three goods, one player. Each cut feature would make a fine sequel; none of them ships in v1. Small scope isn't modesty — it's the strategy that gets a two-person studio to demo day.
The architecture
Nothing below is new. That's the point — every box is a phase you've already beaten, and the capstone is the act of making them shake hands:
┌──────────────────────────────────────────┐
│ SIM CORE — pure OCaml │
│ step : world → world, fixed timestep │
│ Galaxy · Economy · Pathfinding seams │
│ events = effect scripts (4.1–4.2) │
└─────────────────────┬────────────────────┘
one library, two builds
┌────────────────┴─────────────┐
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ NATIVE TOOLS (4.3) │ │ BROWSER GAME (Phase 1) │
│ galaxy gen on domains │ │ canvas starmap + input │
│ economy tick offload │ │ Maks's HTML/CSS HUD │
│ replay + hash checker │ │ the game people play │
└──────────────────────────┘ └──────────────────────────┘
- Sim core (Phase 2). One function rules everything:
step : world -> worldat a fixed timestep. The world is an immutable record;stepbuilds the next one. Inside this box: no drawing, no input polling, no wall clock, no unseeded randomness — nothing that could make two runs differ. - Module seams (Phase 3).
Galaxy,Economy,Pathfindingeach live behind an.mli. The renderer arrives as a first-class module: the ASCII renderer from 3.3 becomes your debugger and your test rig; the canvas renderer becomes the game. - Effects (4.1–4.2). In-sim events are
Waitscripts run by your own scheduler, one beat per sim tick.comet.mlshould read like a screenplay, because it is one. - Domains (4.3). Only in the native tools build: galaxy generation fans out across cores, and the economy tick can run in a worker domain. The browser build never spawns a domain — same sim core, two builds.
- Browser front-end (Phase 1). js_of_ocaml compiles the sim core plus a canvas renderer; Maks's HTML/CSS HUD floats on top, exactly like lesson 1.5 promised it someday would.
The determinism contract
Write this on the wall of the studio: same seed + same inputs ⇒ same world, tick for tick. Everything pleasant flows from that law. Saves become tiny (a seed and an input log). Replays are free (re-run the log). Bug reports become perfect (send the log; the bug happens at tick 8 412 on every machine on Earth). And testing becomes possible: a bug is a reproducible tick number, not a mood.
The enforcement tool is blunt and wonderful — hash the world, compare runs. This runs right here in the page:
Break it on purpose: put + Random.int 3 into the credits
line (the comment marks the spot) and run again —
replay-stable: false. That one-line edit is exactly
the species of bug this tripwire exists to catch. Randomness is welcome
in the sim, but only from a seeded generator carried inside the
world state, so the log can replay it.
Deeper: how honest is Hashtbl.hash?
It's the standard library's polymorphic hash: fast, structural, and it only samples a bounded portion of very large values — fine as a tripwire for a pocket world, but know its manners. The asymmetry is what you're using: equal hashes mean "probably equal", but different hashes mean definitely different — and "different" is precisely what a broken replay produces. For v2, hash a canonical serialization of the world instead and the "probably" disappears.
Warm up the muscle the tripwire depends on — running a pure step
function n ticks forward:
Keep the tiny world and step from the
starter. Write advance : (world -> world) -> int ->
world -> world, which applies a step function n
times: advance step 3 w is step (step (step
w)), and advance step 0 w is w
untouched. Make it survive a million ticks (tail recursion — no
growing stack).
n: zero steps is just
w; otherwise it's advance with n - 1
starting from step w.let rec go n w = if n <= 0 then w else go
(n - 1) (step w) in go n w — the recursive call is the whole
result, so it's tail-recursive and a million ticks cost no stack.Determinism meets domains
One collision course must be named out loud. Lesson 4.3 taught that
domains finish in unpredictable order; this contract demands
identical output every run. The peace treaty is the pattern
bench_domains.ml already used: split deterministically,
compute independently, merge in fixed order. Sector i of
the galaxy is a pure function of (seed, i); whether domain 3
finishes before domain 1 changes nothing, because you join and assemble
by index, never by completion order. Completion order is allowed
to be chaos; assembly order is law. The first checklist below makes you
prove it — the parallel generator must match the sequential one, byte for
byte.
Milestones
Two checklists: the first makes it correct, the second makes it real. Tick items honestly — the coach countersigns. This is a multi-sitting project; expect the first checklist alone to span a couple of weekends.
Core: a simulator you can trust
Ship it
AI as your boilerplate department
Course principle number three, now at capstone stakes: AI kills the grind, not the learning. You have a tireless boilerplate department at your disposal — use it exactly there, and nowhere else. Three prompts that earn their keep:
1. The build scaffold — dune files are the department's home turf:
You are my build-system intern. OCaml 5.5, dune 3.x. Project layout: a library `sim_core` (pure, stdlib only), a native executable `sim_tools` that uses domains, and a js_of_ocaml executable `web_main` for the browser. No ppx anywhere. Write dune-project and every dune file, show the final file tree, and give the exact commands to build both executables.
2. The FFI binding — mechanical, fiddly, perfect delegation:
Write js_of_ocaml external bindings (no ppx) for the browser's localStorage: get, set, remove. Expose them through a storage.mli as save : key:string -> string -> unit load : key:string -> string option Include a short note on what can fail in private-browsing mode.
3. The interface review — the machine as critic, never as author:
Here is my economy.mli (pasted below). Review it as an API designer: which functions leak implementation details? What can't be unit-tested through this interface? Where would a rename improve clarity? List problems and questions only — do NOT rewrite my code; the code is my job.
And the standing rule for whatever comes back: read the diff like a code review from a stranger. If you can't explain a generated line, it doesn't go in.
Checkpoint quiz
Victory lap
When the last box of Ship it turns green, that's not just the
lesson — that's the course. Look at the distance: you started Camel Quest
asking a toplevel what 1 + 1 was, and you're finishing it by
shipping a deterministic, effect-scripted, parallel-tooled simulation
with a browser front-end. The honest way, too: every type and every rule
in that game came out of your two heads.
Take the lap. Your Progress page has
the whole map lit up — XP totals, every badge, and the export button.
Save the file. Frame it if you want; we won't judge. Then go play your
game — and when the feature ideas start shouting, well… you know where
SEQUELS.md is.
Loot collected
- A shippable scope is a small scope, defended in writing — non-goals are a design artefact.
- One pure core, two builds:
step : world -> worldon a fixed timestep, native tools with domains on one side, a js_of_ocaml front-end on the other. - The determinism contract — seed + input log ⇒ identical world — enforced by a world-hash tripwire.
- Parallel and deterministic at once: split by index, merge by index, prove it against the sequential run.
- AI writes the boilerplate; you write the types, the logic and the architecture. That division is the curriculum.