Skip to content
Naftali Lubin

Need a website? Let’s talk

Case study · 2026

Verticopolis

A browser-native homage to SimTower (1994), with a portable simulation engine.

Version
2.33.0
Rule-sets
Classic (1994 rules) or Modern, fixed per tower
Tower bounds
100 floors up, 10 basement levels
Engine
TypeScript by default; Rust/WebAssembly port available as an opt-in
Runs on
Any modern browser; installable as a PWA, saves before updating
A 36-floor Verticopolis tower at night with offices, hotel floors and condos lit up, and the build tools panel on the left.
Actual screenshot A five-star tower at night, Classic rule-set. Art CC BY 4.0.

What it is

Build a vertical city floor by floor. Wire it with elevators, attract offices, condos and hotel guests, set the rents, and climb the star ratings toward TOWER. Classic mode follows the 1994 rules; Modern adds rentals, amenities and household variants. Every sprite is drawn in code.

Classic’s departures from the original are documented, and Modern’s variants include two-to-five-person condos. Built with TypeScript, Excalibur.js, lit-html, Tone.js, Rust, WebAssembly, Vite PWA.

Gameplay and the tower simulation

Tenants are routed one by one, with an aggregate congestion model underneath, and ride elevators dispatched with a SCAN algorithm, so a badly placed shaft shows up as a lobby full of waiting people. Stars unlock at 300, 1,000, 5,000 and 10,000 people; TOWER also needs a Wedding Hall, a metro and a passed VIP inspection.

A crowded Verticopolis lobby at the morning rush, with tenants waiting for elevators.
Actual screenshot The morning rush. Art CC BY 4.0.

Evolution

The first commit, in February 2026, held three SimTower prototypes built from the same basic prompt by agents from ChatGPT, Copilot and Gemini. None of them was particularly good, and the project was rebuilt from scratch in June.

  • A dark, mostly empty grid with a build menu of room types: an early prototype from February 2026.
    Rebuilt from sourceFeb 2026 · One of three first prototypes, the ChatGPT agent’s attempt
  • A pixel-art tower under the Tower Tycoon name, with a tools panel.
    Actual screenshotJun 29, 2026 · Rebuilt from scratch as Tower TycoonView this version
  • A smaller pixel-art tower on the first Excalibur.js build.
    Actual screenshotJun 30, 2026 · First Excalibur.js buildView this version
  • A 40-floor tower in the Verticopolis interface.
    Actual screenshotJul 29, 2026 · 2.0: renamed Verticopolis, Modern rule-setView this version

Status timeline

  1. First prototypesShipped

    The Copilot attempt was later converted to TypeScript.

  2. Rebuilt from scratchShipped

    A clean-room rebuild on Excalibur.js, renamed Verticopolis on July 1.

  3. 2.0 and real 1994 savesShipped

    Import and export of the original game’s .TDT save files; 2.0.0 shipped on July 29.

  4. Fixed-step simulation and conformanceShipped

    A deterministic fixed-step simulation, a conformance suite for both engines, and the Rust port held to it.

  5. WebAssembly opt-in and benchmarkShipped

    The Rust engine runs in the browser behind an opt-in, with the TypeScript engine as the default and fallback, plus a paired TypeScript vs WebAssembly benchmark.

  6. Rust-only 3.0 cutoverIn progress

    The cutover plan and engine catalog are merged. The cutover gate, the read model and the command layer are still open.

Architecture

The game is a TypeScript host on Excalibur.js. The simulation sits behind an engine boundary, so it does not depend on any one frontend: the renderer, interface, audio and saves talk to it through commands and read its state back.

Diagram

Verticopolis architecture todayThe TypeScript host runs the Excalibur.js renderer, the UI panels and inspector, Tone.js audio, saves and .TDT import and export, and a read model of the tower. When the opt-in is on, the host steps the Rust simulation engine compiled to WebAssembly, which reads state and exports back. The Rust engine has no file, network or clock access and produces state digests. Conformance scenarios and a nightly fuzzer compare the TypeScript and Rust engines' digests.TypeScript hostExcalibur.js rendererUI panels, inspectorAudio (Tone.js)Saves, .TDT import/exportRead model of the towerRust engine (WebAssembly)Deterministic fixed stepPeople, transport, economyNo files, network, clockState digestsstep (opt-in)state and exportsConformance scenarios and a nightly fuzzer compare both engines
Illustration: how the TypeScript host and the Rust engine connect today.

From TypeScript to Rust

The simulation has been ported to Rust and compiled to WebAssembly. Today it is an opt-in: the TypeScript engine is the default, and the Rust engine runs when a player adds ?engine=wasm to the address. The plan is a Rust-only 3.0, gated on a written cutover plan; until that gate passes, both engines ship and must agree.

Determinism and conformance testing

Two engines can only coexist if they produce the same tower. The simulation runs on a fixed time step, so it gives the same result at any frame rate. A conformance suite pins state, crowd and event hashes for a library of scenarios and runs them against both engines, and a nightly fuzzer feeds both the same random command streams and compares their digests. The port includes a Mulberry32 random generator rebuilt on wrapping 32-bit integers, so both engines draw the same numbers.

