Skip to main content
CryptoFlex// chris johnson
ShippingSubscribe
§ 01 / The Blog · Game Development with Claude Code

Cann-Cann 2.0: We Have Multiplayer

I called my artillery game pretty much unplayable and asked Claude Code to fix it. One session later it had honest physics, a real soundtrack, a locked-down Neon database, and a friend beating me over the internet.

Chris Johnson··23 min read

My review of my own game was "pretty much unplayable." Every shot felt random, the weapons didn't behave like their descriptions, and the explosions had visual glitches I couldn't unsee. I handed that complaint to Claude Code, and one session later a friend and I finished an online match without a single issue.

That match is in the database, so I can show you the rows. The game is live at cann-cann.vercel.app, free in any browser on a phone or desktop, with nothing to install. Open it in another tab, hit Quick Play, and miss a few shots while you read. By the third one you'll see why I care so much about missing.

First, the part where I explain why my own game was so frustrating.

The Ask#

Cann-Cann is the artillery game I built over the earlier posts in this series (part 4 covers recreating the 1990 original for the web). It's a cartoony remake of Bang! Bang!, a 1990 Windows 3.0 shareware game by David B. Lutton II, built with Next.js 15, React 19, Canvas 2D, and Zustand. By this point it had 14 weapons, a campaign, a daily challenge, achievements, replays, weather, and a half-built online mode.

It also wasn't fun, and I said so. This is the prompt, verbatim:

"I want you to do a detailed analysis of this repo and the intent of this game. I want you to provide a plan to significantly improve the gameplay, physic, fun, sound and music. I want to keep the same infrastucture but today the game is pretty much unplayable. Feedback is every shot feels random where it lands. The physics on different weapons don't feel like you know where it's going to go and what it actually does. There are weird visual issues with collisions. I feel like I still want the game to be cartoony but people should have fun with it."

I also asked it to start with sequential thinking, read the code, quiz me with brainstorming questions, and do deep research on similar games and on the original. That's a lot of homework for one message. Claude Opus 5.5 ran the whole session, orchestrating subagents from the main thread.

Plan First, Code Later#

Sequential thinking framed the problem in one line: this wasn't missing features, it was a broken core loop. Aim, predict, fire, learn. If the player can't learn from a miss, nothing else matters.

Three review agents then went through the audio, the rendering and collisions, and the controls and aiming, while the main session read the physics code itself. In parallel, a deep-research run sent six researchers after the classic lineage, shot predictability, weapon design, game feel and terrain, audio, and player complaints. A seventh agent wrote the report. Its title is the whole design philosophy: "Make Every Miss Teach the Next Shot."

One researcher went further than I expected and downloaded the original Bang! Bang! v1.00 shareware zip from archive.org, then decoded its WinHelp file and the strings inside the executable. Here's what the 1990 game actually did:

  • You aimed with a "Cannon Fire Control" dialog. You typed an angle (0 to 95 degrees) and a velocity (50 to 900 km/h), or nudged them with +/- buttons in steps of 0.1, 1, or 10.
  • The values stuck between shots, "making it a little easier to 'range in' on your opponent."
  • Each battle got one random wind, shown as a whole number, and it never changed mid-battle.
  • One hit destroyed the cannon. No AI, no weapons, and almost certainly no sound (the EXE imports no sound module).

Several web descriptions of the game are wrong, by the way. Arrow-key aiming, multiple weapons, and wind that changes every turn all turned out to be myths.

What is bracketing?

Bracketing is how artillery crews found a target: fire, see whether you landed short or long, adjust, and fire again until your shots straddle the target. The original game's remembered values existed to support exactly this. Each miss narrows the range, so each miss teaches you something.

The report boiled the genre down to one lesson: players forgive visible chaos and call invisible variance cheating. A shell that goes wild because of a big wind arrow on screen is fine. A shell that goes wild for reasons you can't see feels rigged. My game was full of the second kind.

Then came three rounds of brainstorming questions. I picked the recommended option every time, and told Claude so:

"I'll just follow your recommendations but I would like to do all of the upgrades in one session."

Why Every Shot Felt Random#

The analysis found more than a dozen separate reasons, and none of them was a single bug. The randomness was a pile.

Wind was the loudest one. It was re-rolled from scratch every turn on every difficulty: the advanceTurn function never passed the previous wind forward, and the code meant to drift it gradually was dead. Wind maxed out around 27% of gravity, which sounds small. A later review measured that a full swing from -8 to +8 moves a cross-map shot about 375 px, against a tank hit radius of 18 px. You could land a perfect shot, change nothing, and miss because the dice re-rolled.

