A closed circuit, proved by 6,816 launches

· 6 min read · game-dev, physics, three.js, rapier, testing

A white sports car drifting on a wet circuit beside a red and white kerb and green run-off, with a fence and a pit building behind

In Endurance Racing the circuit is the whole world, so the car must not be able to leave it. That sounds like a detail until you try to prove it. A model of a racetrack is full of places a car should not reach, and the car will find all of them, at 160 km/h, in the rain.

My first version of the circuit had walls where the model had walls. The audit I wrote to check it found 336 launches out of 6,816 that ended outside the circuit, one of them 441 meters away. This post is about how that became one.

What "the car stays inside" means #

I needed a definition before I could test anything. The intended area is the ground a car's center can reach by driving from the grid, the lap or the pit lane without crossing a barrier: the track, its run-offs, the pit lane and its connectors, the paddock and the infield that connect to the circuit, all bounded by something solid.

What is not intended: the far terrain beyond the barriers, anything past a fence or a wall, the void, the top of a wall, a roof, the inside of a pit garage, the gap under a wall, and any place a car can enter but not leave without a respawn, which I call a trap.

"Reachable" is physical and comes from the collision the game really loads. An up-facing triangle counts up to 60 degrees, because the arcade car climbs 40 degree grass banks (my first limit was 35 degrees and a car got up an embankment the audit then found). A step of up to 0.85 m per 0.5 m counts, a drop of up to 12 m counts, and the car cannot pass through the band between 0.25 and 1.5 m above the surface of a wall, a steeper face or a fence. In the audit the car is 2 meters wide.

The definition made some decisions for me. Two public roads that cross the circuit on a bridge, and two fenced compounds, are modelled ground that the car cannot reach, so they are out.

What the build adds #

All of this is generated at build time from the collision data, and none of it is drawn.

  • Fences become walls. The model's fences are only pictures. Each panel now also exists as a collision wall, with a twin 0.6 m behind it and 4.5 m taller, because a lone 2.3 m panel threw a 180 km/h car over it.
  • Invisible walls on every open boundary. A flood fill over a 1 m grid finds every edge between ground the car can reach and the far terrain or nothing. Cracks up to 1 m are closed first, corners are cut, and the result is simplified to 0.25 m. Each boundary gets two ribbons 0.6 m apart, from 0.8 m below the lowest surface near it to 7 m above the highest ground. That is 316 polylines and 4,636 segments, and none of it is visible.
  • Barrier tops. The top vertices of tire walls and guard rails are raised by 2.2 m, collision only, because cars rode up a 0.9 m tire wall like a ramp and stayed there.
  • Garages. The pit garages are hollow, sloppy shells, and a car once drove 17 m into one through a door gap. They now have footprint boxes.

The collision grew from 97,000 to 129,000 triangles, and the collision file from 0.73 to 1.07 MB. The drivable surfaces did not change.

After the walls are put in the grid, the fill runs again, and then the build reads back the quantized file it is about to ship and audits that too. The mesh compressor moves vertices by up to 2 cm, and it once opened a cell.

A net that must never fire #

Behind the walls there is a small runtime safety net, 27 KB of data. It does not steer anything. Every tick it checks three things: the car's center is outside the allowed region for more than 0.4 s, or more than 3 m under the ground for 0.25 s, or more than 1.4 m above it with one wheel or none touching and at rest for 2 s, which means it is perched on a wall or a roof. Then the car goes back to the last safe pose with a "BACK ON TRACK" flash.

The rule is that it must never fire in normal driving. The game counts every time it does, and after any test drive that count must be zero.

The audit #

npm run audit:containment has two parts. The static one reads the collision file and lists leak edges, traps and the far annexes in about 10 seconds, and draws a map. The dynamic one uses the real physics with a manual clock, in four browser pages, about 4.5 minutes in all:

  • launches outward from 48 positions on the lap, to both sides, at bearings every 10 degrees from -90 to 90, at 50, 100 and 160 km/h at full throttle,
  • a handbrake slide at 100 km/h and a start off a kerb ramp at 160 km/h,
  • 6,816 launches in total, and 500 standing drops.
Before After
Leak edges in the static check 30,709 in 24 clusters 0
Launches that left the region (of 6,816) 336, up to 441 m outside 1, by 1 m
At 50 / 100 / 160 km/h (1,824 each) 70 / 87 / 107 0 / 0 / 0
Respawns by the old fall rule 268 0
Falls through the world 4 0
Ended balanced on a wall or a roof 9 7 (0.1%, caught by the net after 2 s)
500 drops inside the region: outside or fell 0 / 0 0 / 0

There is a row that got worse. Cars "pinned", moving less than 2 m in 7 seconds, went from 193 to 664. That is not a trap: the cars no longer leave, so they stop against a barrier, and my test's reverse-then-steer routine does not always free them. I checked that with a static test (every free place can drive back to the lap) and by placing cars at three of the four biggest pinned spots, where every one of 8 headings moved the car 6 to 39 m. In the game R or Backspace frees a car.

What the audit found, one by one #

I keep this list because each was a trap for the next change:

  • A single wall with no thickness let a 180 km/h car pitch over it. Now two ribbons and 7 m.
  • A wall triangle lying on a bank blocked the whole bank in the grid. Obstacles are now split to half a cell and marked with their own height.
  • The 35 degree limit was too low for the car.
  • A lone fence panel was cleared. Now a twin, and taller.
  • A hollow garage hull could be entered.
  • Rails and tire walls worked as ramps.
  • The region must be the conservative reach of a 0.5 m car, or the audit counts legitimate tight passages as escapes.

The rule I took from it: after any change to the model, a tile, a wall, a fence or a pit connector, rebuild the circuit, run the static check, and then the dynamic run on a fresh build. If a wall now blocks something it should not, I fix the model. I do not weaken the audit.

Limits #

About one launch in 7,000 grazes the 1 m of slack beyond a wall at 160 km/h, and 0.1% of full-throttle crashes leave the car balanced on a barrier top until the net respawns it. The old fall rule is still there for any place the walls do not cover.

The circuit is in Endurance Racing, and the project page is here.

Related projects