Skip to content

Race simulation

The race is a pure simulation in src/game/sim/: no Phaser, no three.js, no clock, no randomness. The game, the garage analyzer, the career balance tests and the online server (party/) all run the same code, so a race given the same car, tune and input log always ends in the same frame. It started as a port of the 2004 Flash game Drag Racer v3 (docs/drag-racer-v3.md); that port is gone. The race now runs a drag physics model in sim/physics/: a torque curve, real gear ratios, tyre grip with weight transfer, air drag and rolling resistance. Its design and the decisions behind it are in docs/2026-11-07-drag-physics-design.md.

File Role
sim/clock.ts SIM_HZ, DT, ticks(), nthRoot, mph to m/s
sim/types.ts CarStats, Tune, RaceSetup, RacerState, StepInput
sim/specs.ts, sim/upgrades.ts car base plus parts to CarStats (see tuning, parts)
sim/physics/types.ts PhysicsCar, PhysicsState, PhysicsInput; engine types, drive layouts, body styles
sim/physics/torqueCurve.ts engine shapes per type, the torque curve from peak HP and TQ
sim/physics/drivetrain.ts gear ratios, final drive, rpm at a speed, drive force
sim/physics/traction.ts, sim/physics/forces.ts tyre grip with weight transfer; air drag and rolling resistance
sim/physics/buildCar.ts car stats plus its PhysicsSpec to a PhysicsCar
sim/physics/shiftPoints.ts the best shift rpm per gear
sim/physics/stepPhysics.ts one physics tick, the clutch drop on green
sim/initRace.ts CarStats + Tune to a RaceSetup: the tuned PhysicsCar, gear top speeds, shift points
sim/raceSetup.ts initRace plus the car’s best launch rev: the setup every live race uses
sim/step.ts stepRacer, one tick; shiftUp, shiftDown, holdNitrous, startNitrous
sim/engine.ts rev ratio, redline, heat and blowing up
sim/launch.ts staging in neutral, revving, launch grade and grip
sim/roll.ts roll starts
sim/shiftZone.ts, sim/shiftLog.ts, sim/runGrades.ts, sim/shiftPlan.ts grading launches and shifts
sim/bestRun.ts, sim/perfectRun.ts the best run (launch rev search) and the analyzer run
sim/aiDriver.ts, sim/aiRun.ts AI driver mistakes, simulated AI races
sim/inputLog.ts, sim/inputRecorder.ts input event log, replay, network batching
sim/opponentPredictor.ts, sim/predictedDriving.ts the remote car in online races
sim/finishGrace.ts, sim/distances.ts finish grace period, race distances

The sim advances in fixed steps of DT = 1 / SIM_HZ seconds (sim/clock.ts); SIM_HZ is 120. A fixed step is what makes a race reproducible: the result depends only on the inputs per tick, never on the frame rate of the machine that ran it. That is what lets the server replay a client’s race and get the same finish.

Every rate in the sim is written per second and multiplied by DT, and every duration is written in seconds and converted with ticks(seconds). So SIM_HZ can change without retuning anything. decayPerTick() turns a “share taken off per 30 Hz frame” into the per tick share that decays equally fast; the online race’s drawn-opponent correction (net/NetRaceSession.ts) uses it.

The original ran at 30 Hz. docs/2026-11-06-sim-tick-rate.md explains why that was raised: at 30 Hz every input snapped to a 33 ms tick, and one tick of shift timing in first gear could cost over 100 ms of finish time. At 120 Hz a tick is about 8 ms. That doc also lists a possible next step (sub-tick input timestamps) that is not built.

Units are SI: speed v in m/s, distance d in metres, engine speed in rpm. Speeds shown in mph (roll speeds, the HUD) convert with METRES_PER_SECOND_PER_MPH (0.44704) in sim/clock.ts; toDisplaySpeed and formatSpeed in core/unitSettings.ts take m/s.