Engineering challenges

Saves a 1994 game can open

Verticopolis imports and exports the original game’s .TDT save files, and round-trips them against the real game: population, elevators, lobbies and views have to survive the trip in both directions.

Keeping two engines honest

The conformance suite earned its keep early: it caught three places where the Rust port had drifted from the TypeScript engine.

The cost of hosting an engine

Once the game hosts it, the Rust engine is slower than the TypeScript one on every test tower, including the two where it wins on its own. Performance, below, has the numbers and where the time goes.

How it was built

AI coding agents wrote most of the implementation. I made the product and architecture calls and set the rules and review bar they work under.

Tools: Claude Code, the GitHub Copilot coding agent and Codex reviews, working inside the BMAD method, an open-source workflow for planning and reviewing work with AI agents.

Workflow, with a public example for each step

  1. DecideMe

    Product and design calls, written down with the options that were rejected.

    Example: A decision log
  2. SpecifyMe + agent

    Specs, requirements and stories written before code.

    Example: A spec-first change
  3. PlanMe + agent

    Larger changes get a written plan with stages and exit criteria.

    Example: The 3.0 cutover plan
  4. DelegateMe

    Bounded tasks go to Claude Code or GitHub Copilot sessions, each claiming an issue first.

    Example: The claim-an-issue rule
  5. ImplementAgent

    Agents write the code for each task, following the repository’s written rules.

    Example: Commit history
  6. VerifyCI

    Typecheck, lint, tests with coverage floors, conformance and screenshot drift.

    Example: Conformance caught three Rust drifts
  7. ReviewAgents + me

    Adversarial review skills plus Copilot and Codex reviews, repeated until a pass finds nothing new.

    Example: A change reviewed in ten rounds
  8. TriageMe

    Each finding is fixed now or deferred to the backlog with a tracking issue.

    Example: A deferred finding
  9. Approve and mergeMe

    Pull requests merge under my account. Agents may arm auto-merge only after review sign-off and green CI.

    Example: The merge rules
  10. MeasureAgent + me

    Claims about speed are measured with reproducible, paired benchmarks before they are made.

    Example: The TypeScript vs WebAssembly benchmark
  11. Refine the rulesMe

    When something goes wrong, it becomes a written rule for the next session.

    Example: A rule added after an incident

Performance

Snapshot. Measured Oct 14, 2026 on app 2.30.0 (commit 01c6153): headless Chromium 141 in one 4-CPU container, medians of 10 paired runs over 24 in-game hours (720 steps). This is a test machine; players’ hardware will differ. The large-tower bare result is inconclusive; paired 95% intervals are in the source document. Live figures: docs/performance/ts-vs-wasm.md.

The Rust engine does not make the shipped game faster today. On its own, the bare engine ran the small and medium towers in about half the TypeScript time. Hosted inside the game the way it is wired today, the same work took about 3 to 5.5 times as long at normal CPU speed, and up to 8 times under a 4x CPU throttle.

The benchmark write-up puts roughly a fifth of that time in the engine’s step, a fifth in reading its exports, and at most three fifths in host JavaScript.

Measured

Rust/WebAssembly time as a multiple of TypeScript timeSmall tower: bare engine 0.55 times, as hosted in the game 4.02 times the TypeScript time. Medium tower: bare engine 0.54 times, as hosted in the game 3.23 times the TypeScript time. Large tower: bare engine 1.08 times, as hosted in the game 5.45 times the TypeScript time. Below 1 means WebAssembly was faster; above 1 means it was slower.0×1×2×3×4×5×6×← WebAssembly fasterWebAssembly slower →Small tower729 people0.55×4.02×Medium tower3,240 people0.54×3.23×Large tower17,322 people1.08×5.45×
Bare engine (circle) · As hosted in the game today (square) · 1x CPU, medians of 10 paired runs.
WebAssembly time divided by TypeScript time. Below 1× means WebAssembly was faster.
TowerBare engineAs hosted today
Small (729 people)0.55×4.02×
Medium (3,240 people)0.54×3.23×
Large (17,322 people)1.08×5.45×

On the large tower at normal CPU speed, the game averaged 6.83 frames per second on the TypeScript engine and 4.47 on the hosted WebAssembly engine; the slowest 1% of frames dropped from 4.62 to 1.94 fps.

Testing and quality gates

Every change goes through typecheck, lint, unit tests and a production build. Two engines that must agree need more than that, so these run too:

  • Conformance scenariosPinned state, crowd and event hashes, run against both engines
  • Nightly differential fuzzingRandom command streams; TypeScript and Rust digests must match
  • Engine independence testNo file, network, console or clock access inside the simulation
  • Coverage floors93–94% of the TypeScript (86% of branches) and 87% of the Rust
  • Screenshot drift gateThe screenshot gallery is rendered in a pinned Playwright container

Status

Playable today at verticopolis.com (version 2.33.0), in the browser or installed as an app.

Last updated .

Contribute

I’m looking for contributors to Verticopolis. The code is open source, and the contributor guide covers setup, the quality gates and how changes get reviewed.

Receipts

The public pull requests and documents behind this page.