Skip to contents

This guide is a function-discovery map for tournament operations. Every public function is shown in context so you know what it does, when to call it, and what you get back.


Mental model

A tournament definition reads like a rulebook. Stage-type functions (round_robin, single_elim, swiss, …) are the verbs — pipe them onto tournament() or spec() to build the graph.

A tournament runtime feels like a scoreboard. Noun functions (matches, standings, stage_status, winner, …) expose live state without mutating it.


1) Define

tournament() and spec()

tournament(participants) is the entry point for a live runtime. It returns a tournament object that you pipe stage verbs onto.

spec() is the entry point for a reusable blueprint without participants. It returns a bracketeer_spec object; use build(spec, participants) to materialize it later.

teams <- paste("Team", LETTERS[1:16])

# ── Live tournament ───────────────────────────────────────────────────────────
trn <- tournament(teams) |>
  round_robin("groups")

# ── Blueprint / reusable spec ─────────────────────────────────────────────────
my_spec <- spec() |>
  round_robin("groups") |>
  single_elim("finals", take = top_n(8))

tournament() and spec() accept the same stage verbs. The difference is that tournament() materializes the first stage immediately while spec() defers everything to build().

Stage verbs

Each stage verb adds one node to the tournament graph. The verb name is the format; the first argument is a unique stage ID.

Verb Format
round_robin(id, ...) All-play-all
single_elim(id, ...) Single-elimination bracket
double_elim(id, ...) Double-elimination bracket
swiss(id, ...) Swiss-system
two_leg(id, ...) Two-leg knockout
group_stage_knockout(id, ...) Combined group + knockout

Every verb accepts two wiring parameters:

  • from — source stage ID (default previous_stage()).
  • take — routing selector (default: all participants from source).

from = and previous_stage()

In a linear chain, from defaults to the immediately preceding stage. You never write it explicitly.

# These two are identical:
tournament(teams) |>
  round_robin("groups") |>
  single_elim("finals", take = top_n(8))

tournament(teams) |>
  round_robin("groups") |>
  single_elim("finals", from = previous_stage(), take = top_n(8))

For branching — two stages reading from the same source — you must name from explicitly:

trn <- tournament(teams) |>
  round_robin("groups") |>
  single_elim("championship", from = "groups", take = top_n(8)) |>
  single_elim("consolation",  from = "groups", take = remaining())

Routing helpers (take =)

Pass a selector to take = to control who advances from the source stage.

top_n(8)               # top 8 by overall standings
bottom_n(4)            # bottom 4
slice_range(5, 8)      # positions 5 through 8
remaining()            # everyone not already consumed by a prior transition
losers()               # eliminated participants
filter_by(function(df, ...) df[df$points >= 6, ])  # custom predicate

# ── Per-group variants (require grouped source stage) ─────────────────────────
top_per_group(2)       # top 2 from each group
bottom_per_group(1)    # bottom 1 from each group
slice_per_group(3, 3)  # 3rd place from each group

2) Validate (spec path)

validate(spec, n) runs preflight feasibility checks before materializing anything.

my_spec <- spec() |>
  round_robin("groups") |>
  single_elim("finals", take = top_n(8))

validate(my_spec, n = 16)   # passes
validate(my_spec, n = 6)    # fails — can't select top 8 from 6 teams

Errors are identifier-rich: they include stage IDs and participant counts. build() implicitly calls validate() before materializing.


3) Build (spec path)

my_spec <- spec() |>
  round_robin("groups") |>
  single_elim("finals", take = top_n(8))

trn <- my_spec |> build(teams)

build() materializes the first stage immediately and leaves downstream stages in "blocked" state until transitions resolve.


4) Enter results

result() — single match

trn <- trn |> result("groups", match = 1, score = c(2, 1))
  • stage — stage ID.
  • match — match ID from matches().
  • score — length-2+ numeric vector. For best-of series, a per-game score vector (e.g. c(3, 1, 2, 0, 3) for a 5-game series).

results() — batch from data frame

