A battle without a single picture: tests first
Terminal Leave’s first battles ran only in automated tests: the simulation, battle server, bots and replays worked before the first soldier appeared on screen.
The first battle with real network clients looked like two terminals. Two test players connected to the battle server, gave orders, locked them, and the server played out a round on a 24 × 24 industrial-yard map. One of the players got an empty list of events back: the enemy was beyond their vision radius, and a container blocked the line of fire. There was no map and no soldiers on screen — only JSON. But that empty list was the main result: the fog of war worked.
It had no client at all yet.
Why in that order
There were three reasons. The first is in how the game is built. In Terminal Leave only the server computes the battle. The client decides nothing: it sends orders and draws what it is sent. So the rules can be written and verified with no client at all, and the client later becomes just one more consumer of a server that already works.
The second reason was practical. A client needs art — tiles, soldier sprites, animations — and there was none yet. Rather than draw placeholder squares, I stopped short of the client and built whatever did not depend on art. Almost everything didn’t: the simulation, the battle server, accounts, bots, replays.
The third reason is me. I am a backend developer by trade: servers, data, protocols and the tests for them are my work, while a client with art and animation was new ground for me. Starting with what I do best meant getting a solid foundation quickly — something to stand on while I figured out the rest.
Besides, the rules are the riskiest part of the project: simultaneous events, vision, fog of war, randomness that has to repeat exactly. Had they not come together into a working system, no picture would have helped.
Determinism from the first line
A replay in Terminal Leave is not a video recording but a recomputation of the battle. For the same orders to give the same result, the simulation followed a few rules from the start. Time is whole milliseconds from the start of the round. Percentages and angles are integers in thousandths, with no floating point wherever the outcome depends on it. Randomness comes from our own generator seeded per battle and split into named streams, so one soldier’s roll does not shift another’s.
These rules are enforced by the linter as well as by tests. Simulation code may not call Math.random, Date.now, new Date, timers or network requests, or read environment variables. A violation fails the check before any test runs.
The determinism tests themselves are simple. One battle runs 25 times with the same seed, and the event logs must match byte for byte. The result is compared with a golden snapshot, and the build checks it on two versions of Node.js — if the computation depends on the platform anywhere, the snapshot will diverge.
Simultaneity
The most telling test of that time is the mutual kill. Two soldiers fire at each other in the same millisecond, and both must die. In a turn-based game that can’t happen: whoever moves first shoots first. With simultaneous turns it is an ordinary outcome, and the rules have to allow it rather than settle it by whose order the server happened to read first.
Next to it sit tests for movement order, occupied cells, a shot through a wall, and overwatch interrupting someone else’s run.
The battle server
I wrote the battle session so that it knows nothing about sockets: outgoing messages go into a sink handed to it. So the whole loop — joining, planning, locking orders, resolving the round, sending out results — is checked by an ordinary test with no network. Separate tests then run the same thing over real WebSocket connections.
The server does not trust the client. Action times sent by the client are discarded and computed again. An order to someone else’s soldier, a path through a wall, a second lock — each is refused with an error code, and each code has a test.
The fog of war is checked most strictly. A soldier walks behind a wall; the player who moved sees two steps in their result, the opponent sees none, and the full log on the server holds everything. What matters is that the opponent is not merely spared the drawing of those steps: they are absent from the data that reached them.
Bots
Bots came before the client, because a newcomer’s first battle is against a bot, and someone has to lead a disconnected player’s squad off the field. A bot gives orders through the same validation as a player and sees only what its soldiers see. If no enemy is in sight, it heads for the enemy’s deployment zone — something every player knows.
The tests run such a battle from start to finish, over several rounds, and check that it ends with one side winning rather than hanging.
Replays and the end-to-end test
At the end of a battle the server saves a full replay file: the map, the rules, the starting squads, the seed, every round’s orders and the result. A test loads it, recomputes the battle and compares it with the original.
The last to arrive was an end-to-end test that replaced checking by hand. It starts the main API and the battle server with real HTTP and WebSocket. Two guests join the queue, get matched, their soldiers are locked, one player surrenders. The result is applied to the accounts twice, and the test checks that the second time changed nothing. Then the soldiers are free again and the battle count has gone up.
What it gave me
The art arrived "the next day", and the client was handed a verified server. The bugs of those first hours were display bugs: a soldier facing the wrong way, an animation playing too fast, objects drawn as grey squares, the rendering engine failing to load in the browser. The rules did not change.
That order has a cost too. A test checks that a rule holds, not that it is comfortable to play by. The very first look at the screen showed what the tests could not: one order per round is too little — a soldier reaches the cell and then stands idle. That is how multi-step turns came about, and the rules had to change for the picture after all.
The habit stayed. A new mechanic first appears in the simulation together with its tests, and only then on screen. The project has more than two thousand automated tests now, and most of them test combat.
