Snake, Playable Right Here in This Post
Yesterday I wrote about wiring up Godot so Claude Code could build Pong and actually see what it made. That post ended with a working game, but you had to take my word for it. Screenshots are proof, but they are not fun.
So today I gave it a different assignment: build a game with no engine at all, and ship it inside the post about building it. No download, no embed from some game portal, no iframe pointing at another site. The game below is a React component sitting in this article, running on the same page you are reading. Claude Code wrote all of it: the game, and also the change to my blog that lets a markdown post mount a live component in the middle of the text. My contribution was the ask and the QA. Give it a try.
Snake
Arrows / WASD to steer. Swipe on mobile.
space pauses · high score stays in your browser
Snake is the right pick for this. The rules fit in your head, the whole thing is around 300 lines, and yet it still forces you to solve the three problems every real-time game has: a loop, input, and rendering. What follows is what Claude did with each one, and what I learned reading the code afterward.
A game loop that does not fight React
React wants to re-render when state changes. A game wants to update sixty times a second. If you put the snake's position in useState, you get sixty re-renders a second and a component that spends more time reconciling than playing. It works, technically, the way a car with square wheels technically rolls.
Claude's answer was to keep two kinds of state and refuse to let them mix. Everything the game loop touches lives in refs: the snake segments, the current direction, the food position, the tick speed. Refs update instantly and trigger nothing. React state is reserved for the moments a human actually needs the UI to change: the score ticking up, the game over screen, the pause overlay. In practice React re-renders a handful of times per game instead of thousands.
The loop itself is requestAnimationFrame with a fixed-timestep accumulator. The browser calls the frame callback whenever it feels like it, often 60 times a second, sometimes 120 on a fast display, sometimes 11 when the tab is struggling. You do not want your snake moving faster on a gaming monitor. So the callback accumulates elapsed time and steps the simulation in fixed slices. Godot gives you _physics_process for free and this is the same idea, built by hand in about eight lines. I did not ask for any of this. It is just how the component came back.
The oldest bug in Snake
Every Snake implementation eventually meets the same bug. The snake is moving right. A player quickly presses up, then left, inside a single tick. Each input looks legal on its own: up is not the opposite of right, and left is not the opposite of up. But the snake only moved once, so the net effect is a 180 degree turn straight into its own neck. Game over, and the player is certain the game cheated them, because they never pressed left while moving right.
I know this bug personally. A version of me wrote it decades ago and never figured out why the snake sometimes died for no reason. Claude shipped the fix in the first draft, unprompted: a tiny input queue, where new directions are validated against the last queued direction rather than the direction the snake currently faces, capped at two moves. Six lines. There is a comment in the code explaining why, which is more than past me ever did.
Touch was the real work
The keyboard part of input handling is a lookup table: arrows and WASD map to directions, and the handler calls preventDefault so arrow keys stop scrolling the article while you play.
Mobile is where the interesting decisions were, and after fixing this site's missing mobile menu yesterday I was not going to accept a desktop-only toy. Swiping is the natural way to steer Snake on a phone, so the canvas listens for touches and turns any drag past a small threshold into a direction change, then resets the anchor point so you can chain swipes without lifting your finger. The part nobody tells you, and that Claude handled without being told: the listener has to be registered as non-passive and the canvas needs touch-action: none, or the browser interprets every downward swipe as an attempt to scroll the page. Your snake dives and the article scrolls. Both, at once. It is as bad as it sounds.
The details that make it feel finished
None of these were in my request, and together they are most of the difference between a demo and a game:
- The canvas renders at device pixel ratio, so the grid is crisp on a phone screen instead of blurry.
- The game pauses itself when the tab loses focus. Coming back to a dead snake you did not kill is a bad feeling.
- The speed ramps up a little with every food eaten, from a comfortable start toward a cap that stays humanly playable.
- Your high score persists in
localStorage. It never leaves your browser, there is no leaderboard server, and I have no idea what you scored.
This is the part of working with Claude Code that still surprises me. The gap between "make Snake" and a thing that feels finished is all judgment calls, and it made the same calls I would hope a careful developer would make, then wrote them down in comments.
What carried over from the Godot experiment
The lesson from the Pong post was to keep the game's rules in plain code, away from the engine, so they can be tested without a display. That habit transferred directly. The tick function here takes the current state and produces the next one: move the head, check the walls, check the body, grow or shrink. It does not know the canvas exists. Rendering is a separate pass that reads the state and draws it.
That separation was the whole reason the AI feedback loop worked in Godot, and it turns out it is just as valuable when the engine is a browser. Same principle, different rendering target: the rules do not care whether the pixels come from a scene tree or a 2D context.
Two games in two days, one in a real engine and one in a blog post, and I have now written approximately zero lines of game code. I am still deciding how I feel about that.