# Quest Student Leaderboard SDK Guide

Use this document when adding an online leaderboard to a student-deployed browser game.

Quest Student Deploy is static browser hosting only. Student games should use the Quest Leaderboard SDK instead of creating a custom backend or external database account.

## Copy-Paste Prompt For AI

```md
You are helping me build a browser game that will be deployed to Quest Student Deploy.

Important Quest rules:
- My final deployed game must be a static browser build only.
- Do not add any backend server, Express app, custom API, database credentials, Firebase project, Supabase project, or custom leaderboard service.
- Use the Quest Leaderboard SDK only for shared online scores.
- Keep game logic client-side.
- Submit scores only at round end, level completion, or when the player beats a best score.

Quest Leaderboard SDK URLs:
- ESM/module URL: https://app.joinquest.com/student-deploy/quest-leaderboard.mjs
- Script/global URL: https://app.joinquest.com/student-deploy/quest-leaderboard.js

Use this API:
- `QuestLeaderboard.submitScore(options)`
- `QuestLeaderboard.loadTop(options)`
- `QuestLeaderboard.getPlayerKey(options)`
- `QuestLeaderboard.resetPlayerKey(options)`

Use `order: "desc"` for points, money, coins, kills, or anything where higher is better.
Use `order: "asc"` for fastest time, fewest moves, or anything where lower is better.

Please implement a simple leaderboard UI in my game using Quest Leaderboard correctly.
```

## Recommended SDK Usage

If the project supports ES modules:

```js
import QuestLeaderboard from "https://app.joinquest.com/student-deploy/quest-leaderboard.mjs";
```

If the project does not use ES modules:

```html
<script src="https://app.joinquest.com/student-deploy/quest-leaderboard.js"></script>
```

```js
const result = await window.QuestLeaderboard.loadTop();
```

## Submit A Score

```js
await QuestLeaderboard.submitScore({
  playerName: "Ari",
  score: 1200,
  displayScore: "1,200",
});
```

The default board is `leaderboardKey: "default"` and `order: "desc"`.

The numeric `score` is what gets ranked. `displayScore` is only the text shown
to players. Prefer sending a number for `score`; Quest can recover from simple
formatted score strings like `"1,200 pts"`, `"$1.2M"`, or `"1:23.45"`, but
numeric scores are clearer.

## Load Scores

```js
const result = await QuestLeaderboard.loadTop({ limit: 25 });

result.entries.forEach((entry) => {
  console.log(`${entry.rank}. ${entry.playerName}: ${entry.displayScore || entry.score}`);
});
```

If `limit` is omitted, Quest returns up to 25 entries.

Response shape:

```js
{
  leaderboard: {
    key: "default",
    name: "Leaderboard",
    scoreLabel: null,
    order: "desc"
  },
  entries: [
    {
      rank: 1,
      playerName: "Ari",
      score: 1200,
      displayScore: "1,200",
      metadata: null,
      achievedAt: "2026-05-01T12:00:00.000Z",
      isCurrentPlayer: true
    }
  ],
  playerEntry: null,
  playerRank: null,
  playerInTopEntries: false
}
```

## Fastest Times

Use `order: "asc"` when lower is better:

```js
await QuestLeaderboard.submitScore({
  leaderboardKey: "fastest-time",
  leaderboardName: "Fastest Times",
  scoreLabel: "seconds",
  playerName,
  score: elapsedSeconds,
  displayScore: formatTime(elapsedSeconds),
  order: "asc",
});
```

```js
const result = await QuestLeaderboard.loadTop({
  leaderboardKey: "fastest-time",
  order: "asc",
});
```

Do not switch a board between `"asc"` and `"desc"`. If scoring rules change, use a new `leaderboardKey`.

## Friendly Aliases

Prefer the canonical names above. These aliases are supported only to make
simple AI-generated code less brittle:

- `QuestLeaderboard.submit(options)` and `QuestLeaderboard.submitHighScore(options)` call `submitScore`.
- `QuestLeaderboard.loadScores(options)` and `getLeaderboard(options)` call `loadTop`.
- `boardKey` or `key` can be used instead of `leaderboardKey`.
- `boardName` can be used instead of `leaderboardName`.
- `displayName`, `name`, or `nickname` can be used instead of `playerName`.
- `points`, `coins`, `money`, `seconds`, `time`, or `moves` can be used instead of `score`.
- `lowerIsBetter: true` or `sort: "lowest"` can be used instead of `order: "asc"`.

Use the canonical names in new code.

## Suggested UI Pattern

A simple default pattern that works well for most games:

- Show a bounded top list, usually 10 to 25 entries.
- Highlight the current player if they are in the top list.
- If the current player has a saved score but is not in the top list, show their row separately below the list.
- Use `displayScore || score` when rendering, so formatted values appear when provided.
- Load the leaderboard in a modal, side panel, or end-game screen instead of making it block normal gameplay.
- If loading fails, show a small "Leaderboard unavailable" state and keep the game playable.
- After submitting at game end, refresh the leaderboard and show the player's rank or saved score.

Example:

```js
function renderScore(entry) {
  return entry.displayScore || String(entry.score);
}

async function showLeaderboard() {
  const result = await QuestLeaderboard.loadTop({ limit: 25 });

  renderTopRows(result.entries.map((entry) => ({
    rank: entry.rank,
    name: entry.playerName,
    score: renderScore(entry),
    isCurrentPlayer: entry.isCurrentPlayer,
  })));

  if (result.playerEntry && !result.playerInTopEntries) {
    renderPinnedPlayerRow({
      rank: result.playerEntry.rank,
      name: result.playerEntry.playerName,
      score: renderScore(result.playerEntry),
    });
  }
}
```

For end-game flows, a useful default is:

```js
async function finishGame({ playerName, score, displayScore }) {
  const result = await QuestLeaderboard.submitScore({
    playerName,
    score,
    displayScore,
  });

  showEndGameRank({
    rank: result.submittedEntry.rank,
    score: result.submittedEntry.displayScore || result.submittedEntry.score,
    savedBest: result.scoreSaved,
  });
}
```

## Multiple Boards

```js
await QuestLeaderboard.submitScore({
  leaderboardKey: "coins",
  playerName,
  score: coinsCollected,
});

await QuestLeaderboard.submitScore({
  leaderboardKey: "fewest-moves",
  playerName,
  score: moves,
  order: "asc",
});
```

Good leaderboard keys use letters, numbers, periods, underscores, or hyphens.

## Local Testing

Local Quest app:

```js
apiBase: "http://localhost:3000"
```

Production:

```js
apiBase: "https://app.joinquest.com"
```

If the game is previewed outside a real Quest deployed URL, pass the `project` object explicitly:

```js
await QuestLeaderboard.loadTop({
  apiBase: "https://app.joinquest.com",
  project: {
    studentSlug: "student-slug",
    projectSlug: "project-slug",
    versionNumber: 1,
  },
});
```

Do not invent these values. Ask the student or use the values printed by Quest.

## Limits And Safety

- Leaderboards persist across deploy versions for the same project.
- The SDK stores a browser player key with `localStorage`.
- The same browser/player keeps one best entry per leaderboard.
- Each leaderboard board may cap the number of unique players. Existing players can still update their saved best score.
- Scores are submitted by browser code, so they are not cheat-proof.
- Ranking always uses the numeric `score`; use `displayScore` for formatted UI text such as `"$1.2M"`, `"1:23.45"`, or `"14 moves"`.
- Use the leaderboard for friendly game scores, not grades, payments, or private records.
- Metadata must be small JSON.
- If optional metadata is too large or cannot be converted to JSON, the SDK may omit it so the score can still submit.
- Do not store secrets, private information, large save data, images, or audio.
- Do not submit scores every frame.

## What Not To Do

Do not:
- Build or upload a custom leaderboard backend.
- Put database credentials in browser code.
- Create a Supabase, Firebase, or database account for a simple game leaderboard.
- Use Quest Multiplayer shared state for persistent scores.
- Use localStorage when the student specifically wants an online shared leaderboard.