Power was far too touchy, too. One power point moved the landing spot 25 to 30 px, more than a tank is wide, so you couldn't dial in a shot even if you wanted to. And whatever you learned didn't carry over. Each weapon had its own speed and gravity multipliers, so at the same angle and power the Sniper flew 5.4 times as far as the Standard shell, the Heavy 0.41 times, and the Mortar 0.125 times. The aim guide, meanwhile, lied. It started about 21 px below the real muzzle and drew only the first 25% of the flight.

And then there were the weapons that didn't do what they said:

WeaponThe description saidThe code did
HomingSteers toward the targetNever steered (the steering function had no callers)
BouncerBounces 3 timesBounced once
ClusterSplits into 5 bombletsMade 3, and only the first landing counted
LaserStraight beamPushed around by wind
Rain, galeAnnounced as weather effectsGravity and wind changes never applied

Campaign levels showed a wind value that was display-only. The "No Wind" modifier only applied on turn one. Damage was arbitrary too: a Standard shell had to land within about 15 px of the tank to do anything at all, thanks to a quadratic falloff plus a 5-damage threshold.

The collision glitches#

The visual problems I'd complained about had their own list. The animation loop paused during the 0.8 to 1.3 second explosion, so particles froze mid-air and screen shake stuck at full strength. The shell stayed drawn inside the ground. The crater, the tank shove, and the tank fall all popped into place in a single frame afterwards.

The hit circle was 36 px wide around a 28 px sprite, so shots "hit" empty air beside the tank. Collision was only checked at the endpoints of each physics step. Fast shells could tunnel straight through a ridge. Physics also stepped once per animation frame, so on a 120 Hz screen every shell flew twice as fast as on a 60 Hz one. There was even a "hit-stop" value that the code wrote and never read.

I'd described all of this as "weird visual issues." Claude described it with pixel counts.

Sound, and a mode that didn't work#

Audio had its own pile. Every sound bypassed the mixer, so the SFX volume slider did nothing. There was no limiter. The thumps sat at 40 to 80 Hz, which is inaudible on a phone speaker. The music engine produced sub-bass drones plus white noise, with no tempo, no chords, and no melody. And the online mode, as it turned out, never actually submitted a turn.

What I Picked#

Wind became a gentle drift per turn: fixed on Easy, plus or minus 1 on Medium, plus or minus 2 on Hard. Health became 100 HP, with readable splash, so the blast ring is the damage zone. The terrain heightmap stayed. The audience was friends and family, on phone and desktop, and the audio plan was CC0 samples plus synthesis, with cartoon brass music and Worms-style gibberish voices. Tank driving with fuel, a weapon draft, a shop between rounds, and parachute crates went on the list for later.

Two answers carried most of the weight. For weapons, I picked the same flight for almost every weapon, about 10 total, which is the one that fixes the 5.4x problem. If power and angle mean the same thing for every shell, then what you learn with one weapon carries to the next.

For aim help, I picked classic bracketing: a short guide, a ghost of your last shot, and a "SHORT 40" readout, with the full arc only on Easy and Practice.

Annotated crop of the battlefield after a miss. A green dotted ghost trail of the last shot ends in an impact X short of the CPU tank, with a SHORT 45 tag and a readout showing the current angle and power next to the last shot's values. Numbered callouts mark each aid.

The ghost trail shows where the last shot went, and the tag says how far off it was. Your next shot starts from a number instead of a guess.

Because the audience was phones as well as desktops, the controls got two layouts. Desktop gets a docked control bar under the battlefield. Phone portrait stacks the weapon chips, angle, power, fuel, and a large FIRE button below a smaller battlefield.

Desktop and phone portrait layouts of the same aiming screen. Desktop has a docked control bar under the battlefield. Phone stacks weapon chips, angle, power, fuel, and a large FIRE button below a smaller battlefield.

Simulate Once, Play It Back#

So what do you build when the same shot lands differently on a 120 Hz screen than on a 60 Hz one? Claude had a rule for how to build all this: write the contract first, then fan out. The contract was deterministic math (sin, cos, atan2, and exp built from basic IEEE operations, so terrain and shots come out identical in Chrome and Safari), physics constants in pixels and seconds, the new weapon roster, and the types for a simulated shot.

What does deterministic mean?

