brackets

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 7 Imported by: 0

README

brackets

Tournament formats for Go: seeding, brackets, byes, Swiss pairings, round robins, group stages, free-for-all lobby leagues and multi-stage flows.

You give brackets participants and options; it returns a deterministic schedule of matches. You report results; it tells you which slots to fill, which matches to add, the standings and when a stage is over. Stages are plain values you can store anywhere, formats are stateless and safe for concurrent use, and the module uses only the Go standard library.

Formats

Key Type What it does
single_elimination SingleElimination Knockout bracket sized to the next power of two. Standard seed placement (1v8, 4v5, 2v7, 3v6, ...) so top seeds meet as late as possible; empty slots become byes for the top seeds and are resolved at planning time.
double_elimination DoubleElimination Upper and lower bracket for a power-of-two field (at least 4). The top half of the seeds starts in the upper bracket, the bottom half in the lower bracket; upper losers drop into the lower bracket; one loss in the lower bracket eliminates. Lower round 1 is best-of-1, the grand final best-of-5 with no advantage.
swiss Swiss Fixed number of rounds (default ceil(log2 n)), each paired from the current standings once the previous round is complete. Odd fields give a bye (counted as a win) to the lowest-ranked participant without one. Rematch-avoiding pairing by default.
round_robin RoundRobin Everyone meets everyone, Legs times (home/away swapped on even legs), scheduled with the circle method so nobody plays twice in a round. Keeps standings and a winner.
group_stage GroupStage Splits the field into groups (snake or sequential seeding). Head-to-head groups play a round robin; with MatchSize > 2 groups play rotating free-for-all lobbies. Optional strict group sizes and partial lobbies.
ffa_league FFALeague Free-for-all lobby league (battle royale / arena style): groups of 4 play 4 lobby rounds of 4-participant lobbies by default. Results are placements and points per slot.
multi_stage MultiStage Chains stages: group stages and "matchpoint" finals (replayed until someone wins TargetPoints games). Later stages are seeded with placeholders that you fill with the qualifiers of earlier stages.

Install

go get github.com/Aquilabot/brackets

Requires Go 1.23 or newer.

Example

package main

import (
	"fmt"

	"github.com/Aquilabot/brackets"
)

func main() {
	players := []brackets.Participant{
		{ID: "ana", Seed: 1}, {ID: "ben", Seed: 2}, {ID: "cleo", Seed: 3},
		{ID: "dan", Seed: 4}, {ID: "eve", Seed: 5}, {ID: "fay", Seed: 6},
	}

	f, err := brackets.DefaultRegistry().Get("single_elimination")
	if err != nil {
		panic(err)
	}
	stages, err := f.Plan(players, brackets.Options{})
	if err != nil {
		panic(err)
	}
	st := &stages[0] // plain data: store it however you like

	for _, m := range st.Matches {
		switch {
		case m.Bye:
			fmt.Printf("%s: bye for %s\n", m.ID, m.Winner)
		case m.Round == 1:
			fmt.Printf("%s: %s vs %s\n", m.ID, m.Home(), m.Away())
		}
	}

	// Play the bracket: report every match whose two slots are filled.
	for !st.Finished {
		for i := range st.Matches {
			m := &st.Matches[i]
			if m.Status != brackets.Pending || m.Home() == "" || m.Away() == "" {
				continue
			}
			result := brackets.Result{Winner: m.Home(), Scores: []int{2, 0}}
			if _, err := brackets.Report(f, st, m.ID, result); err != nil {
				panic(err)
			}
		}
	}
	fmt.Println("champion:", st.Winner)
}

Output:

r1-m1: bye for ana
r1-m2: dan vs eve
r1-m3: bye for ben
r1-m4: cleo vs fay
champion: ana

Report validates the result (match exists and is open, every slot is filled, the winner plays in it, elimination matches have a winner), records it, calls Format.Advance and applies the outcome to the stage. If you keep matches in your own storage, call Advance yourself: it does not modify the stage, it returns an Advance value with the slots to fill (Updates), the matches to create (NewMatches), the standings, byes, winner and whether the stage finished.

More examples (Swiss, registry lookups) are in example_test.go; multi-stage chaining with Qualifiers and FillPlaceholders is exercised in multi_stage_test.go.

Concepts

  • Participant: ID plus optional Seed. Seeds > 0 sort ascending; unseeded participants (Seed <= 0) go last in input order. IDs must be unique, non-empty and must not start with slot: (reserved for placeholders).
  • Stage: one phase: its effective Options, participants, groups, matches, byes, standings, scoreboard and winner.
  • Match: ID, Round, Number, Slots (two for head-to-head, home first; N for lobbies), Status, Winner, plus structure: bracket side, best-of hint, WinnerTo/LoserTo links, feeder From IDs, group, leg, lobby round, bye/phantom/partial flags.
  • Format: Plan(participants, options) ([]Stage, error) and Advance(stage, finishedMatch) (*Advance, error). Formats are stateless and safe for concurrent use.
  • Registry: formats by key. DefaultRegistry() has all seven; register your own Format under any key.
  • Tally: for matches kept in your own storage. Feed each stored match through one reused Match value with Tally.Add, then read Tally.Standings() or call Swiss{}.AdvanceTally(stage, tally); the stage only needs its configuration, not its matches. Bracket progression has the same kind of entry points: SingleElimination{}.Parent(round, number) gives the next match and slot, DoubleElimination{}.Moves(match) gives where the winner and loser go.

Determinism

  • Plan is a pure function of its inputs: the same participants and options produce the same stages, match IDs, slots and links, every time and on every platform. No map iteration order or clock is involved.
  • Match IDs are stable and human-readable (r2-m1, upper-r1-m3, lower-r4-m1, grand-final, swiss-r3-m7, gA-r2-m1, gB-r1-lobby2, finals-matchpoint-4), unique within a stage.
  • Advance depends only on the stage and the finished match. It recomputes standings from the recorded results on every call, so a corrected result is reflected the next time it runs.
  • Swiss is deterministic by default (round 1 pairs adjacent seeds). With Options.Shuffle, participants with identical records are shuffled before pairing using Options.Rand if given, otherwise a PCG generator seeded with (Options.Seed, round). Persist Seed and every round can be reproduced exactly.

