01
Why a game, and why this one
Everything else in this portfolio proves restraint. A template with nothing in its script budget, a validation package with no dependencies, a payments starter whose README is the deliverable. None of it proves the harder thing: that I can build an interface that moves, holds state, and stays correct while several things happen at once.
It started as one showcase among several in a lab of interaction experiments — a single seeded map, one match, no way to lose twice. This is the version that grew a campaign, its own address, and somewhere to keep your progress. The original has since been retired: two deployed copies of one game is a worse answer than one copy and a link, so the lab now points here instead of holding a smaller version of the same thing.
02
Forking something that already worked
The interesting decision was not what to build but what to do with the code that already existed. Roughly three thousand lines of the game — map generation, the rules, the AI policies, the geometry — were already written and already tested, sitting inside a repository full of unrelated experiments.
The tidy answer is to extract it into a shared package that both sides import. I did not do that, and the reason is worth stating: the two versions were supposed to diverge. The lab's copy was the teaser and it was finished. This one grows. A shared package would have coupled a deliberately-frozen demo to a project still under construction, and bought a publishing step for the privilege — and the decision looks better now than it did then, because the frozen copy was eventually deleted rather than maintained. A shared package would have made that deletion a breaking change instead of a redirect.
So it is a one-way fork, taken at a recorded commit, with that commit written down in the new repository's README. That single line is what keeps the question "has the original had a fix this one hasn't?" answerable — without pretending an automated check could compare two codebases that are meant to disagree.
03
A campaign with no new mechanics
The full version needed a reason to keep playing, which meant a campaign: twenty levels that get harder, and progress that survives closing the tab.
The constraint I set was that not one of them could introduce a new mechanic. A level is a seed and a starting arrangement — how many rivals, what each one is trying to do, how lopsided the opening is, how much of the board is unclaimed. Every one of those was already an input the game accepted. The difficulty curve is entirely a matter of which numbers go where, which means the last level runs the same rules as the first and there is no second code path to keep honest.
The player's own opening never changes from the first level to the last. That was deliberate too: a level is harder because of something the board does, never because the player was quietly weakened.
04
The part that actually took the work
This genre is, almost without exception, mouse-only, animated whether you asked for it or not, and completely silent to a screen reader. Being none of those things is the point of the piece.
Every tower is focusable and every connection can be made from the keyboard. Nothing moves on arrival — the board opens paused for everybody, so a visitor who has asked their system for reduced motion is not ambushed, and one who hasn't still chooses when it starts. Events are announced out loud, batched on a slow timer rather than per frame, because a live region that speaks on every tick is a flood rather than an aid.
Small things carry a surprising amount of that. When you pick a level, focus moves to the board — without it the browser drops focus to the top of the document and a keyboard player has to tab all the way back to the game they just started. A locked level says what unlocks it rather than only that it is locked, because a disabled control with no stated reason gives a screen reader nothing at all.
05
How it is tested
The game logic is pure functions — no framework, no DOM — which is what makes the interesting parts testable at all. Everything that decides something lives there rather than inside a component.
The more useful habit is what happens after a test passes: break the thing it claims to protect, and check that it fails. A test that cannot fail is worse than no test, because it reports safety that isn't there. Every assertion about saved progress here was verified that way — reverting the logic, confirming the suite went red, restoring it. One of them was checked against a version where the game ignored saved progress entirely.
A second suite drives a real browser, and it exists for the two things a unit test structurally cannot reach: whether the interface actually becomes interactive on the built page, and whether anything survives a reload. Both were confirmed by deliberately breaking them first.
06
What it is not
There is no account, no server, no leaderboard and nothing to sign up for. Progress lives in your browser and nowhere else — and if your browser will not store it, every level is still playable and the game says so rather than failing quietly.
The page fetches nothing after it loads. That is enforced rather than promised: the content policy the site ships forbids outbound requests entirely, so a version that phoned home would be refused by the browser before it reached anyone.
