Slagworks Outgrew Its Engine

Every game in this series so far has asked a question about whether a machine can be trusted to build software: can it build a game at all, can it make one fair, can its content be believed, can it balance a strategy space. Slagworks answered the last one, and I wrote it up. Then the game asked a question I did not see coming, which is what happens when a thing you built outgrows the place you built it.
Slagworks was a Godot game. Godot is a real engine, and the web export is a real engine shipped whole: a WebAssembly payload of about forty three megabytes that has to download, boot, and claim a graphics context before the title screen appears. For most games that is a fair trade. Slagworks was, I eventually realized, close to the worst possible game to be paying it.
the TypeScript build · same rules engine the server runs · loads instantly
That is the game, running live in this post. It boots instantly now, because it is no longer a game engine in an iframe. It is a few thousand lines of TypeScript drawing on a canvas. Nothing you can see changed. Proving that nothing changed is the whole story.
The least Godot game I owned
When Claude first built Slagworks, it made a set of choices that were good engineering at the time and turned out, in hindsight, to be an escape plan. The rules lived in a node free file: no scene tree, no engine objects, just a state machine that the tests, the self play arena, and the screen all stepped identically. The interface was immediate mode, every frame redrawn from scratch with plain draw calls. The art was procedural, twenty little vector primitives instead of image files. The only things Slagworks actually used Godot for were a canvas to draw on, a font, some synthesized beeps, and input events.
For that, it paid forty three megabytes and a loading bar. And it paid worse than that on my phone, where every input bug I hit over two weeks, the taps that fired twice, the hovers that stuck, the attacks that would not register, traced back to the same root: mobile browsers synthesize their own mouse events on top of touches, and a real engine faithfully delivers all of them. I was fighting the engine's honesty about a platform it could not change.
None of that is why we moved. Download size is a reason to grumble, not a reason to rewrite a working game. The actual reason was that I wanted a leaderboard.
The reason a card game needs a second copy of itself
A leaderboard for a game like this has exactly one hard problem, and it is not storage. It is trust. If the client tells the server "I won eight games," the server has to either believe it, which means the leaderboard is fiction, or check it. Checking it means the server has to be able to replay the game and see the eight wins for itself. Which means the rules of Slagworks have to exist somewhere the server can run them.
That leaves two futures. In one, the rules live in GDScript on the client and in TypeScript on the server, two engines that must agree forever, with a permanent tax every time a card changes to keep them in lockstep. In the other, there is one engine, in TypeScript, and the client simply is it. I told Claude to take the second future, and to port the game before building anything else on top of it, so we would never build the leaderboard twice.
Here is the part that made it safe to do at all. Slagworks has one design decision that everything else hangs from, the one I put first in the last article too: the only randomness in the entire game is the opening shuffle. Every effect targets deterministically. That decision was made so the bots could search the game honestly. It is also, it turns out, exactly the property that lets you prove two implementations of a game are the same game.
The transplant, and the receipt
Claude ported the rules and the bots to TypeScript first, transliterated structure for structure, the field names kept identical to the original on purpose. Then came the question that would sink the whole project if the answer were fuzzy: is the TypeScript game the same game as the Godot game, or is it merely a game that looks like it?
You cannot answer that by playing both and squinting. You answer it the way the rest of this series answers everything, by making the machine prove it. Claude had the Godot build play four hundred games against itself and record each one completely: the two decks, and crucially every shuffle's resulting order, and every single action both bots took, and the exact final board state serialized down to the last hit point. Recording the shuffle results, rather than trying to make two languages' random number generators match bit for bit, is the trick that makes the test about the rules instead of about the noise. The TypeScript engine replays those recorded shuffles and then has to reproduce, unaided, every mulligan decision, every bot action at every turn, and the final state, exactly.
| corpus | what it exercises | result |
|---|---|---|
| 300 greedy games | the full rules and the shipped bot's every choice | 300 of 300 bit identical |
| 40 planner games | the hard mode search, tie breaks and all | 40 of 40 identical |
| 60 drafts | the draft pick logic from recorded offer sheets | 60 of 60 identical |
Four hundred games, and the two engines agree on all of them down to the bit. The test also has teeth, which matters more than the green checkmark: flip a single rule in the TypeScript version, change when the second player's bonus energy arrives by one turn, and the same suite fails three hundred of three hundred and prints the exact move where the two games first diverge. A test you have never watched fail is just a green light bolted to the wall.
What tried to go wrong
The gap between "the same rules" and "the same bits" is where a port actually lives, and it is full of things that are technically true in both languages but not identically true.
| the trap | why it bites | the fix |
|---|---|---|
| unstable sort | Godot and JavaScript break ties differently, and a card game has ties constantly | give every sort a total order, so ties resolve the same everywhere |
| float accumulation order | the bot scores a board by adding many small numbers, and addition order changes the last bit | mirror the exact accumulation order rather than "cleaning up" the math |
| JSON key ordering | Godot sorts object keys when it serializes, the browser does not | serialize state as arrays, which have an order, not objects, which argue about one |
Each of these was found the same way: the four hundred game suite refused to go green until it was fixed, and pointed at the game and turn where the divergence started. None of them would ever have surfaced by playing.
The body follows the brain
With the rules proven, Claude rebuilt the interface on a canvas. The move that made this almost mechanical was drawing at the same logical resolution the Godot viewport used, so every coordinate in the original, every panel, every button, every hit box, carried over unchanged, including the geometry that a layout audit checks for overlapping tap targets.
The testing got easier as we left the engine, not harder. The old game needed a virtual display and a software renderer to run its interface tests. The new one runs the whole battery, a deterministic walk through every screen plus a fifteen hundred iteration monkey that random taps its way through the game checking that nothing illegal ever happens, in about two seconds of plain Node, no browser at all. On its first run that monkey found a bug that had been hiding in both engines the whole time: while a unit's attack animation played, its tap target traveled with the animation and briefly overlapped the neighboring slot. The Godot version had shipped with it for weeks. The monkey samples animation frames far more densely than a human tester waiting real milliseconds ever could, so it walked straight into the one frame where the hit box was in the wrong place. Claude fixed it in both engines at once.
I kept my traditional role through all of this, which is to hold a phone and find the things the machines cannot feel. The game was too small in the window. A button overlapped another button. The proof that a test suite is not the same as a user is that I found those in about a minute each, and no gate had.
The dividend, paid a third time
Now the payoff, and it is the reason this article exists rather than being a paragraph in a changelog.
Because the only randomness is the shuffle, a complete run of Slagworks is not a recording of everything that happened. It is a seed and a list of the choices a human made, and nothing else, because everything else can be regenerated from those. A full run, twenty draft picks and several games of play, packs into a text string about two hundred characters long. Short enough to send in a message.
That share code is a proof. Anyone can paste it back into the game and watch it re simulate the entire run and report the score, with no server involved. And when the server does get involved, on the daily leaderboard that is now live, the same property closes the trust problem completely. The client does not send its score. It sends that little string. The server decodes it, replays the whole run through the same engine that this article just proved correct, checking every single player action against the rules before it is allowed to happen, and stores only the number it computed for itself. There is no anti cheat heuristic anywhere in it. There is nothing to fake, because the player never asserts a result, they submit their moves and the server discovers the result.
To be sure of that, I asked Claude to build the attacker before the defender: a tester that forges a legitimate run and then tampers with it fourteen different ways, injecting an illegal attack, forging the draft, hand picking an easier daily seed, deleting a game it lost, smuggling a favorable shuffle. All fourteen are caught. The one that is my favorite is the demonstration it prints at the end, which shows that the raw engine will cheerfully execute an illegal move if you hand it one directly. That is not a bug. It is the whole point: the engine is fast and trusting, and the verifier, not the engine, is the wall. The game and the referee are the same code wearing different hats.
The leaderboard has its own fresh database, provisioned for exactly this and nothing else, with a schema that already has room for the things this determinism makes cheap later: replays are stored, so a future mode can pit you against another player's actual deck piloted by the hard bot, no netcode required.
The decision that paid three times
I will end where the last article started, because the two articles turn out to be about the same sentence. The only randomness in Slagworks is the shuffle.
That decision was made so a bot could search the game honestly, and it made the game balanceable. It was the reason the balance work in the last article was possible at all. Then, without anyone planning it, the same property made the game provably portable: you can only prove two implementations are identical if the game is deterministic enough to have a single correct answer to replay against. And then, still without anyone planning it, that same property made a multiplayer leaderboard impossible to cheat, because a deterministic game can be entirely reconstructed and re judged from the player's choices alone.
One design decision, made for one reason, paid off three times in three registers that have nothing to do with each other: honest AI, a provable rewrite, and trustless multiplayer. I did not engineer that. I mostly held a phone and made decisions about which direction to point. But it is the cleanest lesson the whole series has handed me, and it is not really about games. Get the foundational property right, the one everything else stands on, and it keeps paying you in currencies you did not know it could mint.