first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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.