A deterministic simulation gives exactly the same result every time it runs from the same inputs, on any machine. That sounds like a nice-to-have until you want two browsers on different continents to agree about where a shell landed without sending the shell's position over the wire. Browsers' built-in math functions can differ slightly between engines, which is why the contract rebuilt them from basic operations.

The architecture that followed is simulate-then-play. When you press fire, the game simulates the entire shot once, at a fixed 120 Hz, and then plays that result back in real time. Because the simulation is separate from the animation, 120 Hz screens stopped flying shells faster. It also means the aim guide, the AI, replays, and online play can all use the same simulation, and none of them can disagree.

Swipe to see the full diagram · Tap to enlarge

When you press fire the game simulates the whole shot once at a fixed 120 Hz, then everything else reads that one result: the aim guide, the playback, the AI, replays, and the online referee.

The physics got simple on purpose:

ConstantValue
Gravity300 px/s^2
Launch speedV_MAX x sqrt(power / 100)
Distance at 45 degrees on level groundabout 9 px per power point
Wind3.75 px/s^2 per unit

That square root makes power map close to linearly onto distance, and the wind constant makes one unit of wind worth about one power point. If the wind arrow says 5 and you're shooting into it, add about 5 power. You can do that math in your head, which is the entire point. The old game had you doing it with a typed angle and velocity in 1990, and I'd somehow built a more confusing version in 2026.

On top of that came swept collision (checking the whole path of a step, not just its endpoint), a hitbox that matches the sprite, and a blast ring equal to the damage radius. Impacts got choreographed: crater at the moment of contact, knockback and fall as tweens, dirt settling afterward, tanks tilting to the slope.

Here is a Big Bertha on the live site, from the fire button to the bullseye:

Animated GIF of a Big Bertha shot on the live game. The readout shows 55 degrees and power 62 with a tailwind, the shell arcs over a plateau and hits the CPU tank directly, with an impact flash ring, a fireball, BULLSEYE and KA-BOOM pops and a -55 damage number.

The weapons were rebuilt around the same contract. The new roster is Standard, Big Bertha, Cluster (5 bomblets at the top of the arc), Bouncer (reflects off slopes up to 3 times), Roller (new), Digger, Dirt Bomb, Napalm (fire runs downhill), Laser (a straight beam that ignores wind and gravity), and Homing (locks on at the apex). Nuke and Quake arrive from crates and the shop.

The Cluster finally does what its card says. It splits into five bomblets at the top of the arc, and every one of them lands and counts:

Animated GIF of a Cluster shot on the live game. The shell climbs into a headwind, splits into five bomblets at the peak, and they fan out and explode around the CPU tank with blast rings, a BULLSEYE pop and an 18 damage hit.

Six Waves of Agents#

The build ran in six waves, about 25 agent runs plus reviewers. The waves went from the contract, to parallel engine pieces, to rules and AI, to integration and polish, to the new mechanics, to online play and reviews. Many agents ran in parallel git worktrees, each owning a strict list of files so they couldn't step on each other. Every wave got merged, tested, and reviewed before the next one started.

Swipe to see the full diagram · Tap to enlarge

Six waves, about 25 agent runs plus reviewers. Each wave was merged, tested, and reviewed before the next one started.

Strict file ownership

Parallel agents collide when two of them can edit the same file. Giving each agent a fixed list of files it may touch, and nothing else, made the merges boring. Boring merges are the goal.

The test suite grew from 469 tests to 1,901, and the 15 TypeScript errors in two test files went to zero. The Vercel preview build passed, and the feature branch merged as PR #3.

The AI came out too good#

The opponent now ranges in like a human player, using the same simulation I do. The first version was frightening. On Hard it landed a direct hit on its very first shot 24 times out of 50. Nobody wants to play against that.

So it got retuned until it behaved like a person who needs a few tries. Average shots to first damage ended up at 3.98 on Easy, 2.84 on Medium, and 2.16 on Hard, with zero first-shot direct hits on any difficulty. I wanted the opponent to need a few shots, because a first-shot hit teaches you nothing.

Sound you can hear on a phone#

The audio agent used 105 CC0 samples, 868 KB in total, from Kenney packs and the VSCO-2 Community Edition orchestral library. Each license got read before use. It wired them through a real mixer with a limiter (so the SFX slider finally works), added the gibberish voices, and composed cartoon brass music.

