Two bikes, one tick

A head-on collision should kill both riders. In a naïve game loop it doesn't. HexRider, TrailBlazer, and one small decision about order.

5 April 2026 · 4 min read · hexrider, lab, game-dev

Share LinkedIn

In the 80s I wrote a game called TrailBlazer on our Acorn Electron. Two riders, a walled arena, a trail you couldn't cross. My brother and I played it for months. A few years later I rewrote it for the Atari ST and we played it for months more. It was never clever. It was a Tron cycle duel with the good parts stripped out so a kid could fit the whole thing in his head.

HexRider is that same game, forty years on. Canvas instead of MODE 2, TypeScript instead of BBC BASIC, and — the thing that actually made me open the editor — we can now play it on our phones without being in the same room. Everything else in the build exists to keep a childhood game alive between two adults who don't share a living room carpet any more.

But there's a thing early version cheated on, and I'd never really thought about until I had to write it properly.

What happens when two bikes ride head-on into the same cell?

In the original, whoever's update loop ran first "won". If the program scanned player one before player two, player one got there and player two crashed into a fresh trail. That was the physics. Chris and I didn't care — we were kids — but sitting in front of HexRider's repo it started to nag me. Every head-on 50/50 in the old game was already decided by the order of two variables in memory. That's not a game. That's a coin flip pretending to be skill.

The naïve fix is worse. If you resolve both moves, then stamp trails, then check collisions, you end up with both bikes trying to occupy the same cell before anyone has died. Stamp the trail first and now whichever order you check determines who crashes into whose fresh paint. You can't fix this with another if at the end. The ordering is the problem.

The fix is to split the tick into phases.

ts
// app/games/hexrider/physics.ts
export function stepGame(state: GameState, inputs: Inputs) {
  // 1. Resolve intents — direction, turbo, teleport
  applyIntents(state, inputs)

  // 2. Detect symmetric collisions BEFORE anyone moves:
  //    - head-on (both targeting each other's next cell)
  //    - swap    (both targeting the cell the other is leaving)
  const dead = detectSymmetricCollisions(state)

  // 3. Commit movement for survivors, stamp trails,
  //    then check trail/wall collisions as normal
  commitMovement(state, dead)
}

Three phases, but really one idea: decide the symmetric cases on paper, before the simulation touches pixels. If two bikes point at each other's next cell on the same tick, they both die. Full stop. No "whoever got scanned first wins". The unfair cases are resolved in a phase where "who went first" doesn't exist yet.

This is the only substantive bit of the whole game loop. Turbo, teleport, energy regen, the bot — none of it matters if the collision is quietly rigged. Get the 50/50 right and the rest is decoration.

I wrote this tick alongside Claude and Cursor. They were useful for the boilerplate and unhelpful about the ordering, which I had to draw on actual paper before it clicked. That feels about right for where we are in 2026 — the interesting decisions are still mine, the agents are fast typists with opinions.

A small thing I didn't expect: once fairness was real, the bot got more fun to play against. A naïve loop lets a greedy bot "win" head-on races because of the turn order, so it learns to rush. A fair loop makes those rushes genuinely 50/50, and the bot has to think about angle and timing instead. Making the rules honest made the opponent smarter. I don't think that's a deep truth about AI. I think it's a deep truth about games.

I have one regret about this piece, which is that my brother can't read the BBC version of TrailBlazer's collision code any more. The floppy is long gone. If it still existed I'd paste it here and we could all have a laugh at twelve-year-old me. What's there instead is HexRider, a tests folder that's larger than the game, and a simulation where — if we ride straight at each other on the same tick, forty years late — we both lose. Fairly.

Next up

Continue reading

More from the feed — same tags when we can, otherwise fresh picks so you always get a next read.

All articles →