Logo Valérian de Thézan de Gaussan

I built a multiplayer Pong game in 3 hours with $1.06

Valerian Valerian
June 1, 2026
6 min read
Table of Contents

Yesterday afternoon I decided to build a dumb little web game. Two balls bouncing in a square, fighting over territory. A pixel-art tug-of-war you can watch with friends. It cost me $1.06 in DeepSeek V4 Pro tokens and three hours of my Sunday.

The model

Let me talk about DeepSeek V4 Pro for a second. It’s been absurdly cheap since launch: I burned through 1.5 million tokens and the bill came to roughly a dollar. That’s the kind of pricing where you don’t think twice about iterating. You just say “nah, that color palette is sad, try again” a dozen times and it costs less than a liter of gas (especially at the time of writing…).

But here’s the thing: this pricing is promotional. It jumped 4×, as the price per million tokens was only for a while, as a promotional offer. It is now $0.435 per million input tokens, and $0.87 per million output tokens. It is still a pretty good deal compared to other models, like GPT5.5 at $5.00 per million input tokens and $30.00 per million output tokens.

What it does

The game is called Pong Battle. Open it in two browser tabs and you’ll see the exact same thing. The grid is 20×20. The left half starts pink, the right half starts cyan. Two white balls bounce around autonomously, when a ball touches an enemy cell, that cell flips to its color. The ball bounces back and the territory line shifts. It’s an infinite back-and-forth with no end condition. Like watching a tennis match between two Roombas.

How it actually works

The core trick is determinism. The entire game engine is a pure function of a single seed and a tick counter. No WebSockets, no server push, no streaming state. The server just ticks the simulation 60 times per second and occasionally saves the state to disk. When you open the page, your browser fetches one JSON blob: { tick, seed, grid, balls, viewers }, deserializes it, and runs the exact same engine locally in a requestAnimationFrame loop. You’re not watching a video stream of the game, it’s your computer replaying it from the seed.

This means 1,000 concurrent viewers cost the server the same as 1. The server just runs the tick loop and serves a tiny JSON file on request. Each client does its own rendering. It’s the laziest possible multiplayer architecture and I love it.

A few implementation details worth mentioning:

The engine is a single file (src/lib/engine.ts) shared between server and browser. It uses a seeded mulberry32 PRNG for the initial ball directions and a deterministic jitter function (hash(x, y, tick)) to slightly perturb bounce angles. No Math.random() anywhere in the game logic. The grid is a Uint8Array because it’s fast to iterate and serializes cleanly to a number[] for JSON.

Viewer counting is done via a heartbeat. Clients POST a random session ID every 10 seconds. The server keeps a Map<sessionId, timestamp>, prunes entries older than 15 seconds, and returns the count in the heartbeat response. It’s primitive but it works.

Persistence is JSON to disk every 5 seconds. If the server restarts, it loads the saved state and picks up where it left off. Graceful shutdown hooks save on SIGTERM. The Docker setup mounts ./data:/app/data so the state survives rebuilds.

The ball hitbox went through a whole arc. At first it was just a point, the ball’s center cell determined territory flips. Then I added a BALL_RADIUS so the ball’s body sweeps a bounding box, and any enemy cell touched by the edge gets flipped and bounced. The wall bounce logic mirrors this: the ball bounces when its edge hits the boundary, not its center.

Responsive layout has side counters on desktop, a compact bottom bar on mobile. The counters, tick display, and viewer count all sit below the canvas, not overlaid on the grid.

The AI did the typing, I did the steering

This is the part I want to be honest about. DeepSeek wrote all the code. The engine, the canvas renderer, the heartbeat system, the responsive CSS, the Dockerfile, all generated from prompts.

But I had to drive.

The model’s first instinct for “shared state between viewers” was a full WebSocket setup with per-tick broadcast. I had to push back and say: the game is deterministic, just share the seed and let clients simulate. Once that clicked, it produced a much cleaner design.

It also tried to make the balls move at random speeds, which I vetoed: the speed should be constant, only the bounce angle jitters. It added a glow effect behind the balls that I removed ten minutes later because it looked like a lens flare from a 2004 forum signature. It wanted grid lines everywhere until I said kill them. It over-engineered the color system with manual hex dimming, and then I asked for a BRIGHTNESS config knob.

The dance was: I’d describe the desired behavior at a high level, it would produce something that worked but felt slightly off, I’d say “simpler” or “less harsh” or “no, the ball edge, not the center,” and it would iterate. The architecture choices (deterministic client-side replay, heartbeat-based viewer counting, Uint8Array grid, constant ball speed) those were mine. The AI would have built something that worked too, but it would have been heavier, more complex, and less elegant.

I use AI everyday and I can still say that in 2026 the AI still can’t properly do architecture. I’m not saying it never will, because considering how fast it’s genuinely evolving, it will probably in 2027. But today it’s not there and I still have to steer on the architecture, just like I do with my human colleagues. So, as a software architect, my job hasn’t changed much, except that instead of having the implementation work done in days, it’s now in minutes.

The real cost

Three hours and $1.06. The $1.06 part is ridiculous and probably unsustainable. The three hours were genuinely fun. It felt like pair programming with someone who types at 2,000 WPM but occasionally needs you to grab the wheel and say “no, the other left.”

If you want to check it out, it’s at dtdg.fr/pong. The source is on GitHub (see the repo). Inspired by pong-wars.