# leaderboard

sdk.leaderboard.submit() sends a score to a board declared in the console; top() and me() read it by period.

You declare boards per game in the console: `name` (`^[a-z][a-z0-9_]{0,31}$`), `sort` (`desc` or `asc`),
`min` and `max`, `keep` (`best` or `latest`) and a rate per minute (10 by default). The API rejects a score
outside the bounds. Periods are `day`, `week` and `all`.

## `leaderboard.submit(board, score, meta?): Promise<SubmitResult>`

Once per finished run, after `gameplay.stop()`, in a run that called `gameplay.start()`. `score` is a safe
integer. `meta` is at most 512 bytes of JSON with string or number values, shown as the entry's detail
(tries, time). A board or a meta key named `time` is in seconds; the hub formats it as `m:ss`.

<CodePanel title="Submit a score">
  <Fragment slot="javascript">
    ```js
    async function endRun(metres, tries) {
      sdk.gameplay.stop();
      const result = await sdk.leaderboard.submit('height', metres, { tries });
      if (result.status === 'accepted') showRank(result.rank.week, result.best);
      else if (result.status === 'rejected') console.warn('score rejected:', result.reason);
    }
    ```

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

    async function endRun(metres: number, tries: number): Promise<void> {
      sdk.gameplay.stop();
      const result: SubmitResult = await sdk.leaderboard.submit('height', metres, { tries });
      if (result.status === 'accepted') showRank(result.rank.week, result.best);
      else if (result.status === 'rejected') console.warn('score rejected:', result.reason);
    }
    ```

  </Fragment>
</CodePanel>

## `leaderboard.top(board, period): Promise<TopResult>`

Up to 10 entries, ranked. The hub draws the top 5 and the player's row itself; the SDK gives the raw list.
`total` is the number of entries on the board for that period.

## `leaderboard.me(board, period): Promise<MeResult>`

The player's own entry for that period, or `null` when they have none.

<CodePanel title="Draw a board">
  <Fragment slot="javascript">
    ```js
    const top = await sdk.leaderboard.top('height', 'week');
    const me = await sdk.leaderboard.me('height', 'week');
    if (top.status === 'unavailable') return hideBoard();
    drawRows(top.entries.slice(0, 5));           // { rank, nickname, score, meta, isMe }
    if (me.status === 'ok' && me.entry) drawMyRow(me.entry);
    ```

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

    const top: TopResult = await sdk.leaderboard.top('height', 'week');
    const me: MeResult = await sdk.leaderboard.me('height', 'week');
    if (top.status === 'unavailable') return hideBoard();
    const rows: LeaderboardEntry[] = top.entries.slice(0, 5);
    drawRows(rows);
    if (me.status === 'ok' && me.entry) drawMyRow(me.entry);
    ```

  </Fragment>
</CodePanel>

## Types

<CodePanel title="Leaderboard types">
  <Fragment slot="javascript">
    ```js
    // Period: 'day' | 'week' | 'all'
    // LeaderboardEntry: { rank, nickname, score, meta, isMe }
    // SubmitResult: { status: 'accepted', rank: { day, week, all }, best }
    //             | { status: 'rejected', reason }     'bounds' | 'rate' | 'signature' | 'board' | 'session' | 'nonce' | 'clock' | 'meta'
    //             | { status: 'unavailable' }
    //             | { status: 'error', message }       meta not serializable, network failure
    // TopResult: { status: 'ok', entries, period, total } | { status: 'unavailable' }
    // MeResult:  { status: 'ok', entry, period } | { status: 'unavailable' }
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```ts
    type Period = 'day' | 'week' | 'all';

    interface LeaderboardEntry {
      rank: number;
      nickname: string;
      score: number;
      meta: Record<string, string | number> | null;
      isMe: boolean;
    }

    type SubmitResult =
      | { status: 'accepted'; rank: { day: number; week: number; all: number }; best: boolean }
      | { status: 'rejected'; reason: 'bounds' | 'rate' | 'signature' | 'board' | 'session' | 'nonce' | 'clock' | 'meta' }
      | { status: 'unavailable' }
      | { status: 'error'; message: string };     // meta not serializable, network failure

    type TopResult =
      | { status: 'ok'; entries: LeaderboardEntry[]; period: Period; total: number }
      | { status: 'unavailable' };

    type MeResult =
      | { status: 'ok'; entry: LeaderboardEntry | null; period: Period }
      | { status: 'unavailable' };
    ```

  </Fragment>
</CodePanel>

## Signed scores on filudi.com

`gameplay.start()` opens a play session and receives a session key. The SDK signs
`pid|gameId|board|score|nonce|ts` with HMAC-SHA256 through `crypto.subtle` and sends the signature with the
score. Replaying a nonce or submitting outside a session is `rejected`. The game does nothing for this;
it only brackets play. Without `crypto.subtle` (an old browser on plain HTTP) `submit` answers `rejected`
with reason `signature` and everything else works.

## Rejected reasons

| Reason                        | What it means                                                                                                  |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `bounds`                      | The score is outside the board's `min` and `max`, or not a safe integer. Also the mapping of an unknown reason |
| `rate`                        | More scores than the board's rate per minute (a 429)                                                           |
| `session`                     | No play session: `gameplay.start()` was not called in this run                                                 |
| `signature`, `nonce`, `clock` | The signature failed, the nonce was replayed, or the clock is off by too much                                  |
| `board`                       | No board of that name on this game                                                                             |
| `meta`                        | `meta` is over 512 bytes or has a value that is not a string or a number                                       |

## Per host

| Host       | `submit`                                                                                                                           | `top` and `me`                          |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| filudi.com | Signed, to the board of the game                                                                                                   | Work, by period                         |
| CrazyGames | Encrypted for `user.submitScore` when `init({ crazygames: { leaderboardKey } })` is given, else `unavailable`; `rank` values are 0 | `unavailable`: the portal draws its own |
| Poki       | `unavailable`                                                                                                                      | `unavailable`                           |
| Playgama   | Through `bridge.leaderboards` unless its type is `not_available`; `rank` values are 0                                              | Work when the type is `in_game`         |
| mock       | Accepted against the seed entries of `mock.leaderboard`; standalone answers `unavailable`                                          | Same                                    |

`features.leaderboard` from `init()` says ahead of time whether a board is worth drawing. Which boards to
declare and how to pick their bounds is in [leaderboards](/docs/games/leaderboards/).
