I Built a Multiplayer Typing Game in a Weekend and Wrote None of the Code

On Saturday I had a one-page idea for a game.
By Sunday it was live at keyboardwarriors.online with:
- real-time matchmaking and private rooms
- accounts, match history and a weekly leaderboard
- seven fighters, six arenas, music and sound effects, all generated by code: no art files, no audio files
- a pixel-art sprite editor, with an AI that checks every drawing before anyone else sees it
- a marketplace with its own currency
- an admin panel, an audit log, and a Discord announcer
- an anti-cheat that listens to the rhythm of your typing
- a logo
I wrote none of the code. I built it pair-programming with Claude Fable 5.1, Anthropic’s AI, in Claude Code. I made decisions, argued, playtested, pasted API keys, and lost a lot of typing duels.
This is the story, with screenshots.
The idea
You fight another player by typing.
The original pitch was spell-casting: type cast fireball, then hold Shift and punch in an arrow combo, up, right, down, left, and release to fire. Longer spells hit harder. There would be a turn-based mode and a frantic “type rush” mode, buffs, unlockable characters, and a screen that shakes and goes red when you are on a streak.
I pasted the whole thing into Claude Code with one instruction of my own:
Before you begin coding, make a plan, talk to me about it.
The plan
What came back was not code. It was a plan, some pushback and five questions.
The pushback was the useful part.
Shift plus arrow keys does not exist on a phone, so “play from anywhere” had to mean “any computer”. The server had to be the referee, or people would win by editing the page. Arrow combos could be fixed per spell (muscle memory, like a fighting game) or random (reaction), and that was a design decision, not a technical one.
We argued about the stack. It proposed React for the menus. I replied that I have “an allergy from the obnoxious framework known as react”.
It took that well. We landed on:
- Svelte for menus, PixiJS for the battle scene
- TypeScript everywhere, with the rules in a module shared by client and server
- everything on DigitalOcean
I also asked it to draw the pixel art. It explained how it would: every sprite is a grid of letters in a source file, one letter per pixel. I was sceptical.
“First, I want to see a game”
Then I changed my mind, which became a theme.
Forget the grand design, I said. Two sprites facing each other. Words above their heads. Type a word, fire a shot. Get hit mid-word and you lose your progress. Playable over my home network. Now.
A little later there was a wizard on my screen. This is the wizard, exactly as he lives in the source code:
....RR..........
....RRR.........
.....RRR.....g..
.....RRRr...gGg.
....RRRRr....g..
....BBBBBB...W..
..rRRRRRRRRr.W..
....SSSSSS...W..
....SSSESE...W..
....SwwwwS...W..
....wwwwww.SSW..
...RRwwwwRRRRW..
...RRRwwRRRr.W..
...RRRRRRRr..W..
...RRBBBBRr..W..
...RRRRRRRr..W..
..RRRRRRRRRr.W..
..RRRRRRRRRr.W..
..RRRRRRRRRr.W..
..KKK...KKK..W..

R is “team colour”, so the same wizard is red on one side and blue on the other. G is where the projectiles come out.
I stopped being sceptical.
The first bug was a good one. I opened the game on two computers and both were told the arena was full and they were spectating. The AI had tested the game with two browser tabs of its own, left them open, and its phantom players had reconnected and taken both seats on my server. It found the cause in a minute, fixed the real gap underneath (a spectator now claims a seat the moment one frees up), and wrote itself a note never to test on my port again.
Then I played it, against a human, for an embarrassingly long time.
How the game plays

Two fighters, 120 HP each. Above your fighter are five words: short, medium, long, ultra-long, and one with digits and symbols, like w1z@rd or gg-ez!.
You do not pick a word. You just start typing. Your five words always begin with different characters, so the first keystroke locks on. Esc drops the word. Backspace fixes a slip. Wrong keys are ignored, with a buzz.
Finish the word and it flies.

Every word shows two numbers: its damage (the orange number) and its [armour] (in brackets).
Damage grows faster than length. A three-letter word does 3 or 4. An ultra does 30-odd, a quarter of a health bar, and it flies faster too: big words cross the screen in a second, small ones drift for nearly two. Rare letters and symbols add a bonus.
So why not spam long words?
Because of the rule that turned this from a typing test into a game.
The armour rule
In the first version, any hit wiped the word you were typing. Playtesting killed that in about ten minutes: whoever spammed three-letter words won, because nobody could ever finish anything longer.
The fix came out of that playtest. A small word should not be able to interrupt a big one by itself.
So every word has armour: short 1, medium 2, long 4, ultra 8, symbols 4. A hit knocks the attacking word’s armour off the word you are typing. At zero, your progress is wiped.
- two shorts break a medium
- two mediums break a long
- an equal or bigger word interrupts instantly