Then it did something I liked a lot: it rendered the music in headless Chromium and measured it. Sixty to seventy percent of the energy sits between 250 Hz and 3 kHz, a range phone speakers can actually play. Its first render of the explosions showed 85 to 97% of the energy in the sub-bass, so it went back and fixed the gain staging.

The gotchas#

The funniest one: a zsh loop variable named path wiped PATH, and curl "disappeared" from the shell. The rest were less amusing.

  • Agent worktrees were created from main instead of the feature branch. Every agent had to fast-forward first.
  • After many merges, a stale .next cache crashed webpack. rm -rf .next fixed it.
  • Playwright screenshots captured stale frames mid-animation, so flight timing got measured from the DOM instead. Fire to next turn is about 3.2 seconds, of which the CPU's think time is 1.1.

zsh ties path to PATH

In zsh, the lowercase path array is tied to the uppercase PATH variable. A loop like for path in ... overwrites your PATH, and every command afterwards vanishes. Name the loop variable anything else.

Sudden Death#

What happens when two players who can't quite land a hit keep trading misses? I watched a test battle play out and asked:

"how long is that battle going to go on, I feel like if you can't accurately shoot this can go on for hours"

With physics you can read, the answer was: indefinitely. So Claude added Sudden Death: from turn 21 (after 10 shots each), there's no wind, blasts are 1.5 times bigger, and every hit is fatal. If turn 30 arrives with no winner, the match is a stalemate. Practice mode is exempt from both.

I like that it came from watching a battle instead of from a design document.

Two Reviews, Four Bugs, and a Browser Playing Music#

A full-branch code review found no critical bugs. Its words: "the engine and the local turn loop are solid: no determinism leaks, no double-fire, no stuck phases." It validated 2,081 saved states. It did find four real problems around the edges, all fixed:

  • Campaign stars were lost on reload.
  • Challenge links dropped the rules, so a Quick Play Draft turned into Classic for the friend who opened it.
  • Online drive chunks were sent faster than the server's 250 ms limit.
  • Remote actions were refused during the round-over pause.

Security reviews found two HIGH issues in the online server, also fixed. A malformed database URL could leak the connection string into logs. And refused actions could force unlimited server-side rebuilds, which would have made the referee a free denial-of-service button.

The deploy I caught#

One catch of the session was mine. I pasted a note into the chat:

"Rolling back this big deploy may not be clean, because the new version rewrites saved data in friends' browsers."

If 2.0 changed everyone's saved game in place and something broke, rolling back to the old build would hand it save data it didn't understand. The fix: 2.0 saves under a new key, cann-cann-save-v2. It reads the old save once, and it never writes or deletes the old key. A rollback leaves the old build its own untouched save.

Version your storage keys

When a new release changes the shape of persisted data, write to a new key and leave the old one alone. Rollback should be a deploy, not a data migration.

The demo that wouldn't stop#

Midway through, I messaged:

"are you running a demo right now? There is an open demo browser and its playing the music non-stop. I just want to make sure that when the testing is done you close that local host and also a reminder to clean up and temp files related to testing and open ports"

An automated browser had been left open, playing the brass soundtrack on a loop, and nobody had told me. After that, every agent prompt carried a cleanup step. The final cleanup freed all the test ports and deleted 206 MB of scratch files.

Setting Up the Database Together#

Online play had never worked. It's also where I got to watch Claude work on something real instead of on a codebase.

The architecture#

The old online mode lived on a Neon database shared with my other production site. Cann-Cann got its own: a dedicated Neon project named cann-cann, in AWS us-east-1, running Postgres 18 on the free plan. I created the project and said, "Ok lets go through this together setting up the new neon DB." Claude recommended switching the region from US East 2 (Ohio) to US East 1, because Vercel functions run in iad1. Same region means a short hop between the function and the database.

Three tables hold everything:

TableWhat it holds
cann_games_v2One row per match: seed, settings, physics version, status, hashed seat tokens
cann_actions_v2The ordered action log, primary key game id plus sequence number
cann_rate_limits_v2Per-IP limiter windows

The multiplayer model is lockstep. Each client sends its own actions, the server stores them in order, and everyone replays the same ordered log through the same deterministic engine. A unified action log (shot, move, draft_pick, purchase, shop_done) drives local play, replays, and online alike.

What is lockstep?

In lockstep multiplayer, nobody sends game state. Players send only their inputs, and every machine runs the same simulation on the same inputs in the same order. It's cheap on the network and hard to cheat, as long as the simulation is deterministic. That's why the earlier work on identical math in Chrome and Safari mattered.

