Patrick Hulin wanted to play SimTower again — the 1994 elevator-simulation game is effectively unplayable without an emulator and no engine spec exists. His plan was the sensible engineering one: have LLMs reverse-engineer the binary into a complete spec, then reimplement from that spec.
That plan failed. The artifact shipped anyway — towers.world is a co-op clone of the original — but it only worked once he stopped asking the model to reason about a spec and started letting it hill-climb against traces from an emulated binary.
The static-analysis phase failed in three specific ways:
- Premature conclusions stick. The model records an early guess about a subsystem, then struggles to abandon it. It also picks the wrong abstraction level — “runtime entity” for a sim, “queued-car continuation” for a sim queueing for an elevator. It named the multi-floor lobby code the “lower-atrium band,” a term still lodged in Hulin’s Ghidra database because the model used it everywhere.
- It won’t record what it learns. Prompts demanded function-by-function analysis with every local and parameter named. The model skipped the naming anyway.
- It can’t separate design from implementation. Asked for a clean-room spec, it either leaked binary layout details or went so abstract the result couldn’t be reimplemented.
Underneath all three: context. Disassembly and decompiler output are too verbose, and the model “compacted and compacted, forgetting the details of what it just explored.” Reverse engineering needs every precise detail in working memory — summaries do not survive contact with the task.
The fix was to give the model something to be wrong against:
- Build a Unicorn emulator with mocks for each of the 195 Windows 3.1 API functions the game calls. One-sentence prompt to Claude Code, roughly thirty minutes of work, 99% correct — one loader bug, which the model later found and fixed itself.
- Drive the emulated binary to build test towers, snapshot tower state every few game ticks, and diff the reimplementation against those traces.
- Let the agent fix divergence autonomously. Longest unprompted run: about eight hours, five parity bugs fixed and committed.
Parity turned out to require RNG call ordering to match exactly — which meant reproducing something like the original’s slab allocator for sims, because rooms claiming and releasing slots changes when RNG calls happen. That is the level of detail where “looks like the game” becomes “is the game.”
The takeaway is uncomfortable for anyone doing spec-driven work with agents: the closed loop is the capability. “Repeating ‘make it better’ does not solve the problem,” Hulin writes — you need dynamic verification on the backend. Same shape applies to any port or migration with an oracle available. The cost is real too: an “absolutely ridiculous number of tokens,” a $200/month plan, and deliberately working around peak-usage windows.
And it worked well enough that the function-by-function constraint got abandoned — the reimplementation converged on the original’s structure. Once you have a perfect reproduction with named functions and comments in a high-level language, you can translate it again later.