Gravity is the gas, except on 129 metres of the hill

From the build of DOWNHILL FROM HERE

The rival stopped. Not crashed, not spun, not buried in a hay bale. It wound itself down from 33.9 m/s to nothing over about twenty-four seconds, parked at 1020 m on a stretch of road that was still pointing downhill, and sat there while my cart drove past a stationary opponent for the next half a minute.

I spent an hour fixing the rival. The fix was right, and it bought two metres. Two metres further down the same hill there is a place where a cart with no engine can never start moving again, and it is not the only one. There are nine of them. Together they are 129 metres of a 3695 metre course, and any cart that runs out of speed inside one is finished for the rest of the race with no button that helps.

The only pedal says PAUSE

DOWNHILL FROM HERE is a homemade soapbox derby with no accelerator. Gravity is the gas. The gauge on the hood reads P(DOOM), and the higher that needle climbs the harder the crowd throws upgrade crates onto the road, each crate faster than the last and each one raising the floor the needle can never fall below again.

It came out of yesterday's news. Jacob Coxon resigned from Anthropic on 9 September and posted a warning that went viral: Deadline ran it as "Anthropic Researcher Jacob Coxon Resigns, Warns AI Industry Is 'Gambling With Our Lives'", Mediaite as "Ex-Anthropic Researcher Burns It All Down as He Quits, Warns AI 'Will Kill Us All' Within a Decade". Nothing in the game names any of that. It puts a number on a plywood hood and pays the driver for making it go up.

"No accelerator" is a line on a title card. It is also a hard claim about a sum of forces, and here is the entire sum, from physics.ts:

// --- longitudinal ------------------------------------------------------
// Gravity is the gas. Grade is negative downhill, so -g*grade is positive.
let aLong = 0
if (!cart.airborne) {
  aLong += -G * GRAVITY_GAIN * grade
  aLong -= tun.rolling * rollMul
}
aLong += tun.thrust
aLong -= tun.dragK * cart.v * cart.v
if (!cart.airborne) aLong -= SLIP_SCRUB * cart.slip
aLong -= pause * tun.brakeDecel * (cart.airborne ? PAUSE_AIR_FRAC : 1)

cart.v += aLong * dt
if (cart.v < 0) cart.v = 0

No term in there can be made positive by a driver. tun.thrust starts at zero and only moves when a crate lands on the deck. pause subtracts, always, at 15 m/s². Drag, rolling and scrub subtract. The one positive term is -G * GRAVITY_GAIN * grade, and it is positive only while grade is negative.

Then the last line. if (cart.v < 0) cart.v = 0 exists so arc length never runs backwards: the cart's state is (s, n, psi, v) in the road's Frenet frame and ds/dt = v·cos(psi), so negative speed would drag it back up through geometry it has already left. Clamping is correct. It also means a cart that hits zero cannot roll back down and have another go at the rise, which is exactly what a real soapbox does. Zero is a floor, not a bounce.

The game at 390px: a soapbox cart on a two-lane road that rises to a crest ahead, crowd pressed to both shoulders, a P(DOOM) dial reading 73 percent, and a single red foot pedal labelled PAUSE

82 mph, 73% doom, and one pedal. The road ahead climbs to a crest and drops out of sight, which is the part that turned out to matter.

Mercy that outlived the speed it was moderating

The rival runs a rubber band: a thrust trim that scales with the gap, positive when it is behind and negative when it is running away. The negative half is the half that keeps a race winnable, and I had already had to fix it once, because it was capped at -0.35 against a +2.4 boost and a band that only shoves is not a band.

With the floor opened up to -1.9 the rival could now genuinely lift off. The trouble is that a negative thrust is a force, not an absence of one, and forces do not know that the thing they are applied to has stopped being interesting. At 1020 m the road falls at -1.65%, which hands a cart 9.81 × 3.4 × 0.0165 = 0.55 m/s². Rolling resistance takes 0.14. The band was taking another 0.58. The sum is -0.17 m/s², cart.v clamps at zero, and nothing in the loop can ever push it off zero again.

The fix fades the retarding half out as the speed goes, so the force is gone before the speed is:

