cd /news/ai-tools/a-whole-replay-in-a-url-how-flappyca… · home › topics › ai-tools › article
[ARTICLE · art-152852] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

A whole replay in a URL: how Flappycar's ghost links work without a backend server

A developer built Flappycar, a browser game created in a few hours with an AI coding agent, that shares entire runs as self-contained URLs with no backend server, accounts or database. The game runs physics at a fixed 120 steps per second and generates each daily course from a date-derived seed, so a run is fully described by the course, the step numbers of taps and kickflips, and the crash step; these are encoded as varints and base64url, with a 92-second run fitting in about 351 characters.

by read6 min views1 publishedOct 19, 2026

I built a small browser game, Flappycar, in a few hours with an AI coding agent. It is one

button: a tap fires a sports car's cold-gas thrusters, and holding the tap kickflips it.

Everyone gets the same course each day.

The part I want to write about is the share. When you send a friend your score, the link carries

your entire run. Their game replays it as a translucent ghost car beside them, with your name on

it, and they race it. There is no backend server behind this, no accounts and no database. The game is a

static site, and everything a friend needs travels in the URL.

One tap fires the thrusters. The course gets darker, and faster, the further you get.

The usual way to let people race each other is an account system, a database of runs and an API

to fetch them. I didn't want to operate any of that for a toy. Flappycar is about 4,900 lines of

plain JavaScript with no dependencies, served as static files from a free hosting tier. So the

question became: what is the smallest thing that, sent in a link, lets someone else's browser

rebuild my run exactly?

The answer turned out to be very small, because two design decisions do most of the work.

The physics runs at a fixed step. The game simulates at 120 steps a second regardless of the

screen's refresh rate. Each animation frame adds real elapsed time to an accumulator and spends it

in whole steps:

acc += dt;
while (acc >= STEP) {
  update(STEP);   // STEP = 1 / 120
  acc -= STEP;
}

Given the same starting state and the same inputs on the same step numbers, a fixed-step

simulation produces the same run on every machine.

The course is a function of a seed. The daily course is generated from the date. Daily #1

was 16 October 2026; the day number is scrambled by a small integer hash into a 32-bit seed,

and that seed drives a tiny pseudo-random generator (mulberry32). The obstacle spawner draws

only from that generator. Anything purely visual, like smoke and sparks, uses other randomness, so

it can never shift the course.

Put those together and a run is fully described by three things: which course it was on, the

step numbers where the player tapped or started a kickflip, and the step it crashed on.

Replaying a friend's run means stepping a second car in lockstep with yours and firing its

thrusters on their recorded steps:

while (run.idx < ev.length && ev[run.idx][0] === run.step) {
  if (ev[run.idx][1] === 1) startFlip(run.car);
  else fireThrusters(run.car);
  run.idx += 1;
}
stepCar(run.car, STEP, speed);
run.step += 1;

No positions, no velocities, no frames. Just the taps.

The encoding is a list of unsigned varints, then base64url so it survives being pasted into a

chat. The header is the format version, whether it is a daily or a random course, the day or

seed, the score, the step the run died on and the number of events. Each event is stored as the

gap since the previous one, doubled, plus one bit for "tap" or "kickflip":

export function encodeGhost({ day = 0, seed = 0, score, deathStep, events }) {
  const bytes = [VERSION];
  const endless = !day;
  pushVarint(bytes, endless ? 1 : 0);
  pushVarint(bytes, endless ? seed >>> 0 : day);
  pushVarint(bytes, score);
  pushVarint(bytes, deathStep);
  pushVarint(bytes, events.length);
  let prev = 0;
  for (const [step, kind] of events) {
    pushVarint(bytes, (step - prev) * 2 + (kind ? 1 : 0));
    prev = step;
  }
  return toBase64Url(bytes);
}

Taps come roughly every 0.7 seconds, about 86 steps apart, so each one costs about two bytes.

Two runs on Daily #2:

Run Taps Bytes Characters in the link
44 seconds, 26 obstacles 61 127 170
92 seconds, 60 obstacles 128 263 351

A good run fits comfortably in a link a chat app will preview and nobody will notice. The rest of

the link is plain query parameters: the sender's name, their car's colour and the day.

What a friend sees when they open your link, and what they see when they beat it.

Once ghosts worked, the obvious next request was a leaderboard, which normally means a backend server

again. Instead, each link carries a short crew list: every person whose ghost the link has passed

through, with their score, up to five. When you share after racing a link, your sender is added

to it. A group chat builds up standings as the link goes round, and the game shows them on the

result panel and in the share text.

Standings from a link that has been round four people. The URL is the database.

The trust model is honest about what it is: anyone can edit a URL. Among friends in a chat,

that is fine. It is not a global high-score table, and it doesn't pretend to be.

Tuning changes break old links, silently. A replay is only exact if the physics and the course

are the same as when it was recorded. Widen a gap or change a speed curve and an old ghost flies

straight through a tower. That is why the first byte is a format version, bumped for every change

that touches the course, the physics or the scoring. The header layout never changes, though, so

an older link still opens on its day with the sender's name and the score to beat. It just races

without the ghost car, instead of drawing one flying through scenery that has moved.

Links get damaged in transit. A friend's link refused to load the day after launch. The version byte

was fine. Decoding it showed that three characters had changed from A to B somewhere between

the sender's phone and the recipient's. In this encoding, that turned three 80-step gaps between

taps into 336-step gaps. Correcting those three characters replayed the run to its exact score.

The format has no checksum, so the game can't tell a damaged link from garbage. Anything that

travels through chat apps should carry one.

Analytics can't see the clever part. The hosting's privacy-friendly analytics records the path

and referrer, but drops the query string, which is exactly where the ghost lives. So the paths

themselves carry the signal: every challenge link lands on /c/, and the number of /c/ visits

compared with the number of links sent is a direct read of whether the game spreads.

The thing I didn't expect: the agent was fastest at the parts I would have procrastinated on,

like the share card, the emoji result strip and this replay encoding. It needed the most steering

on feel: how high a tap lifts the car, how wide the gaps are, how the crash lands. Those took

playing, not prompting.

What made it trustworthy was making it build its own checks. A headless soak flies the game's

autopilot through a hundred courses at a time to prove every gap is passable. The GIFs and screenshots in

this post were filmed the same way, by driving the real game frame by frame in a headless browser

with the autopilot flying. They are the game, not mock-ups.

Flappycar is at flappycar.com. It is free, works on a phone, has no

sign-up and installs to your home screen if you want it to. Today's course is the same for

everyone. If you beat a friend's ghost, the game makes a link with yours in it; send it back.

A friend's ghost goes down, and the run carries on past its score.

Tell me where you crashed.

── more in #ai-tools 4 stories · sorted by recency
── more on @flappycar 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/a-whole-replay-in-a-…] indexed:0 read:6min 2026-10-19 · —