results_df <- data.frame(
  match  = 1:4,
  score1 = c(2, 1, 3, 0),
  score2 = c(1, 0, 2, 1)
)

trn <- trn |> results("groups", results_df)

Columns must be match, score1, score2. Batch entry triggers auto-advance when the final result completes the stage.

Auto-advance

By default, entering the last match in a stage automatically materializes all downstream stages — no explicit advance() call needed.

# All group matches entered → finals stage materializes automatically.
group_ms <- matches(trn, "groups")

for (i in seq_len(nrow(group_ms))) {
  trn <- trn |> result("groups", match = group_ms$id[i], score = c(1, 0))
}

stage_status(trn)  # finals is now "active"

Manual advance (opt-in)

trn <- tournament(teams, auto_advance = FALSE) |>
  round_robin("groups") |>
  single_elim("finals", take = top_n(8))

# ... enter results ...

trn <- trn |> advance("groups")  # explicit trigger

Overwrite policy

  • Overwriting a result before downstream stages materialize is allowed; standings recalculate automatically.
  • Overwriting a result after downstream materialization is blocked with an error that identifies the blocking downstream stage ID.
  • Use teardown() to un-materialize downstream stages and unlock the upstream result for editing.

5) Inspect state

All inspection functions return plain data frames or scalar values.

# ── Print ───────────────────────────────────────────────────────────────────
trn
# Tournament [2 stages]
#   groups   in_progress   90/120 matches
#   finals   blocked

# ── Stage overview ──────────────────────────────────────────────────────────
stage_status(trn)
#   stage    status        complete  total  materialized
#   groups   in_progress         90    120          TRUE
#   finals   blocked              0      0         FALSE

# ── Matches ─────────────────────────────────────────────────────────────────
matches(trn)                           # all stages, pending (default)
matches(trn, "groups")                 # one stage, pending
matches(trn, "groups", status = "all") # one stage, all

# ── Standings ───────────────────────────────────────────────────────────────
standings(trn, "groups")   # one stage
standings(trn)              # all stages

# ── Outcomes ────────────────────────────────────────────────────────────────
winner(trn)       # tournament winner (NA until complete)
rankings(trn)     # final placement table
routing_log(trn)  # transition audit trail

6) Teardown

teardown(trn, stage) un-materializes a stage and all downstream dependents, cascading along the DAG. Source-stage results and standings are preserved.

trn <- tournament(teams) |>
  swiss("open", rounds = 5) |>
  single_elim("top_cut", take = top_n(8))

# ... enter all open results, top_cut materializes automatically ...

# Decide to revise an open result:
trn <- teardown(trn, "top_cut")     # un-materializes top_cut
trn <- result(trn, "open", match = 3, score = c(0, 2))  # overwrite now allowed
# top_cut will re-materialize when open is completed again.

After teardown, torn-down stages revert to "blocked" in stage_status().


7) Full lifecycle example

library(bracketeer)

teams <- paste("Team", LETTERS[1:16])

# ── Define ────────────────────────────────────────────────────────────────────
trn <- tournament(teams) |>
  swiss("open", rounds = 5) |>
  single_elim("top_cut", take = top_n(8))

# ── Enter results ─────────────────────────────────────────────────────────────
open_ms <- matches(trn, "open")

for (i in seq_len(nrow(open_ms))) {
  trn <- trn |> result("open", match = open_ms$id[i], score = c(1, 0))
}

# top_cut auto-materialized after final open result.

# ── Inspect ───────────────────────────────────────────────────────────────────
stage_status(trn)
matches(trn, "top_cut")
standings(trn, "open")

# ── Run top_cut ───────────────────────────────────────────────────────────────
cut_ms <- matches(trn, "top_cut")

for (i in seq_len(nrow(cut_ms))) {
  trn <- trn |> result("top_cut", match = cut_ms$id[i], score = c(1, 0))
}

# ── Outcomes ──────────────────────────────────────────────────────────────────
winner(trn)
rankings(trn)
routing_log(trn)