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…

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     │
└──────────────────────────┘   └──────────────────────────┘

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).

Recursion on 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