Byes

  • Elimination: the bracket is padded to a power of two and the empty slots face the top seeds, so with n participants and bracket size S seeds 1..S-n advance without playing. Bye matches are marked Bye, already Finished, and their winners are placed in round 2.
  • Swiss: with an odd field, the lowest-ranked participant who has not had a bye sits out and is credited a win; a second bye is only given once everyone has had one.
  • Round robin with an odd field: one participant rests each round.
  • Lobbies: groups that do not divide evenly produce partial lobbies (flagged Partial) unless Options.Strict or PartialDeny is set.

Standings and tie-breakers

Standings(stage) (and the Standings returned by Advance) ranks participants by:

  1. Wins, descending (a bye counts as a win).
  2. Losses, ascending.
  3. Score difference (points for minus points against), descending.
  4. Points for, descending.
  5. Seed order, ascending (position in the stage's participant list; anyone first seen in results comes after, in order of appearance).

Only finished matches count. Head-to-head: an empty winner is a draw, each side scores its own Score for and the opponent's against. Lobbies: points for are the slot's Points (or Score if no points were reported), placement 1 is a win and any other placement a loss; without placements the match winner decides. GroupStandings gives one table per group.

Swiss pairing

  • AvoidRematches (default): participants are taken in standings order and each is paired with the highest-ranked opponent they have not met. If that greedy choice would force a rematch later in the round, the search backtracks (depth-first, bounded to about a million candidate checks per round). A rematch only happens when no rematch-free pairing of the round exists, or the budget runs out; then the Greedy pairing is used.
  • Greedy: the same order without backtracking. It never produces a rematch while an unmet opponent is still available for the participant being paired, but it can corner the bottom of the table into a rematch that a different pairing would have avoided.

The bye is chosen before pairing and is not reconsidered.

Errors

Every error wraps a sentinel, so errors.Is works, and carries details for errors.As:

Type Sentinel When
*OptionError ErrInvalidOptions An option is out of range or unknown.
*CountError ErrParticipantCount Too few participants, not a power of two, not a multiple of the group size, or no playable match.
*ParticipantError ErrInvalidParticipant Empty, duplicate or reserved participant ID.
*StageError (wrapped error) A stage of a multi-stage plan failed; Stage names it.
*LinkError ErrMatchNotFound A match or the target of a winner/loser link is missing.
(internal) ErrInvalidStage The stage lacks the data needed to advance.
(internal) ErrInvalidResult Report got a result that cannot apply.
ErrUnknownFormat Registry.Get with an unregistered key.

Performance

Measured with go test -bench . -benchmem -count 6 -benchtime 500ms (Go 1.27.2, linux/amd64, AMD Ryzen 7 5800X3D, pinned to two cores with GOMAXPROCS=2). Cells are the median of 6 runs, ± half the min-max spread. Scenarios are in bench_test.go:

  • Plan/*: plan the first stage for n participants (round_robin_group is one round-robin group of n; ffa_league uses the defaults).
  • Run/*: plan, then report every match one at a time with ReportAt until the stage finishes (Swiss: default rounds, or 12).
  • SwissNextRound/*: one Advance that closes round 6 of 12 and pairs round 7.
  • Standings/*: standings after 6 Swiss rounds.

Advance recomputes standings from the recorded results on every call, so per-report cost grows with the number of matches in the stage; Swiss runs are dominated by that.

Benchmark time/op B/op allocs/op
Plan/single_elimination/n=8 795 ns ±6% 3.72 KiB 12
Plan/single_elimination/n=64 6.65 µs ±2% 28.6 KiB 12
Plan/single_elimination/n=512 54 µs ±3% 226 KiB 12
Plan/single_elimination/n=4096 606 µs ±4% 1.72 MiB 12
Plan/double_elimination/n=8 1.34 µs ±38% 5.62 KiB 12
Plan/double_elimination/n=64 9.57 µs ±8% 47.1 KiB 14
Plan/double_elimination/n=512 86.2 µs ±9% 373 KiB 14
Plan/double_elimination/n=4096 1.06 ms ±11% 2.85 MiB 14
Plan/swiss/n=8 1.05 µs ±3% 4.02 KiB 17
Plan/swiss/n=64 8.13 µs ±7% 28.4 KiB 17
Plan/swiss/n=512 59.6 µs ±21% 220 KiB 17
Plan/swiss/n=4096 639 µs ±4% 1.62 MiB 17
Plan/round_robin_group/n=8 2.67 µs ±2% 12 KiB 15
Plan/round_robin_group/n=64 147 µs ±2% 754 KiB 18
Plan/round_robin_group/n=512 7.79 ms ±14% 46.9 MiB 21
Plan/ffa_league/n=64 11.4 µs ±7% 50.6 KiB 33
Plan/ffa_league/n=512 92.7 µs ±4% 386 KiB 174
Plan/ffa_league/n=4096 985 µs ±6% 2.98 MiB 1967
Run/single_elimination/n=8 1.41 µs ±1% 5.09 KiB 32
Run/single_elimination/n=64 12.4 µs ±6% 41.3 KiB 200
Run/single_elimination/n=512 101 µs ±2% 330 KiB 1544
Run/single_elimination/n=4096 1.01 ms ±6% 2.53 MiB 12296
Run/double_elimination/n=8 2.54 µs ±2% 7.89 KiB 44
Run/double_elimination/n=64 21.8 µs ±6% 69 KiB 326
Run/double_elimination/n=512 191 µs ±12% 552 KiB 2566
Run/double_elimination/n=4096 2.48 ms ±3% 4.26 MiB 20486
Run/swiss/n=8 20.2 µs ±8% 37.8 KiB 205
Run/swiss/n=64 2.57 ms ±6% 4.02 MiB 3882
Run/swiss/n=512 336 ms ±6% 464 MiB 56735
Run/swiss_12_rounds/n=64 7.34 ms ±2% 11.1 MiB 8433
Run/swiss_12_rounds/n=512 528 ms ±6% 751 MiB 78803
Run/swiss_12_rounds_avoid_rematches/n=64 7.35 ms ±3% 11.1 MiB 8433
Run/swiss_12_rounds_avoid_rematches/n=512 531 ms ±2% 751 MiB 78803
SwissNextRound/greedy/n=8 4.07 µs ±2% 6.48 KiB 31
SwissNextRound/avoid_rematches/n=8 4.11 µs ±3% 6.48 KiB 31
SwissNextRound/greedy/n=64 27 µs ±12% 51.5 KiB 38
SwissNextRound/avoid_rematches/n=64 33.7 µs ±21% 51.5 KiB 38
SwissNextRound/greedy/n=512 329 µs ±17% 411 KiB 42
SwissNextRound/avoid_rematches/n=512 273 µs ±17% 411 KiB 42
SwissNextRound/greedy/n=4096 2.69 ms ±3% 3.87 MiB 64
SwissNextRound/avoid_rematches/n=4096 2.68 ms ±3% 3.87 MiB 64
Standings/n=8 2.61 µs ±1% 3.52 KiB 15
Standings/n=64 17.6 µs ±3% 28.3 KiB 20
Standings/n=512 153 µs ±1% 231 KiB 24
Standings/n=4096 1.93 ms ±2% 2.52 MiB 46

License

MIT. See LICENSE.

Documentation

Overview

Package brackets generates and runs tournament formats: single and double elimination brackets, Swiss, round robin, group stages, free-for-all lobby leagues and multi-stage flows. It uses only the Go standard library.

Give a Format participants and Options; it returns Stage values holding a deterministic schedule of Match values. When a match finishes, record the result on the stage and call Format.Advance (or use Report, which validates the result, advances and applies the outcome in one call). Formats are stateless: the stage is plain data you can persist between calls, and every call recomputes what it needs from the stage, so corrected results are picked up on the next call. Callers that keep matches in their own storage can feed them to a Tally one at a time instead of building Stage.Matches.

Formats are registered by key in a Registry; DefaultRegistry holds every built-in format.

Example

A six-player single-elimination bracket: the two top seeds get byes, the higher seed wins every match.

package main

import (
	"fmt"

	"github.com/Aquilabot/brackets"
)

func main() {
	players := []brackets.Participant{
		{ID: "ana", Seed: 1}, {ID: "ben", Seed: 2}, {ID: "cleo", Seed: 3},
		{ID: "dan", Seed: 4}, {ID: "eve", Seed: 5}, {ID: "fay", Seed: 6},
	}
	f := brackets.SingleElimination{}
	stages, err := f.Plan(players, brackets.Options{})
	if err != nil {
		panic(err)
	}
	st := &stages[0]
	for _, m := range st.Matches {
		switch {
		case m.Bye:
			fmt.Printf("%s: bye for %s\n", m.ID, m.Winner)
		case m.Round == 1:
			fmt.Printf("%s: %s vs %s\n", m.ID, m.Home(), m.Away())
		}
	}

	for !st.Finished {
		for i := range st.Matches {
			m := &st.Matches[i]
			if m.Status != brackets.Pending || m.Home() == "" || m.Away() == "" {
				continue
			}
			if _, err := brackets.ReportAt(f, st, i, brackets.Result{Winner: m.Home(), Scores: []int{2, 0}}); err != nil {
				panic(err)
			}
		}
	}
	fmt.Println("champion:", st.Winner)
}
Output:
r1-m1: bye for ana
r1-m2: dan vs eve
r1-m3: bye for ben
r1-m4: cleo vs fay
champion: ana

Index

Examples

Constants

View Source
const (
	KeySingleElimination = "single_elimination"
	KeyDoubleElimination = "double_elimination"
	KeySwiss             = "swiss"
	KeyRoundRobin        = "round_robin"
	KeyGroupStage        = "group_stage"
	KeyFFALeague         = "ffa_league"
	KeyMultiStage        = "multi_stage"
)

Built-in format keys.

View Source
const PlaceholderPrefix = "slot:"

PlaceholderPrefix marks slot references that will be filled by the qualifiers of an earlier stage (see Placeholder). Participant IDs must not start with it.

Variables

View Source
var (
	// ErrUnknownFormat is returned by [Registry.Get] for unregistered keys.
	ErrUnknownFormat = errors.New("brackets: unknown format")
	// ErrInvalidOptions wraps every [*OptionError].
	ErrInvalidOptions = errors.New("brackets: invalid options")
	// ErrParticipantCount wraps every [*CountError].
	ErrParticipantCount = errors.New("brackets: unsupported number of participants")
	// ErrInvalidParticipant wraps every [*ParticipantError].
	ErrInvalidParticipant = errors.New("brackets: invalid participant")
	// ErrMatchNotFound is returned when a match (or the target of a
	// winner/loser link) does not exist in the stage.
	ErrMatchNotFound = errors.New("brackets: match not found")
	// ErrInvalidStage is returned when a stage lacks the data a format needs
	// to advance it.
	ErrInvalidStage = errors.New("brackets: invalid stage")
	// ErrInvalidResult is returned by [Report] for results that cannot be
	// applied to the match.
	ErrInvalidResult = errors.New("brackets: invalid result")
)

Sentinel errors. Every error returned by this package wraps one of them, so callers can branch with errors.Is and inspect details with errors.As.

Functions

func DefaultSwissRounds

func DefaultSwissRounds(n int) int

DefaultSwissRounds returns the default number of Swiss rounds for n participants: ceil(log2(n)), and 1 for n <= 2.

func FillPlaceholders

func FillPlaceholders(st *Stage, source string, qualifiers []string) int

FillPlaceholders replaces the placeholders of st that reference source with the given qualifiers (qualifiers[0] fills "slot:<source>:1"), in participants, groups and match slots. It returns the number of slots filled.

func GroupName

func GroupName(i int) string

GroupName returns the conventional name of the i-th group (0-based): "A".."Z", then "G27", "G28", ...

func GroupStandings

func GroupStandings(st *Stage) [][]Standing

GroupStandings returns one standings table per group of the stage, in group order, ranked within the group.

func IsPlaceholder

func IsPlaceholder(ref string) bool

IsPlaceholder reports whether ref is a placeholder produced by Placeholder rather than a participant ID.

func NextPowerOfTwo

func NextPowerOfTwo(n int) int

NextPowerOfTwo returns the smallest power of two >= n (1 for n <= 1).

func Placeholder

func Placeholder(stage string, rank int) string

Placeholder returns the reference used for the rank-th qualifier (1-based) of the stage with the given key, for example "slot:groups:3".

func Qualifiers

func Qualifiers(st *Stage, n int) []string

Qualifiers returns the IDs of the best n participants of a finished stage by its standings (see Standing); for matchpoint stages the champion comes first. n <= 0 returns every ranked participant.

func SeedOrder

func SeedOrder(size int) []int

SeedOrder returns the seed expected in each first-round slot of a standard elimination bracket of the given power-of-two size. Adjacent slots meet in round 1, and the top seeds cannot meet before the rounds their seeding earns them: for 8 slots it returns [1 8 4 5 2 7 3 6] (quarterfinals 1v8, 4v5, 2v7, 3v6).

Types

type Advance

type Advance struct {
	// Updates fill slots of existing matches (bracket progression).
	Updates []SlotUpdate
	// NewMatches are matches to append to the stage (next Swiss round, next
	// matchpoint game).
	NewMatches []Match
	// Standings are the recomputed standings, for formats that keep them.
	Standings []Standing
	// NewByes are byes granted while scheduling NewMatches.
	NewByes []Bye
	// CurrentRound is the round the stage moved to (0 = unchanged).
	CurrentRound int
	// Scoreboard is the updated matchpoint scoreboard (nil = unchanged).
	Scoreboard map[string]int
	// Winner is set when the stage produced a winner.
	Winner string
	// Finished reports that every match of the stage is resolved (or, for
	// formats that end on a specific match, that the deciding match ended).
	Finished bool
}

Advance is the outcome of a finished match.

func Report

func Report(f Format, st *Stage, matchID string, r Result) (*Advance, error)

Report records r on the match with the given ID, advances the stage with f and applies the outcome. It validates the result: the match must be pending or live with every slot filled, the winner must be one of its participants, and elimination matches need a winner.

func ReportAt

func ReportAt(f Format, st *Stage, i int, r Result) (*Advance, error)

ReportAt is Report for the match at index i of st.Matches; it skips the lookup by ID.

type Bracket

type Bracket uint8

Bracket identifies the side of a double-elimination bracket a match belongs to. Formats without sides leave it as NoBracket.

const (
	NoBracket Bracket = iota
	UpperBracket
	LowerBracket
	GrandFinal
)

func (Bracket) String

func (b Bracket) String() string

type Bye

type Bye struct {
	Round       int
	Participant string
}

Bye records a participant that sat out a round and was credited a win.

type CountError

type CountError struct {
	Format string
	Rule   CountRule
	Need   int
	Got    int
}

CountError reports a participant count the format cannot schedule.

func (*CountError) Error

func (e *CountError) Error() string

func (*CountError) Unwrap

func (e *CountError) Unwrap() error

type CountRule

type CountRule uint8

CountRule is the participant-count constraint a CountError violated.

const (
	// AtLeast: fewer than Need participants.
	AtLeast CountRule = iota
	// PowerOfTwo: the format needs a power-of-two participant count.
	PowerOfTwo
	// MultipleOf: the count must be a multiple of Need (strict groups).
	MultipleOf
	// NoPlayableMatches: the configuration produced no playable match.
	NoPlayableMatches
)

type DoubleElimination

type DoubleElimination struct{}

DoubleElimination plans an upper/lower bracket in the style used by many esports main events: the top half of the seeds starts in the upper bracket, the bottom half starts in the lower bracket, and a participant is eliminated after a loss in the lower bracket.

The participant count must be a power of two and at least 4. With n participants the upper bracket has n/2 participants and log2(n/2) rounds; the lower bracket has 2*log2(n/2) rounds that alternate between "lower winners meet upper losers" (even rounds) and "lower winners meet each other" (odd rounds). Upper-bracket losers of upper round r drop into lower round 2r. The grand final has no game advantage for the upper bracket winner.

Series hints: upper matches and lower rounds 2+ are best-of-3, lower round 1 is best-of-1, the grand final is best-of-5.

Match IDs are "upper-r<round>-m<n>", "lower-r<round>-m<n>" and "grand-final". Match.Round is a schedule column: upper round r is column 2r-1, lower round r is column r, the grand final is column 2*upperRounds+1. The stage finishes when the grand final is reported.

func (DoubleElimination) Advance

func (f DoubleElimination) Advance(st *Stage, finished *Match) (*Advance, error)

Advance sends the winner along WinnerTo and the loser along LoserTo. Targets are resolved by match ID, so the links stored on the matches are the source of truth.

func (DoubleElimination) Key

func (DoubleElimination) Key() string

func (DoubleElimination) Moves

func (DoubleElimination) Moves(finished *Match) Moves

Moves returns the outcome of a finished double-elimination match from the match alone: the winner follows WinnerTo, the loser (if any) follows LoserTo, and a decided grand final ends the stage. A match without a winner moves nobody. Callers that store matches themselves can resolve the target IDs in their own storage instead of building a Stage.

func (DoubleElimination) Plan

func (DoubleElimination) Plan(participants []Participant, opts Options) ([]Stage, error)

func (DoubleElimination) Validate

func (DoubleElimination) Validate(Options) error

type FFALeague

type FFALeague struct{}

FFALeague plans a free-for-all lobby league: participants are split into groups (default 4) that play LobbyRounds (default 4) lobbies of MatchSize (default 4) participants, as in battle-royale or arena-shooter leagues. Results are reported per slot (Placement, Points or Score). The stage keeps standings and finishes, with the top of the standings as winner, once every lobby is resolved.

func (FFALeague) Advance

func (FFALeague) Advance(st *Stage, _ *Match) (*Advance, error)

func (FFALeague) Key

func (FFALeague) Key() string

func (FFALeague) Plan

func (FFALeague) Plan(participants []Participant, opts Options) ([]Stage, error)

func (FFALeague) Validate

func (FFALeague) Validate(opts Options) error

type Format

type Format interface {
	// Key is the stable identifier the format is registered under.
	Key() string

	// Validate checks options without planning anything.
	Validate(opts Options) error

	// Plan seeds the participants and returns the initial stages with their
	// matches. Matches whose participants are not known yet (later bracket
	// rounds, later Swiss rounds, stages fed by earlier stages) have empty
	// or placeholder slots. Byes and phantom matches are resolved up front.
	// The output depends only on the inputs: same participants and options,
	// same stages, byte for byte.
	Plan(participants []Participant, opts Options) ([]Stage, error)

	// Advance is called after a match of stage finished (its Status, Winner
	// and slot results already recorded in stage.Matches, and passed again
	// as finished). It does not modify stage; it returns the effects: slots
	// to fill, new matches to schedule, updated standings and whether the
	// stage is complete. Use [Stage.Apply] to apply them, or [Report] to do
	// all of it in one call.
	Advance(stage *Stage, finished *Match) (*Advance, error)
}

Format is the strategy every competitive structure implements.

Formats are stateless and safe for concurrent use: all state lives in the Stage values they return, which the caller stores and hands back.

type Group

type Group struct {
	Name         string
	Participants []string
}

Group is a pool of participants inside a group stage.

type GroupStage

type GroupStage struct{}

GroupStage splits participants into groups and schedules them.

With MatchSize 2 (default) every group plays a round robin (circle method, Legs times, home/away swapped on even legs); match IDs are "g<group>-r<round>-m<number>". With MatchSize > 2 every group plays LobbyRounds free-for-all lobbies of MatchSize participants, rotating the group order by one position each lobby round; match IDs are "g<group>-r<round>-lobby<number>". Match numbers count across groups within a round.

Groups are filled with Options.Seeding (Snake by default) using ceil(n/GroupSize) groups, so the last groups may be smaller. With Options.Strict the participant count must be a multiple of GroupSize. Lobbies smaller than MatchSize are scheduled (and flagged Partial) unless partial lobbies are disabled; lobbies of fewer than 2 are never scheduled.

The stage finishes when every match is resolved. Advance also returns the overall standings; use GroupStandings for per-group tables.

func (GroupStage) Advance

func (GroupStage) Advance(st *Stage, _ *Match) (*Advance, error)

func (GroupStage) Key

func (GroupStage) Key() string

func (GroupStage) Plan

func (GroupStage) Plan(participants []Participant, opts Options) ([]Stage, error)

func (GroupStage) Validate

func (GroupStage) Validate(opts Options) error
type Link struct {
	Match string // target match ID
	Slot  int    // 0 = home, 1 = away
	// Index is a hint: the position of the target in Stage.Matches when the
	// link was created. It is verified against Match before use, so a stale
	// or zero hint only costs a lookup by ID.
	Index int
}

Link points at a slot of another match: where the winner (or the loser) of a match goes next.

type LinkError

type LinkError struct {
	From   string // finished match ID
	Target string // target match ID, when known
	Round  int    // target round, for formats that address matches by round/number
	Number int
}

LinkError reports that the match a result should flow into is missing.

func (*LinkError) Error

func (e *LinkError) Error() string

func (*LinkError) Unwrap

func (e *LinkError) Unwrap() error

type Match

type Match struct {
	ID     string
	Round  int // schedule column; 1-based
	Number int // 1-based position within the round
	Slots  []Slot
	// Winner is the winning participant ID. An empty winner on a finished
	// head-to-head match is a draw.
	Winner string
	Status Status

	// Bracket structure (elimination formats).
	Bracket     Bracket
	Elimination bool // losing this match eliminates the loser
	// Bye is set on matches resolved at planning time because one side was
	// empty; Phantom on matches cancelled because both sides were empty.
	Bye     bool
	Phantom bool
	Partial bool // lobby with fewer participants than the configured size

	BracketRound int // round inside Bracket (upper/lower are counted separately)
	BestOf       int // series length hint (1, 3, 5); 0 = unspecified
	WinnerTo     *Link
	LoserTo      *Link
	// From lists the IDs of the matches whose results fill this match.
	From []string

	// Group structure (group stages, round robin, lobby leagues).
	Group      string // group name ("A", "B", ...); "" when not grouped
	GroupIndex int
	Leg        int // 1-based leg for multi-leg round robins
	LobbyRound int // 1-based lobby round for free-for-all lobbies
	Size       int // configured participants per match (lobby size)
}

Match is a single game or series between the participants in Slots.

Match IDs are deterministic and unique within a stage: the same input always yields the same IDs, so callers can use them as stable keys.

func (*Match) Away

func (m *Match) Away() string

Away returns the participant in slot 1, or "".

func (*Match) Has

func (m *Match) Has(id string) bool

Has reports whether id occupies one of the match slots.

func (*Match) Home

func (m *Match) Home() string

Home returns the participant in slot 0, or "".

func (*Match) Loser

func (m *Match) Loser() string

Loser returns the first slot participant that is not the winner, or "" when the match has no winner.

func (*Match) LoserSlot

func (m *Match) LoserSlot() int

LoserSlot returns the index of the slot Loser reads, or -1.

type Moves

type Moves struct {
	Winner, Loser     string
	LoserSlot         int   // index of the loser in the match slots, or -1
	WinnerTo, LoserTo *Link // nil when that participant goes nowhere
	Final             bool  // the match decided the stage
}

Moves describes where the participants of a finished match go next.

type MultiStage

type MultiStage struct{}

MultiStage chains stages described by Options.Stages. Each stage either takes every participant or, when StageSpec.Source names an earlier stage, StageSpec.Take placeholders ("slot:<source>:<rank>", see Placeholder) to be replaced by that stage's qualifiers once it finishes (see FillPlaceholders and Qualifiers).

Stage types:

  • "group_stage": a GroupStage (round robin groups or lobbies).
  • "matchpoint": a single lobby of up to MatchSize participants (default all) that replays games until someone has won TargetPoints games (default 1). Each finished game without a champion schedules the next one ("<key>-matchpoint-<round>"); the first is "<key>-matchpoint".

Advance dispatches on Stage.Type; group stages finish when every match is resolved, matchpoint stages when the champion is decided.

func (MultiStage) Advance

func (MultiStage) Advance(st *Stage, finished *Match) (*Advance, error)

func (MultiStage) Key

func (MultiStage) Key() string

func (MultiStage) Plan

func (MultiStage) Plan(participants []Participant, opts Options) ([]Stage, error)

func (MultiStage) Validate

func (MultiStage) Validate(opts Options) error

type OptionError

type OptionError struct {
	Format string // format key
	Field  string // Options field (or "Stages[i].Field")
	Reason string
}

OptionError reports an invalid Options field.

func (*OptionError) Error

func (e *OptionError) Error() string

func (*OptionError) Unwrap

func (e *OptionError) Unwrap() error

type Options

type Options struct {
	// Name labels the planned stage. Defaults to a per-format name.
	Name string

	// Rounds is the number of Swiss rounds. Default: ceil(log2(n)), 1 for
	// two participants.
	Rounds int
	// Pairing selects the Swiss pairing algorithm. Default AvoidRematches.
	Pairing Pairing
	// Shuffle randomizes the order of Swiss participants whose records are
	// identical before each round is paired (including round 1, which is
	// otherwise paired 1v2, 3v4, ... by seed). Randomness comes from Rand,
	// or from a PCG generator seeded with (Seed, round) when Rand is nil,
	// so a persisted Seed reproduces every round exactly.
	Shuffle bool
	Seed    uint64
	Rand    rand.Source

	// Legs is how many times each pair meets in round robin and group
	// stages (home and away swap on even legs). Default 1.
	Legs int
	// GroupSize is the number of participants per group. Default 4.
	GroupSize int
	// MatchSize is the number of participants per match: 2 for head-to-head
	// round robin groups, more for free-for-all lobbies. Default 2 for
	// group stages, 4 for FFA leagues, every participant for matchpoint.
	MatchSize int
	// LobbyRounds is the number of lobby rounds when MatchSize > 2.
	// Default 1 for group stages, 4 for FFA leagues.
	LobbyRounds int
	// Seeding distributes participants into groups. Default Snake.
	Seeding Seeding
	// Strict requires the participant count to be a multiple of GroupSize.
	Strict bool
	// Partial controls partial lobbies. Default PartialAuto.
	Partial PartialPolicy
	// Advances and AdvancesPerGroup are informational qualification counts
	// carried on the stage for callers that chain stages.
	Advances         int
	AdvancesPerGroup int

	// TargetPoints is the number of game wins a participant needs to take a
	// matchpoint stage. Default 1.
	TargetPoints int

	// Stages describes the stages of a multi-stage format, in order.
	Stages []StageSpec
}

Options configures a format. Every field is optional; zero values select the documented defaults and fields a format does not use are ignored.

type Pairing

type Pairing uint8

Pairing selects the Swiss pairing algorithm.

const (
	// AvoidRematches (default) pairs participants in standings order, each
	// with the highest-ranked opponent they have not met, and backtracks
	// when that greedy choice would force a rematch later in the round.
	// A rematch is only produced when no rematch-free pairing exists for the
	// round (or the search budget is exhausted), in which case the result is
	// the Greedy pairing.
	AvoidRematches Pairing = iota
	// Greedy pairs the best remaining participant with the best remaining
	// opponent they have not met yet, or with the next participant in the
	// standings if they have met everyone left. Never backtracks, so it can
	// produce avoidable rematches near the bottom of the standings.
	Greedy
)

type PartialPolicy

type PartialPolicy uint8

PartialPolicy controls whether lobbies with fewer participants than Options.MatchSize are scheduled.

const (
	// PartialAuto allows partial lobbies unless Options.Strict is set.
	PartialAuto PartialPolicy = iota
	PartialAllow
	PartialDeny
)

type Participant

type Participant struct {
	ID   string
	Seed int
}

Participant is an entrant of a tournament: a player, a team, a roster.

Seed ranks the participant before play starts. Seeds greater than zero are ordered ascending (1 is the strongest); a Seed of zero or less means "unseeded" and those participants are placed after every seeded one, in the order they were given. Ties keep input order.

func SortParticipants

func SortParticipants(ps []Participant) []Participant

SortParticipants returns a copy of ps in seed order: seeds > 0 ascending, then unseeded participants (Seed <= 0). The sort is stable, so equal seeds and unseeded participants keep their input order.

type ParticipantError

type ParticipantError struct {
	ID     string
	Reason string // "empty id", "duplicate id", "reserved placeholder prefix"
}

ParticipantError reports an invalid participant ID.

func (*ParticipantError) Error

func (e *ParticipantError) Error() string

func (*ParticipantError) Unwrap

func (e *ParticipantError) Unwrap() error

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry maps format keys to formats. The zero value is not usable; use NewRegistry or DefaultRegistry.

Example
package main

import (
	"fmt"

	"github.com/Aquilabot/brackets"
)

func main() {
	reg := brackets.DefaultRegistry()
	f, err := reg.Get("double_elimination")
	if err != nil {
		panic(err)
	}
	stages, _ := f.Plan([]brackets.Participant{{ID: "a"}, {ID: "b"}, {ID: "c"}, {ID: "d"}}, brackets.Options{})
	for _, m := range stages[0].Matches {
		fmt.Println(m.ID, m.Bracket, "best of", m.BestOf)
	}
	fmt.Println(reg.Keys())
}
Output:
upper-r1-m1 upper best of 3
lower-r1-m1 lower best of 1
lower-r2-m1 lower best of 3
grand-final grand_final best of 5
[double_elimination ffa_league group_stage multi_stage round_robin single_elimination swiss]

func DefaultRegistry

func DefaultRegistry() *Registry

DefaultRegistry returns a new registry with every built-in format.

func NewRegistry

func NewRegistry(formats ...Format) *Registry

NewRegistry returns a registry with the given formats. Later formats replace earlier ones with the same key.

func (*Registry) Get

func (r *Registry) Get(key string) (Format, error)

Get returns the format registered under key.

func (*Registry) Keys

func (r *Registry) Keys() []string

Keys returns the registered keys, sorted.

func (*Registry) Register

func (r *Registry) Register(key string, f Format)

Register adds f under key (or under f.Key() when key is empty), replacing any previous format with that key.

type Result

type Result struct {
	// Winner is the winning participant ID. Leave it empty for a draw
	// (head-to-head formats without elimination) or when lobby placements
	// decide the result.
	Winner string
	// Scores, Points and Placements are per slot, in slot order. Any of
	// them may be nil. A non-nil Points marks every slot as having points.
	Scores     []int
	Points     []int
	Placements []int
}

Result is the outcome of a match, reported through Report.

type RoundRobin

type RoundRobin struct{}

RoundRobin plans a single group in which every participant meets every other participant Legs times (home and away swap on even legs). With an odd count one participant rests each round. Match IDs are "gA-r<round>-m<n>". Standings are kept on the stage; when every match is resolved the stage finishes and the top of the standings is the winner.

func (RoundRobin) Advance

func (RoundRobin) Advance(st *Stage, _ *Match) (*Advance, error)

func (RoundRobin) Key

func (RoundRobin) Key() string

func (RoundRobin) Plan

func (f RoundRobin) Plan(participants []Participant, opts Options) ([]Stage, error)

func (RoundRobin) Validate

func (RoundRobin) Validate(opts Options) error

type Seeding

type Seeding uint8

Seeding controls how participants are distributed into groups.

const (
	// Snake distributes seeds A,B,C,C,B,A,A,B,C,... so every group gets a
	// comparable mix of strong and weak seeds. Default.
	Snake Seeding = iota
	// Sequential fills group A first, then B, and so on.
	Sequential
)

func (Seeding) String

func (s Seeding) String() string

type SingleElimination

type SingleElimination struct{}

SingleElimination plans one knockout bracket.

The bracket size is the next power of two. Participants are placed with the standard seed order (SeedOrder), so the empty slots (byes) always face the top seeds: with n participants and size S, seeds 1..S-n advance to round 2 without playing. Bye matches are Finished with Bye set at planning time and their winners are already placed in round 2.

Match IDs are "r<round>-m<number>". The stage finishes when every match is resolved; the winner of the final is reported as the stage winner.

func (SingleElimination) Advance

func (SingleElimination) Advance(st *Stage, finished *Match) (*Advance, error)

func (SingleElimination) Key

func (SingleElimination) Key() string

func (SingleElimination) Parent

func (SingleElimination) Parent(round, number int) (nextRound, nextNumber, slot int)

Parent returns where the winner of match (round, number) plays next: the first match at (round+1, ceil(number/2)), home slot for odd numbers and away slot for even ones. Callers that store matches themselves can use it to advance a bracket without building a Stage.

func (SingleElimination) Plan

func (SingleElimination) Plan(participants []Participant, opts Options) ([]Stage, error)

func (SingleElimination) Validate

func (SingleElimination) Validate(Options) error

type Slot

type Slot struct {
	// ID is the participant occupying the slot, or "" while unresolved.
	ID string
	// Placeholder is set instead of ID when the seat will be filled by a
	// qualifier from an earlier stage.
	Placeholder string

	// Score is the raw score of the slot: maps/goals/rounds in head-to-head
	// matches, an arbitrary score in lobbies.
	Score int
	// Points are ranking points earned in a lobby. When HasPoints is set,
	// standings use Points instead of Score as "points for".
	Points    int
	HasPoints bool
	// Placement is the 1-based finishing position in a lobby (0 = not
	// reported). Placement 1 counts as a win, any other placement as a loss.
	Placement int
}

Slot is one seat in a match. Head-to-head matches have exactly two slots (index 0 is "home", index 1 is "away"); free-for-all lobbies have one slot per participant.

func (Slot) Ref

func (s Slot) Ref() string

Ref returns the participant ID, or the placeholder when unresolved.

type SlotUpdate

type SlotUpdate struct {
	Index       int    // index of the match in Stage.Matches
	Match       string // ID of the match
	Slot        int    // 0 = home, 1 = away
	Participant string
}

SlotUpdate places a participant into a slot of an existing match.

type Stage

type Stage struct {
	Key    string // stage key inside a multi-stage plan; "" otherwise
	Name   string
	Format string // key of the format that planned the stage
	Type   string // structural kind: "bracket", "double_elimination", "swiss", "round_robin", "group", "ffa_league", "matchpoint"
	Order  int    // 0-based position inside the plan

	Options      Options
	Participants []string // participant IDs (or placeholders) in seed order
	Groups       []Group
	Matches      []Match

	Rounds       int // total rounds when known up front (0 = derive from matches)
	CurrentRound int
	Size         int   // bracket size (elimination formats)
	SeedOrder    []int // seed expected in each first-round slot (elimination formats)
	UpperSize    int   // double elimination: participants starting in the upper bracket
	UpperRounds  int
	LowerRounds  int

	// Source and SourceCount are set when the stage is fed by the
	// SourceCount best participants of an earlier stage.
	Source      string
	SourceCount int

	Byes       []Bye
	Standings  []Standing
	Scoreboard map[string]int // matchpoint: games won per participant
	Winner     string
	Finished   bool
}

Stage is one phase of a tournament: its configuration, its participants and its matches. A Stage is plain data; persist it however you like and pass it back to Format.Advance when a match finishes.

func (*Stage) Apply

func (s *Stage) Apply(a *Advance)

Apply applies the outcome of Format.Advance to the stage: fills slots, appends new matches, records byes, standings, scoreboard and winner.

func (*Stage) Match

func (s *Stage) Match(id string) *Match

Match returns a pointer to the match with the given ID, or nil.

type StageError

type StageError struct {
	Stage string // stage key
	Err   error
}

StageError wraps an error raised while planning one stage of a multi-stage format.

func (*StageError) Error

func (e *StageError) Error() string

func (*StageError) Unwrap

func (e *StageError) Unwrap() error

type StageSpec

type StageSpec struct {
	// Key identifies the stage; later stages reference it in Source.
	Key string
	// Name labels the stage. Defaults to Options.Name, then Key.
	Name string
	// Type is "group_stage" or "matchpoint".
	Type string
	// Source is the key of an earlier stage whose top Take participants
	// fill this stage through placeholders. Empty means "every participant".
	Source string
	Take   int
	// Options configures the stage (group sizes, lobby rounds, target
	// points, ...).
	Options Options
}

StageSpec describes one stage of a MultiStage format.

type Standing

type Standing struct {
	Rank          int
	ID            string
	Played        int
	Wins          int
	Losses        int
	Draws         int
	Byes          int
	PointsFor     int
	PointsAgainst int
	Order         int
}

Standing is one row of a standings table.

Ranking (tie-breakers in order):

  1. Wins, descending. A bye counts as a win.
  2. Losses, ascending.
  3. Score difference (PointsFor - PointsAgainst), descending.
  4. PointsFor, descending.
  5. Order, ascending: the participant's position in the stage's seed order (participants first seen in match results come after, in order of appearance). This makes the table total and deterministic.

Only Finished matches count. In head-to-head matches (two slots) a finished match with an empty Winner is a draw for both sides, PointsFor is the slot's own Score and PointsAgainst the opponent's Score. In lobbies (any other slot count) PointsFor adds the slot's Points (or Score when no points were reported), Placement 1 is a win and any other reported placement a loss; without a placement the match Winner decides.

func Standings

func Standings(st *Stage) []Standing

Standings computes the standings of a stage from its participants, byes and finished matches. See Standing for the tie-breakers.

func (*Standing) Diff

func (s *Standing) Diff() int

Diff returns PointsFor - PointsAgainst.

type Status

type Status uint8

Status is the lifecycle state of a match.

const (
	// Pending matches have not started. Unresolved slots may still be empty.
	Pending Status = iota
	// Live matches are being played.
	Live
	// Finished matches have a result (a winner, or a draw when Winner is "").
	Finished
	// Cancelled matches will never be played (for example a phantom bracket
	// match whose two feeders were both byes).
	Cancelled
)

func (Status) Resolved

func (s Status) Resolved() bool

Resolved reports whether the match no longer blocks progression.

func (Status) String

func (s Status) String() string

type Swiss

type Swiss struct{}

Swiss plans a Swiss-system stage: a fixed number of rounds in which participants with similar records meet, without eliminations.

Round 1 pairs adjacent seeds (1v2, 3v4, ...) unless Options.Shuffle is set. Every later round is generated when all matches of the current round are resolved: participants are ordered by the current standings (see Standing) and paired with Options.Pairing (AvoidRematches by default).

Byes: with an odd number of participants the lowest-ranked participant that has not had a bye yet sits out and is credited a win (if everyone already had one, the lowest-ranked participant gets another).

Match IDs are "swiss-r<round>-m<number>". The stage finishes when the last round is resolved, or earlier if no pairing is possible; the winner is the top of the standings.

Example

Swiss rounds are generated one at a time, as soon as the previous round is complete.

package main

import (
	"fmt"

	"github.com/Aquilabot/brackets"
)

func main() {
	f := brackets.Swiss{}
	stages, err := f.Plan([]brackets.Participant{
		{ID: "ana", Seed: 1}, {ID: "ben", Seed: 2}, {ID: "cleo", Seed: 3},
		{ID: "dan", Seed: 4}, {ID: "eve", Seed: 5},
	}, brackets.Options{Rounds: 3})
	if err != nil {
		panic(err)
	}
	st := &stages[0]
	for i := 0; i < len(st.Matches); i++ { // new rounds are appended
		m := &st.Matches[i]
		if _, err := brackets.ReportAt(f, st, i, brackets.Result{Winner: m.Away()}); err != nil {
			panic(err)
		}
	}
	for _, b := range st.Byes {
		fmt.Printf("round %d bye: %s\n", b.Round, b.Participant)
	}
	for _, s := range st.Standings[:3] {
		fmt.Printf("%d. %s %d-%d\n", s.Rank, s.ID, s.Wins, s.Losses)
	}
}
Output:
round 1 bye: eve
round 2 bye: cleo
round 3 bye: ben
1. ana 2-1
2. ben 2-1
3. dan 2-1

func (Swiss) Advance

func (f Swiss) Advance(st *Stage, _ *Match) (*Advance, error)

func (Swiss) AdvanceTally

func (Swiss) AdvanceTally(st *Stage, t *Tally) (*Advance, error)

AdvanceTally is Swiss.Advance for callers that keep matches in their own storage: the stage's matches are read from t (fed with every match of the stage) instead of st.Matches. st supplies the configuration: Participants, Rounds, CurrentRound and Options. AdvanceTally finalizes t.

func (Swiss) Key

func (Swiss) Key() string

func (Swiss) Plan

func (f Swiss) Plan(participants []Participant, opts Options) ([]Stage, error)

func (Swiss) Validate

func (Swiss) Validate(opts Options) error

type Tally

type Tally struct {
	// contains filtered or unexported fields
}

Tally accumulates a stage's matches one at a time: standings, the pairs that already met, and round completion. It lets callers that keep matches in their own storage advance a stage without first copying every match into Stage.Matches: build the tally, feed each stored match through a reused Match value with Add, then read Standings or call Swiss.AdvanceTally.

The result is identical to the stage-based functions over the same matches in the same order (Standings, Swiss.Advance).

func NewTally

func NewTally(participants []string, byes []Bye) *Tally

NewTally starts a tally over the stage's participants (in seed order) and the byes granted so far.

func (*Tally) Add

func (t *Tally) Add(m *Match)

Add records one match. The tally keeps no reference to m or its slots, so callers can reuse the same Match value for every call.

func (*Tally) AllResolved

func (t *Tally) AllResolved() bool

AllResolved reports whether no added match is pending or live.

func (*Tally) Grow

func (t *Tally) Grow(n int)

Grow reserves room for n more matches. It is optional and only saves reallocations when the number of matches is known up front.

func (*Tally) MaxRound

func (t *Tally) MaxRound() int

MaxRound returns the highest round number added (0 if none).

func (*Tally) RoundResolved

func (t *Tally) RoundResolved(r int) bool

RoundResolved reports whether round r has matches and all are resolved.

func (*Tally) Standings

func (t *Tally) Standings() []Standing

Standings returns the ranked standings (see Standing). It finalizes the tally: do not call Add, Standings or AdvanceTally on it afterwards.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL