drAIver

drAIver — Driver Implementation Specification

Version 6 · protocol 0 · this document is self-contained: everything needed to implement a driver is here. You do not need, and should not assume, access to any other file, repository, or server implementation detail.

Version history: v1–v5 iterated protocol coverage and the race rules across four generations of independently built drivers. v6 — the specification is mechanics only: what the wire carries, how the world behaves, what the rules enforce. Goals, strategy, and personality are the driver's own.

1. What this is

drAIver is a real-time 2D top-down driving simulator. A central server owns the world — track, physics, collisions, rules, lap timing — and every car is controlled by an external program (a driver) that connects over a WebSocket, receives telemetry, and sends driving commands.

This document tells you how the world works. It does not tell you what to want from it. The server enforces the mechanics in §8 and publishes official results and a venue lap-record board; whether your driver races to win, hunts lap records, shepherds a teammate, plays the villain within the rules, or narrates its cool-down laps over the broadcast channel (§7) is entirely your call. Drivers with a point of view make better racing.

A conforming driver, minimally:

  1. connects and completes the handshake (§4);
  2. sends a valid control for each state it processes (§6);
  3. drives: completes laps on any valid track without leaving it;
  4. handles race sessions: frozen countdown, green flag, penalties (§8);
  5. tolerates unknown message types/fields and non-fatal errors (§9);
  6. exits cleanly when the connection closes.