The server acts as a referee. When an action arrives, it rebuilds the match from the stored log with the same engine (about 40 ms for 200 actions) and rejects anything illegal. It appends with optimistic concurrency on the sequence number, so a client with a stale view gets a 409 instead of silently corrupting the log.

Swipe to see the full diagram · Tap to enlarge

Each browser sends only its own actions. The server referee rebuilds the match from the stored log with the same deterministic engine, appends with a sequence check (a stale sequence gets a 409), and clients poll for what they missed.

Local online dev had never worked, and the reason was a good one. next dev bundles each route separately, so every route got its own private in-memory store, and two routes could never see the same game. Moving the store onto globalThis fixed it.

Migrations by hand, then by agent#

Migrations 1 and 2 went in through the Neon SQL Editor. Claude put each file on my clipboard after reviewing it, I pasted, I ran. That worked fine, and then I got lazy:

"I haven't dont step 3 but can't I just give you access to the DB and you can make the changes?"

Claude offered two options. Option A: a connection string for this one project in a private file. Option B: Neon's agent onboarding, which has access to every project on my account. I chose B, partly because I want the same setup for my cryptoflex repo later.

The command I'd copied from Neon's docs was:

bash
npx neon@latest skills -s neon -s neon-postgres -y

Claude didn't run it as written. As written, it would install into the current folder and into every coding app it could detect on my machine. Claude checked that the neon npm package is published from neondatabase/neon-pkgs, then ran the command with --global and --agent claude-code, so it landed only where I wanted it:

text
Scope       Skills               Agents       Status
user-level  neon, neon-postgres  claude-code  installed

I signed in with neon login (Auth complete (profile DEFAULT)). Migration 3 then ran through psql, with my owner connection string fetched inside the command and never printed:

text
BEGIN
CREATE INDEX
COMMIT

Verification came back as three tables, ten indexes, a nullable IP-hash column, and zero rows.

Locking It Down#

Giving an agent access to a database sounds scary until you see how much the agent tightens things.

Seat tokens are 32 random bytes, stored only as SHA-256 hashes and compared in constant time. If someone dumped the table, they'd get hashes that can't be replayed as seats. IP addresses get the same treatment: they're never stored raw, only as HMAC-SHA256 hashes keyed by a secret pepper (ONLINE_IP_PEPPER), and they're cleared after an hour.

On the request side, everything is parameterized SQL, and bodies must be JSON up to 8 KB (anything else gets a 415). The server allows one action per seat per 250 ms. Per-IP limits cover game creation, polls, joins, and failed requests. There's also a storage cap, a waiting-game cap, and a two-hour window for a friend to join.

The least-privilege role#

The app doesn't connect as me. It connects as cann_online, a role Claude created with SQL, because roles created in the Neon console join neon_superuser, which is exactly what you don't want. The checks that ran afterward: exactly three tables, with games and rate limits fully readable and writable and actions read and append only. No TRUNCATE, no sequences, no CREATE anywhere, not a superuser, no role memberships, and a 5 second statement timeout.

Why the actions table is append-only

The action log is the source of truth for every match. If the app role can only read and append, then even a bug or a stolen credential can't rewrite history after the fact.

There was one funny moment. The security checklist's own SQL failed:

text
ERROR:  "pg_toast_24576" is not a sequence

The query filtered on relation kind with AND conditions, assuming Postgres would test them in the order written. Postgres doesn't guarantee that, so it tried a sequence check on a TOAST table. A CASE guard fixed it, and Claude corrected the doc. I enjoyed watching a security document trip over its own test.

There's also one accepted risk, written down: cann_online can still connect to Neon's built-in postgres database. It's owned by Neon's cloud_admin, so the connect privilege can't be revoked. There, the role can read two pg_stat_statements views and nothing else.

The password nobody saw#

Claude generated the role's password (192 bits), set it, tested it, and piped it straight into vercel env add in one command. It was never displayed and never saved. Here's what came back:

text
ok: cann_online password set (192-bit, never shown)
ok: app url shape = postgresql://cann_online:***@<endpoint>-pooler.<region>/neondb?<ssl params>
ok: cann_online can read cann_games_v2 through the pooler (0 rows)
ok: cann_online is refused CREATE TABLE (permission denied)
ok: NEON_DATABASE_URL added (Production, Sensitive)
ok: ONLINE_IP_PEPPER added (Production, Sensitive)

I like the fourth line best. The test that the role can't do something is as important as the test that it can do its job.

