Two laps of a 250 m track, two riders, identical legs. You lost the toss, so you lead. Behind you is somebody getting a discount on the air, and the only question in the game is how big a discount you are willing to hand them and for how long.
Hold SPACE and you spend everything your legs have left. Let go and you hold the wheel in front of you, or hold your speed if there is nobody there. Z sits up. ↑ and ↓ move you up and down the banking. That is the whole input surface, and two of those four keys are about the air.
The cube is the whole game
Aerodynamic drag goes as the square of speed and the power to overcome it goes as the cube. A slipstream is a fixed fraction off the drag, so the wheel in front is not worth a fixed number of watts — it is worth a fixed fraction of a number that quadruples between a warm-up and a sprint:
| speed | alone | on the wheel | the wheel is worth |
|---|---|---|---|
| 35 km/h | 170 W | 111 W | 59 W |
| 40 km/h | 246 W | 158 W | 88 W |
| 50 km/h | 462 W | 290 W | 172 W |
| 60 km/h | 782 W | 485 W | 297 W |
| 65 km/h | 987 W | 609 W | 378 W |
| 70 km/h | 1225 W | 753 W | 472 W |
378 ÷ 88 = 4.29. (65/40)³ = 4.29. There is no modelling in that agreement; it is the same exponent printed twice.
And critical power — the ceiling you can hold indefinitely — is 340 W. So:
- Leading at 40 km/h costs 246 W, which is 94 W below CP. You are recovering while you lead, and the wheel behind you is getting 88 W.
- Leading at 65 km/h costs 987 W, which is 647 W above CP, and the wheel behind you is getting 378.
The front is not an expensive place to be. The front at speed is an expensive place to be, and the person deciding the speed is the person on the front. That is the whole tactical content of a match sprint in one line, and it is why the real ones crawl.
Which is why you can win having done all the work
Sixteen seeded rivals, each a full race:
| how you ride it | result |
|---|---|
| lead it out at 46 km/h, jump at 200 m | 16/16, +0.083 s |
| lead it out at 50 km/h, jump at 250 m | 15/16, +0.057 s |
| force them through, come off the wheel at 50 m | 0/16, −0.027 s |
| force them through, come off the wheel at 80 m | 0/16, −0.049 s |
| force them through, come over the top at 170 m | 0/16, −0.326 s |
| lead it from the balustrade, all the way | 0/16, −2.055 s |
One of those winning races, read off the end panel:
YOU RIVAL
time 36.611 s 36.682 s
at the pedals 24.1 kJ 21.6 kJ
saved by the wheel 1.7 kJ 3.8 kJ
metres led 406 94
W′ left 8.0 kJ 8.4 kJ
You led 81% of it. You gave them 3.8 kJ of free air. They finished fresher than you. You won by seven hundredths of a second, which is 1.2 m, which is most of a bike.
The reason is in the first table, and it splits cleanly. Averaged over the sixteen races, the wheel saves the rival 3.71 kJ — of which 2.79 kJ arrives before the sprint has started and only 0.92 kJ during it — and the first number is the worthless one. At 46 km/h they were already under CP with a full bucket, and a saving you cannot store is a saving thrown away. Only the 0.8 kJ banked after the jump was ever going to turn into metres. The rest was a discount on something that was not costing them anything.
That is also why the jump is worth so much: it does not merely put metres into the rider behind, it takes their discount away at the one moment the discount is worth 378 W, and it buys a fifth of a second of them deciding whether to believe it.
The counter is real, though, and it is in the table too: ride the front at 58 km/h and every one of those races flips. Lead slowly or do not lead.
The banking is not a battery
A 250 m track is banked 13° on the straights and 42° in the turns. At the balustrade that is 1.57 m up on a straight and 4.68 m up in a turn, which on an 83 kg rider is 3.8 kJ of potential energy just sitting there. Everyone who sees it has the same idea: climb while it is cheap, dive when the legs are capped, arrive at the line doing a speed no human could pedal.
It does not work, and the demo can show you exactly why, because runSolo()
lets you give a rider the height for free. Flat out from 62 km/h at the entry
to the last turn, 125 m from home, diving all the way down:
| start from | height | time to the line | vs the sprinters’ lane | speed at the line |
|---|---|---|---|---|
| the lane | 0.16 m | 6.875 s | — | 68.1 km/h |
| u = 0.30 | 0.97 m | 6.800 s | −0.075 s | 68.9 km/h |
| u = 0.50 | 1.62 m | 6.804 s | −0.071 s | 69.6 km/h |
| u = 0.70 | 2.26 m | 6.862 s | −0.012 s | 70.5 km/h |
| u = 1.00 | 3.23 m | 7.029 s | +0.154 s | 71.9 km/h |
Read the last row twice. The rider who dives from the balustrade crosses the line at 71.9 km/h, 3.8 km/h faster than the one who never left the sprinters’ lane, and loses by 0.154 s. The speed is real and it arrives too late, because to collect it you rode a turn at 1.226 m of tyre per metre of track and gave away more ground than the dive could buy back. There is an optimum and it is a third of the way up.
Then there is the other half of the ledger, which that table pointedly does not charge anybody for. Climbing up there over 250 m of cruise:
| climb to | extra at the pedals | extra ground |
|---|---|---|
| u = 0.30 | +0.93 kJ | +10.6 m |
| u = 0.50 | +2.24 kJ | +17.0 m |
| u = 1.00 | +6.27 kJ | +30.8 m |
The best free dive in the game is worth 0.075 s. The cheapest climb that buys it costs 0.93 kJ and 10.6 m of extra ground. The banking returns every joule you put into it and charges you rent for the storage — which is why, in the policy table above, every strategy with a climb in it finishes behind every strategy without one. The swoop is a real thing that real sprinters do, and in a race where both riders have the same legs it is a way of losing elegantly.
The hole in the air, drawn where it is
The pale wedge behind the lead rider is the slipstream, at its real extent, and it is the reason two of the four keys are steering:
| same line | 0.4 m across | 0.8 m across | |
|---|---|---|---|
| 0.3 m back | 378 W | 219 W | 60 W |
| 1 m back | 294 W | 170 W | 46 W |
| 3 m back | 128 W | 74 W | 20 W |
| 8 m back | 16 W | 9 W | 3 W |
A metre off the wheel costs you a fifth of it. Eighty centimetres across the track costs you six sevenths. This is why the jump works: the half-second of acceleration before the rider behind reacts does not just put metres into them, it takes the discount away at the exact moment the discount is worth 378 W.
The rival
Four states and one personality. It sits on your wheel; it comes through if you sit up long enough to insult it; it winds the pace up from the front if it ever finds itself there; and it jumps between 72 and 114 m out — late, which is correct, because leaving the hole early is the mistake the whole game is about. It answers a jump of yours in 0.20–0.32 s, and only inside the last 300 m: an earlier version treated any gap-closing effort as an attack and sprinted the last lap and a half for nothing, which flattered the player enormously.
The one thing it will not do is pass you and then lead patiently. If you force it through it rides at 46–49 km/h and holds the sprinters’ lane, which is the right answer and is also, per the first table, nearly free. Forcing it through buys you less than it looks like it should.
What’s under it
src/sprint.mjs is the engine: framework-free, headless, no canvas anywhere in
it. SI units throughout. Two distances are kept rigorously apart — v is speed
along the rider’s own path and travelled is the projection of that onto the
measurement line, because riding high covers more ground and earns no credit
for it. The track is two straights and two true semicircles with the bank angle
blended smoothly across each transition; turnFactor() is the one function
that drives both the bank and the extra path length, since on a real track they
are the same geometry seen twice.
Physiology is Monod–Scherrer: a critical power of 340 W, a 20 kJ bucket of W′ above it, and a peak that falls from 1550 W full to 340 W empty as
Pmax = CP + (Pmax₀ − CP)·(W′bal/W′)^0.6
which is the fade you can feel in the last 80 m, and the reason a dive can hand you speed your legs cannot.
src/render.mjs paints the track surface once into an offscreen canvas —
height-shaded in eleven bands, so the banking reads as a slope rather than a
diagram — and blits it per frame.
Run node scripts/measure.mjs and every table on this page comes back out of
the engine. node scripts/smoke.mjs plays the demo through the real keyboard
path in headless Chromium — the site’s validation pass loads the page but never
presses anything, so without it nothing checks that a key reaches the engine.
Reuse
createRace()thenstepRace(race, dt, {gas, ease, lane}),summary(race).holdPower(v, draft, dhdt),aeroDrag,maxPoweranddraftOnare general: give them a speed and a slipstream fraction and they answer any “what does this cost” question about a bicycle. No imports.runSolo({p0, u0, v0, distance, plan})runs one rider with no rival and no draft. It is how the banking tables were made, and it is the right tool for any question about the track rather than about the race.POLICIESholds the named ways to ride it as plain functions of the race, returning the same input the keyboard does. Writing a better one is the most interesting thing you can do with this folder.demo/carries its own copies of both modules (ADR-0002). Editsrc/, thencp src/*.mjs demo/.
Gotchas
- The track is geometrically honest and not a real velodrome. Real 250 m tracks use clothoid transitions and have longer straights with a tighter turn radius than two semicircles give you; this one has 52.74 m straights and a 23 m radius because that is what makes the lap exactly 250. The lap lengths, heights and bank angles are right; the turn-in is a simplification.
- The lap at the balustrade is 286.31 m, not 250. It is tempting to treat lane choice as free because the drawing is flat. It is 14.5% further and the engine charges every metre.
- Holding a lane is not holding a height. The bank angle changes between straight and turn, so a rider sitting at the balustrade climbs 3.1 m twice a lap and descends it twice a lap, whether they meant to or not. It is net zero over a lap and it is not free inside one: the speed pulses, visibly, and that is the physics and not a bug.
- Both riders have identical legs. Every margin in this folder is therefore a margin created by tactics, which is the point; it also means the game is decided by hundredths and photo finishes are normal.
- The rider starting second rides 1.1 m further, because the race is measured to a fixed line and they start a wheel behind it. Small, real, and it shows up in the ledger.
window.__demo.pose(seconds, policy, seed)steps the real engine under a named policy, which is how every screenshot inmedia/was taken and the quickest way to look at the drawing in a fixed state.



