first commit
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# TournamentManager
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`TournamentManager` orchestrates single-elimination and round-robin
|
||||
tournaments. Participants register during the `pending` phase, then a
|
||||
single `start()` locks the field, applies seeding (or RNG-driven
|
||||
shuffling), and constructs the bracket. From there the manager tracks
|
||||
match results, propagates winners through `nextMatchId` links, updates
|
||||
standings, and emits stable error codes whenever the caller would
|
||||
otherwise corrupt the bracket.
|
||||
|
||||
The manager injects its `Clock`, `IdGenerator`, and `Rng` so deterministic
|
||||
test rigs and seeded replays produce byte-identical brackets each run.
|
||||
OTel counters record per-match creation and completion so production
|
||||
operators can monitor tournament throughput without bespoke
|
||||
instrumentation.
|
||||
|
||||
## Features
|
||||
|
||||
- Two bracket types: single-elimination, round-robin.
|
||||
- Automatic first-round bye propagation for non-power-of-two fields.
|
||||
- Deterministic seeded shuffling via injectable `Rng`.
|
||||
- Standings table with stable points/wins/losses tie-break.
|
||||
- Stable `GAME_SERVER.*` error codes (`INVALID_BRACKET_SIZE`,
|
||||
`TOURNAMENT_INVALID_STATE`, `TOURNAMENT_FULL`,
|
||||
`TOURNAMENT_DUPLICATE_PARTICIPANT`,
|
||||
`TOURNAMENT_INSUFFICIENT_PARTICIPANTS`, `TOURNAMENT_MATCH_NOT_FOUND`,
|
||||
`TOURNAMENT_INVALID_RESULT`).
|
||||
- Zod-validated config.
|
||||
- OTel match counter tagged by `op` (`created`, `completed`).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { TournamentManager } from "@suaveplan/game-server/enhanced/tournament/tournament-manager";
|
||||
|
||||
const tm = new TournamentManager({
|
||||
id: "weekly-cup",
|
||||
name: "Weekly Cup",
|
||||
bracketType: "single-elimination",
|
||||
maxParticipants: 16,
|
||||
});
|
||||
tm.addParticipant("alice");
|
||||
tm.addParticipant("bob");
|
||||
tm.start();
|
||||
const firstMatch = tm.getTournament().rounds[0]!.matches[0]!;
|
||||
tm.recordResult(firstMatch.id, "alice");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { TournamentManager } from "@suaveplan/game-server/enhanced/tournament/tournament-manager";
|
||||
|
||||
const tm = new TournamentManager<PlayerProfile, MatchTelemetry>(
|
||||
{
|
||||
id: "monthly",
|
||||
name: "Monthly Championship",
|
||||
bracketType: "round-robin",
|
||||
maxParticipants: 8,
|
||||
shuffleSeeds: true,
|
||||
winPoints: 3,
|
||||
drawPoints: 1,
|
||||
lossPoints: 0,
|
||||
},
|
||||
{
|
||||
clock: () => now,
|
||||
rng: seededRng,
|
||||
idGenerator: () => `m-${++counter}`,
|
||||
}
|
||||
);
|
||||
|
||||
for (const player of players) {
|
||||
tm.addParticipant(player.id, player.profile);
|
||||
}
|
||||
tm.start();
|
||||
|
||||
for (const round of tm.getTournament().rounds) {
|
||||
for (const match of round.matches) {
|
||||
const winnerId = await playMatch(match);
|
||||
tm.recordResult(match.id, winnerId, { latencyMs: 27 });
|
||||
}
|
||||
}
|
||||
|
||||
const champion = tm.getWinner();
|
||||
const standings = tm.getStandings();
|
||||
const stats = tm.getStats();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new TournamentManager(config, deps?)`
|
||||
- `addParticipant(id, data?, seed?): Participant`
|
||||
- `start(): void`
|
||||
- `getMatch(id): Match`
|
||||
- `recordResult(matchId, winnerId, data?): Match`
|
||||
- `cancel(): void`
|
||||
- `getTournament(): Tournament`
|
||||
- `getWinner(): Participant | null`
|
||||
- `getStandings(): readonly Standing[]`
|
||||
- `getStats(): TournamentStats`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Bracket generation is split into `tournament-bracket.internal.ts` and
|
||||
standings/match utilities into `tournament-standings.internal.ts` to keep
|
||||
this file under the project's 300-line hard limit while preserving full
|
||||
behaviour and 100% coverage through the public surface. Internal modules
|
||||
are not exported through the package barrel — their tests run via the
|
||||
manager.
|
||||
|
||||
Single-elimination brackets pad participants to the next power of two
|
||||
with `null` slots that become first-round byes. Byes auto-advance their
|
||||
opponent into the next round during `start()`, so the bracket is
|
||||
immediately consistent before the first `recordResult()` call. Tournament
|
||||
completion is detected when exactly one participant remains active
|
||||
(single-elim) or every round has completed (round-robin).
|
||||
|
||||
Seeding has two paths: explicit `useSeeding=true` sorts by the
|
||||
caller-supplied `seed` value, while `shuffleSeeds=true` overrides that
|
||||
ordering by Fisher-Yates shuffling with the injected `Rng`. An `Rng`
|
||||
implementation that does not provide `nextIntInRange` is treated as a
|
||||
no-op shuffle — the conservative default for tournament harnesses that
|
||||
prefer explicit ordering errors over silent non-determinism.
|
||||
Reference in New Issue
Block a user