Game ·

Starbound Settlers

A space colony sim built solo in Godot, and tested like a backend rather than a game, because a simulation that has quietly stopped being correct still looks fine.

StackGodot 4 · GDScript
StatusIn progress

You build a station, keep a crew fed, watered, rested and reasonably sane, run power and water out to the machines that need them, farm, keep livestock, treat illness, and mine asteroids for the things you cannot make yourself. There is no win condition. It is a survival sandbox, so the only real question is how long you last.

It is on a site otherwise about running systems in production because the problem underneath turned out to be the same one.

A cross section of a space station interior. Crops growing in soil beds, two colonists standing among them, a light overhead, the purple of space outside the hull.

A colony about twenty minutes in. Everything on screen is a system that can fail on its own.

A shared pool is a check that cannot fail

Power and water started out as global pools. A machine asked the colony whether there was enough power, the colony said yes, and the machine ran.

That worked, and it was wrong in a way that took a long time to see. A machine plugged into nothing behaved exactly like a machine wired correctly to a reactor. Conduits were decoration. You could delete every cable on the station and the smelters would carry on. The colony reported itself healthy the entire time, because the only thing being checked was a number going down somewhere.

Two panels showing the same machine. On the left it is joined to a power source by a broken line with a cross through it and still runs. On the right a solid line joins them and the caption notes the failure would be visible.

The same machine, drawn twice. On the left it works because a number went down somewhere.

Every consumer now has to trace a live connection back to something that produces the thing it wants. No pipe, no heat. It made the game harder and it made it honest, and it is the rule I now apply everywhere in the project: if a thing depends on something, the dependency has to be real at the moment it is used, not assumed from a total.

That is the same failure as a health check that returns a cheerful 200 because it never calls the database it is supposedly checking. The check passes, the dashboard is green, and the only thing being tested is that the endpoint exists.

Tested like a backend

Games are usually tested by playing them. That works for feel and it does not work for a simulation, where the systems all touch each other. Hauling feeds the feeders, feeders keep the livestock alive, the livestock produce what the kitchen needs, illness takes a colonist out of the rota, and the rota is what makes hauling happen. Change one and something three systems away breaks in a way you will not notice in a ten minute play session.

So the whole thing runs headless, with no renderer, from one script. There are 1,740 checks across 61 suites, and a full run takes fourteen minutes and fifty-two seconds without me watching it.

A bar chart of sixty-one bars, one per test suite. The first bar, incidents, is more than three times the height of the next, and the rest tail off into a long flat run of small bars.

One bar per suite, from a real run. Incidents is 266 checks on its own because reactive triggers have the largest surface in the game.

Each suite gets its own Godot process. Autoloads are global singletons and a colony sim leaves a lot behind it, so sharing a process means a test can pass because an earlier suite left a colonist standing in a convenient place. That is a check that cannot fail, which is worse than no check.

A wider view of the station showing a cow on a bed of straw with a two of two counter beside it, a chicken coop reading two of six, and machinery along the wall.

Livestock, feed, and the counters that say whether either is actually working.

The rule I hold to is that a bug found is a regression test written, every time, before the fix. It is slower on the day and it is the only reason a project this size is still movable by one person. A bug you fixed without a test is a bug you have agreed to fix again later. Writing it first is also the only way to know it would have caught the thing: a regression test that passes before you fix anything is not a regression test.

It has earned its keep. Crop growers that cached the wrong configuration and silently defaulted every data-driven behaviour off. An animal harvest that lived in a method nothing calls, so a farmer stood at the pen in the working animation forever. Produce that was harvested and then routed to a worker reference that had already been cleared. Hand-drawn crop art that never loaded once, because the loader reported success and fell back to procedural shapes.

Not one of those produced an error, a crash or a visibly wrong frame, which is the whole argument. I wrote the long version up separately: testing a Godot game without opening it.

The music is generated

The score needed to react to how badly things were going, which is a lot of composing for one person. So it became Hum: music described as constraints rather than notes, and rendered to separate layers the game fades in as pressure rises. That started as a feature of this project and turned into a tool that works on its own.

Where it is

In progress and unreleased. It is the project I use to try things that would be reckless to try at work, which is most of why it exists.

Reading