ScreepsAiSLP Build Log · Part 5 of 8
ScreepsAiSLP Build Log – Part 5: From Broken Creeps to v0.1
"All tests pass"
Milestone one, M1, was supposed to be the easy one: spawn creeps, harvest energy, feed the spawn, upgrade the controller, build whatever needs building. Every Screeps tutorial covers it in the first hour.
The agent finished its first supervised run, committed, wrote a blog post about it, and reported that everything worked and the tests passed. It took about a day and a half of reviewing, rewriting tests, and one embarrassing mistake of my own before that was actually true.
This is that day and a half.
The first version, and my own wall
Reading the code told a different story than the summary:
| What it did | Why that's wrong |
|---|---|
| Harvesters tried to hand energy to the controller | The controller doesn't accept energy transfers. You upgrade it with upgradeController. |
| Some creeps were spawned without MOVE parts | A creep without legs never leaves the spawn. |
Creep names contained NaN | A name counter in Memory was never set up, so the math produced "not a number". |
| The builder took energy out of the spawn | That starves the spawn of the energy it needs to make new creeps. |
| "Integration tests written and passing" | There were none. Its blog post even claimed they needed MongoDB, which wasn't true. |
The last one was the worst, because it was a confident report about work that didn't exist. That's when I stopped trusting summaries and started protecting tests. I wrote an acceptance test myself: run a real Screeps engine for 800 ticks and check that energy actually reaches the spawn, every role exists, every creep can move, no name contains NaN, and the controller makes progress. The launcher restores it from a root-owned copy before every check, so the agent can't edit it.
Then the acceptance test failed. For every version of the code, including the fixed one.
I first blamed the agent's harvester. That was wrong. The real cause was mine: the test placed the spawn at a spot that, on that map, sits inside solid wall with walls all around it. No creep could ever leave, so nothing could ever pass. Once I moved the spawn to open ground and checked that the old code failed (0 deliveries) and the fixed code passed (22 deliveries, level 2), the test finally meant something.
Lesson I keep relearning: a test you haven't seen fail is a test you haven't checked.
Harvesters that freeze, and a test that notices
The cleanup run fixed most of the list (it also tried to cheat its tests, which got its own story in Part 4). It introduced a "working" flag for every creep: switch to work mode when your store is full, back to gathering when it's empty. Classic Screeps pattern.
Except the harvester kept one old line: only deliver if the store is completely full. Picture a harvester carrying 50 energy while the spawn only needs 20. It delivers 20 and keeps 30. Now it's not full, so it won't deliver again. It's not empty, so it won't go harvest. It just stands there. Forever. One by one, every harvester parks itself.

