Skip to content

Repository files navigation

F0 — Driver's Manual

F0 is a racing simulator where the cars are yours. The server runs the world, the track, and the physics. You bring an HTTP service that the server polls every tick to ask "what do you want to do next?". Your job is to write that service.

This document tells you everything you need to put a car on the grid and then everything you can do to make it actually fast.


Running the server

The server needs a Postgres database and a JVM (Kotlin/JVM 21, Ktor 3).

Option A — everything in Docker

Brings up Postgres and the app together:

docker compose up --build

The app is then on http://localhost:8080.

Option B — local dev (./gradlew run)

./gradlew run only starts the app, so you need a Postgres running first. The simplest way is to start just the database from the compose file:

docker compose up -d postgres   # Postgres on localhost:5433
./gradlew run                   # app on http://localhost:8080

The app reads its database connection from env vars, falling back to the local Postgres above if they're unset:

Env var Default
DATABASE_URL jdbc:postgresql://localhost:5433/f0
DATABASE_USER f0
DATABASE_PASSWORD (see application.yaml / compose)

Migrations are manual. SchemaUtils.create only creates missing tables; it never alters existing columns. After a schema change, drop pgdata/ (or run the ALTER TABLE by hand) before starting.

Set the weekend schedule

A fresh server has no useful schedule, so nothing will run until you set one. Open the admin config page and pick the timings:

  1. Go to http://localhost:8080/weekend-config.html.
  2. Sign in when prompted with the admin basic-auth credentials (user admin, password from Routing.kt).
  3. Hit a Quick Preset (e.g. Start from Now or Start in 5 minutes) to fill in the standard FP1 → FP2 → Qualifying → Race schedule, or set each phase's start time / duration (and the race lap count) by hand.
  4. Click Save Configuration.

The weekend advances through its six phases — PRESEASON → FP1 → FP2 → Qualifying → Race → Podium — based on these times. Drivers register during PRESEASON; once FP1's start time passes, the world starts ticking and polling your driver. You can re-open the page any time to reschedule.

Watch the action live at http://localhost:8080/track-viewer.html, and the final results at http://localhost:8080/podium.html.


TL;DR — the absolute minimum driver

A car that just rolls out of the pit and idles on track is enough to be a registered participant. You need three HTTP endpoints, all on one host:

POST  /<your-route>/pit         body: PitView          → { "leavePit": true }
POST  /<your-route>/drive       body: DriverView       → { "pedal": 1, "steer": 0, "enterPit": false }
GET   /<your-route>/heartbeat   no body                → 200 OK

That's it. If you reply {"leavePit": true} to every /pit poll and anything sane to /drive, you'll be on the track. If your handler throws or your service times out, the server defaults to "stay where you are" and the race goes on — your last pedal is reused.


1. Registering your driver

POST /driver once at startup. The server uses host to call you back on every poll, so it must be reachable from the server.

{
  "driverNumber": 17,           // 1..99
  "driverName": "Speedy",       // 3..50 alphanumeric, not "admin"
  "driverPass": "secret123",    // ≥ 3 chars
  "livery1": "#FF0000",         // hex colour
  "livery2": "#000000",
  "engine":   4,                // engine + breaks + steering MUST sum to 10
  "breaks":   3,
  "steering": 3,
  "host": "http://my-host:9000/speedy"
}

After registration the server starts polling host + /pit, /drive, /heartbeat.

Stat budget

Your 10 points buy three properties via a curve, so the first point is worth more than the tenth:

Stat What it raises Range
engine Top-end power → acceleration & top speed 550 kW → 850 kW
breaks Braking friction coefficient 1.2 → 2.2
steering Lateral friction → cornering speed 1.4 → 1.7

Any allocation summing to 10 is legal. There is no single best build.


3. How the server talks to you

The world ticks every GameConfig.TICK_MILLIS (default 250 ms). On every tick the server polls every active driver in parallel and waits up to 200 ms for each response.

When you are… The server calls You return
In the pit POST /pit with PitView PitAction
On track POST /drive with DriverView DriverAction
In a paused phase GET /heartbeat 200 OK

/heartbeat fires during phases where the world is not ticking (PRESEASON, between FP1/FP2 etc.). Treat the absence of /heartbeat as your "the server has gone away" signal — it's pinged often enough to make a disconnect obvious.

DriverAction (response to /drive)

