Skip to content

Leaderboards

A leaderboard holds each player’s best time for one distance and one car class. Only times the race server measured itself get on a board: it replays the run from the player’s inputs, so a modified game cannot send a time of its own. Career races and online races both count. Roll races do not, because their cars start moving.

Race Recorded in Time
Career event, wager, bracket judgeRace (party/src/race/judgedRace.ts) playerTime from replayEventRace, in seconds
Boss fight or tournament round judgeSeriesRound (party/src/race/seriesRound.ts), at the round’s own distance the same
Online heat RaceRoom.recordTimes (party/src/RaceRoom.ts) once the verdict is out each runner’s judged finish, in frames

careerTime() drops roll races and runs without a finish; onlineTimes() (party/src/race/onlineTimes.ts) drops runners without a finish. Online, ownedEntry() returns the racer (account and car model) next to the entry; the room keeps it on the server and never sends it to clients.

best_time (migrations 0007_leaderboard.sql, 0008_board_season.sql) has one row per player and board, keyed by user, season, distance and car class. recordBestTimes() (party/src/db/bestTime.ts) inserts the time, or replaces the row only when the new time is faster. A board read goes along the index (season, distance, car_class, finish_ms).

build (0013_best_time_build.sql) keeps the car that set the time, as a SharedCar (player/sharedCar.ts) in JSON: model, parts, look and the tune it raced with (a career race’s saved tune, an online heat’s sent one), but not the other tune slots or test runs. It is written with the time and replaced with it. Times set before the column have none.

  • Car class is the model’s carClass (1 = S Street, 2 = T Tuner, 3 = P Pro, 4 = H Hyper, BOARD_CLASSES in player/leaderboard.ts). Boss cars and the GT have one too, unlike the career districts’ fits.
  • Season is BOARD_SEASON in player/leaderboard.ts. Bump it by hand when the physics change enough that old times no longer compare; every board then starts empty. It is not SIM_VERSION: a small physics fix bumps that (forcing an app update) without making old times meaningless.

Deleting an account deletes its rows (USER_TABLES), and the data export lists them. When a guest links Apple or Google, its best times move to the account; where both have a time on the same board, the faster one stays (party/src/db/linkGuest.ts).

party/src/api/leaderboardApi.ts:

  • GET /api/leaderboard?distance=quarter&class=1&page=1: public, 50 rows per page (BOARD_PAGE_SIZE). Equal times share a rank within the page. With a bearer token the answer also has you, the caller’s own row and rank wherever it ranks.
  • GET /api/leaderboard/build?distance=&class=&ms=<finishMs>&at=<at>: signed in only, the build behind one entry, found by its board, time and moment, so rows carry no user id. 404 when the entry has none. Rows say whether they have one (drivable).
  • PUT /api/profile/nickname { nickname }: sets the name the boards show. Answers { nickname }, or { refused } with length, characters or blocked (422) or taken (409).

player/nickname.ts holds the rules both sides run: 3 to 16 letters or digits with single spaces, dots, dashes or underscores between them, and a short blocklist (matched inside the name with digits read as letters, plus a few words matched only whole). The unique index on profile.nickname (case-insensitive) decides when two players want the same name. A player without a nickname shows as defaultNickname(userId), for example “Racer 4F2A”.

  • Game: the leaderboard feature module. Leaderboard (from the main menu) picks a distance and class with neon toggles, shows the top 10 and the player’s own row (view/boardTable.ts), and the name bar (view/nicknameBar.ts) changes the nickname through a DOM input (ui/textEntry.ts). A row with a build shows DRIVE and is tappable: view/entryDrive.ts offers a test drive of that car over the board’s distance (TestRun with a borrowed car, race/testCar.ts: no prize, no board time, nothing saved, tune never shown), and Boards on its end panel returns to the same board. Requests go through player/account/leaderboardRequests.ts.
  • Site: site/src/routes/leaderboard.tsx, the first page that hydrates. The board is in the URL (?distance=&class=&page=); BoardTable fetches it from SITE.apiUrl in the browser.

npm --prefix party run test:leaderboard (against a running wrangler dev on port 8788) races a guest’s career event and checks the board, a slower run, a run without a finish, the public answer, the entry’s build for test drives and the nickname rules. party/scripts/twoClients.ts fills the online boards.