initRace (sim/initRace.ts) runs once per race and computes everything that does not change during it. It builds setup.physics, a PhysicsCar, with buildPhysicsCar (sim/physics/buildCar.ts) from the car’s HP, TQ (lb-ft), WT (kg) and its physics spec: engine type, drive layout (fwd, rwd, awd) and body style. Each car model sets that spec in player/carModels.ts.

  • Torque curve (sim/physics/torqueCurve.ts): the engine type gives idle, peak torque, peak power, redline and limiter rpm (ENGINE_SHAPES). Torque is 60% of peak at idle, flat at TQ up to the rpm where it makes HP, then falls to 92% of that at the redline and 85% at the limiter. An engine that would make its HP only past the redline makes it at the redline.
  • Gearing (sim/physics/drivetrain.ts): six geometric ratios from 3.4 to 0.85, so every upshift drops the rpm by the same share. The final drive is set so the stock car reaches its top speed (where drag and rolling resistance eat all its power) at the redline in sixth; parts never change it (CarStats.finalDrive).
  • Body (BODIES in sim/physics/buildCar.ts): drag area, centre of gravity height, wheelbase, rear weight share, tyre radius and tyre grip per body style. The hypercars (Hyper R8, Hyper GX) have their own hyper body: mid-engine (rear share 0.62) on the widest tyres (grip 1.5), or a maxed one in RWD would spin all the way down the strip and lose to the AWD supercars.
  • Drivetrain efficiency 0.85 for FWD and RWD, 0.8 for AWD; shift time 0.25 s.
  • Parts (CarStats.parts, sim/partEffects.ts): tyre grip, squat (weight transfer towards the driven axle), efficiency, shift time and a turbo whose torque multiplier rises from 1 to 1 + turboGain between 45% and 65% of the redline. See tuning.
  • Tune: each gear’s ratio is divided by its slider, ratio / GEARn. Above 1 a longer gear, below 1 a shorter one. CAMADJUST shapes the torque curve and BOOST scales the turbo. Tune values are clamped to TUNE_MIN..TUNE_MAX.

From the physics car initRace also derives top[gear], the speed (m/s) at the limiter in each gear, and the shift points (below), and baseTemp from AIRINTAKE and (with a turbo) BOOST.

How the sliders trade off is on the tuning page; car stats are on /reference/cars/.

raceSetup (sim/raceSetup.ts) wraps initRace and adds the best run’s launch rev and the rev climb at each shift (below). Both are always found over a quarter mile, whatever the race distance, because they are a property of the car, not the race.

stepRacer(setup, state, input) in sim/step.ts advances one racer by one tick. In order:

  1. Staged (gear 0, on the line): run the launch logic (stepStaged, below), update heat, stop.
  2. Roll race before green (input.green === false while in gear): cruise at the current speed, stop.
  3. AI controls for AI racers: shift and fire nitrous (see AI driver).
  4. Physics (stepPhysics in sim/physics/stepPhysics.ts) with full or no throttle and the nitrous torque, if any:
    • Engine torque from the curve at the current rpm, none during a shift or at or past the limiter (fuel cut). Without gas there is no torque and drag slows the car.
    • Drive force at the tyres: torque times gear ratio, final drive and efficiency, over the tyre radius.
    • Traction (sim/physics/traction.ts) limits that force, then air drag and rolling resistance (sim/physics/forces.ts) are taken off and a, v and d are integrated over DT.
    • The rpm follows the wheels (lockedRpm), or the slipping launch clutch, never below idle.
  5. Track the run: top speed so far, and shiftPointTicks (ticks the rev has been at or past this gear’s shift point, used for grading).
  6. Heat (updateHeat).

Shifting and the player’s nitrous are not inside stepRacer: callers apply them to the state before the tick with shiftUp, shiftDown and holdNitrous. A shift (startShift) cuts the drive for the car’s shift time and ends a launch clutch slip.

The driven axle’s load holds the drive: the front axle for FWD, the rear for RWD, and the whole car for AWD (its centre differential sends drive to the axle that grips). Acceleration moves weight from front to rear by mass * a * cgHeight / wheelbase, so RWD gains grip as it pulls and FWD loses it. Tyres that hook up hold drive up to static grip times that load. Above it they spin and give only sliding grip (87% of static) until the demand drops below the sliding limit. state.spinning holds whether they spin; isWheelspinning in sim/launch.ts reads it for the view.

Every car has traction control (tractionControl in physics/traction.ts, standard, not a part): it caps the drive force at 0.999 times the grip the tyres have now (static grip while hooked up, sliding grip while spinning, so they hook up the next tick). Power past the grip is wasted instead of spinning the tyres. The cap ignores the launch’s grip scale (below), so a launch above the perfect zone still spins for its 1.5 s and a launch too low still bogs. Before it (2026-11-08), a car with more power than grip spun through each gear, the flare chain-shifted the gearbox into a tall gear and a higher stage could be slower than the one before (docs/2026-11-08-stage-traction-design.md). npm run stages:check holds the promise: every uniform stage quicker on every car and distance, and full Stage 5 the quickest build.

