Lifecycle: HISTORICAL (published KEEP). For the promoted reader route and current implementation caveat, see Nakama Ranking Cheatsheet.

🏆 Nakama Ranking & Leaderboards — Funday Cheatsheet

A condensed view of how Nakama leaderboards rank owners and how Funday integrates them.


🧠 Core Nakama Model

  • Leaderboard config (immutable)

    • id: canonical name (e.g. minigolf_highscores, fungame_highscore)
    • authoritative: true = only server/runtime can write; false = clients can write
    • sort: "asc" or "desc" — determines whether lower or higher scores rank higher
    • operator: "set" | "best" | "incr" | "decr"
    • max_num_score: how many historical scores per owner to keep (ranking still uses one “active” record)
    • reset_schedule: CRON expression for automatic resets
    • metadata: arbitrary JSON, e.g. labels, metric type
  • Leaderboard record

    • owner_id: user or group IDranking is per-owner
    • score: primary integer used for ranking (after operator applied)
    • subscore: secondary integer used as deterministic tiebreaker
    • num_score: submission counter for that owner
    • metadata: arbitrary JSON (build details, map, mode, device info, etc.)
  • Owner ranking mechanics

    • One active record per (leaderboard_id, owner_id) in the ranking.
    • On each write:
      • set → replace the active record with the new score.
      • best → replace only if new score is better according to sort:
        • sort = "desc"higher score is better.
        • sort = "asc"lower score is better (e.g. lap time).
      • incrscore := score + newScore (cumulative).
      • decrscore := score - newScore.
    • Ranking is computed from score and subscore with sort; ties fall back to server-defined stable ordering.

🎯 Funday Leaderboards — System Pattern

1) Canonical IDs via Plugin Metadata

  • Single source of truth: each game declares its leaderboards in its manifest:

    // games/<id>/funday-plugin.json
    {
      "id": "minigolf",
      "leaderboards": {
        "default": "minigolf_highscores",
      },
    }
  • Examples:

    • fungameleaderboards.default = "fungame_highscore"
    • minigolfleaderboards.default = "minigolf_highscores"
  • The platform loader (frontend/src/lib/server/plugins.ts) injects this into the Game type as game.leaderboards.

2) Nakama Seeding

  • Runtime module (nakama-modules/data-seed.lua) creates leaderboards on startup:

    local boards = {
      'minigolf_highscores',
      'fungame_highscore',
      -- ...other boards
    }
     
    for _, id in ipairs(boards) do
      nk.leaderboard_create(id, false, 'desc', 'best', '', { game = id })
    end
  • Rule: canonical IDs in manifests must match the IDs seeded in Nakama.

3) Submission Path (Guest‑First + Bridge)

  • Games → Host

    • Web/iframe games use FundayBridge:

      bridge.submitScore("fungame_highscore", score, { mode: "demo" })
      // -> emits { type: 'funday:score-submitted', leaderboardId, score, meta }
  • Host → API → Nakama

    • Game viewport receives funday:score-submitted and POSTs:

      await fetch("/api/leaderboards/submit", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          leaderboardId: "fungame_highscore",
          score,
          meta,
        }),
      })
    • POST /api/leaderboards/submit:

      • Validates leaderboardId (canonical) and score.
      • Uses authenticated session if present; otherwise uses device ID:
        • Cookie funday-device-id (guest-first).
        • If missing, create one and authenticate with Nakama (authenticateDevice).
      • Calls writeLeaderboardRecord(session, leaderboardId, { score, subscore?, metadata }).
    • Owner identity:

      • Authenticated player → owner_id = user_id.
      • Guest player → owner_id = device ID (persistent via cookie).

4) Reading Leaderboards (Game Detail Page)

  • Frontend uses the game’s default leaderboard:

    <LeaderboardTable
      leaderboardId={data.game.leaderboards?.default ?? data.game.id}
      title={`${data.game.title} Leaderboard`}
      limit={20}
    />
  • GET /api/leaderboards/[id]:

    • Treats id as canonical (no alias maps).
    • Uses a guest or user Nakama session to call listLeaderboardRecords.

🧩 Complex / Structured Scoring (Funday Pattern)

  • Primary score: single integer, used for rank.
    • Examples: total strokes, total points, milliseconds, MMR bucket index.
  • Subscore (tiebreaker): secondary integer.
    • Examples: time_ms, under_par, deaths, rounds_won.
  • Metadata: structured JSON for UI and analytics.
    • Examples: { courseId, mapId, mode, platform, build, region }.

Examples:

  • Minigolf

    • score = totalScore (strokes; lower is better)
    • subscore = totalScore - totalPar (under/over par)
    • Prefer sort = "asc", operator = "best" on the Nakama board.
  • Arcade highscores

    • score = points (higher is better)
    • subscore = time to reach score, combo count, etc.
    • sort = "desc", operator = "best".
  • Season XP

    • score = accumulated XP
    • Use operator = "incr" to accumulate.

🪣 Bucketed Leaderboards (Scaling Pattern)

When global boards get too dense, use buckets:

  • ID pattern:
    • minigolf_highscores:eu:gold, minigolf_highscores:na:silver, etc.
  • Bucket key sources:
    • Region, platform, skill tier, playlist, season.
  • Implementation:
    • Compute bucket key in runtime code.
    • Write to leaderboardId = baseId .. ':' .. bucketKey.
    • Use Nakama’s bucketed leaderboards guide for details.

For Funday, this can be layered on top of the manifest model:

"leaderboards": {
  "default": "minigolf_highscores",
  "buckets": {
    "region_skill": "minigolf_highscores:{region}:{skillTier}"
  }
}

✅ Universal Funday Leaderboard Rule (Condensed)

  • Canonical IDs only

    • No server-side alias maps. IDs in games, manifests, and Nakama must match.
  • Config in manifests

    • Every game must declare leaderboards.default.
    • Extra boards (modes/buckets) also live in leaderboards.*.
  • Nakama as source of truth

    • All boards are seeded (or lazily created) in a Nakama runtime module.
  • Guest-first owners

    • If no auth session → authenticate by device ID and use that as owner_id.
  • Single submission path

    • Games emit funday:score-submitted via FundayBridge.
    • Host forwards to /api/leaderboards/submit → Nakama.
  • Ranking semantics

    • Choose sort + operator per game mode:
      • Time trials → asc + best.
      • Highscore / arcade → desc + best.
      • Long-term XP → desc + incr.
  • Structure for complexity

    • Use score + subscore + metadata for multi-dimensional scoring.
    • Never encode complex state in score alone when a tiebreaker is needed.

Ask Docs

AI assistant to help answer questions about the documentation. Answers are read-only and cite docs/source.

Hi! How can I help you with the documentation today? Answers are read-only and cite docs/source.

Ctrl+Enter to send