Files
skills/docs/gaming/games/game-server/src/enhanced/tournament/tournament-manager/tournament-manager.md
2026-05-29 08:20:56 +01:00

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?): 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.