Spinning tyres let the engine rev above road speed: the drive force the road does not take spins up the engine and driven wheels (SPIN_INERTIA in physics/wheelSlip.ts) and slipRpm holds the extra revs, so the tach and engine sound flare and the limiter comes early. Once the tyres hook up the revs fall back to road speed within HOOK_UP_TIME. A shift scales slipRpm by the ratio change (rescaleSlip in physics/stepPhysics.ts, on the first tick in the new gear), as the same tyre speed turns the engine slower in a taller gear. Before that, a dual-clutch gearbox (Drivetrain Stage 4 and 5) upshifted straight through the gears on the flared revs and made a car slower. Shift windows are sized on the rev at road speed (roadRev in sim/bestRun.ts), so a spin rushes the needle through them.

The rev the tach, the launch and the shift points use is revRatio = rpm / limiterRpm (sim/engine.ts), or the free rev while staged. redlineRev(setup) is the car’s redline on that scale; it differs per engine type. The HUD’s tach (race/view/Hud.ts) shows it as real rpm, revRatio x limiterRpm, on a scale up to the limiter rounded up to the next 1000.

The engine only heats above redlineRev(setup) in gears 1 to 5 (in sixth there is no shift left to miss, and a built car on its stock final drive reaches the limiter there): from REDLINE_HEAT per second at the redline up to LIMITER_HEAT at the limiter (a downshift past the limiter heats like the limiter). Below it, it cools at COOL_RATE per second back to the setup’s baseTemp (set by boost and air intake). At ENGINE_BLOW_TEMP the engine blows and the race is lost. Heat runs on the line too, so revving against the limiter while staged costs temperature.

CarStats carries the nitrous kit (nitrous, 100 when installed), reserve bottles and a Dual-Port flag (sim/specs.ts derives them from installed parts). Every bottle is one shot: once fired it runs for NITROUS_SHOT_FRAMES (1.2 s) and cannot be stopped, and the tank (nitrousTank) drains with it. When a shot ends the next reserve bottle loads. While it runs and gas is held, it adds 1.5 times the engine’s peak torque (NITROUS_TORQUE_SHARE, 1.5 times that again with Dual-Port) in any gear. Like a real kit’s throttle and clutch switches, the shot pauses with the gas off and during a shift. On the limiter it runs on without any push (the engine gives nothing there), so a late shift wastes it. The extra torque goes through traction control like any other, so a shot in first or second gear on a traction-limited car is mostly wasted: on most cars a shot pays most from third or fourth, and grip parts are what make a shot count. No shot fires below second gear (NITROUS_MIN_GEAR): on a phone the nitrous button sits next to the gas, and a player tapping gas for the launch would otherwise fire it by accident. The best run fires once a shift is done.

There are two ways to fire it:

  • holdNitrous(state, held): the player, called every tick. A press fires one shot; keeping the button down does not fire the next bottle (nitrousHeld remembers last tick).
  • startNitrous(state): fires a shot when none runs, used by the AI and the best run.

A race starts with both cars staged in gear 0 (stageRacer in sim/launch.ts). The countdown is STAGING_FRAMES ticks (four lamps of 700 ms); frame 0 is the first tick after green, so frames are negative during the countdown.

On the line, gas revs the free engine (REV_RISE_PER_TICK) and releasing it lets the rev fall back to idle. Hitting the limiter bounces the rev back. On the first green tick the clutch drops by itself at the held rev: there is no jump start and no reaction time skill. While the clutch slips it pulls the engine toward the wheels’ rpm (CLUTCH_ENGAGE_TIME, 0.5 s, in sim/physics/stepPhysics.ts), so the launch rev sets the pull for the first moments. Too low a rev bogs and too high a rev spins the tyres, by the physics alone.

Each car has its own best launch rev, setup.launchRev, found by the best run. The launch is graded against it (revZone), and the grade also scales the tyres’ grip for LAUNCH_GRIP_TIME (1.5 s):

Zone (revZone) Rev at green Grip (LAUNCH_GRIP)
perfect within PERFECT_ZONE_HALF_WIDTH of setup.launchRev 1
near up to NEAR_ZONE_WIDTH below that 0.9
low lower 1 (it already bogs)
over above the perfect zone 0.6 (wheelspin, even on a weak car)

