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 judgeSingleRace (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).

  • Car class is the model’s carClass (1 = C, 2 = S, 3 = R, 4 = X, 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, which also changes with server-only commits.

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.
  • 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). 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 and the nickname rules. party/scripts/twoClients.ts fills the online boards.