{ "pedal": 3, "steer": 0, "enterPit": false }
  • pedal[-5, +5]. Positive = throttle (×0.2 each), negative = brake, 0 = coast.
  • steer{ -1, 0, +1 }. Changes lane immediately by ±1. Other values are ignored.
  • enterPit true to head to the pit on the next tick (only honoured in phases that allow pitting).

Steering is applied once and reset to 0 after a successful lane change. You must re-send steer: 1 each tick you want to keep moving. See §6 for the anti-weaving penalty.

PitAction (response to /pit)

{ "leavePit": true }

Returning false keeps you in the pit; the server will keep polling /pit until you say true. On exit the server places you at lane +2, position 3600 m (start of the pit-exit window, length 150 m) at 100 km/h with pedal=2.


4. The world you're racing in

  • Track: Zandvoort, 427 segments, each with a real-world arc length and signed radius. Negative radius = right-hander, positive = left-hander, very large absolute radius ≈ straight.
  • Lanes: 5 lanes total, indexed -2, -1, 0, +1, +2.
    Lane Role
    0 Racing line
    ±1 Inner / outer racing lines
    -2 Gravel (slow)
    +2 Pit lane (gravel everywhere except the pit-exit window [3600, 3750])
  • Same segment id on every lane. A given lateral position is segment 42 whether you're at lane −1 or +1; only the along-track distance differs.

Phases

The weekend runs through six phases in order. Each can be configured via weekend-config.html.

Key Display Ticks? Collisions punish? Notes
PRESEASON Pre-season no Cars idle, registrations open
FP1 Practice 1 yes no (1-tick flag) Free practice
FP2 Practice 2 yes no (1-tick flag) Free practice
Q Qualifying yes no (1-tick flag) Best lap counts
R Race yes yes Real damage; lap target enforced
P Podium no Results frozen

In FP/Q a car-on-car contact briefly flags both drivers but neither is relocated. In R it's a full crash for both — gravel, ~1 s stun, lost lap.


5. Reading DriverView

Every /drive poll receives this:

data class DriverView(
    val order: Int,                       // your current race order
    val driverNumber: Int,
    val pedal: Int, val steer: Int,       // your last *committed* inputs
    val speed: Double,                    // km/h
    val lane: Int,                        // current lane (-2..+2)
    val crashed: Boolean,                 // true while you're stunned
    val segments: Map<Int, Map<Int, Lane>>,
    val cars: List<CarInView>,
)
data class Lane(val radius: Double, val distance: Double)
data class CarInView(
    val order: Int, val driverNumber: Int,
    val braking: Boolean, val speed: Double,
    val lane: Int,                        // their lane
    val distance: Double,                 // signed metres ahead, projected onto YOUR lane
)

segments window

A rotated map keyed by offset from your current segment: keys run -10..+20. So segments[0] is your current segment and segments[5] is five ahead. The inner map is keyed by lane: segments[5][0] is the racing line of the segment 5 ahead of you.

For each Lane you get:

  • radius — corner radius in metres on that lane. Use this for braking points. The bigger the absolute value, the gentler the corner (or a straight).
  • distancesigned along-track metres from your current position to the start of that segment, measured on your lane and wrapped to [-halfLap, +halfLap]. Negative means it's behind you. At offset 0 it's the negative of how far you are into your current segment.

This is your lookahead. It's enough to plan an entire braking/throttle sequence to a corner that's, say, 6 segments away.

cars

Every other car within the same segment offset window (-10..+20 segment ids) shows up here.

  • lane is their lane (you can use it to plan an overtake on a different lane).
  • distance is signed and projected onto your lane, so you can compare directly against your own segments[*] distances. Negative = behind you.
  • braking is a leading indicator that they're slowing — useful for anticipating a slipstream gain or a same-lane closure.

6. Driving cleanly: penalties to avoid

Lane-change cost

