Building a Browser Game With Claude Code: Automated Level Testing

Between June 14 and 17, 2026, Claude Code and I made 170 commits to CrowdSurfer: Dive In, a browser physics game, under one rule: no level ships without passing the solver. When I audited the repo on October 13, 2026, the solver covered 58 of the game’s 98 playable stages, and it had passed three levels that were broken on screen.

This is a game, not a website, and it has no search traffic worth reporting. I am writing it up here because the solver is the same thing as the publish gate on my generated county pages: a check that decides what an AI agent is allowed to ship. The game made the gate’s blind spots easier to see.

What was the rule, and how did the solver check it?

The game is one HTML file running Matter.js physics. You throw a music note at grumpy fans; pop them all, then reach the goal. The project’s status doc, PROJECT_STATUS.md, calls the gate “the cardinal rule (never break it)”: “No level ships without passing the solver.”

The solver, verify.mjs, does not play like a person. For a slingshot level it fires 195 simulated shots, 39 angles at 5 power settings, and records what each one hits. A level passes if every target is hit by some shot and some shot reaches the goal. It exits non-zero if any level fails.

Every one of the 170 commits carries a Claude co-author line. Here is how the work split by day, and how often the solver itself was touched:

Day (2026)CommitsCommits that changed verify.mjsWhat the day was
June 14358Baseline, design docs, unified solver
June 15978Albums 1 to 3, scoring, art, music, landing page
June 16226Album 4, first real-time mode, two solver-checked modes
June 17160Five more real-time modes

Which levels did the solver actually reject?

Three times, by the commit messages. A Festival album “Note Hunt” level had a token boxed in by hazards and a bumper; the message says it was “relocated after the solver flagged it (15/16 -> 16/16).” On the Aftershock album, removing filler pegs from board 5 broke reachability, because the pegs “turned out to be load-bearing,” so its targets were moved.

The third rejection changed the design. Compact, bell-curve Plinko boards would not verify: “a true bell-curve pegboard resists being provably solvable; the wide drop boards verify, the compact ones didn’t.” They were reverted. The gate decided which kind of level could exist at all.

What did the solver pass that was broken?

Three levels from the very first commit. On level 4, the bottom wall sat under the launcher, so the note “spawned embedded” in the wall. On Garage tracks 9 and 10, a fan sat on top of a bumper, which the fix commit calls “unhittable.” I traced all three with git log -S: each layout is in the baseline commit of June 14 at 18:28.

Level 4 was fixed at 13:03 the next day, 18.5 hours later. Tracks 9 and 10 were fixed at 15:59, after 21.5 hours. In between, more than 30 commit messages report the solver green. None reports a failure. The solver was answering its question correctly. “Can some shot hit every fan?” was yes. “Does the level look right when it loads?” was never asked.

The same gap shows up twice more. On June 15 the Neon and Festival boards all passed, but the commit that fixed them calls them “‘any shot’ levels”: a bouncy ball “eventually touches every sensor in an open field regardless of aim.” Passing meant solvable, not good. And verify.mjs computes a par, the shots a level needs, but never compares it with the notes the level gives you. The Aftershock Vault was added on June 16 with 3 notes and, per its commit, a par of 3. Later commits reworked it. I could not re-run the solver for this post, because its packages are not installed and installing them needs the network.

Google’s guidance on generative AI content says “it is critical to manually factcheck and review all AI-generated content for accuracy and trustworthiness before publishing.” The solver is a fact check. It is a narrow one. The layout bugs were caught by someone playing the game; the commit messages do not say who.

Why did the ball fly off the screen when every level verified?

Because the solver checked a model of the game, not the game. On June 14 at 22:42 a commit records that in the Brave browser a spinner sent the ball off-screen. Its diagnosis: “under the verified FIXED-step sim the spinner kick peaks at speed ~34 and the ball stays contained on all 20 levels.” The browser ran Matter’s real-time Runner, with variable frame timing, which amplified the kicks.

Claude’s first two fixes were patches: a speed cap, then a position net. Both were mirrored in the solver, and re-running it “shows all 30 levels still solvable with identical density, proving the cap never touches a real shot.” Seventeen minutes later the real fix landed. The browser now steps physics in fixed 1000/60 increments, “identical to the solver.” The product was changed to match what the gate tested.

SEO has the same trap. Google’s JavaScript SEO basics say “Google also uses the rendered HTML to index the page,” and “If the content isn’t visible in the rendered HTML, Google won’t be able to index it.” Anthropic’s Claude Code best practices say to run /verify “after Claude’s check passes to confirm the change against the running app.” A gate that reads your templates is checking the model.

Doesn’t a green gate mean the agent’s work was checked?

Only the work the gate reads. This is the assumption I would have made, and the repo shows it is wrong. On June 16 at 10:38 Claude added the first real-time mode, a shooter called Soundcheck, “OUTSIDE the puzzle solver (tuned by feel, as an action mode should be).” That is a fair call: you cannot sweep shots for a mode driven by live input. But it set a pattern.