One exception: a player sitting at idle with no gas at green stays put until gas is pressed, then launches bogged. That is the only way a late start costs time.

While the launch clutch slips (state.clutchRpm > 0) the rev is the engine’s, not the wheels’, so nothing makes a shift decision on it: not the AI, the online prediction, the shift timing count or the best run. Shifting out of the slip by hand is allowed and ends it.

Once launched the player’s gas is held for them (RaceControls.gasDown in race/RaceControls.ts returns true whenever the car is not staged). In a race the player controls three things: the rev on the line, the shifts, and nitrous.

RaceClock (race/RaceClock.ts) draws the lamps from elapsed milliseconds (LIGHT_INTERVAL_MS), but the sim’s green is the tick count, not the clock: the session passes green: frame >= 0 into every step.

In a roll race (sim/roll.ts) both cars start already moving at setup.rollSpeed (in mph, converted to m/s; ROLL_SPEED for career roll events) with no staging, launch or rev. lineUp puts each car in rollGear, the lowest gear whose shift point is above the speed (the one that pulls hardest), and places it behind the roll point by exactly the distance it covers during the countdown. During the countdown the car cruises (step 2 above), so it passes d = 0 on green and the race measures from there. lineUp falls back to stageRacer when rollSpeed is 0, so callers use it for both kinds.

Each gear’s best shift point is computed, not searched: bestShiftRpms (sim/physics/shiftPoints.ts) walks the rpm up from idle in 25 rpm steps until the next gear, at the same speed, pulls at least as hard as this one, and stops at the redline at the latest (a flat torque curve would otherwise put it on the limiter’s fuel cut). It assumes the tyres hook up. initRace stores the result as rev ratios in setup.shiftPoints (index 1..5; sixth is 1).

simulateBestRun (sim/bestRun.ts) finds the fastest way to drive a car with a given tune: every upshift at its shift point, and a search for the best launch rev. The launch rev is tried from idle to the redline every COARSE_STEP (0.1), then every FINE_STEP (0.02) around the best of those, without nitrous. Then nitrous is tried at that launch from each gear in NITROUS_FROM_GEARS (1 to 5), held only while the tyres hook up, and the fastest is kept: on a traction-limited car, nitrous in the low gears is wasted. A roll race has no launch to search. While the launch clutch slips the run does not shift.

It records the launch rev, each gear’s shift point and how fast the rev was climbing there (shiftRevRates), the gear the nitrous started in (nitrousFromGear) and how long the tyres spun (spinFrames, for the parts shop’s warning). The search runs the race some 20 times, so results are cached by car, tune, distance and roll speed (MAX_KEPT_RUNS); treat a cached result as read-only.

This best run is what the player is measured against: raceSetup copies its launchRev (also the AI’s starting launch rev) and shiftRevRates into every live RaceSetup. The older simulatePerfectRun (sim/perfectRun.ts) drives with the AI shift rule instead; it shifts early. The tests use it to check the AI never beats the best run. MAX_RACE_FRAMES in sim/perfectRun.ts caps every simulated race.

A shift is graded by when it lands, not by its rev (sim/shiftZone.ts). Each gear’s ideal moment is the tick the rev reaches that gear’s best shift point. A shift within PERFECT_SHIFT_WINDOW seconds of it, before or after, is perfect. Later is over. Earlier is near if the rev is within NEAR_ZONE_WIDTH of the shift point, else low.

Why time, not rev: the rev climbs at very different speeds per gear. A rev band gave first gear a window of a few milliseconds (luck) and fourth gear over a second (free). A time window makes every gear equally hard. The measurements are in docs/2026-11-06-sim-tick-rate.md.

shiftTiming finds the distance to the ideal moment: after it, from shiftPointTicks; before it, by stepping a copy of the car forward on full gas until it reaches the point. revBand converts the window back to a rev range for the tach colours (approximate, from the best run’s rev rates).

The same zone feeds the live shift callout (race/view/ShiftCallout.ts) and the results screen.

After a race, gradeRun (sim/runGrades.ts) replays the player’s input log and records the launch zone and every upshift (gradeUpshift in sim/shiftLog.ts). It also puts a cost in seconds on each mistake with what-if replays (planFinish in sim/shiftPlan.ts):

  • The run is re-driven with upshifts triggered by rev (plus how long the player sat there) instead of by frame, so this base replay matches the real run.
  • Then once per shift, with only that shift moved to the ideal point, and once with an ideal launch. The difference in finish time is the time that mistake lost.

