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