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.