Nobody Could Land It, and the Difficulty Was Never the Problem
A browser game where a Starship-shaped vehicle falls belly-first at terminal velocity, flips, and lands on a ring or on a droneship deck heaving in a swell. It was unlandable for weeks, and never because it was hard.
A Starship-shaped vehicle drops in belly-first at terminal velocity. You steer the fall by banking the belly, flip it tail-down, and set 200 tonnes of dry mass inside 26 metres of the centre of a landing ring — or, on the last three levels, on a droneship deck heaving in a swell.
Play it in your browser — three files, no install, no account, no network call of any kind. The best score lives in this browser and goes nowhere else.

Keyboard, mouse and touch all work, together if you like: ← → or A D to rotate, ↑ W or space for the engines, R to restart.
The cue was asking for a commitment that could not be paid
For the first few tuned versions almost nothing landed, and every failure looked like the player's fault. You flipped when the HUD said FLIP, you burned when it said BURN, and you arrived at the ring doing 30 m/s. That reads as bad piloting. It was not.
The cue came from stopAlt, the constant-acceleration hoverslam altitude: s.vy * s.vy / (2 * a), where a is thrust over mass minus gravity, 17.6 m/s² with a full tank. That number is honest about the burn and completely silent about everything that has to happen before it. An unpowered flip takes about 3.5 seconds, and because rotating out of the belly-flat attitude collapses the drag area the vehicle is falling faster at the end of it than at the start — 72 m/s in, 80 m/s out: roughly 260 metres of altitude gone before the engines have contributed anything. Flip under full thrust and it is 1.9 seconds and 140 metres, which is the number I quoted myself for weeks before I measured the case that actually matters. The cue was lighting at the point where the burn alone would just barely work, which is several seconds after the last point where the whole manoeuvre would.
The fix is one term. flipAlt is stopAlt(s) + Math.abs(s.vy) * 4.2 — 4.2 seconds of free fall added as lead, with the cue firing 12% above it. I had guessed three before measuring it.
It still felt unfair afterwards, and that was the control model
Landings were possible now and the game still felt like it was cheating. The reason was not a tuning constant. Attitude was an acceleration command: torque in, angular acceleration out, with nothing opposing it. A key is a binary input, so holding one spun the vehicle up without limit and every keyboard pilot over-rotated, every time. Pointer steering is proportional and felt fine, which is exactly how I talked myself into believing for a week that the game was fine.
What fixed it is a single line, tq -= K.RDAMP * s.om at 3.5e7 N·m per rad/s, which turns an acceleration command into something close to a rate command. Hold a key and the rotation settles at a bounded rate; release it and the vehicle stops in I / RDAMP, 1.19 seconds with dry tanks and 1.50 with full ones.
Damping on its own made the windy levels worse. Rate damping cannot tell the difference between your input and a gust, so the same term that stopped the over-rotation ate the authority needed to correct one. It only worked paired with more cold-gas torque and more flap power. What shipped is 8.0e6 N·m of RCS, always available and costing no propellant, plus a flap term that scales with dynamic pressure.
An accident fell out of that pairing. RCS alone settles at 0.229 rad/s, and the landing gate rejects any touchdown spinning faster than 0.25 rad/s, so with the engines off in still air a pilot cannot bust the spin gate by holding a key. Down in thick air the flaps push the same steady rate to 0.81 rad/s, three times the gate, and the protection quietly disappears exactly where it would have mattered. I designed neither half of that.
A lift term turned ten seconds of dead time into flying
Before it, the belly flop was a cutscene. You fell, you waited, you flipped.
Adding a flat-plate lift term perpendicular to the airflow — K.CL of 1.4 against the full 450 m² side area, worth an L/D of 0.49 at 45° of flow angle and peaking near 0.69 much closer to nose-on — means banking the belly steers the fall. The first ten seconds became where the landing is won, because that is while the vehicle is still cheap to move.
Drag helps because it is so lopsided. Both the reference area and the drag coefficient interpolate with attitude and are then multiplied together, so the effective drag goes as the square of the direction cosine: 12.6 to 1 between belly-flat and nose-on. Terminal velocity in the belly flop is about 73 m/s; point the nose down and the same vehicle has almost nothing holding it up.
The weathervane torque is the other half of it. It is stable belly-flat and unstable nose-first, so the airflow actively fights the landing attitude the whole way down and only lets go as dynamic pressure falls. That is the flip.
The droneship gate is closing speed on the deck, not speed over the water
The deck rides on two harmonics about a mean nine metres above the waterline, and the model carries the exact analytic derivative of that motion, so the gate compares vy - deckVy and not your speed over the sea.
| Level | Swell | Deck height | Peak deck speed |
|---|---|---|---|
| DRONESHIP | 2.2 m | 6.8 to 11.2 m | 2.37 m/s |
| HIGH SEAS | 4.6 m | 4.4 to 13.6 m | 4.95 m/s |
| ENGINE OUT | 6.0 m | 3.0 to 15.0 m | 6.46 m/s |
Against a 9 m/s descent gate, the deck on the last level supplies 6.46 m/s of your entire allowance by itself, with the sign depending on when you arrive. Touch down as the deck drops away and you pass with room to spare. Touch down as it comes up to meet you and 2.5 m/s is the whole budget.
That level also fails an engine mid-burn: 6.9 MN down to 4.6 MN, plus a constant off-axis torque on a 1.15 m arm. The interesting part is not the thrust, it is that the required stop altitude roughly doubles at the instant it happens, because net deceleration falls from about 20.3 to about 10.3 m/s². That level loads 56% propellant and so never carries a full tank, which is the only reason those numbers are not the 17.6 and 8.4 a full one would give. The trigger also requires the engine to be lit, and the window has no floor, so coasting unpowered past 240 to 460 metres does not spend the failure — it defers it to whenever you finally light, which is the worst possible moment to receive it. That is not a feature I wrote. It is what the condition says.
There are seven levels and the seventh repeats forever, so a run ends when you crash rather than when you finish.
You cannot play-test a browser game by letting it play
requestAnimationFrame is frozen whenever the page is hidden, so the obvious approach — let the game run and read the result — does not exist. That constraint decided the architecture. physics(s, cmd, dt) is a pure step function that touches no DOM, no globals and no timer, and window.__lander exports it in production alongside the state factory and a live state getter. A PD autopilot pasted into the console flies thousands of headless attempts at a fixed 240 Hz without ever rendering a frame.
Every difficulty number on this page was measured that way, and measuring taught me the caveat the expensive way. When I added the rate damping, the autopilot's landing rate fell — the dynamics its gains were tuned against no longer existed, so it was reporting a regression that no human would ever experience. The pilot has to be re-tuned per configuration and the best case compared. It sweeps three gain pairs per level now.
A fixed-gain autopilot does not measure your game, it measures its own gains, and the moment you change the physics it will report a regression nobody can feel.
Sample size is 15 attempts per gain pair, with randomised start offset, wind sign and swell phase. That is enough to tell 15 of 15 from 8 of 15 and nothing finer. Run-to-run swings of a few landings on the hard levels are noise, and I have talked myself into believing one more than once.
There is no test runner. The autopilot prints one line per level giving the name, how many of the 15 attempts landed, the average propellant left, and the failure reasons that came up: ditched, hard, sideways, tilted, spin, missed, timeout.
The harness ships in production on purpose. It means anyone can drive the same physics from their own console, and it also means anyone can cheat; for a game with no leaderboard and a score kept in their own browser, the first is worth more than the second costs.
Publishing it took the single file apart
It started as one HTML file and could not stay that way. This site sends style-src 'self'; script-src 'self' with no unsafe-inline, which refuses an inline <style> block, an inline <script> and every style= attribute, including the ones written later through innerHTML. The word that matters is that it refuses them silently, and a local static server sends no policy at all, so nothing you do locally shows you a single one of them.
So the game is three files: markup, a stylesheet holding every rule that would otherwise have been an attribute, and the script. Every tuning constant lives in one table and nowhere else. Nothing was lost in the split. It simply had to happen before the first upload instead of after it.
The thing I would tell you before you tune anything: when a game feels unfair, check what the HUD is promising and what a held key actually does, because both of mine were lying and neither of them was a difficulty setting.

Comments