Sixteen commit messages say the work sits outside the solver or that its gate is “npm test (boots/runs) + playtest.” At the last commit the game has 58 solver-checked levels. I ran npm run verify on that commit on October 13, 2026: all 58 printed SOLVABLE, and the run ended “ALL LEVELS VERIFIED SOLVABLE.” The game also has 35 levels in five other modes plus five Soundcheck styles, 40 stages that only a boot test and a player check. The solver never got smaller. It just covered less of the game each day.

The boot test shows what that costs. A polish commit on Soundcheck reported “npm test (46) + verify green.” The next commit, eight minutes later, says the band “wouldn’t move”: a renamed variable made the drag handler throw “on first touch.” On June 15, the fix commit for a separate bug says every on-screen button “did nothing on tap” on mobile. The tests passed both times.

Anthropic’s guide puts it plainly: “Claude stops when the work looks done.” Its fix for plausible-looking work is “If you can’t verify it, don’t ship it.” In this repo, new kinds of content shipped with a weaker check, and the commit messages said so out loud. Nobody reading only “all green” would notice.

Can you trust the status doc an agent writes?

Not its numbers. PROJECT_STATUS.md says: “34 levels + 4 encores + 1 bonus all solver-verified.” I loaded the level arrays from the commit that wrote that line. There were 30 base levels, 4 encores and 1 bonus: 35 levels. The “34” already included the encores, so the line counts them twice.

The doc was last changed on June 15 at 15:11. Seventy-six commits followed, including the Neon Pulse, Festival and Aftershock level sets and eight new game modes. The older HANDOFF.md still tells the next reader to run solve.mjs and solve_pinball.mjs. Both were retired on June 14 when the solvers were merged into verify.mjs.

The status doc also says “work on a branch off main.” The local reflog shows feature branches for the first six pieces of work, the last merged at 00:37 on June 15. That rule was written into the doc at 10:43 the same morning. All 130 commits after the last merge went straight to main.

Anthropic’s guide lists “Information that changes frequently” under what to exclude from CLAUDE.md, and says to “prune it regularly.” A level count changes with every commit. Typed into a doc that each new session reads first, it becomes the next session’s starting error.

What did I decide, and what did Claude decide?

From the commit messages only, the split is clear. The first design doc ends “Awaiting user sign-off before any implementation.” I made the calls that changed direction: “Per feedback that 10 near-identical Plinko boards felt pointless,” the “2-4 min” session length, the match-3 mode the design doc “had steered toward a physics ‘Cascade’ instead.” The last commit reads “Tested locally, approved.”

Claude wrote the solver port in six commits in six minutes, wrote every diagnosis, and kept game and solver in step for new mechanics: amp chains, armored fans, wormholes, hazard zones. The flipper mechanic was the exception. It shipped as an “OPTIONAL ASSIST” that “the solver ignores,” and was removed 39 minutes later. The message does not say why.

What does this mean for an SEO gate?

The game’s own landing page showed it. Its structured data listed "gamePlatform":["Web browser","iOS","Android"], and the advertiser page promised “Web + mobile (iOS/Android).” The repo has no iOS or Android project, and the Apple Developer and Google Play steps in GO_LIVE.md are unchecked. No test in the repo reads the landing page. I removed both claims on October 13, 2026, after writing this section.

Google’s generative AI guidance says the review “also applies to metadata like <title> elements, meta description elements, structured data, and alternate texts for images.” A gate that checks the game, and not the page describing it, leaves the markup unreviewed. What Google did with that platform claim while it was live, I can’t say.

I have no Search Console or analytics data for stage-dive.vercel.app, so I won’t put a traffic number on any of this. The site returned 200 with a public cache header when I checked it. My voice-rule post covers the opposite failure: a rule enforced so hard the pages bent to it.

How should you set up a verification gate for agent-built work?

  1. Write the gate’s coverage down. List the page types it reads. When the agent invents a new type, it starts outside the gate until you add a check.
  2. Write the question the gate answers. “Every target reachable” is not “the level looks right.” “Citation field filled” is not “the page says something.”
  3. Test the shipped output. Run checks on the live, rendered URL, and use URL Inspection’s rendered HTML, not the template.
  4. Put metadata and structured data in the gate. Diff every schema claim against what exists, such as platforms, prices and availability.
  5. Never type a count into an agent’s status doc. Have a script print it from the code, and delete commands for scripts that no longer exist.
  6. Keep a person on what the gate cannot see. Three broken levels here passed over 30 green runs. A player found them.

Sources

Rob Boirun
Rob Boirun

Rob Boirun is the founder of PopNet Media and has worked in web and search for 25 years. He writes SEO Lab's case studies from sites he builds and runs with Claude Code.

We will be happy to hear your thoughts

Leave a reply

SEO Lab
Logo