Hello, the smallest number in this post is one. One developer, fourteen weeks of devlogs, and a question I had been avoiding: what did all of that actually add up to? So this week I counted everything. The code, the content, the world, the words, and then the unglamorous machinery that keeps all those numbers honest, because a stats post that ends at the stats would be missing the actual lesson of the last fourteen weeks.
Counting turned out to be the first bug
I started with the obvious command:
find src \( -name "*.ts" -o -name "*.tsx" \) | xargs wc -l | tail -1
It reported 24,131 lines across 1,493 files. That is 16 lines per file, which would make me the most disciplined programmer alive. I am not. When the file list is longer than the command line limit, xargs silently splits it into batches, wc prints a total per batch, and tail -1 reads only the last batch’s subtotal. The fix is to pipe the file contents instead of trusting the per-batch totals: find ... -print0 | xargs -0 cat | wc -l. The real number is 232,008 lines of TypeScript. So the first measurement of the measurement post was wrong by a factor of ten, which I choose to read as this series staying on brand to the end.
With the methodology debugged, here is Mutarch as of this morning. Every number below was measured fresh today, on the working tree, with find, wc, ls, and a couple of node one-liners.

The whole game in one card. The same numbers, row by row, in the table below.
| Code | |
TypeScript files in src/ | 1,493 (1,017 source, 476 test) |
| Lines of TypeScript | 232,008 (159,122 source, 72,886 test) |
| Storybook stories | 82 |
Generation scripts in scripts/ | 57 |
| Content | |
Skills (src/content/skills/json/) | 64 |
| Unit definitions | 45, across 34 species families |
| Rival clans | 5 |
Evolution web nodes (src/game/skilltree/) | 385 |
PNG files under src/ | 7,363 |
| World | |
| Region map cells | 129,024 (448 by 288) |
| Words | |
| Locale namespaces | 28 per language |
| Translated keys | 1,944 per language, 3,888 across en and de |
A few of these deserve commentary. Almost a third of every line I have written is a test, and 476 of the 1,493 files exist only to check the other 1,017. For a solo project that ratio is not virtue, it is survival: the test suite is the only colleague who remembers how the splice bench worked in April. The 7,363 PNGs are almost entirely machine-generated frames from the art pipeline in #11, which is why 57 generation scripts exist to produce, slice, and wire them. And the prompt I wrote for myself when planning this post said six rival clans; the code says five (Blood, Khargol, Ossari, the Tallymen, the Moult of Vhel). I trust the code.
Some derived ratios, strictly for entertainment: 3,625 lines of TypeScript per skill, 14 test files per species family, and 69 translated strings per namespace, where a namespace is roughly one screen. The first ratio sounds damning for a game about skills until you remember the 232,008 lines also contain the world, the renderer, the politics layer, and the part of the codebase whose entire job is described in the next section.
One pinned number guarding a hundred systems
The most valuable test in the repository is src/game/combat/golden/combatGolden.test.ts, and it asserts almost nothing. It builds the standard demo fight with the seed "demo-combat", runs it to completion, and snapshots four values: the winner (ally), the tick count (208), the total damage dealt (904), and the per-unit damage contribution (the third ally does 340 of it, the laziest enemy manages 11).
That is the whole test. It works because of the two bets this series opened with: one seed drives everything (#3), and a fight is computed once and then played back (#4). The same seed plus the same constants must produce the same 208 ticks and the same 904 damage, every run, on every machine. Which means the snapshot is a tripwire stretched across the entire deterministic pipeline. Retune a cowardice threshold in the AI tunables from #6, and the number moves. Adjust a skill grade curve from #7, nudge a director budget from #9, reorder two RNG draws anywhere in the combat path, and the number moves. When it moves, the suite screams, and I have to either explain the change or revert it.

Pull any thread, anywhere in the sim, and the number moves. When the number moves, the bell rings.
The comment block above the test doubles as a changelog of every time the bell rang on purpose: lane targeting was demoted to a soft preference, the fight settled at a new pairing pattern, the pinned numbers were re-blessed. Re-blessing is a deliberate act with a diff attached, and that is the point. In a game built on determinism, one number can guard a hundred systems, because every system eventually feeds the same fight.
The golden fight has two quieter siblings. src/content/skills/catalog.parity.snap.json pins the full compiled output of all 64 skills, and the units catalog has the same arrangement, so a refactor of the skill compiler cannot silently change what spore_burst does. Together the three snapshots mean I can rip out internals with a confidence no solo developer has earned by discipline alone.
The rename that rewrote every save
This month the rival system changed vocabulary: what the code called a nemesis is now a champion. The reasons are not technical and not interesting to this blog; the migration is both. The problem was a cascade. The old rank ladder already had a rung named champion, so when the system took that word, the rank had to become reaver, and every existing save file contained both old words in thousands of object keys and string values.
src/game/save/migrateTerms.ts handles it with two ordered, case-aware rewrites over the entire parsed save: first champion becomes reaver, then nemesis becomes champion. The order is the whole trick. Run the second rewrite first and the freshly written word gets clobbered by the first one on the next pass. The migration walks every key and every string value recursively, returns a new payload, and bumps the save to v6.
The detail I am most pleased with is what it does not touch. Opaque ids like nem-…, nemgear-…, and nemfollower-… contain no complete rewritten term, so they pass through byte-identical, and that is correct: an id only needs to stay consistent within its own save, not read nicely. Exactly one id family embeds a real term, the sig:{clan}:{rank}:{id} promotion slot tokens, because new code regenerates those from the rank word and compares against the stored copy. Those get rewritten; everything else is left alone.

The after panel is not typed in: the figure runs the real ordered rewrites from
migrateTerms.ts and asserts the panels match. The ids never change; the rank word inside
the sig: token does.
The guard that exists because I shipped the bug
Mutarch ships every UI string in English and German, 1,944 keys each, and a test enforces it: src/i18n/localeParity.test.ts. It loads every locale JSON, flattens the keys, and fails the build if German is missing a key English has, or has one English does not, or disagrees on plural forms (_one/_other families are matched as families), or uses a different set of {{placeholder}} names, or contains an empty string. It even rejects em dashes in the German strings, because the site-wide copy rule from this very blog is apparently also a unit test now.
I would love to present this as foresight. It is scar tissue. The honest origin is that I kept doing the obvious thing: build a feature, write the English strings inline with the work, tell myself I would add the German in a follow-up pass, and find the raw key name sitting in the German UI a week later. After shipping exactly that bug during the politics work, the rule moved from my head into the suite. Now a missing translation is not a review comment, it is a red build, and red builds do not rely on my memory at 1 a.m.
Closing the arc
Devlog #1 opened with a confession: a strategy game on a browser engine, against all standard advice, with a sealed simulation core as the load-bearing wall. Fourteen weeks later, the bet I would defend hardest is not the browser part, it is the determinism that the sealed core made cheap. One seed (#3) made fights recordable (#4), recordings made the golden test possible, and the golden test made every refactor since affordable. Determinism has paid compound interest in every post of this series, including this one, where it let four numbers stand guard over 232,008 lines.
The ledger has a debit column too, and closing the series without reading it out would be cheating. The regionMapVersion stamp I deferred in #2 still does not exist, so every worldgen tweak still quietly reshapes existing worlds. The dry-streak worry from #8 is still unresolved; I have no pity counter and I am still not certain the penetration curve alone keeps long bad runs from feeling punitive. And the garbage collection doubt from #1 remains unmeasured, because no GC pause has hurt me badly enough yet to schedule the painful evening of finding out.
That is fourteen weeks: one developer, five clans, 64 skills, 385 nodes, 129,024 cells, two languages, and one pinned fight that screams when anything lies. As always, let me know what you think.