first commit
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# LeaderboardManager
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`LeaderboardManager` provides multi-leaderboard score tracking for game
|
||||
servers with three independent ranking modes — raw score, win-count, and
|
||||
Elo. Each leaderboard is configured separately, sorted independently, and
|
||||
capped at a configurable max-entry size that evicts the lowest-ranked
|
||||
entries to keep memory bounded under unbounded submission volume.
|
||||
|
||||
The manager injects its `Clock` so deterministic tests and seeded replays
|
||||
can advance time without resorting to fake timers, and emits OTel counters
|
||||
(`game_server.leaderboard.submit_total`, `game_server.leaderboard.eviction_total`)
|
||||
so production deployments can monitor submission throughput and eviction
|
||||
pressure without bespoke instrumentation.
|
||||
|
||||
## Features
|
||||
|
||||
- Three ranking modes: `score`, `wins`, `elo`.
|
||||
- Ascending (golf) or descending (default) sort.
|
||||
- Tie-break by secondary score.
|
||||
- Per-board `maxEntries` cap with rank-based eviction.
|
||||
- Elo update with configurable K-factor.
|
||||
- Injectable `Clock` for deterministic time.
|
||||
- Stable `GAME_SERVER.*` error codes (`LEADERBOARD_NOT_FOUND`,
|
||||
`INVALID_LEADERBOARD_CONFIG`, `INVALID_SCORE_SUBMISSION`,
|
||||
`LEADERBOARD_ENTRY_INVALID`).
|
||||
- Zod-validated configs and submissions.
|
||||
- OTel `Counter` instrumentation.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { LeaderboardManager } from "@suaveplan/game-server/enhanced/leaderboard/leaderboard-manager";
|
||||
|
||||
const lm = new LeaderboardManager();
|
||||
lm.createLeaderboard({ id: "global", rankingMode: "score" });
|
||||
lm.submitScore("global", {
|
||||
playerId: "alice",
|
||||
displayName: "Alice",
|
||||
score: 4200,
|
||||
});
|
||||
const top = lm.getTopN("global", 10);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { LeaderboardManager } from "@suaveplan/game-server/enhanced/leaderboard/leaderboard-manager";
|
||||
|
||||
let now = 1_700_000_000_000;
|
||||
const lm = new LeaderboardManager<{ region: string }>(
|
||||
{ defaultMaxEntries: 500, defaultEloKFactor: 24, defaultInitialElo: 1500 },
|
||||
{ clock: () => now }
|
||||
);
|
||||
|
||||
lm.createLeaderboard({
|
||||
id: "ranked-1v1",
|
||||
rankingMode: "elo",
|
||||
maxEntries: 1000,
|
||||
eloKFactor: 32,
|
||||
});
|
||||
|
||||
lm.submitScore("ranked-1v1", {
|
||||
playerId: "alice",
|
||||
displayName: "Alice",
|
||||
score: 0,
|
||||
isWin: true,
|
||||
opponentElo: 1450,
|
||||
metadata: { region: "eu-west" },
|
||||
});
|
||||
|
||||
now += 60_000;
|
||||
const entry = lm.getEntry("ranked-1v1", "alice");
|
||||
const stats = lm.getStats();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new LeaderboardManager(config?, deps?)` — `deps.clock?: Clock`, `deps.meter?: Meter`.
|
||||
- `createLeaderboard(config: LeaderboardConfig): LeaderboardConfig`
|
||||
- `deleteLeaderboard(id: string): boolean`
|
||||
- `submitScore(id, submission): LeaderboardEntry`
|
||||
- `getTopN(id, n): readonly LeaderboardEntry[]`
|
||||
- `getEntry(id, playerId): LeaderboardEntry | undefined`
|
||||
- `removeEntry(id, playerId): boolean`
|
||||
- `resetLeaderboard(id): boolean`
|
||||
- `listLeaderboards(): readonly LeaderboardConfig[]`
|
||||
- `getStats(): LeaderboardStats`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Ranks are recomputed in full on every submission. For the configured
|
||||
ceiling of 1000 entries this is `O(N log N)` per write and stays well
|
||||
under a millisecond on commodity hardware; if profiles show this becoming
|
||||
a bottleneck, the sort can be swapped for an order-statistic tree without
|
||||
changing the public surface.
|
||||
|
||||
The Elo update follows the standard logistic formulation with a 400-point
|
||||
spread and rounding to the nearest integer rating. K-factor is per-board
|
||||
so high-volatility competitive ladders can run alongside calibration
|
||||
boards with low K. Submissions without an `opponentElo` are treated as
|
||||
unranked sessions and do not alter the Elo rating, which avoids polluting
|
||||
ladders with practice games or AI matches.
|
||||
|
||||
Eviction operates on rank rather than insertion order so the cap reflects
|
||||
the leaderboard's intent ("top 1000 players") rather than churn. The
|
||||
`eviction_total` counter is tagged by `leaderboardId` so per-board churn
|
||||
is observable in production dashboards without code changes.
|
||||
Reference in New Issue
Block a user