DEV Community

Cover image for I wanted to know how they got GTA V into a browser. I ended up with an arcade and a monkey in a tutu
dav123
dav123

Posted on

I wanted to know how they got GTA V into a browser. I ended up with an arcade and a monkey in a tutu

Earlier this month, videos started going around of GTA V running in a web browser. The site went offline shortly after. GTA V came out in 2013 and is still the latest GTA you can actually play, since GTA VI isn't out until November. And the way I understood the story, the people behind it had got there with the help of an LLM.

I wanted to understand how. So I did what any reasonable person does when faced with a huge open-world game ported to the web: I asked an AI to make me a bubble shooter.

This is the story of what happened next. Spoiler: I still don't know how they did GTA.

A bubble shooter and some postage stamps

I had two coding agents to try out. Codex would write the code. Claude would orchestrate and, above all, refuse to believe Codex when it said "all green". I've written about that review loop elsewhere, so I won't repeat it here. The short version: the ticket pinned the stack (strict TypeScript, Phaser 3, Vite, Vitest, Playwright), and each game engine had to be pure, seeded, and testable without a browser.

Less than an hour after the first ticket, the bubble shooter worked. My first request (quotes from my tickets are translated from French) was the obvious one: "let me play it on my phone". By the evening I had a review, and it wasn't glowing:

"the phone display needs work, it feels like playing on postage stamps"

So the canvas got the whole screen and everything else moved into a ☰ menu, with the touch controls in a single row under your thumbs.

Then I got greedy. I asked for a game picker on the home screen and more arcade games, 30 levels each, and I listed my childhood: Tetris, Arkanoid, the snake from the Nokia 3210, Metal Slug, Galaga, Space Invaders, Pac-Man. What came out were Serpentin, Briqueterie, Empilo, Orbite, Essaim, Dédale and Ferraille. Original names, original levels, everything drawn in code, and no brand anywhere in the code. The ticket was blunt about why: some rights holders go after clones, even free ones.

The next morning I said "go for Opencade, state of the art", and it went public: MIT license, a CI that runs lint, unit tests, build and end-to-end tests, and a demo on GitHub Pages.

The backlog that wouldn't stop growing

Every time I played, I came back with a ticket.

Between the first bubble shooter ticket and the last card I opened, two and a half days went by. My backlog gained 40 numbers, 33 of them about games, and the game tickets all started the same way: I'd just played something.

"Put a picture on the home page for each game so you can see what it is." That one became animated thumbnails, rendered by each game's own engine. A lunar lander, a crate-pushing puzzle, a road-and-river crossing game and a kart racer all landed on the same day. Then, one evening: "Among the games I really liked, there was Zuma." Ticket. "I really loved Lemmings." Ticket. "I really loved Worms." Ticket. "And Boulder Dash." Ticket, one minute later.

Five releases, from 0.1.0 to 0.3.2. I'm not proud. I'm a little proud.

The kart that couldn't be driven

Dérapage is a top-down kart racer. My first review listed four things: the controls weren't practical on a phone, the kart should be much slower on grass, the track needed boost strips, and there should be trees. My second review was shorter: "in Dérapage, it's always the same circuit."

So I asked for tracks inspired by Formula 1: a street circuit in the spirit of Monaco, one that loops around Les Invalides in Paris, a fast, hilly one through the forest like Spa. Thirty tracks, chequered finish lines, a lap timer and best lap. Version 0.3.1 shipped with 527 green tests, 299 unit and 228 end-to-end.

Before six the next morning, I sent my verdict as a player: "Dérapage is unplayable."

Claude didn't argue. It wrote a simulated thumb that reads the game state every 150 ms and steers by its heading error. That's a driver with perfect information, a much better one than me. On the first circuit, in 40 seconds, it reached gate 2, hit things 121 times, spent 32% of its time off the track, and sat at a standstill for 23 samples, stuck against the rail near the start line.

So it wasn't just me. The causes were design choices: a tight street circuit as level one, steering relative to a camera that rotates with the kart, and rails that stuck to you. Version 0.3.2 switched to an absolute 360° joystick with a fixed camera, north always up, rails that push you back onto the track, and a wide, easy first circuit. Same simulated thumb, this time with ±0.35 rad of random noise on every gesture to simulate a clumsy one:

Circuit Gate reached Standstills Off track
1 10 0 0%
3 5 0 0%
15 9, lap 2 at 24 s 0 4%
The old first circuit, now fifth 11, lap 2 at 25 s 0 0%

I'd love to end the kart story there. I can't: a later run of the same clumsy thumb got stuck on lap 1 of the first circuit after a minute and a half, so the kart isn't officially declared playable again yet. 527 green tests, and unplayable. No test tells you a game isn't fun. You need someone to play it, even badly.

"Ferraille is super repetitive"

Ferraille is the run-and-gun one. After a few levels, I wrote: "Ferraille is super repetitive."