Roll races report no launch loss. race/raceResult.ts calls gradeRun and simulateBestRun to build the results screen.

AI racers hold gas and drive themselves inside stepRacer: they shift up a reaction time (setup.aiShiftDelayTicks) after the rev at road speed (roadRev, without the flare of spinning tyres) passes their shift point for the current gear (setup.aiShiftPoints), and fire nitrous shots. In a gear where the rev climbs fast the reaction time overshoots further. They also shift once the real rev (with the flare) reaches the redline, where the engine starts to heat: a powerful car spinning its tyres would otherwise sit on the limiter until the engine blew. Their mistakes are decided once per race, outside the sim, by rollAiDriver in sim/aiDriver.ts, which writes them into the setup. The sim itself never calls a random function.

rollAiDriver(setup, sloppiness, random, precise) takes a sloppiness from 0 to 1 from the career event (see career and /reference/career/):

Normal driver Precise driver (bosses)
Launch rev setup.launchRev +- a miss from MIN_LAUNCH_MISS up to MAX_LAUNCH_MISS setup.launchRev +- MAX_LAUNCH_MISS * sloppiness, no floor
Shift points the best shift point times (0.8 + aiShiftOffset) / 0.95, the original AI’s early share (stronger cars shift later), +- MAX_SHIFT_MISS * sloppiness per gear the car’s best shift point +- the same spread
On the line blips the engine for show, then in the last second holds gas until its launch rev eases the rev onto its launch rev with a fine throttle in the last second
Reaction leaves the line REACTION_PER_SLOPPINESS * sloppiness s (1 s for a rookie) after green, +- 25% the same, so near 0 for a boss
Shift reaction SHIFT_DELAY (0.03 s) plus up to SHIFT_DELAY_PER_SLOPPINESS * sloppiness (0.12 s at 1) the same
Nitrous from third gear, only in a strong car; a gear early with chance EARLY_NITROUS_PER_SLOPPINESS * sloppiness from the best run’s gear (setup.aiNitrousGear), the same chance of a gear early

Shift points are clamped between LOWEST_SHIFT (0.6) of the redline rev and the limiter. Like the best run, the AI makes no shift while its launch clutch slips, and it fires a shot only once a shift is done and its tyres grip. A precise driver near sloppiness 0 is close to the best run. Reaction is most of the dial: on a traction-limited car a missed launch rev or shift point costs little, so a rookie at sloppiness 1 or 2 ran the same time before it.

simulateAiRun (sim/aiRun.ts) runs a full AI race outside the game. seededAiRunFrames and averageAiRunFrames use a seeded generator (mulberry32) so balance checks such as bracket dial-ins (player/career/bracketDial.ts) and the career balance tests give the same numbers on every run. The live race passes Math.random, except a career race the race server judges: it races with the seed from the server’s ticket (SessionOptions.random, race/RaceSession.ts), so the server can run the same opponent again (race/replayEventRace.ts, below).

Because the sim is fixed-step and pure, a run is fully described by its car, tune, distance and a list of input events (sim/inputLog.ts):

  • InputEvent is { f: frame, k: kind } with kinds gasOn, gasOff, up, down, nos, nosOff. Gas and nitrous are held states, so only their on/off edges are logged. An event at frame f is applied before tick f.
  • EventCursor walks a sorted log and applies it to a state, the same way the race scene applies live input.
  • replayRun replays a log over a distance and returns the fractional finish frame (interpolated inside the crossing tick, so two cars finishing in the same tick still have an order) or blown.
  • InputRecorder (sim/inputRecorder.ts) turns live input into that log and batches it for the network, with a heartbeat (HEARTBEAT_FRAMES) so the other side knows which frames are complete.

Every race records its log, solo races included: gradeRun grades from it. Online, the server never trusts a client’s claimed time. party/src/judge.ts replays each runner’s log with replayRun and judges on that, and party/src/inputGuard.ts rejects malformed or out-of-order events (isInputEvent). Client and server must run the same sim, so clients send a SIM_VERSION (net/simVersion.ts) and the room turns away a mismatch. On the client, OpponentPredictor (sim/opponentPredictor.ts) replays the remote car exactly up to its last reported frame and predicts beyond it with predictedGas (sim/predictedDriving.ts): hold the last gas and nitrous, shift at the best shift point, lift past the redline where a shift cannot help. Details on the online page.

This is why the sim has no Math.random, no wall clock and no Phaser: any of those would make the server’s replay disagree with the client.

