An AI wrote the code. I decided what survives.
This is the making of Words Are Snake, a game where you steer a snake by typing words. Every post tells the true story of a stretch of its commit history — what we tried, what broke, and what quietly got rewritten — as one person and one agent figure out how to build a game together.
A 404 is not an answer
Skipping CI for blog-only PRs looked safe once branch protection came back 404. It wasn't: a separate ruleset required the same four checks by name, and the fix for that needed its own fix once a paths-filter default turned out to match everything.
Three gaps, one bug
Unifying daily and AI-duel score submission surfaced three suspected asymmetries. Two turned out to be features AI-duel simply has no use for, and the third was an error message with no reason to exist.
The number was never the decision
A PvP host can now pick a turn time limit from 30 seconds to 3 minutes, for accessibility. Almost everywhere the old fixed 60 seconds showed up, the actual design underneath it never depended on that number at all.
The opposite tradeoff
The AI never volunteering a profanity was a free filter. Filtering what a player types into their own initials is the same list applied to the opposite tradeoff.
The second time needed no spike
AI duel gets the same injectable-factory treatment the PvP store just proved out. With the pattern and the risk already settled, the ticket for this one only had to say what was smaller.
Silence meant leave it
The Norwegian UI shipped in one evening and stayed unreviewed by a native speaker ever since. Closing that gap meant building a doc that could only ask, never answer.
The store that had never been tested
pvpGameState.svelte.ts had never had a single unit test of its own. Turning it into an injectable factory meant answering, before touching it, whether Svelte 5 runes would even tolerate the shape.
Eight strings, one function
A five-level nested ternary picked the caption for how a PvP game ended. The only way to exercise all eight of its outcomes was rendering the whole page.
Load-bearing in one mode
Both duel pages held their own copy of the same reveal-timing math. One input to the shared version looks redundant for AI duel and is anything but for PvP.
The comment that confessed
A second architecture review found duplication a module's own doc comment had already admitted to, describing itself as a mirror of logic living somewhere else.
Fast, but not zero
Speeding up the e2e suite by cranking animation speed risked breaking the one test that watches an animation happen. Reduced motion turned out to be the wrong kind of fast.
The apple that shouldn't have been there
A test asserting that hearts are growth-neutral failed on CI, intermittently, on a PR that never touched the file it failed in.
The plan had three slots
screenshot.ts had grown to 626 untested lines drawing both share images and the board underneath them. The obvious three-way split needed a fourth module once someone actually drew the boundary.
Verbatim, not cleaned up
GameEngine.ts held two unrelated engines in one 621-line file. The split needed no design round at all, just a careful move and one very literal constraint.
The deletion test
GameRecap and DuelRecap still carried the same state machine twice. Four candidate shells, judged by one question: does deleting it reduplicate the machine back into both callers?
A date too coarse to sort by
Two posts about the blog machine itself both ended their narrated git ranges on the same calendar day. The frontmatter date field had no way to say which came first.
Descriptors, not strings
The PvP lives-teaching hints get their own pure reducer. Four candidate designs, decided on two separate axes, one of them a rule the sync-engine round had already settled.
The design with no second copy
Four candidate shapes for pulling PvP sync logic out of an 898-line Svelte store. Two of them carried a duplicate copy of the game state; the winner needed none.
Illegal states, unrepresentable
An architecture review found both duel pages still carrying the same ninety-line derivation chain by hand. The fix collapses sixteen flat props into one discriminated model.
One parser for every mode
Three near-identical physical-keyboard parsers, one bug that only showed up in duel modes, and a wayfinder map that carried its own execution instead of stopping at a spec.
The image that repaints itself
The daily share image gets real fruit icons, an OG-ratio canvas, and a gold rank line that fills in the moment a leaderboard submission lands.
The game never volunteers that
The Norwegian AI opponent played a profanity as its move. The fix keeps every word valid for players and just stops the game from choosing one on its own.
The same trap, a third time
A word input overflowing the screen at 320px turned out to be two unrelated bugs, and fixing the second one surfaced a third case of a CSS trap this codebase keeps hitting.
One tooltip, anywhere on the page
Detail tooltips reach the fruit on the board itself, and the "only one open at a time" rule stops being a property of the game log and becomes a property of the page.
The log that remembers both players
The shared game log learns to show two players at once, PvP gets a live panel of its own, and a scoring bug that only hurt guests surfaces along the way.
One recap, two modes
A blocking modal in PvE and an inline overlay in PvP get replaced by one shared recap component, ported wholesale from the Daily Challenge pattern.
One word at a time
Two long words typed at once could break the whole duel layout. The fix drops the side-by-side grid for a single panel that follows whoever is acting.
Fifty-six turns of twenty
A Daily Challenge that can never exceed twenty words showed a game log toggle claiming fifty-six turns. Fruit events were counting as turns.
The crash that used to win
A player ahead on score could just ram the wall and win. PvP duels get lives, hearts, and a reason not to crash on purpose.
A menu worth a second look
Four throwaway prototypes, reviewed live behind a query-string switch, turn three identical mode buttons into a hero card and a duo below it.
The score that almost got away
A tab switch could strand a finished Daily Challenge with no way to submit it. The fix merges two screens into one and stops trusting the client for the score.
The apple moved overnight
A date-seeded apple invalidates the daily recording route, so the scripted player learns to search for a verified two-word opener.
The allowlist was too generous
Two routine blog handoffs expose a less routine problem: broad agent permissions were approving destructive commands without review.
Eight posts, sixteen handoffs
The blog backfill completes eight draft-review-publish cycles in one evening without weakening its one-draft lock.
More than one agent in the workshop
Codex and Antigravity join Claude Code inside the devcontainer, with pinned versions, shared skills, and checks that the safety boundaries are real.
The backfill finds its rhythm
Three posts move through the draft-review-publish loop, and the workflow starts improving its own workshop.
The interface does not end at the board
Physical-key feedback, host sleep, and an iOS zoom gesture expose three boundaries the game still needs to design around.
The first drafts were too long
Three approved posts expose chronology, verification, and writing-style assumptions that the new blog workflow did not yet know to question.
Building the machine that writes the blog
A writing workflow becomes a small publishing system with a frontier, a security gate, and exactly one draft in flight.
Let the game explain itself
Game-log chips gain explanation-on-demand tooltips, while the recording script learns to demonstrate the game it claims to show.
Harder combos, not more kinds of fruit
Five new combos turn a design review into grape, melon, carrot, and a reusable tier system for harder versions of existing skills.
the norwegian umbrella: full bilingual gameplay across every mode, and the main menu restructure
Bringing Norwegian back properly as a PRD-driven umbrella project, introducing a 29-letter alphabet, per-language letter rarity, and a brand new main menu.
the pvp duel system: server-authoritative state
Building the biggest single system in the game: dueling a friend asynchronously, backed by a server-authoritative engine and a compare-and-set turn model.
storing less, submitting once
Privacy deletion of rejected words and making the leaderboard submission idempotent to ensure a service players can trust.
Tuning the agent workbench: from game log polish to MCP servers
A solo-plus-agents project spends real time on its own tooling. Three days of making the workshop pleasant.
A room where the agent can work
The project moves into a devcontainer with local data, restricted networking, scoped Git identity, and enough browser tooling to run the whole development loop safely.
A camera for a game that keeps changing
A scripted Playwright player records every combo on demand, while a browser prototype replaces platform emoji with a smaller, themeable icon language.
Nine seams in two days
A test-first architecture pass turns duplicated rules and wide interfaces into nine small, reviewed seams without changing the game players see.
Teach what the player has not found
A pure hint engine turns missing player actions into timely guidance, combo rewards and hints share one notice system, and the abandoned native-client backend comes out.
The narrow screen gets to decide
Foundation tokens make a mobile-first redesign tractable, then a second design review turns layout stability and colour contrast into regression tests.
Reward the risk, explain the fruit
Snake growth becomes a scoring lever, combo fruit gets feedback at the moment it appears, and the interface stops teaching before the player can act.
A contract before the client
The planned native client gets an offline submission path, a bundled dictionary, and an API compatibility rule before its first install exists.
What the end of a game should say
The end-game dialog gets a hierarchy, sharing survives the trip to the daily countdown, and the final board becomes part of the result without storing the whole game.
Showing the snake where it's about to go
A ghost-path overlay that simulates each keystroke before it happens, a collision rule that took two attempts to get right, and the interactive demo it made redundant.
Quiz should score high
Word scoring drops the inherited word-frequency model for Scrabble-style letter values, the duel AI is recalibrated to match, and a domain glossary lands first to give the whole decision a shared vocabulary to be written in.
Giving the logic a name
Two days pulling shared logic out of stores and routes into small, named, independently tested modules, one of which immediately surfaced a real duel bug and got its own same-day fix.
A reason to come back
The daily challenge gets a real midnight boundary instead of an arbitrary UTC one, the wait for it becomes a countdown instead of a dead end, and web push notifications arrive, built around the one hard constraint the hosting platform imposes on anything running on a schedule.
The clock comes off
The thirty-second per-word timer added three weeks ago comes out entirely, scoring reverts to pure word length times rarity, and a UI freeze traced to backdrop-filter blur turns up in three more dialogs with a blunter diagnosis than the first time.
Not like this
The engine gets pulled apart into deep, single-purpose modules, and a day later the entire day-two Norwegian layer, carried for three weeks without a dictionary worth playing against, comes out for good.
The week the game started watching itself
A three-PR refactor trio makes the word-submission pipeline immutable and side-effect-free, then the newly-launched game gets its first stats page, a same-week AI tuning pass informed by it, docs finally caught up to the Postgres stack, and a WCAG 2.2 audit closes the week.
Two kinds of ready
A second design pass swaps oklch approximations for exact designer hex values and a machine-readable spec, then a seven-part bundle declares the game launch-ready: once-per-day enforcement, a streak counter, analytics, and a way to help pay the hosting bill.
The boring stack, on purpose
Render and Turso replaced by Railway and PostgreSQL in a single pull request, deleting the hand-written migrator Turso had forced into existence, plus a CI workflow retired in favor of agents bumping their own version.
Same names, new values underneath
A font and color rewrite that touched zero component files, a layout pass built on the tokens it introduced, and a leaderboard that started checking its own math.
The screen that would not respond
A WCAG reflow fix that became a permanent layout rule, assisted mode deleted nine days after it shipped, and an end-game screen that froze the browser until profiling found why.
Thirty seconds to decide
A tuning pass over daily mode: a per-word timer with a speed bonus, a background-timer bug that punished new players for reading the tutorial, and a board size that flip-flopped within the same day.
A second snake, same engine
Duel mode lands in one day: a turn-based match against an AI opponent that shares the daily engine and a first, cautious idea of what a safe move looks like.
Serving today's puzzle
Three days of stale PWA caches, a Turso migration table that rejected its own tracking schema twice, and the first real players finding their way in.
The first pull requests
Two days of committing straight to main end in one day: a CI workflow, a licensing swap, a WCAG audit, and the design question the game keeps coming back to.
The translation that arrived too early
Day two added a leaderboard page, a unified toolbar, and full Norwegian support. Two of those were deleted within weeks.
Someone else's phone
The first production deploy, a snake that moved without appearing to, and what the first real players found within a day.
Seven phases in one afternoon
From an empty directory to a playable daily challenge in one day, and why the game engine never learned what a framework is.