allanlotta
~/blog/rewriting-empire-earth-with-agents

Rewriting a 2001 RTS with agents that can test their own work

Empire Earth came out in 2001. I wanted it on macOS, with tens of thousands of units and an AI that fights back. Nobody was going to ship that, so I started a rewrite in Rust.

Most of the code was written by agents. My job was designing the system they work inside: what they can see, what they can touch, how they prove they are right, and how they get better at it. This post is about that system, because the shape of it has nothing to do with games.

The loop

Everything the agents do is one loop. The order bends depending on the question, but every piece is always there.

 ┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
 │ collect  │──▶│ analyse  │──▶│  build   │──▶│ test in  │
 │ original │   │ evidence │   │  remake  │   │  the lab │
 └──────────┘   └──────────┘   └──────────┘   └────┬─────┘
       ▲                                           │
       │                                           ▼
 ┌─────┴────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
 │ improve  │◀──│  record  │◀──│   fix    │◀──│ compare  │
 │  tools   │   │  memory  │   │          │   │ original │
 └──────────┘   └──────────┘   └──────────┘   └──────────┘

Collect what the original does. Analyse it until there is evidence. Build the equivalent in the remake. Test it in a lab where the result is a number. Run the same scenario in the original and compare. Fix what differs. Write down what was learned, including what turned out to be wrong. Then improve the tool or the rule that would have caught it earlier, and go again.

The agent is not a step in this loop. The agent runs the loop.

The layers

              ┌──────────────────────────────┐
              │  me: goals, review, priority │
              └──────────────┬───────────────┘
                             │
              ┌──────────────▼───────────────┐
              │   coding and research agents │
              └───────┬──────────────┬───────┘
                      │              │
        ┌─────────────▼────┐   ┌─────▼──────────────┐
        │      memory      │   │       tools        │
        │                  │   │                    │
        │ one file/finding │   │ 96 python pipelines│
        │ topic retrieval  │   │ disassembly, RTTI  │
        │ canon pages      │   │ asset parsers      │
        │ superseded flags │   │ game CLI (windows) │
        └─────────────┬────┘   └─────┬──────────────┘
                      │              │
              ┌───────▼──────────────▼───────┐
              │          evaluation          │
              │                              │
              │  original game lab           │
              │  remake scenario lab         │
              │  UI parity lab               │
              │  fidelity comparator         │
              └──────────────────────────────┘

I sit at the top and decide what matters. Agents own the middle. Memory and tools are what make the next session start further along than the last one. Evaluation is what stops "looks right" from being an acceptable answer.

Collect

Before writing anything new, agents took the original apart. Six proprietary archives, 4,940 files. All 730 3D models parsed, 727 of them byte-exact on round trip, exported to glTF for the new renderer. All 58 AI scripts flattened into 3,115 concrete state transitions. Every database table, every scenario, 3,398 localized strings.

None of this was done by hand. Each format got a parser, each parser became a pipeline with a CLI and a validator, and each pipeline stayed in the repo for the next agent.

Analyse

Static first, runtime second. Disassembly explains why; the running game says what. Agents work on the 32-bit Windows executable from macOS with LIEF and Capstone: 1,132 C++ classes catalogued through RTTI, vtables named, function addresses mapped to class and slot, and the whole lockstep message layer recovered: 103 command types with their wire layouts.

The best example of the method is a mistake. An early pass concluded the AI tuning table was editor metadata, because a search for references to the table found six. A later pass noticed the compiler had folded each access into an absolute address, scanned the whole range, found over 200, and proved at runtime that the table is the AI's control panel. The wrong finding was not deleted. It was marked superseded, and the rule "scan the interval, not the base" went into the process.

Build

The remake is Rust on Bevy ECS, three crates: data, simulation, AI. Agents write against the canon pages, not against their memory of the game. When a fact is missing, building stops and the loop goes back to collect.

Test in the lab