Again, Claude didn't argue, it measured. It built a repetitiveness score from 0 to 100 for all twelve games. A "novelty" is a game element that appears for the first time (changing a number doesn't count), a "clone" is a level too close to the previous one, and a "plateau" is a run of levels with nothing new:

repetitiveness = 100 × [ 0.50 × share of clones
                       + 0.25 × (1 − novelties / (n − 1))
                       + 0.25 × longest stretch without novelty / (n − 1) ]
Enter fullscreen mode Exit fullscreen mode

Ferraille scored 84.5. Four new things, all before level 7. After that, only numbers changed. And it wasn't even the worst: the maze game scored a perfect 100.

Ferraille has since gained six enemy behaviours, three weapons, wagons, hostages, guardians and a three-phase reactor. It's oddly satisfying to see your thumbs proven right in a table.

My pencils, then ChatGPT, then Codex

Next I wanted recurring characters across all the games: a panda, a red panda, a dragon, a monkey in a tutu and a unicorn. They're called Boubou, CaliRoux, Pestouille, Tutu and Lumi. The rule I set: they're redrawn in code, vector style, and no raster image goes into the public repo.

I also made a generous offer: "if you need graphics or visuals, put the request in a dedicated card, I'll get my pencils out."

The card exists. It has received zero requests so far. In the end I sent eight reference sheets anyway, without being asked. In my own words: "I made the sheets with ChatGPT, starting from hand drawings." So the characters went from my pencils to ChatGPT, and from ChatGPT to Codex, which redraws them in TypeScript. One more AI in the chain. I'm starting to lose count.

My favourite design debate was about Tutu. There's a chain-shooter game on the way, the genre where a line of coloured balls rolls towards a hole and you fire balls into it. My first idea: the chain is the monkey's tail. Then I hesitated, and the alternative that came back made a fair point: popping beads is a lot less strange than popping bits of tail. Final decision: Tutu dances along the track dragging a necklace of coloured beads, the beads pop, the necklace pulls itself back together, and as a bonus the red panda grabs the necklace and pulls it backwards. That game is still being written as I type.

Built for airplane mode

I wanted to play with no connection at all. Version 0.2.0 turned Opencade into an installable web app. A service worker precaches the whole build, Phaser included, so there's nothing left to fetch once it's installed. Saves live in localStorage, so they work offline too. I still owe it the real test, phone in airplane mode; so far only the end-to-end test has done that.

One morning I was convinced I was playing an old version. Maybe I was: service workers are very good at keeping things. Version 0.3.2 now shows the version and build date at the bottom of the home screen, checks for updates on launch, every 30 minutes and when the app comes back to the foreground, and has a "Force update" button in the menu. I'll admit the button exists mainly for me.

"No ads, no trackers (I hope that's true)"

When I asked for this article, the sentence I wrote was: I had a great time on my phone, with no ads and no trackers, and I hope that's true. I never wrote the code. My ticket said "no requests to third parties", which is not the same thing as checking.

So I had Claude check.

First, the code. In the public repository's src/, there is exactly one fetch: the update check, which asks the app's own version.json. No XHR of its own, no sendBeacon, no WebSocket, no cookie. The only production dependency is Phaser, which brings in one tiny event library and nothing else. The CSS uses system-ui, so there's no web font to download either.

Second, what GitHub Pages actually serves. The built bundle mentions two outside URLs: phaser.io, in Phaser's default config and its console banner, and a bit.ly link inside a Workbox warning message. Neither is ever requested.

Third, a real session. In a Playwright container on a small VPS, on a simulated phone, Claude recorded every request the browser made, service worker included, while playing three games with taps, then reloading offline:

ctx.on('request', r => reqs.push({ url: r.url(), sw: !!r.serviceWorker() }));
// ... play Dérapage, Alunir, Orbite, then ctx.setOffline(true) and reload
const third = reqs.filter(r => new URL(r.url).origin !== origin);
Enter fullscreen mode Exit fullscreen mode

The result: 30 requests, all 30 to the Pages origin, 11 distinct files, zero to anyone else. Offline, the twelve games were still there. In storage, a single localStorage entry, the lunar lander save: {"unlocked":1,"best":0}. No cookies, no IndexedDB, one cache, the app's own.

So, no ads and no trackers in Opencade. Two honest caveats. GitHub itself says that when a Pages site is visited, "the visitor's IP address is logged and stored for security purposes", signed in or not, and I can't see or change that. And the page has no Content-Security-Policy, so nothing in the browser would stop a future commit from adding a third-party call. Only an audit like this one would catch it. I checked Chromium and three games, not every browser.

So, how did they do GTA?

I still don't know. I started out wanting to understand how people got a huge open-world game running in a browser tab, and I ended up choosing between a monkey's tail and a bead necklace. And typing "unplayable" with one thumb before six in the morning turned out to be a perfectly valid bug report.

If you know how they actually got GTA V into a browser, tell me in the comments. I clearly need a hint before I start asking for a pseudo-3D kart view. Which, by the way, is already in the backlog.

You can play Opencade in your browser, or install it on your phone: the demo. The code is on GitHub.

Top comments (0)