The acceptance test passed anyway, because it only asked for at least one delivery in 800 ticks, and the early deliveries work fine. The agent's own new unit test passed too, because it only checked the flag, not whether anything was delivered.
So I added a freeze detector to the acceptance test. Every tick it watches each harvester that's in delivery mode, and counts how long it keeps exactly the same energy at exactly the same spot. A working harvester is always doing something: walking, delivering, or upgrading. If one stands still with unchanged energy for 30 ticks, the test fails.
Then the important part: proving it works on the real engine, with both versions of the harvester.
| Harvester code | Longest freeze | Controller progress | Result |
|---|---|---|---|
| Old ("only when full") | 342 ticks | 82 | fails: frozen harvester |
| Fixed (working flag only) | 14 ticks | 289 | passes |
The fix also more than tripled the controller progress in the same 800 ticks, which tells you how much of the colony had been standing around. A later run rewrote the agent's unit test properly too: with 20 of 50 energy, the harvester must actually call transfer on the spawn, or upgradeController when the spawn is full. That one fails on the old code as well.
The colony that froze, and it was me
I deploy by hand: build the TypeScript into one main.js on my main computer, paste it into the game's script editor, save. I'd been testing on the real official server, not just a private one. The fixed harvesters went live, and for half an hour the colony looked great. Builders were even repairing old walls (a little too enthusiastically, which became a later fix).
Then everything stopped. Every creep, mid-step.
We went through the usual suspects from the game console, one at a time:
- Is the game ticking?
Game.timewent up. Not a server problem. - Out of CPU? The bucket was full at 10,000, and the shard had its CPU assigned.
- Out of energy? One source was refilling, the other was full and untouched.
- Traffic jam? No. I had killed the builder to see if the spawn would replace it. It didn't.
- Is our code even loaded?
Object.keys(require('main'))returned[].
That empty list was the answer. The loaded script didn't export a loop function, so the game had nothing to call each tick, and it silently did nothing. No error, no warning. The built file on my computer was fine. The copy in the game wasn't. Most likely an incomplete paste.
I pasted the whole file again and the colony came back to life within a tick. Now there's a one-line check after every deploy: run JSON.stringify(Object.keys(require('main'))) in the console, and it must print ["loop"].
v0.1
With the freeze fixed, the new test proven and the colony running again, I tagged the first known-good version as v0.1 on GitHub (after deleting a v.01 tag I fat-fingered first). Tags are what goes to the official world from now on: a fixed bookmark I can always roll back to.
How the milestone actually went:
| When | What happened |
|---|---|
| Day 2, evening | First run with no internet (wall repairs, a real harvester unit test, real game constants); passed the gate by hand after a test-server hiccup |
| Day 2, afternoon | Tagged v0.1 |
| Day 2, afternoon | Colony froze on the official server; cause: an incomplete paste |
| Day 2, afternoon | Freeze detector added to the acceptance test; fails on the old harvester, passes on the new |
| Day 2, midday | Fix run: harvester freeze fixed, honest Correction section in the agent's blog; first fully clean gated push |
| Day 2, morning | Cleanup run: real fixes, plus an attempt to patch its own tests (caught, discarded) |
| Day 1, night | Acceptance test written; spawn-inside-a-wall bug found and fixed |
| Day 1, evening | First supervised run: five bugs and a made-up claim about tests |
What I learned
- Review the code, not the summary. The summary said "done" three times before it was true.
- Prove your tests can fail. Both of my acceptance tests were useless until I watched them fail on broken code.
- Measure behaviour, not flags. "Is the working flag set" passed on frozen harvesters. "Did anything move or deliver" didn't.
- Corrections belong in public. The agent's own blog now has a dated Correction section listing everything it got wrong, including the cheating. That's more convincing in a portfolio than pretending it was perfect.
- Add a check after every manual step. The
["loop"]check takes two seconds and would have saved me twenty minutes of debugging.
What's next
The agent now works on its own. A timer starts a run at midnight, 6 am, noon and 6 pm. Each run picks up the next milestone, M2 (extensions and roads), and keeps going until a test I wrote passes. The gate decides what gets pushed; I review afterwards and decide what gets tagged.
Two small additions make that safe to leave alone. A lock means a scheduled run and one I start by hand can never edit the project at the same time. And after every gate, the launcher writes the result into a file called LAST_GATE.md: which checks passed, whether it pushed, and the last lines of the test output. The next run reads it first. That file is the agent's memory between runs.
Postscript: the first run on its own
I started the first scheduled run by hand to make sure the agent works without a terminal window. It did. Ten minutes later it had written a 258-line construction manager and committed it. The gate then took about 17 minutes and said:
| M2 check | Result |
|---|---|
| Controller at level 2 | passed |
| All 5 extensions placed | passed (5 construction sites) |
| Roads placed, at least one finished | passed (2 built, 48 more planned) |
| At least one extension finished | failed (0) |
So, not bad for ten minutes, and not done. It planned 48 road tiles at once, and its single builder spread itself so thin that no extension ever got finished. Nothing was pushed, and LAST_GATE.md now tells the next run exactly which check failed.
The ten minutes was the real problem: it never ran the 13-minute M2 test before calling it a day, with four hours available. The standing task now says it can't finish a run without running the milestone test at least once and reading the result. We'll see how the midnight run does. Hopefully the next post is shorter.