Long words are a gamble. Short words are how you stop someone else’s gamble. That one rule is the game.
When it gets tense
Under 50 HP your fighter starts to pulse. Under 20 it flashes red, the health bar turns red, and the music changes gear: faster, sixteenth-note bass, war drums, the melody doubled an octave up.

No art files, no audio files
Everything you see and hear in a match is code. (The only image files in the project are the logo, and a script draws those too.)
The seven fighters (wizard, cat in a wizard hat, knight, necromancer, robot, ninja, dragon) are letter grids like the one above. The six arenas are painted by small functions onto a 640×180 canvas and scaled up so the pixels stay chunky, with a bit of life in each: falling leaves, a flickering campfire, a waterfall made of particles, sand blowing across Dubai.

The music is a step sequencer written with the Web Audio API. It is in D minor, over the most reliably epic chord loop there is, with three moods: menu, battle, danger. The keystroke sounds climb the D minor pentatonic scale, so typing a word plays a little run that always fits the soundtrack.
The AI was upfront that it cannot hear anything, and that I would have to be its ears. It sounds better than it has any right to. It will still be replaced: the admin panel already has an upload slot per mood for real tracks.
Then I decided to launch it
At some point “a weekend toy” became a list: accounts, a leaderboard, a sprite editor, a marketplace, an admin panel, a Discord presence.
I asked for a plan again, and again the pushback was the valuable part:
- The game had exactly one room. Launching meant real matchmaking, private rooms by link, and surviving a wifi blip.
- Players will draw things you do not want on a game kids play. So nothing a player draws is visible to anyone until it has been checked, there is a report button, and enough reports take a sprite offline automatically.
- Paying creators real money turns you into a bank. Identity checks, tax forms, and creators who are mostly minors. So the marketplace runs on gems, a currency with no cash-out.
- “Points = damage, doubled if you win” is farmable. So only matches between two real accounts score, and the same two players can only score against each other five times a day.
I reversed myself several times, and I am including that because it is how projects actually go.
I planned Discord and Google login, then switched to plain username and password, because I wanted signing up to take ten seconds. It suggested Prisma; I asked why we needed Prisma; it agreed we did not, and we used Drizzle. It wanted to keep extending our small hand-rolled game server; I asked about Colyseus, a multiplayer framework. It changed its recommendation, but not for my reason. I thought Colyseus would be faster. It said it would not be, and that the real argument was that the next phase was precisely Colyseus’s feature list, so this was the cheapest moment to adopt it.
And I wanted a free, self-hosted AI model for moderation. It did the arithmetic: a server big enough to run a “free” model costs far more per month than a fraction of a cent per sprite. Then it built the code to support either, plus a labelled test set to measure any model before trusting it. I read the numbers and used the API.
Six phases, one Sunday
Each phase ended deployed and tested.
0. Pipeline. Git, Docker, DigitalOcean App Platform, the domain, HTTPS. Every push to main deploys.
1. Rooms. Quick match, “play a friend” links, a 20-second seat hold with automatic reconnection, forfeits. Load-tested with 500 bots typing at 110 words a minute: median keystroke round trip 1.6 ms, on less than half of one CPU core.
2. Accounts and leaderboard. Salted password hashes, sessions, profiles, match history, weekly and all-time boards.

3. Admin panel. Dashboards, player management, a match browser, word lists you can edit live, uploadable arenas and music, and an audit log of everything an admin does.
4. Sprite editor and moderation. More on this below.
5. Shop and marketplace. Also below.
6. Hardening. Anti-cheat, rate limits, terms and privacy pages, account deletion, monitoring, and a weekly leaderboard post to Discord.
The sprite editor and the robot moderator

Players draw their own 16×20 fighter in the browser. Draw, erase, fill, pick a colour, undo. You can start from any built-in fighter. “Team colour” swatches recolour with your side, so your sprite works as red or blue, and a “shot point” tool sets where your projectiles come from.
Free accounts get one slot. Supporters get twelve.
Every save is rendered to an image and shown to a Claude model with one question: is this fine for a game children play? Allowed sprites go live. Refused ones, and anything the check could not assess, wait for a human.
![]()
“No verdict” never means approval. An edit sends a sprite back through the check, so you cannot get a harmless sprite approved and then redraw it. Five player reports take a sprite offline until an admin looks.
Money, but pretend money