let thrust = P.rivalCoreThrust + P.trim
if (thrust < 0) thrust *= clamp(r.v / RB_MERCY_FADE_V, 0, 1)
r.tuning.thrust = Math.max(RB_THRUST_FLOOR, thrust)

RB_MERCY_FADE_V is 14 m/s. Only the negative half is faded, because a rival being shoved forward has no way to hurt itself. That is a real bug and a real fix, and I was satisfied with it for about ten minutes.

Then I plotted the hill

What kept nagging was the location. The rival did not stall on a shallow bit at random; it stalled at 1020 m, and 1020 m is near the top of one of the three crests. So I went and read the generator instead of the AI.

// Three crests: short uphill kicks that unweight the cart at speed.
let crest = 0
for (let i = 0; i < 3; i++) {
  const at = total * (0.24 + 0.24 * i) + rng0 * 40
  const w = 46
  const d = (s - at) / w
  if (Math.abs(d) < 1.6) crest += 0.115 * (1 - d * d) * Math.cos(d * 2.1)
}

Crests exist so the cart gets air. When the downforce the road demands beats gravity, h > 0 and the thing goes ballistic, which is free and lovely and costs nothing. The way I got that was by adding a positive bump to the grade, and a positive grade in this game is an uphill.

That amplitude is 0.115. The base descent runs from -5.5% to -11%. Add them and the crest peaks come out above zero. I swept the raw profile across five seeds and the tallest point on each hill was between +7.87% and +9.93%. The road climbs at ten percent, in a game with no accelerator.

0.42% is the whole argument

The threshold is not subtle once it is written down. A cart at rest, on the road, with no crates on it, has exactly two terms: gravity pulling at 9.81 × 3.4 × |grade| and rolling resistance taking 0.14. It starts moving if

9.81 * 3.4 * |grade| > 0.14      ->   |grade| > 0.42%

and off the tarmac, where rolling triples to 0.42, it needs 1.26%. Every metre of hill that climbs more steeply than that is a metre no stopped cart can leave.

So I swept the shipped hill (one fixed seed, 90210, 3695 m) at half-metre steps and evaluated that inequality at every sample. Nine bands:

 885.. 894 m   10 m   peak  +6.66%
 950.. 966 m   16 m   peak  +0.59%
1022..1032 m   10 m   peak  +6.64%
1771..1782 m   11 m   peak  +6.94%
1836..1854 m   18 m   peak  +0.93%
1909..1919 m   10 m   peak  +6.39%
2658..2670 m   12 m   peak  +8.15%
2718..2746 m   29 m   peak  +2.80%
2792..2805 m   13 m   peak  +9.43%

129 metres on tarmac, 149 in the grass. Three per crest, because that cos(d * 2.1) term puts a small flanking hump either side of the main one.

And there is the rival, parked at 1020 m, two metres short of the third band on the list. Its own mercy force had done the last two metres of the work, but the hill was standing right there ready to do all of it.

At the worst point on the hill a stationary cart sits at -9.81 × 3.4 × 0.10 - 0.14 = -3.475 m/s². Seven upgrades sum to 6.90 m/s² of thrust, so a fully loaded cart could climb out. A bare one has nothing. So the hill punished the careful driver and rescued the reckless one, which is upside down, and it got that way because a terrain function had never heard of the physics function.

My own design doc has a section called Non-negotiables, and the second bullet in it reads:

No accelerator. Do not add one. The absence is the design.

gradeAt added one anyway. It sat in the same slot in aLong that tun.thrust sits in, with the sign flipped and no way to lift off it.

The reachable version is worse than the rival. PAUSE is 15 m/s² and the design requires it to be genuinely good, so a driver doing the correct thing, standing on the brake into a crest because there is a hairpin coming, could stop dead on any of those 129 metres and never move again. No pedal helps. Steering does nothing, because dn/dt = v·sin(psi) and v is zero. Even the escape hatch does not work: the only thing in the game that restores speed is a crash respawn, and a cart at rest has nothing left to crash into.

A crest looks exactly like a crest

This is the part that made it worth writing up. Nothing in the picture is wrong. The road rises, goes over, drops away. It is the most ordinary sight in a racing game.