The remake has a headless scenario lab. An agent writes a JSON scenario: units, seed, telemetry to record, when to stop, what to assert. It runs the real simulation through the normal command path and gets JSON lines back.

 scenario.json ──▶ ee_sim (headless) ──▶ events.jsonl
   seed: 42                                 damage
   units: 30 v 30                           moved
   stop_when: t > 120s                      trained
   assert: red wins                         tick_ms

No pixels, no mouse, no asking me to play. Combat, pathfinding, economy, formations, AI behaviour and performance are all questions answered in seconds. Anything that once found a bug stays as a regression. A fuzzer generates random command streams and checks invariants like resource conservation and "no unit is stuck in a state that makes no progress".

The interface has the same treatment. The original's HUD is data, not images, so a lint compares the remake's UI tree against the original's tables, clicks real controls, and diffs screenshots per control.

Compare with the original

There is a Windows machine on the network with the game installed. From the Mac, agents can launch it, screenshot it, read its memory, and, since the message layer was decoded, drive it by code.

 mac (agent)                       windows (original game)
 ─────────────                     ───────────────────────
 ee-inject select 0x01000001  ──▶  same message the UI sends
 ee-inject move 388 196       ──▶  lockstep queue ──▶ engine
 ee-scan selected             ◀──  memory read: position, state
 ee-call FindUnitById         ◀──  ask the engine directly

Twenty native commands are proven: select, move, gather, attack, build, train, research, patrol, formations, game speed. The same JSON scenario runs in both engines and a comparator normalizes tick rate, origin and map scale before comparing movement, combat and economy.

That is how the remake got its numbers. Wood is 15 units per trip at 0.209 per second. A citizen hits a hippo for 3.75 every 1.955 seconds. Buildings rise out of the ground during construction, they do not scale. A zero in the damage matrix means immune, not one damage. Walls are a cut across the path to the enemy capitol, not a fence.

Fix

A fix is only a fix if the number moves. In one bug hunt, eleven fixes shipped with before and after measurements. One proposed fix was measured, made things worse, and was rejected in the same document. That document is the evidence for the next agent who wonders why the obvious fix is not there.

Record

Every finding is one file with topics, evidence, status and links. A query tool loads only the topic at hand and flags entries that were later refuted, superseded or reverted. Canon pages hold what is currently true; the log holds how we got there.

 question ──▶ findings_query --topic combat
                 │
                 ├── F-0212 current      ← cite
                 ├── F-0198 superseded   ← do not cite, read why
                 └── canon/combat.md     ← the truth today

This replaced a single report that had grown to 375k tokens, which nobody, human or agent, could read. Contradictions had been piling up because the rule said "read everything" and nothing could.

Improve

Two rules make the loop compound. Any task done twice becomes a tool. Any mistake that cost time becomes a line in the operating rules, next to the check that catches it.

The project file has a list of these: a modal screen freezes the game while the tick counter keeps rising, so require the modal flag to be false before concluding a command failed. The shared Windows box returns contaminated screenshots in silence, so check the image, not its name. A test that passes under a tuning the real game never uses proves nothing, so always state which tuning is in play.

Each one was a day lost once. None of them has been lost twice.

Why this is not about games

Swap the nouns and the loop is a business process.

 game                        any company
 ─────────────────────       ─────────────────────────────
 original executable         the legacy system, or the way
                             the work is done today
 findings + canon            what is actually true about it
 parsers and pipelines       the tools your agents build
 scenario lab                a test that returns a number
 original game comparison    the same test against reality
 superseded findings         mistakes kept, not hidden
 rules in CLAUDE.md          the process improving itself

The hard part was never the model. It was giving agents memory that does not lie, tools they can extend, and a way to check their own work against reality before I look at it. That is the same three things for a QA agent, a hiring agent or a content pipeline.

What I did

I did not write the pathfinder or the AI placement code. I designed the loop, built the first version of each tool, wrote the rules, reviewed what came out and measured it. Then I made sure every lesson ended up in a tool or a rule, not in my head.

Where it stands: 96 reusable pipelines across 26 suites, 275+ findings, 20 native commands into the original, 27+ preserved regression scenarios, and a remake where the AI builds a base, walls it, and advances an epoch on its own.