Supporters can list up to three sprites for sale, priced in gems. Gems are bought through Stripe and have no cash value. The creator keeps 70% of each sale, in gems.
Two details I like.
A gem balance can only change together with a ledger entry, inside one database transaction, and can never go below zero. There is exactly one function in the codebase that moves gems.
A buyer gets a frozen copy, not a reference. So if a creator redraws or deletes the original after you paid for it, yours does not change.
The Stripe webhook verifies signatures and delivers each payment exactly once, however many times Stripe retries. The tests sign their own fake events and replay them to prove it.
The shop itself stays closed until I have a proper business account. Everything else is live.
The anti-cheat caught my own robot
This is my favourite piece.
It is a typing game, so the obvious cheat is a script that types for you. The server therefore measures the rhythm of your keystrokes during a match.
Humans do not sustain more than about fifteen keys a second. And humans never type with metronome-even gaps. A match typed too fast, or too evenly, is recorded but voided: no points, no best-speed record, and a note in the admin panel explaining why.
I know it works, because it caught me.
To take the screenshots for this post, a script drove a real browser and typed the words with a fixed 70 ms between keys. It won the match. Then this happened:

Faster than most people, slower than the speed limit, and far too regular to be a person. The screenshot script had to be taught to type like a human, with jitter, before the game would give it any points.

The AI’s own test bots hit the same wall. All of them now type with random gaps.
What broke
I trust this build more because of how it failed than because of how it worked.
The first deploy with a database crashed. DigitalOcean’s cheapest database tier forbids CREATE SCHEMA, which the migration tool insisted on running. The platform rolled back by itself, the site never went down, and the fix was a twenty-line migration runner of our own.
A production bug that was not one. A test reported that logged-in players were being treated as guests, but only in production. The cause was the test, not the game: Cloudflare adds its own cookie to responses, and the test’s cookie handling kept the wrong one. Real browsers were fine. It proved that before moving on.
A blank page. A new route for uploaded files lived at /assets/:id. So does the page’s own JavaScript.
A guard that never fired. A check for “was this sprite edited while the AI was looking at it?” compared timestamps. Postgres keeps microseconds. JavaScript keeps milliseconds. They never matched. It became a revision counter.
Moving the database. The old database could not be reached from outside the app, so the app copied its own data to the new one at startup. It was rehearsed locally first, which caught two bugs before they could touch real data, and every copied table was checksummed against the original.
Each of these was found by a test or by looking at the running thing, explained plainly, and fixed at the cause.
And every status report separated three things I rarely see separated: what was tested, what could not be tested (“I can’t hear audio”, “I couldn’t open the live site from here, please click through it”), and what I needed to decide.
What I actually did
I made decisions. Which trade-off, which vendor, which risk.
I created the accounts and pasted the keys. It never asked to see a secret, told me more than once not to paste one into the chat, and built a safeguard so that a redeploy could not wipe the ones I had set.
I said no to React.
I playtested, which is where every good rule in the game came from.
Everything else, the code, the sprites, the arenas, the music, the logo (three concepts; I picked the sword in the keycap), and six end-to-end test suites that play full matches, make test purchases and deliver signed payment events, came from the other side of the conversation.
The stack, for the curious
- Client: Svelte 5 for menus and pages, PixiJS 8 for the battle scene, Web Audio for all sound, Vite
- Server: Node 22, Colyseus 0.18 for rooms and matchmaking, Express for the API. One process, scaled up rather than out, because every match lives in its memory
- Shared: the game rules are one TypeScript module used by both sides, so the client can predict your typing instantly and the server still has the final word
- Data: Postgres with Drizzle. Uploaded arenas and music live in the database too, for now
- Payments: Stripe Checkout
- Moderation: Claude, through the Anthropic API
- Hosting: DigitalOcean App Platform and a managed Postgres cluster, deploy on push
What’s next
Stripe, properly, with a business licence, and then the shop opens.
A Discord server.
Real music.
And the original pitch, which is still sitting there: cast fireball, hold Shift, up, right, down, left, release. The engine was built so that a new mode is a set of rules, not a rewrite.
Go and fight somebody. Type fast. Hit hard.
Just do not type too evenly.