4.3 KiB
TournamentManager
(no submodules)
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
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
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?): Participantstart(): voidgetMatch(id): MatchrecordResult(matchId, winnerId, data?): Matchcancel(): voidgetTournament(): TournamentgetWinner(): Participant | nullgetStandings(): 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.