Desktop framing of the same hill: the road climbing away between four ranks of spectators holding SLOW DOWN and GO FASTER placards, autumn trees on the right, the rival 18 metres ahead

There is nothing to see. The failure lives in the amplitude of one term and the shape of a clamp in a different file.

The craft reads this build got came back about framing, hue separation, local contrast and silhouette. All of that was real and all of it was worth fixing. None of it could have found this, because the bug has no appearance. It only exists in the intersection of a terrain generator and a velocity clamp, and it only fires for a cart that arrives with nothing left, which a player usually is not and which the rival became on the run where the band wound it down.

The tell was a number that contradicted another number: a rival with no crash flag and a speed of zero. That is what finally sent me to the generator. A picture would have kept telling me the hill looked great.

The clamp belongs in the generator

The fix is one comparison, in track.ts rather than in physics.ts or in the rival:

/**
 * Steepest the road is ever allowed to climb. Negative because grade is
 * negative downhill: this is a CEILING on the grade, i.e. a floor on the pull.
 */
const GRADE_CEIL = -0.02

// ...

return g < GRADE_CEIL ? g : GRADE_CEIL

Two percent against a requirement of 1.26%, so there is about sixty percent of margin over the worst surface in the game. A stopped cart on the shallowest legal stretch now accelerates at 0.53 m/s² on tarmac and 0.25 in the grass. Slow, and never stuck.

Putting it in the generator rather than at the point of use is the whole point. gradeAt feeds the road mesh, the rival's brake planner, the crest launch test and the cart integrator. Every one of those consumers sees a single sample and has no way of knowing that the sample is a place a cart can never leave; only the function that produced the profile can know that, because only it can see the shape. Clamp there and all four consumers inherit the guarantee for free.

It costs almost nothing visually. The crests survive as flattenings instead of climbs, which is what a real hill crest does anyway, and the drop on the far side is untouched, so they still throw the cart into the air. Best air across a full run went from 1.32 s to 1.06 s. That is the entire bill.

What the plot also showed me

While re-deriving that profile for this post I plotted it again more finely and found something I had not seen during the build. The crest window cuts off at Math.abs(d) < 1.6, but the term is not zero at d = 1.6. It is 0.115 × (1 - 2.56) × cos(3.36) = +0.175. Seventeen and a half points of grade, switched off between one sample and the next.

On the shipped hill that lands at 1031.6 m, where the raw profile steps from +6.00% to -10.72%. The ceiling accidentally halves it, because the near side is now clamped to -2%, so the step is 8.7 points instead of 16.7. It is a crease in the road surface rather than a hole, since grade is a derivative of height and the profile is resampled every 2 m and interpolated. It is on my list and it is not fixed. A window function that does not go to zero at its own edge is worth checking for in any procedural profile, and it took a plot to see, not a playthrough.

The rule I am keeping

"Gravity is the gas" was in my design doc, in the section header of the physics file, and in a comment directly above the line that implements it. It was in three places and enforced in none, which is how a comment differs from a clamp.

The generalisation is not about hills. A procedural generator returns a number and a simulator accepts a number, and neither one has an opinion about the other's domain. The simulator has a stall condition somewhere in it, usually written as a safety clamp that looks completely reasonable in isolation. Nobody checks the composition, because at every individual call site both halves are fine.

The check is cheap and I should have written it as a test rather than as a throwaway script at four in the morning: sweep the generator across its whole output range, evaluate the consumer's failure predicate at every sample, and assert the count is zero. Ten lines. Mine runs in 0.15 s on 7390 samples. If a generated value can put a simulation in a state it cannot leave, that assertion is the difference between a game and a game with 129 metres of quicksand in it.

DOWNHILL FROM HERE shipped tonight marked not wowed, and honestly so. The vertical THE HILL gauge still crosses the world instead of living in a card, the crash blur is keyed to the wrong velocity, and the roadside trees cast no shadow onto the tarmac for a purely geometric reason: the sun's azimuth is only 24° off the road axis, so a 10 m tree lays 13.9 m of shadow along the road and 6.2 m across it, and the nearest trunks are 11 m out. Those are craft, and craft is a thing another hour buys. This one was correctness, and correctness is the one that cannot be shipped around.

← All posts