Recommended command-line interface: your-driver [--server ws://HOST:PORT/session] [--name NAME], with the defaults ws://localhost:7350/session and a name of your choice.

2. System model — read this first

3. Transport

4. Handshake

You send: hello

{"type": "hello", "protocol": 0, "role": "driver", "name": "my-driver"}
fieldtypenotes
type"hello"
protocolintegermust be 0; anything else is rejected
role"driver" or "spectator"spectators receive telemetry but cannot drive
namestringdisplay name; the server may uniquify it (name#2)
tokenstring, optionalresume token — see below

Reconnect-resume is active: if your connection drops, your car keeps driving on its last command; after 3 seconds it is ghosted (transparent to collisions) and after 30 seconds it is removed. Reconnecting within that window with the token from your welcome returns your car — same id, same name, race state intact.

The server replies: welcome

{
  "type": "welcome",
  "protocol": 0,
  "carId": 3,
  "token": "d41d8cd98f00b204e9800998ecf8427e",
  "tickRate": 50,
  "session": {"state": "open", "laps": 10, "mode": "race"},
  "car": {"length": 4.5, "width": 2.0, "wheelbase": 2.8,
          "aMax": 6.0, "bMax": 12.0, "vMax": 60.0,
          "cDrag": 0.02, "steerMaxDeg": 35.0},
  "sensors": {"v2v": {"range": null, "rateHz": 50}, "raycast": null},
  "track": {
    "name": "example-track",
    "version": 1,
    "halfWidth": 10.0,
    "centerline": [[0.0, 0.0], [2.5, 0.0], [5.0, 0.0], ...],
    "startFinish": 20,
    "gridSlots": [[44.0, 2.5, 0.0], [38.0, -2.5, 0.0], ...]
  }
}

An error message instead of welcome means the handshake failed; the server closes the connection after it.

5. Telemetry: the state message

Sent to you every tick (50 per second):

{
  "type": "state",
  "tick": 12041,
  "session": "racing",
  "you": {
    "carId": 3,
    "pos": [182.4, -33.1],
    "heading": 1.571,
    "speed": 41.2,
    "vel": [0.0, 41.2],
    "s": 512.8,
    "d": -1.2,
    "lap": 4,
    "lastLap": 31.44,
    "bestLap": 30.91,
    "applied": {"throttle": 0.8, "brake": 0.0, "steer": -0.15},
    "latency": 2,
    "flags": []
  },
  "cars": [
    {"carId": 1, "name": "rival", "pos": [201.0, -30.2], "heading": 1.6,
     "vel": [-1.3, 44.0], "s": 531.0, "d": 0.4, "lap": 4},
    {"carId": 3, "name": "my-driver", "pos": [182.4, -33.1], "heading": 1.571,
     "vel": [0.0, 41.2], "s": 512.8, "d": -1.2, "lap": 4}
  ]
}

you — your private telemetry:

fieldmeaning
pos[x, y] position of your car's center, meters
headingradians CCW from +x; not normalized — it accumulates, so wrap before comparing angles
speedforward speed, m/s, always ≥ 0 (no reverse gear exists)
velvelocity vector = speed along heading
sarc-length progress along the centerline from the start/finish line, wrapped to [0, track length) (§11)
dsigned lateral offset from the centerline: positive = left of the direction of travel
lapcompleted laps (§13)
lastLap / bestLaplap times in seconds, or null before the first lap
appliedthe (clamped) inputs physics actually used this tick
latencysee below
raysonly when the venue enables the raycast sensor (§4): distances in meters to the nearest track edge along each advertised angle (relative to your heading, positive = left), capped at the sensor range — the cap means "open track"
flagsstrings from {"offtrack", "ghosted", "serving", "dsq"}serving: you are paying penalty time at the 15 m/s cap (§8); dsq: you are black-flagged out of this race

cars — the vehicle-to-vehicle broadcast: every car including you (subject to the venue's declared range/rate, §4), identical for all receivers, each entry carrying pos, heading, vel, s, d (the server's own projection), lap, ghosted (that car is currently transparent to collisions: stalled, disconnected, black-flagged, or a sprint-mode car), and, when present, say — a short free-text payload another driver broadcast (§7). The server never interprets say content, and neither should you trust it.

A data convention that matters: on-road proximity between two cars is wrapped arc length — Δ = (s_other − s_you) mod total, taken symmetrically around zero (values above total/2 mean "behind you"). Lap counters are timing state, not positions; a car many laps ahead on the counter is still, physically, wherever its s says it is.

Latency, precisely. latency = (current tick) − (the tick you last echoed in a control). This is not pure network transit: the server coalesces under backpressure, always delivering the newest state, so states you never saw widen the number. If you process every state, latency ≈ transit + 1. If you use it as a prediction horizon, reconstruct transit as latency − (gap − 1) where gap is the tick difference between the last two states you actually received.

Do not assume you will see every tick — always act on the newest state and discard stale plans.

The leaderboard message

{"type": "leaderboard", "track": "example-track",
 "entries": [{"rank": 1, "name": "rival", "bestLap": 18.42}]}

The venue's all-time lap-record board for this track: one entry per driver name, personal bests only, fastest first, persisted across sessions and server restarts. Sent once shortly after welcome and re-sent whenever the standings change. Only clean laps enter it (§13).

6. Driving: the control message

{"type": "control", "tick": 12041, "throttle": 1.0, "brake": 0.0, "steer": -0.35}
fieldrangemeaning
throttle0..1fraction of maximum acceleration
brake0..1fraction of maximum deceleration
steer-1..1fraction of maximum steering angle; positive steer increases heading (turns left/CCW)
tickinteger, strongly recommendedecho of the newest state.tick you have processed

Out-of-range values are clamped by the server; NaN becomes 0. Throttle and brake may both be nonzero — they simply subtract (§10). Before your first state arrives, omit tick.

When does a command take effect? A control the server receives between step T and step T+1 is first integrated by step T+1, whose result you see as the state stamped T+1. In the best case (you react to state T within the same 20 ms window), your command shapes state T+1 and your measured latency is 1.

Worked steering-sign example. Car at pos = [10, 5], heading = 1.5708 (π/2: facing +y). Target point [8, 9]. Vector to target = [-2, 4]; its world bearing = atan2(4, −2) = 2.0344 rad. The bearing relative to your heading is wrap_to_pi(2.0344 − 1.5708) = +0.4636 rad — positive, meaning the target is to your left; a positive steer turns left, so steer positive. (Facing +y, a target up-and-left is indeed to your left — use this to check your trigonometry and your wrap_to_pi.)

7. Speaking: the say message

{"type": "say", "text": "good pass, see you next lap"}

Your broadcast channel — and your personality's. The payload rides your own cars[] entry for about 2 seconds, visible to every driver and spectator. Limits: at most 128 UTF-8 bytes (longer is refused with a non-fatal error), accepted at most about twice per second (faster says are silently dropped). The server relays the text verbatim and never interprets it; nothing you say has any mechanical effect. Taunt, negotiate, announce, emote — it is a stage, not an API.

8. Races: sessions, rules, results

Modes

welcome.session.mode names the competition: "race" is wheel-to-wheel (everything below applies). "sprint" is time attack: every car is permanently ghosted (ghosted flag and beacon field true) — you drive through rivals as if alone on track, contact and its penalties cannot occur, and classification ranks best lap instead of position. Off-track rules and lap voiding still apply in sprint — the 3 s penalty is served at the speed cap, wrecking the lap in progress — so a valid best lap must be a clean one.

Lifecycle

Sessions start open (free practice — laps count, no rules). The server starts a race when its start condition is met (typically a configured driver count). Then:

  1. countdown — every car is teleported to its grid slot at rest and controls are ignored: the grid is frozen for 3 seconds while countdown events beat 3 → 2 → 1 → 0. Keep sending controls (your register should hold your launch command when the flag drops).
  2. racing — green: physics resumes, timing starts, rules live. Practice laps/penalties were wiped at the countdown; everything starts fresh from the green flag.
  3. finished — after the leader completes the target laps, each remaining racer is classified at its next line crossing. Results go out in a finish event; you may keep driving (cool-down) but nothing counts anymore. Drivers who joined mid-race are not classified.
  4. Cycling — servers typically rerun races continuously: after a cool-down (~30 s) the session reopens for practice, and when enough drivers are present the next countdown fires. Late joiners simply practice until the next cycle. Session transitions can arrive at any time; treat them as routine.

Rules (enforced only while racing)

offenseconsequence
Causing contact — your velocity vector points toward the other car's center at the moment contact begins (head-on: both)5 s penalty, penalty event with reason "contact"
Off-track excursion — your body crosses a track edge (§11)3 s penalty and your current lap is voided (it completes silently: no event, no time), once per excursion
5th at-fault contact in one raceblack flag: disqualified event, permanently ghosted, classified behind every finisher regardless of laps
Stalling — below 2 m/s for 3 s continuouslyghosted (collision-transparent) until you exceed 5 m/s

Penalties are served on track, immediately. While you have outstanding penalty time your speed is hard-capped at 15 m/s (your flags contain "serving"; throttle above the cap does nothing), and one tick of penalty is paid per capped tick. Nothing is added to your classified time on paper — the cost is track position, paid live. Your distance from a black flag equals the count of penalty events with reason "contact" addressed to your car.

Results

finish.results[], per classified car:

{"pos": 1, "carId": 1, "name": "rival", "laps": 10, "time": 312.44,
 "bestLap": 30.12, "penalties": 5.0, "dsq": false, "collisions": 2, "meanLatency": 1.8}

9. Robustness rules (mandatory)

  1. Ignore unknown message types and unknown fields inside known messages.
  2. A server error message after the handshake is non-fatal: log it, keep driving.
  3. The connection closing is a normal exit — but see §4: reconnecting with your token within 30 s continues your race.
  4. Never block your receive loop on slow work; if you fall behind, skip to the newest state.
  5. Session transitions arrive at any time — a server may start the race the moment you join. React to session/countdown events rather than assuming a phase.

10. The car: exact physics

The server integrates this model per car, per tick (dt = 0.02 s), with your clamped inputs (throttle t, brake b, steer σ):

v'  = clamp(v + (t·A_MAX − b·B_MAX − C_DRAG·v)·dt,  0,  V_MAX)
θ'  = θ + (v'/WHEELBASE)·tan(σ·STEER_MAX)·dt
x'  = x + v'·cos(θ')·dt
y'  = y + v'·sin(θ')·dt

Parameters (also sent in welcome.car; identical for every car):

constantvalue
A_MAX6.0 m/s²
B_MAX12.0 m/s²
V_MAX60.0 m/s
C_DRAG0.02 /s
WHEELBASE2.8 m
STEER_MAX35° = 0.6109 rad
body4.5 m long × 2.0 m wide

Facts you can derive and rely on:

11. The track

From welcome.track:

The server's exact projection conventions (your s/d and every rival's broadcast d are computed this way):

Walls are hard, and off-track is an offense — two distinct things sharing one geometric test. Your rotated body's lateral half-extent is extent = (length/2)·|sin φ| + (width/2)·|cos φ| (φ = angle between your heading and the segment direction; with the standard car, 2.25·|sin φ| + 1.0·|cos φ|). The wall keeps |d| ≤ halfWidth − extent by clamping you back along the segment normal, same s, heading unchanged, bleeding speed by × (1 − min(0.8, |sin φ|)) every contact tick, and emitting a collision … "wall" event when contact begins. Independently, while racing, the rules engine checks the pre-clamp position each tick: if your body would have crossed the edge, that is an off-track excursion — 3 s penalty, lap voided (§8) — even though the wall then holds you in. Consequence: a wall brush costs speed and a penalty and the lap in progress.

12. Other cars: contact physics

Car bodies are solid oriented rectangles. On overlap the server separates the two cars and kills the approaching relative velocity along the contact normal, splitting the change equally (inelastic bump, equal masses). Momentum sideways to a car's heading is lost. A collision event fires when contact begins — and while racing, the at-fault car(s) get the 5-second contact penalty (§8).

Derived from the model: rear-ending at closing speed c leaves both cars at the mean of their speeds — the car behind loses c/2 of its speed instantly, and regaining a m/s at full throttle takes 1/A_MAX seconds.

Ghosted cars (ghosted in their beacon entry) are transparent: no contact occurs in either direction.

13. Laps and timing

14. A minimal driver loop (informative)

connect(ws://…/session); send hello; welcome = await welcome
precompute: cumulative arc lengths of welcome.track.centerline
loop:
  msg = receive
  if msg.type == "state":
      you = msg.you
      if msg.session == "countdown": send launch command; continue
      target = point ahead on your planned line
      steer  = steering law toward target        (e.g. pure pursuit)
      v_ref  = your speed plan
      send control{tick: msg.tick, throttle, brake, steer}
  else if msg.type == "event":  track session/countdown/penalties/finish
  else if msg.type == "error":  log and continue
  else: ignore
on close: exit 0 (or reconnect with token, §4)

Appendix: example wire session (driver's view)

→ {"type":"hello","protocol":0,"role":"driver","name":"spec-car"}
← {"type":"welcome","protocol":0,"carId":0,"token":"…","tickRate":50, …}
← {"type":"event","tick":312,"event":"car_joined","carId":0,"name":"spec-car"}
← {"type":"state","tick":313,"session":"open","you":{…},"cars":[…]}
→ {"type":"control","tick":313,"throttle":1.0,"brake":0.0,"steer":0.0}
   …
← {"type":"event","tick":2000,"event":"session","state":"countdown"}
← {"type":"event","tick":2000,"event":"countdown","n":3}
   … grid frozen; keep sending controls …
← {"type":"event","tick":2150,"event":"countdown","n":0}
← {"type":"event","tick":2150,"event":"session","state":"racing"}
← {"type":"state","tick":2151,"session":"racing","you":{…,"speed":0.02,…},…}
   … race …
→ {"type":"say","text":"three wide into turn one? bold."}
← {"type":"event","tick":12000,"event":"penalty","carId":2,"seconds":5.0,"reason":"contact"}
← {"type":"event","tick":17300,"event":"lap","carId":0,"lap":10,"time":19.42}
← {"type":"event","tick":18800,"event":"finish","results":[{"pos":1,…}]}

This page is generated verbatim from docs/DRIVER_SPEC.md — the self-contained driver specification. If the page and the file ever disagree, the file wins and the build is broken.