Every successful lane change costs you 2 % of your current speed and locks your steering for 1 tick (you can't steer again until the tick after next). This was added so weaving back-and-forth to block overtakes isn't free. Lane changes are still strong — they just have a real price. Use them.

Crashes

Cornering too fast for the corner's grip limit (v > cornerMaxSpeed(radius)) is a hard crash regardless of phase: you're relocated to a random gravel lane (±2), zeroed to 0 km/h, and stunned for 1 s (≈4 ticks at TICK_MILLIS=250). Your pedal and steer are forced to 0 on relocation.

Cornering speed:

v_max = sqrt( r · μ · m · g / (m − r · μ · β) )    if (m − r·μ·β) > 0
        ∞                                          otherwise (downforce dominates)

with r = lane radius, μ = lateral friction (your steering stat), m = 740 kg, g = 9.81, β = 1.8 (downforce coefficient). Concretely: invest in steering to corner faster, and brake into the corner so your speed at entry is below v_max for that segment's radius.

Same-lane car contact

If your motion ends within 5 m of another car on your lane, you collide. In the Race phase this is a full crash for both of you. In FP/Q you both get a one-tick crashed=true flag, you don't move, your inputs survive — but you see the flag in the next DriverView, which is your cue to back off or change lane.

Gravel

Lanes ±2 cap your top speed at 30 km/h with an extra 30 m/s² decel applied while above the cap. If you're not in the pit-exit window on lane +2, you're on gravel.


7. Ways to actually go faster

The minimum driver from the TL;DR will plod around at low speed. Here's the rest of the toolbox.

7.1 Use the segment lookahead to plan the corner

segments[0..20][lane].radius lets you precompute, every tick, the next safe speed for each upcoming segment. A robust loop:

  1. For every segment in segments[0..N] on your current lane, compute v_max(radius) using the formula above (or your stat-tuned approximation).
  2. Walk back from the slowest upcoming corner, subtracting how much speed your brakes can shed per tick (your breaks stat × normal force × tickSeconds).
  3. Pick the most restrictive pedal that keeps every future entry speed ≤ that segment's v_max. Default to pedal: +5 if every corner is fine.

7.2 Slipstream

If another car on your lane is between 10 and 80 metres ahead, your effective drag area is reduced by up to 35 %. Closer = stronger; ≤10 m is the maximum benefit. Your top speed and acceleration both rise.

The slipstream isn't reported directly in DriverView, but you can derive it from cars:

  • Find any car with lane == yourLane, distance > 0, distance < 80.
  • The closer they are, the more slipstream you get.

Strategic implications:

  • On a long straight, sit 15–25 m behind a faster car, then jump to a free lane right before the corner.
  • Don't change lane out of someone's tow on a straight unless you can make it pay back in cornering.

7.3 Read the rest of the field

cars only covers the segment window around you. The server publishes the rest of the world over REST — poll it from your driver to plan races, not just laps:

Endpoint What you get
GET /api/status Current phase key + timing
GET /api/standings Order in the current phase
GET /api/fastestLaps/{driver} Segment-by-segment speeds of that driver's best lap
GET /api/podium Final podium + honorary mentions (after Podium)

/api/fastestLaps/{driver} is gold: it's a target speed per segment lifted from a real fast lap. You can use the leader's profile as your benchmark and look for segments where you're underperforming.

7.4 Defend without wasting laps

The lane-change tax means weaving back-and-forth tick-after-tick isn't viable. But a single well-timed lane move on the approach to a braking zone is still very effective. Combine with cars[*].braking and cars[*].distance to detect dive-bombs and pre-empt them.

7.5 Practice phases are free

FP1, FP2 and Q don't enforce car-to-car damage. Use them to:

  • Try aggressive overtakes and see whether the flag fires (it will if you would've crashed in the race).
  • Calibrate your braking model end-to-end against the real server, not your local sim.

7.6 Mind the lap-counter trap

A lap counts only when you cross segment 0 and your lapStats already contains every segment id 0..426. If you crashed mid-lap and got relocated, you may be missing segment ids; a return to a racing lane and a clean drive to segment 0 is required for the next lap to count. On gravel (capped at 30 km/h) this is slow — the strategic answer is usually to recover to lane −1, 0 or +1 ASAP.

7.7 Plan the pit exit

When you enterPit: true and re-launch from the pit, you start at lane +2, position 3600 m, at 100 km/h with pedal=2. The pit-exit window is [3600, 3750]. Outside that window, lane +2 is gravel — so plan to merge back to lane +1 (or just steer −1) before you reach 3750 m, or you'll bog down to 30 km/h.


8. Where to look in the code

You want to … File
Match the JSON shapes server-side game/presentation/DriverView.kt, game/presentation/PitView.kt
Tune your AI against the real physics game/physics/CarPhysics.kt
Understand crash & collision rules in detail game/Driving.kt
See what counts as a lap recordSegmentEntries in game/Driving.kt
Tick rate / timeouts / config game/GameConfig.kt

Good luck. See you on the grid.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages