workshop private

← all games

Cheap At Forty

games · created 2026-10-13

A two-up match sprint on a 250 m velodrome, where the wheel you are towing is worth 88 W at 40 km/h and 378 W at 65 — the cube of the speed, exactly — so the rider on the front is the one who decides what the rider behind them is getting. Lead it at 46 and jump at 200 m and you win 16 times out of 16, having done 81% of the work.

physicssimulationcanvasgame-feel

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:

speedaloneon the wheelthe wheel is worth
35 km/h170 W111 W59 W
40 km/h246 W158 W88 W
50 km/h462 W290 W172 W
60 km/h782 W485 W297 W
65 km/h987 W609 W378 W
70 km/h1225 W753 W472 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:

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 itresult
lead it out at 46 km/h, jump at 200 m16/16, +0.083 s
lead it out at 50 km/h, jump at 250 m15/16, +0.057 s
force them through, come off the wheel at 50 m0/16, −0.027 s
force them through, come off the wheel at 80 m0/16, −0.049 s
force them through, come over the top at 170 m0/16, −0.326 s
lead it from the balustrade, all the way0/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 fromheighttime to the linevs the sprinters’ lanespeed at the line
the lane0.16 m6.875 s—68.1 km/h
u = 0.300.97 m6.800 s−0.075 s68.9 km/h
u = 0.501.62 m6.804 s−0.071 s69.6 km/h
u = 0.702.26 m6.862 s−0.012 s70.5 km/h
u = 1.003.23 m7.029 s+0.154 s71.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 toextra at the pedalsextra 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 line0.4 m across0.8 m across
0.3 m back378 W219 W60 W
1 m back294 W170 W46 W
3 m back128 W74 W20 W
8 m back16 W9 W3 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

Gotchas