Vercel's Hobby plan allows one rate-limit rule, so there's a WAF rule on the path prefix /api/v2/: 300 requests per 60 seconds per IP. Claude staged it as a draft, I reviewed it, and then it got published:

text
1   cann-online-api-rate-limit   Rate Limit (300/60s)   Enabled

Finally, the online API answers 503 until a public flag, NEXT_PUBLIC_ONLINE_ENABLED=true, is set. Claude asked me to flip it. I typed, "Yes, lets goo!" and after the deploy, a probe of a nonexistent game confirmed the API was alive and wearing its seatbelt:

text
{"success":false,"data":null,"error":{"code":"not_found","message":"Game not found"}}
HTTP 404

The First Match#

A friend joined a Draft match online and we played it through. My report afterward: "Finished a multiplayer game no issues."

Every Quick Play and online Draft match opens the same way, with both players taking turns stamping weapons from a random pool:

Animated GIF of the weapon draft screen. Player 1 takes Cluster, Big Bertha, Bouncer and a rare Quake while the CPU takes Homing, Roller and Napalm, with PLAYER 1 and CPU stamps landing on the cards and both trays filling up.

The screenshot below comes from a local dev build with the in-memory backend, not from that match. It shows the same setup with two browser views of one game: the host's turn on the left, and the friend's screen on the right with FIRE disabled until their turn.

Two browser views of the same online match side by side. The host sees a Your turn pill, an aim guide and a live FIRE button. The guest sees Friend is aiming and a disabled FIRE button reading Not your turn.

The database remembers the real one:

text
  status  | action_count | format | friend_joined
----------+--------------+--------+---------------
 complete |           35 | draft  | t

 action_type | count
-------------+-------
 shot        |    22
 draft_pick  |     8
 move        |     5

Thirty-five actions: 22 shots, 8 draft picks, 5 moves. The match ran about eight minutes, from 00:27:29 to 00:35:09 UTC, and I won 2 to 1. Nothing in those rows is an error. Every draft pick, drive, and shot went through the same engine on two machines, and the two machines agreed.

About Opus 5.5#

I'll say this plainly, in my own words from the end of the session:

"I do want to call out how powerful Opus 5.5 and the amazing results I had with it."

The taste calls stayed mine: keep it cartoony, ask how long a battle could possibly run (which is how Sudden Death happened), and notice that a rollback would strand friends' saved data.

Lessons Learned#

The sequence check that broke the security checklist is still my favorite part, and the closest thing to a lesson I have: checklists need tests too. A document written to prove the database was safe tripped over a TOAST table named pg_toast_24576, and the fix was one CASE guard.

Go Play It#

Everything in this post is live at cann-cann.vercel.app. Start with Quick Play, a weapon draft against a Medium CPU. Miss on purpose once, then use the ghost trail and the SHORT tag to range in, and count how many shots it takes you to land a hit.

Then go find a real opponent. Hit Play Online, send the invite link to a friend (it works for two hours), and settle something. Your shells land in the same spot on both screens. That's what all those rows were for.

Tell me in the comments how many shots your first hit took, and whether the CPU got you first.

Related Posts

Rebuilding the Galaxy: The Claude Fable 5 Transformation. An intelligence and measurement layer (300 games-per-second self-play harness, Goal-Oriented Action Planning) feeds the shift from invisible logic to immersive experience: animated turn playback in a 3.5-second budget, a three-layer parallax atmosphere, placebo controls wired to real logic, and growth from 924 to 1,145 passing tests with 25 verified bug fixes

I know very little about game development. My retro 4X remake now has animated turn playback, a parallax starfield, a working AI planner, and 221 new passing tests anyway, thanks to one session with Claude Fable 5 and seven agents running in parallel. Also included: the difficulty system we caught making easy mode stronger than hard.

Chris Johnson··13 min read

How I turned a functional web port of a 1991 game into a full-featured modern 4X strategy game across four feature phases and a wiring plan, using Claude Code as the primary development engine.

Chris Johnson··18 min read

How I used Claude Code and four parallel AI agents to rebuild Second Conflict, a forgotten 1991 Windows 3.0 space strategy game, as a modern web app. Complete with reverse-engineered game mechanics, 261 passing tests, and more nostalgia than I knew what to do with.

Chris Johnson··18 min read

Comments

Subscribers only — enter your subscriber email to comment

Reaction:
Loading comments...

Navigation

Blog Posts

↑↓ navigate↵ openesc close