Every rotary pilot trains for the day the engine quits. The procedure is unambiguous: lower the collective immediately to keep the rotor spinning, glide down on the energy already stored in that rotor, flare near the ground to trade airspeed back into rotor RPM, then spend it in one pull of collective to cushion the touchdown. Get the sequence right and a dead engine becomes a controlled arrival. Get it wrong: hold too much collective and the rotor droops towards a stall, hold too little while carrying too much speed and it can run towards an overspeed, and there's no second attempt.
I built a browser game around that manoeuvre. Not a helicopter sim with autorotation as one feature among many. A small, focused game where the entire challenge is those four phases and the one number that ties them together: rotor RPM, or Nr.
The bet: model one number, not a helicopter
The tempting version of this game models a whole helicopter: engine, airframe, weight and balance, the works. I didn't build that. I modelled rotor RPM as a single scalar energy state, Nr = 1.0 at nominal, and made almost everything else in the game a function of it.
The reason is a real aerodynamic fact, not a design shortcut: rotor thrust scales with the square of RPM.
const acc = h.bladesGone ? 0 : (h.collective * T.maxThrust * h.nr * h.nr) / env.mass;
Drop Nr to half and you don't get half your lift, you get a quarter. That single relationship is why an autorotation lives or dies on RPM discipline rather than on airspeed, altitude or anything else a pilot might instinctively watch first. Getting that one coupling right didn't finish the model by itself, there's a fair amount of tuned scaffolding stacked on top of it, but it gave every other term something honest to push against. So the whole flight model is built around a five-term equation for dNr/dt, and everything the player does is either feeding that equation or spending its output.
The four phases, in code
Dump collective. The moment the engine dies, holding pitch is the wrong instinct, and it's the first thing every pilot has to unlearn under stress. Raising collective increases blade drag, and with no engine supplying torque, that drag pulls straight from the rotor's own kinetic energy. Lowering collective does the opposite: it lets upward airflow through the disc, generated by the descent itself, do what the engine used to do.
const upKill = Math.max(0, 1 - T.cCol * h.collective); // clamped
const driven = T.kUpflow * Math.max(0, h.vy) * upKill * upflowFade(h.nr);
That's a tuned stand-in for the real feedback loop, not a derivation of it: descent rate drives a term that spins the rotor up, and raising collective suppresses that term, the same direction real collective pull chokes off the airflow keeping an unpowered rotor alive. upKill is the price of holding pitch. Wait too long to dump it and there's roughly a second and a bit before Nr decays past the point of recovery. Most players lose their first few runs right there, which is exactly how it should feel.
Glide. This is where the early version of the game was, honestly, wrong. In the first build, cyclic did nothing to the physics during the descent. Autorotation looked right and felt like a vertical parachute drop with an RPM side-game bolted underneath it. The fix was vectoring the descent-resisting force through pitch attitude instead of applying it straight up:
// autorotative parachute: the turning rotor resists descent (∝ nr², only
// against downward motion — zero at hover, so powered parity holds).
// The force acts along the DISC NORMAL, so pitch vectors it...
const para = h.bladesGone ? 0 : T.kRotorPara * h.nr * h.nr * Math.max(0, h.vy);
ay -= para * Math.cos(h.pitch);
ax += para * Math.sin(h.pitch) * T.kParaTilt;
Once that force acts along the disc normal rather than straight down the y-axis, pitching nose-down converts altitude into airspeed, which is the glide. Two more terms bow that descent into a U-shaped curve instead of a flat one: a sink penalty that grows with speed, and a separate one that grows as speed drops towards zero. It's a tuned analogue of a real glide polar, not a drag-derived one, but it produces the shape that matters: a band of workable speeds with a cost on either side of it, instead of one number that's always correct.
Flare. Near the ground, aft cyclic increases the disc's angle of attack and converts forward speed back into rotor RPM, banking energy for the cushion. The code enforces something that matters more than it looks: you can't flare-spool from zero airspeed.
const satVx = Math.min(Math.max(0, h.vx), T.flareNrSatVx) / T.flareNrSatVx;
const flare = T.kFlareNr * Math.max(0, -h.pitch) * satVx * upflowFade(h.nr);
satVx saturates the airspeed contribution, so the flare only pays out Nr when there's forward speed to draw on, not RPM manufactured from a standstill. The airspeed itself bleeds off through a separate set of terms, so the two effects land together without being the same line of code. It's a small gate, but it's the difference between a flight model and a cheat code.
Cushion. The nicest part of this whole system isn't a formula, it's the absence of one. There's no separate "landing" code path. The touchdown cushion runs on the exact same collective × maxThrust × nr² relationship as the glide and powered flight, just executed a few feet off the ground with whatever RPM the player managed to preserve, while the flare's own lift terms are still tapering out underneath it. Protect the rotor all the way down, cash it in at the very end: that's the entire skill loop.
Making it feel like a resource, not a readout
None of that matters if the player can't feel it happening. Nr drives an audible rotor note that tracks it in real time, airframe shake that increases as the rotor droops, a disc that visibly narrows and dims as energy bleeds off, and a tape on the HUD that reads green, amber or red off the same thresholds the physics uses internally. Nr is a proxy for the rotor's stored energy, a normalised RPM value whose real kinetic energy scales with its square, and nothing more dressed up than that. With the engine dead, it's the number the player has to protect above everything else.
The debrief screen after each run plots the whole Nr and collective trace with failure, flare and touchdown markers on it, the same way a flight data recorder would. The idea was simple: don't tell the player what they did wrong, show them the trace and let them find it themselves.
Getting it wrong first
The physics didn't arrive correct. It arrived in eight versions, and most of the interesting decisions were fixes.
Version one had the flaw already mentioned: cyclic was cosmetic during the glide. Version two fixed that by vectoring the descent force through pitch, which is what actually made it autorotation rather than a fall with a side-game attached.
Then version four closed something I hadn't caught until I'd been diving straight at the pad in playtesting and noticed it cost nothing. At flat pitch, Nr was self-stabilising near a safe value regardless of airspeed, so diving hard for extra distance was a free lunch. I added a term that drives Nr towards overspeed above a forward-speed threshold, so diving hot now trades reach for rotor health, the same trade a real autorotation forces on a pilot.
Version six flipped the zero-collective equilibrium so it sits above the safe RPM band instead of inside it: dump and forget the collective and the rotor now over-revs by default, so a pilot has to hold a standing collective through the entire glide instead of getting away with one good decision at the start.
Version eight is the one I'm least proud of and gladdest I caught. The physics had been right for a while, but the scoring wasn't. I ran a batch of simulated clean landings and found the grading curve was charging sink-rate penalty from zero, hard enough that it could burn an entire A-grade budget on a touchdown the game's own verdict system was calling "GREASED IT." Best-quartile landings were earning an A on fewer than 2% of runs. The problem wasn't the physics. A grading curve I'd written was still punishing a landing I'd already decided was flawless. The fix was anchoring both penalties to an excellence floor, so the first slice of sink and the first fraction of a second spent out of the safe band cost nothing, because that's the ordinary noise of a clean landing.
One smaller find, only surfaced while writing this: the engine-on governor computes a lag towards Nr = 1.0, then immediately overwrites it to exactly 1.0 on the next line, discarding the calculation. It's been dead code since version one, harmless since the whole game lives in the engine-out state, and nobody, including me, noticed for eight versions.
Testing it properly
Manual testing is the first pass, not the whole pass. The backtick debug overlay exposes the individual dNr/dt terms live, so I can watch which force is actually dominating a manoeuvre as I fly it, not just the net Nr number. Anything that touches feel gets flown by hand afterwards, and for the mobile build that meant a real pass on an actual phone: touch gestures fail in ways a keyboard test on a desktop never surfaces. That pass turned up eleven real bugs before the mobile build shipped, including a viewport resize mid-drag that killed a live touch gesture permanently, and a second finger's touch dropped whenever the first one lifted.
Most of the confidence, though, comes from tests that don't need me at all. test.html runs 139 invariants against the physics, scoring and determinism logic on every change: integration stability at three different frame rates, seed determinism, share-hash version rejection, and regression tests for two exploits I found and closed myself, diving for free Nr and gaming the old grading curve. Tuning changes get measured before they get felt. physics.js and tuning.js have no DOM dependencies, so a plain Node script can import them directly and print the exact equilibrium a change produces, rather than me guessing from a playtest:
// measure.mjs
import { T } from './js/tuning.js';
import { stepHeli, makeHeli } from './js/physics.js';
const DT = 1 / 120;
const env = { wind: 0, turb: 0, mass: 1.0, surfaceY: 100000 };
function glideAt(vxTarget) {
const h = makeHeli(0, 0);
h.engineOn = false; h.collective = 0; h.vx = vxTarget;
let t = 0;
while (t < 16) {
const inp = {};
if (h.vx < vxTarget - 3) inp.right = true;
else if (h.vx > vxTarget + 3) inp.left = true;
stepHeli(h, inp, env, DT);
t += DT;
}
return h.nr;
}
for (const vx of [80, 140, 175, 210, 260, 300]) {
console.log(`vx=${vx} → nr=${glideAt(vx).toFixed(2)}`);
}
Two more layers run entirely without a human in the loop. A fuzz harness fires 200 runs of random input at the same debug handles the test suite uses and checks for a NaN or a run that never terminates, zero of either, every time. A scripted autopilot flies the actual correct technique, dump, glide, flare, cushion, and it lands successfully around 99% of the time, while random input always crashes. That gap is the point: it's the proof that skill, not luck, decides the outcome. The same approach scaled up to find the version-eight scoring bug: 34,992 simulated landings across 8 seeds and 4,374 flying policies, run headlessly overnight, is what actually surfaced it, not a hunch that something felt harsh.
Why bother getting this right
I could have built a game where an engine failure just means "the ground gets closer, dodge stuff." It would have taken a fraction of the time. But the manoeuvre itself, the one I trained in the simulator and flew for real as a Blackhawk pilot, has a genuine internal logic. It rewards discipline and punishes both panic and overcorrection. Handled right, a catastrophic failure becomes a solvable problem the moment you manage the one resource that matters, and that logic is worth more than any difficulty curve I could have invented from scratch.
AutoRotate is the second of three small browser games built on the same disciplined base: pure-function physics, deterministic dual-stream random number generation and zero per-frame allocation, with a shared refusal to ever put "GAME OVER" on the screen. The first, Ropes Out, is a fast-rope insertion game about holding a precise hover envelope over a pitching ship's deck. The third, Aerial Firefighter, replaces a single failure state with a continuously shrinking power margin under heat, altitude and gross weight, a slung Bambi bucket swinging on a full pendulum underneath. Different failure modes, same conviction: model the aerodynamics honestly and the game design mostly does itself.
Views expressed here are my own and don't represent any other parties.