Skip to content
Subterrans

The ants learned to be afraid

A month of work in one post: seven sim versions of new ant behavior — fleeing, risk-aware foraging, battlefield scavenging — plus a full architecture review and the thirty-odd PRs it caused, touch groundwork, and a licensing decision that lets anyone ship Subterrans to an app store.

The last post was about the controls — making the game answer to the player. That shipped in late June. The month since has gone in two very different directions at once.

One is the interesting one: the ants got a relationship with danger. They can be frightened now. They run from things, they route around things, and — as of this week — they eat the things that die. That is four sim versions of genuinely new behavior, and it changes how a round feels.

The other is the unglamorous one: I had the codebase reviewed against where it is actually going, and that review turned into about thirty pull requests of structural work. Seven sim versions landed in total — V31 through V37 — along with the groundwork for touch, a real performance pass, and a licensing decision I want to explain properly.

The ants learned about danger

Before this month, an ant’s relationship with a threat was binary and late: it was fine, and then it was in a fight. There was one exception — the spider’s reticle made nearby workers scatter — and that was it. Danger existed as a pheromone on the grid, but almost nothing read it.

Four changes fixed that, in order.

Fleeing and the idle reserve (#285, V34). This started as a bug report that the colony “looked hung”: when food storage filled up, surplus workers stopped having anything to do and froze in a motionless clump. They now mill around the entrance in a deterministic wander so the clump breaks up, and — the real addition — when the danger pheromone above them spikes, they dash for shelter down the nearest open entrance, hold at the shaft, and poke their heads back out once the danger overhead decays. There’s a nice detail in the safety logic: an ant only flees home if home is safe, so a panicking worker never sprints toward a spider parked on its own doorstep. The same PR added a cross-colony alarm — when an enemy kills one of your surface workers, that death seeds a danger mark on your pheromone grid. Your colony finds out that somewhere is dangerous by losing someone there, which is exactly how it should work.

Risk-aware foraging (#291, V36). Fleeing is a last-second reaction. This is the anticipatory version: a forager choosing its next step now weighs the food trail against the danger trail at each candidate cell. A spider sitting on a supply line visibly bends traffic around itself instead of just generating a stream of panicked flees. Crucially it is a soft bias rather than a wall — a strong enough food trail will still pull ants through mild danger. That trade-off is the whole point. Ants that refuse all risk starve.

Battlefield scavenging (#292, V37). Merged this week, and my favorite of the four because of what it does to the other three. Surface combat deaths now drop forageable food where they fall. An enemy killing one of your ants leaves a pickup; killing the spider leaves a hundred — a genuine windfall for finally bringing it down. Deaths underground, brood deaths, starvation, and anything the spider eats drop nothing.

Put those together and you get an actual dilemma. A battle leaves food on the ground. The battlefield is also, thanks to V34, the most danger-marked patch of the map. So the pile your foragers most want is sitting exactly where V36 tells them not to go, and the value of going anyway scales with how big the fight was. The greed-versus-fear crossover is now a real thing you can watch happen, and it emerged out of three separately-designed mechanics rather than being scripted.

Underground idle wander (#286, V35) rounds it out — the same de-clumping for surplus workers below ground. That one has a story of its own.

The 44% regression nobody could see

The underground wander should have been trivial: it is the surface milling from V34, applied to the tunnels. The first implementation did exactly that, and it passed everything. Every unit test green. Determinism intact. Replays byte-identical. Code review clean.

It also cut the AI colony’s economy by about 44%.

The cause is a rule that only bites underground: same-colony ants are one per tile. Idle workers wandering through tunnels were bumping productive foragers and diggers off their routes — thousands of tiny displacements a round, each individually invisible and collectively catastrophic. On one seed the AI colony went from 23 workers to 1.

Nothing in the test suite could see it, because nothing was wrong. The sim was correct, deterministic, and reproducible. It was just much worse. What caught it was a paired seed sweep — run eight seeds on the old version and the new one, twelve thousand ticks each, and compare the colonies that come out the other end. Five experiments narrowed it down, and the fix was to confine the wander to chamber footprints, which happen to be exempt from the one-ant-per-tile rule and are also exactly where the frozen clumps were in the first place. Post-fix the same sweep came back at a 1.05 worker ratio — slightly better than before, no colonies dead.

I think this is the most useful thing I have learned in months of building this way. I have a lot of gates: type checking, lint, thousands of unit tests, determinism proofs, byte-identical replay, two external AI reviewers on every PR. Every single one of them passed a change that would have quietly ruined the game. Correctness gates cannot see badness. The only thing that caught it was running the actual game a lot of times and looking at the outcome — and that is now a standing part of the process for anything that touches ant movement.

An architecture review, and the month it caused

At the start of July I stopped shipping features and had the whole codebase reviewed — not for bugs, but against where it is going next: bigger maps, mobile, and networked play. That produced about twenty tracked issues, shipped over three waves.

The review found real bugs, which is the part I did not expect. The spider was ignoring surface passability and walking through terrain it should not have (#248, V31). The AI’s operations were never validated, so a malformed one could be issued and quietly do something incoherent (#249, V32). The occupancy resolver left shifted ants slightly off tile center (#251, V33). And at 2× and 4× speed, ants were snapping between positions instead of interpolating, because the smoothing divided by a constant tick period rather than the live one (#246) — a thing I had been looking at for weeks without registering as a bug.

The rest was structural. Simulation scratch buffers that lived as process globals became owned by the world (#255), so two simulations in one process can no longer contaminate each other. The command pipeline got a single chokepoint with provenance and overflow visibility (#259). Three batches went into reaping dead compatibility gates (#260, #261, #262) — every old sim version that no accepted save could still reach was branches the code no longer needed to carry. Saving got tougher too: an async storage seam, a caption when an autosave fails instead of failing silently, and validation of loaded data rather than trusting it (#264, #266, #267).

And a proper performance pass, mostly on things that ran every tick without needing to: the per-tick brood-field rebuilds are now gated on whether the brood actually changed (#257), the pheromone overlay is LOD-gated and cached (#276), detailed-mode drawing reuses scratch buffers instead of churning allocations (#277), and the static minimap layer is baked once into a texture rather than redrawn every frame (#280).

Two engines, one hash

The single result I am happiest with is small and hard to screenshot. Subterrans has always been deterministic — same inputs, same round — but that was only ever proven within one engine. #253 runs an identical three-thousand-tick scenario in Node and in headless Chromium and requires the two worlds to hash to the same bytes. #283 added WebKit as a third.

That matters because everything interesting on the roadmap depends on it. Mobile means running the sim inside a WebView. Networked play means two machines stepping the same simulation and trusting they stay in agreement. Both assume the engine underneath does not change the answer. That was an assumption; now it is a test.

Getting ready for fingers

The controls post said touch was a later phase. Five PRs of groundwork landed anyway (#271#275): a two-pointer state machine so a second finger is visible at all, pinch zoom over the camera built last month, a drag threshold that scales with zoom, long-press to open the chamber menu, and a press-and-hold hover preview — because touch has no hover, and the green/red placement cues from the controls rework are load-bearing enough that they needed a touch equivalent.

None of it is switched on as a shipped mobile mode. But the input layer no longer assumes a mouse, which was the actual blocker.

Anyone can ship it to an app store now

One non-code decision worth explaining. Subterrans is AGPL-3.0, which is a strong copyleft license and stays that way. The problem is that AGPL’s “no further restrictions” clause conflicts with app store terms — DRM, device limits, mandatory ToS — which is the old VLC-on-the-App-Store problem.

The usual fix is a contributor license agreement: consolidate the copyright, then ship your own official binary under different terms. That works, and it means only the copyright holder can do it. I did not want that. So Subterrans now carries an AGPL section 7 additional permission granted to everyone (#287) — anyone can build and distribute it through an app store, as long as the full source stays available under the AGPL. No CLA, no signing gate, and because contributions come in under the same terms they go out under, external contributors get it automatically.

The game stays copyleft. Nobody needs my permission to put it on a phone. That is the right shape for this project.

What’s next

The honest state of things: the simulation is in better structural condition than it has ever been, the ants are meaningfully smarter than they were a month ago, and there is now a real dilemma on the surface that I did not design directly — it fell out of three mechanics meeting.

The next open question is a big one, and it is about map size. Several of the remaining architecture items are blocked on deciding how large a world Subterrans should support, because that decision changes how terrain is stored and streamed. Everything else is queued behind it.

In the meantime, the corpse-food economy needs a tuning sweep — the same kind of paired seed run that caught the 44% regression, this time asking whether a defender reclaiming its own dead makes losing a fight too cheap. As ever: play it, lose a round, and the playtrace tells us both what happened.