Leaderboards
A game declares its boards once with defineLeaderboards. A room submits a player’s score with submitScore, and the score rides the checkpoint that saves the progress that earned it: the board and the save never disagree. A page shows a board with useLeaderboard. A game with no rooms sends its scores from the page with sendScore, and its board says the scores are unverified.
Roblox gives a game an ordered data store and leaves periods, trust and prizes to its scripts. Steam, PlayFab and Unity store a score per board and reset it on a schedule. Here a board has its period built in, so a weekly board needs no new store each week, and a room’s score is written in the same transaction as the player’s save.
Declaring the boards
Section titled “Declaring the boards”Each board sits under a camelCase key, which is its identity once published:
writtenBy:"room"for a board a room writes in its checkpoint’s commit, which makes it trusted;"page"for a game with no rooms, whose page sends its scores.keyed: true: one board for each key the game names, such as a track.parent: "world": one board for each world, which the world’s wipe closes, apart fromkeyed. A world board’s handle is.for(worldId), and.for(worldId, key)where it is keyed too, with the idreadHeldWorld(world)names.audience: who reads the board."game", the default, is everyone who plays;"world"is anyone who has entered the board’s world, and"members"the world’s owners and members alone, so a farm ranks its own members. Both needparent: "world".sortOrder,min,maxandunitfor a board that ranks on one number;score: [...]for up to three named fields, compared in order.update:"best"keeps the better score,"latest"the last one, and"total"adds a whole-number delta.period:"all-time","daily","weekly"or"monthly"on UTC boundaries,"world"for each reset of a world, or{ windows }for competition windows.metadata: a schema for a row’s detail, such as the car of a lap, 256 bytes at most.
A published board keeps its key’s identity: a later version that changes its writer, its keying, its fields’ order, its rule, its period or its audience is refused at upload. Declare a new board under a new key instead, and mark the old one retired: true.
Submitting from a room
Section titled “Submitting from a room”A game with rooms exports its boards from the room’s scene file, beside game: export { leaderboards } from "../leaderboards". A room whose scene exports none refuses each score as no-board.
In the room’s step, submitScore(step, handle, player, score) keeps the score with the player’s record until the next checkpoint. A keyed board’s handle comes from for, such as leaderboards.fastestLap.for("sunset-loop"). A total takes a delta, with an id from createScoreId(step) where the game may submit the same event again: the same id adds once.
import { createScoreId, defineLeaderboards, submitScore, type Authority, type PlayerEntity,} from "@spawnite/engine/core";
export const leaderboards = defineLeaderboards({ fastestLap: { name: "Fastest lap", writtenBy: "room", keyed: true, sortOrder: "ascending", min: 0, unit: "milliseconds", update: "best", period: "weekly", }, coinsFound: { name: "Coins found", writtenBy: "room", sortOrder: "descending", update: "total", period: "all-time", },});
/** In the room's step, as a lap on `track` ends. */export function finishLap( step: Authority, player: PlayerEntity, track: string, milliseconds: number,) { return submitScore( step, leaderboards.fastestLap.for(track), player, milliseconds, );}
/** In the room's step, as a player picks up coins. */export function findCoins( step: Authority, player: PlayerEntity, coins: number,) { return submitScore(step, leaderboards.coinsFound, player, { delta: coins, id: createScoreId(step), });}- The score is stamped with the period of the tick it was submitted on.
- A player’s commit carries at most 8 changed rows; the rest wait in the player’s record and go out with the next commit.
submitScoreanswers a refusal, such asout-of-boundsorno-periodbetween two windows, and null where the score waits. A score of the wrong shape throws in development.ScoreCommittedEventtells the game each row’s outcome on the tick after its commit:changed,unchanged,latefor a period that had closed, orrefused.
Reading a board
Section titled “Reading a board”useLeaderboard(handle, { period, view }) reads a board on the page: its top, the player’s friends, or the rows around-player. The top is a snapshot every player shares, rebuilt every 2 minutes while anyone looks, so builtAt says how old it is. A period that ended reads its final result, which keeps each rank as it was at the close.
import { Text, useLeaderboard, type LeaderboardHandle } from "@spawnite/engine";
/** A track's lap board, such as `leaderboards.fastestLap.for(track)`. */export function LapBoard({ board }: { board: LeaderboardHandle }) { const read = useLeaderboard(board);
return ( <Text as="ol"> {read.rows.map((row) => ( <Text as="li" key={row.subject.id}> {`${row.rank}. ${row.name} ${(row.score[0] / 1000).toFixed(2)} s`} </Text> ))} </Text> );}A room reads a board with fetchLeaderboard(step, handle, { period, limit }), answered on a later tick by LeaderboardFetchedEvent, for a top-10 sign in the world. To award last week’s winners, fetch period: "previous" once its state is final, and send each winner a deposit whose id is createAwardId(handle, { period, prize, playerId }): two rooms that award one result send one id, and the prize lands once.
What a score means is the game’s. A room that lets a player set their own lap time has a fair board of unfair scores. A page game’s board takes the page’s word, since a page can edit anything it holds, and the board shows it as unverified. The creator and support remove a row, exclude a player and reset a board’s key with spawnite leaderboards and the MCP’s leaderboards tool.
In development
Section titled “In development”The game’s dev server keeps a stand-in board in .spawnite/leaderboards.json: the development room’s commits write it, a page’s sendScore writes it, and useLeaderboard reads it, so an agent sees a board fill before the game is published.
A playtest’s rooms keep their saves and their boards in the playtest’s own folder, which the playtest’s page reads. So a playtest starts with no rows, its page shows what its room commits, and the game’s own file stays as it was. A playtest started with --room-env ROOM_SAVES=1 keeps its boards in the game’s file, beside its saves.