# Leaderboards

Declare boards in the console with bounds the API enforces, submit one signed score per run, and draw what the host gives you.

A board is declared in the console before any score reaches it. The API enforces the bounds and the rate;
the SDK signs each score with the play session on filudi.com. The reference is
[leaderboard](/docs/sdk/leaderboard/).

## Declaring a board

In the console, the game, Leaderboards: add a board, then "Save boards".

| Field           | What it is                                                                                                                     |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `name`          | `^[a-z][a-z0-9_]{0,31}$`, the string you pass to `submit()`. `height`, `crowns`, `time`                                        |
| `sort`          | `desc` for a score to beat, `asc` for a time to beat                                                                           |
| `min`, `max`    | The API rejects a score outside. Pick the real range of your game, not the integer range: a bound is what stops a forged score |
| `keep`          | `best` keeps the player's best entry, `latest` their last one                                                                  |
| `ratePerMinute` | Scores accepted per player per minute, 10 by default                                                                           |

A board or a meta key named `time` is in seconds and the hub formats it as `m:ss`. Scores are integers:
send tenths of a second as an integer if you need them, and say so in the board's name.

## One score per run

Submit once, when the run ends, after `gameplay.stop()`, in a run that called `gameplay.start()`. The
session opened by `start()` gives the key that signs the score; a score outside a session is rejected with
reason `session`.

<CodePanel title="End of a run">
  <Fragment slot="javascript">
    ```js
    async function endRun() {
      sdk.gameplay.stop();
      const result = await sdk.leaderboard.submit('height', metres, { tries: run.tries, route: run.route });
      if (result.status === 'accepted' && result.best) celebrateBest(result.rank.all);
    }
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    async function endRun(): Promise<void> {
      sdk.gameplay.stop();
      const result = await sdk.leaderboard.submit('height', metres, { tries: run.tries, route: run.route });
      if (result.status === 'accepted' && result.best) celebrateBest(result.rank.all);
    }
    ```

  </Fragment>
</CodePanel>

`meta` is up to 512 bytes of JSON with string and number values, shown next to the score as its detail.
Use it for what explains the score (tries, time, the route taken), not for anything the score already says.

## Drawing a board

`top()` gives up to 10 entries and `me()` the player's own; both take a period. The hub draws the top 5 and
the player's row on the game's page, so an in-game board is for what the hub does not show: a per-level
board, a weekly race, a board under the game's own art. Handle `unavailable`: CrazyGames and Poki draw
their own boards and give you nothing to read.

<CodePanel title="A board in the game">
  <Fragment slot="javascript">
    ```js
    async function drawBoard(period) {
      const [top, me] = await Promise.all([
        sdk.leaderboard.top('height', period),
        sdk.leaderboard.me('height', period),
      ]);
      if (top.status !== 'ok') return hideBoard();
      drawRows(top.entries, me.status === 'ok' ? me.entry : null);
    }
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    import type { Period } from '@filudi/game-sdk';

    async function drawBoard(period: Period): Promise<void> {
      const [top, me] = await Promise.all([
        sdk.leaderboard.top('height', period),
        sdk.leaderboard.me('height', period),
      ]);
      if (top.status !== 'ok') return hideBoard();
      drawRows(top.entries, me.status === 'ok' ? me.entry : null);
    }
    ```

  </Fragment>
</CodePanel>

## Signed scores

On filudi.com the SDK signs `pid|gameId|board|score|nonce|ts` with HMAC-SHA256 and the session key. The API
checks the signature, the nonce (once), the clock (within a window) and the bounds. The game does nothing for
this and cannot weaken it: a bundle that posts a score by hand gets `rejected`. What a forged client can
still do is play badly on purpose; the bounds and the rate are your limits on that, and the console lets you
hide a score.

## On portals

CrazyGames takes scores through `user.submitScore` once you pass the key from its developer portal as
`init({ crazygames: { leaderboardKey } })`. Keep the key in a build variable, never in the repository. Poki
has no leaderboard. Playgama takes scores and, when its board type is `in_game`, gives them back. `rank`
values are 0 where the portal reports none.

## Scores in the console

The game's Leaderboards screen lists the boards and their scores with the nickname, the meta and the date.
Hide a score that should not be there; the entry goes out of `top()` and the player keeps their session.
Statistics give each board's count, best and median.
