Files
2026-05-27 23:29:53 +01:00

3.9 KiB

OpenSpec Change: enterprise-gaming-leaderboard

  • Change ID: enterprise-gaming-leaderboard
  • Package: @suaveplan/leaderboard
  • Category: gaming/multiplayer
  • Target path: gaming/multiplayer/leaderboard
  • Status: PROPOSED
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/leaderboard.md

Summary

Deliver @suaveplan/leaderboard on the genesis branch from scratch at enterprise quality. The package provides a leaderboard system with ranked score submission, seasonal resets, and around-player rank queries. A Leaderboard uses an in-memory sorted structure (array + binary search for O(log n) insert/rank); a SeasonManager tracks active season with start/end timestamps and archives previous season results; aroundPlayer returns N entries above and below a given player for contextual ranking display. Genesis adds Zod schemas, structured errors, OTel submission counter, injectable clock, and 100% coverage with property tests for rank monotonicity.


Current-state audit (genesis)

Package does not exist on genesis yet — fresh build.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) LeaderboardError; LB.* codes
@suaveplan/types (workspace:^) Clock
@suaveplan/events (workspace:^) lb:score_updated, lb:season_reset, lb:rank_achieved events
zod (catalog:forms) LeaderboardEntry, Season schemas
@opentelemetry/api (catalog:observability) Submission counter, entry count gauge

Public API surface

interface LeaderboardEntry {
  playerId: string
  displayName: string
  score: number
  rank: number
  updatedAt: number
  metadata?: Record<string, unknown>
}

function createLeaderboard(options?: LeaderboardOptions): Leaderboard

interface LeaderboardOptions {
  name?: string
  maxEntries?: number       // default unlimited (in-memory); set for top-N boards
  order?: 'desc' | 'asc'   // default 'desc' (higher = better)
  clock?: Clock
}

interface Leaderboard {
  submit(playerId: string, displayName: string, score: number, metadata?: Record<string, unknown>): LeaderboardEntry
  getEntry(playerId: string): LeaderboardEntry | undefined
  getRank(playerId: string): number | undefined
  getPage(page: number, pageSize?: number): LeaderboardEntry[]
  aroundPlayer(playerId: string, radius?: number): LeaderboardEntry[]
  remove(playerId: string): void
  reset(): void
  readonly size: number
}

// Seasons
function createSeasonManager(options: SeasonManagerOptions): SeasonManager

interface SeasonManagerOptions {
  seasonDuration: number    // seconds
  clock?: Clock
}

interface SeasonManager {
  getCurrentSeason(): Season
  tick(dt: number): void    // check for season expiry
  getArchive(): ArchivedSeason[]
  archiveCurrent(leaderboard: Leaderboard): void
}

interface Season {
  id: string
  number: number
  startedAt: number
  endsAt: number
  isActive: boolean
}

interface ArchivedSeason extends Season {
  topEntries: LeaderboardEntry[]
}

Error codes

type LbErrorCode =
  | 'LB_ENTRY_NOT_FOUND'
  | 'LB_INVALID_SCORE'
  | 'LB_SEASON_NOT_ACTIVE'
  | 'LB_PAGE_OUT_OF_RANGE'

File layout

gaming/multiplayer/leaderboard/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/index.ts
    types/common.ts
    types/common.md
    core/
      leaderboard/
        leaderboard.ts
        leaderboard.md
        leaderboard.types.ts
        leaderboard.test.ts
        index.ts
      season-manager/
        season-manager.ts
        season-manager.md
        season-manager.types.ts
        season-manager.test.ts
        index.ts
  tests/
    property.test.ts
    integration.test.ts

Impact

Downstream: @suaveplan/react-game-hud renders mini leaderboard widget; @suaveplan/api-plugin-game-server exposes leaderboard HTTP routes. Prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/events.