A car finishes when d reaches the race distance (sim/distances.ts: eighth, quarter and half mile in metres). A blown engine ends its run. Once one car is over the line, the other has finishGraceFrames(distance) ticks to finish too (sim/finishGrace.ts), longer for races over a quarter mile, or it counts as not finished. The online server applies the same grace (applyFinishGrace in party/src/judge.ts). MAX_RACE_FRAMES is the hard cap for any run.

The race records nothing extra while it runs. buildRaceResult (race/raceResult.ts) replays it with sim/telemetry.ts, which steps a car tick by tick and samples time, distance, speed, rpm, gear, acceleration in g, wheelspin and nitrous:

  • playerTrace: the player from the input log, the same replay the judge and the grades use.
  • opponentTrace: the AI from the RaceSetup it raced with (its driver rolled once at the start), timed from its own green, so a bracket race’s head start does not count.
  • perfectTrace: the best run, driven by perfectControls and startRun from sim/bestRun.ts.

Tests check each replay ends on the live finish. sim/timeslip.ts turns a trace into a timeslip (60, 330, 660, 1000 and 1320 ft, cut at the race’s own line, speed at the eighth and the line) and the gap to the perfect run by distance.

The results screen (race/view/resultTabs.ts) puts tabs left of the card: RESULT (the card), TIMESLIP (timeslipView.ts), SPEED (speed of both cars and the player’s rpm, shifts marked), GRIP (acceleration with wheelspin and nitrous shaded) and VS PERFECT (seconds behind the perfect run along the strip), all in resultCharts.ts on the small lineChart.ts. A tab is built the first time it opens.

The architecture page covers the scene and module layout; here only the sim side.

RaceSession (race/RaceSession.ts) holds both cars’ setups and states for one solo race, with no Phaser and no input of its own. Its constructor builds both setups with raceSetup (the opponent on STOCK_TUNE), rolls the AI driver, and lines both cars up. Options cover a precise (boss) opponent, a bracket race’s head start (opponentStart: the frame the opponent’s green comes on; the player’s is always frame 0) and a roll speed. tick(gas) steps the player, then the opponent, then advances frame. checkEnd() reports finish, blown or timeout (grace or MAX_RACE_FRAMES). finishOpponent() runs the opponent to the line off screen when the player finished first, because its time is still needed for the result.

RaceClock (race/RaceClock.ts) is the fixed-step accumulator. Each render frame advance() adds the frame’s delta (capped by MAX_FRAME_DELTA_MS, so a background tab does not fast-forward the race), runSteps() runs one tick per whole 1000 / SIM_HZ ms and keeps the remainder, and stepAlpha() returns how far the render frame is between two ticks. The view interpolates positions and wheel angles with that alpha, so motion is smooth at any display rate while the sim stays at SIM_HZ.

The Race scene (race/scenes/Race.ts) ties them together. Ticks start with the first lamp, so the player can rev in neutral. Each tick it reads gas and nitrous, logs their edges into the event’s InputRecorder, applies holdNitrous, and calls session.tick(gas). Shift keys apply shiftUp/shiftDown straight away and log the shift at the current frame, which is the next tick to run, so replay applies it at the same point. The shift callout grades an upshift just before it lands. At the end, race/raceResult.ts grades the log and compares against the best run.

Online races use net/NetRaceSession.ts and net/NetRaceClock.ts instead, with the opponent coming from OpponentPredictor rather than the AI.

  • npx tsx scripts/physics-et.ts checks every stock car’s best quarter mile on the drag model (no nitrous, best launch rpm searched, shifting where the next gear pulls harder). On a prepped strip (1.5 times the tyres’ grip), cars the Huntington formula ET = 5.825 * (lb / hp)^(1/3) puts at 12 s or slower must land within 5% of it. On the street (plain grip), the cars with a real counterpart (McLaren F1, Porsche Carrera GT, Skyline GT-R R34) must land within 10% of its road test. It exits 1 when a check fails.
  • docs/drag-racer-v3.md (the original’s formulas from the decompiled SWF) is history: the drag model replaced that sim, and its reference port and sim:check are gone.
  • npm test runs the Vitest suite in tests/, which covers the drag model (physics.test.ts), the sim (sim.test.ts, launch.test.ts, shiftZone.test.ts, runGrades.test.ts, roll.test.ts, predictedDriving.test.ts, finishGrace.test.ts, aiDriver.test.ts) and the career balance built on it.