A tournament engine for goatformat.com
goatformat.com is the home of GOAT Format, retro Yu-Gi-Oh played on the 2005 card pool.1 Its weekly tournaments run over Discord: a bot takes signups and results.
For six years the brackets lived on Challonge, a hosted bracket service.2 Players went there for pairings and standings, and the bot and our database worked around it. I replaced it with our own tournament engine. At its core is the bracket engine: a TypeScript package that turns reported results into pairings, brackets, and standings, verified against captured Challonge brackets with 4,440 playouts.
What was wrong with Challonge
Before the engine, Challonge hosted the bracket and our database held the players and results.
- Challonge can't compute Mantis 2.0, the tiebreaker our scene uses to rank tied records by opponent strength3, so standings were wrong.
- Bracket on Challonge, everything else in our database: the two disagreed constantly, and reconciling them was our top source of bugs.
- Every Challonge plan caps monthly API requests.4 Nothing pushes, so the bot polls, and polling alone eats the cap.
Engine design
A tournament format is a procedure: given seeds and results, its rules determine the pairings, the bracket, and the standings. FIDE publishes Swiss pairing as exactly that5, and Mantis 2.0 is a formula over results. The bracket engine implements the procedures directly, as pure functions: tournament state is a function of results.
Why that's the right design:
- Nothing can drift. There is no second copy of bracket state; every read derives from the results.
- Corrections are free. Late reports, admin fixes, and drops are normal in a Discord-run scene: fix the result and everything downstream re-derives.
- The industry's own interfaces agree. Reporting a match on start.gg or Challonge sends winner and score and reads back derived state6. The engine is that contract as a function.
- A function can be replayed. That's what makes the verification below possible.
How the engine works
The bracket engine operates on one phase at a time. Three phase types cover everything the scene plays: Swiss, single elimination, double elimination.
Three functions cover a phase: generate the opening matches from seeds, advance the phase by one reported result, compute standings from the match list. A tournament is a chain of phases, and the app feeds one phase's standings in as the next phase's seeds. The engine only ever sees one phase.
Single elimination
Lose once, out. Two things have to be right before a card is played:
- Seeds placed so the top two can only meet in the final.7
- When the field doesn't fill the bracket, the free wins go to the top seeds.
The engine settles both at generation, so the bracket opens already correct.
Double elimination
Lose once and you drop to a second bracket; lose twice, out.8 Three things had to be right:
- Where losers land. A player must not immediately rematch whoever just beat them, so each round's losers drop into the lower bracket by a fixed rotation.
- Byes with no loser. A bye sends nobody down, so a lower match fed only by byes never gets anyone, and the player waiting on it waits forever. The engine marks those matches dead and hands out the bye. Until it did, 28 of the 63 player counts up to 64 could not finish.
- The grand final. The lower-bracket survivor can force a reset match by winning once; the engine wires that as bracket structure, not a special case.
Swiss pairing
Nobody is eliminated. Each round you play someone on your record; after a set number of rounds the standings decide.9 A round is only right if it holds for everyone at once:
- Same record where possible.
- Never the same opponent twice.
- The bye goes to the lowest record without one.
Pairing players one at a time breaks those rules for whoever comes last, so the engine solves the whole field as one matching problem with the blossom algorithm10. When drops leave no clean round it still returns the closest one.5 The matching is an Edmonds blossom port.11
Verification against Challonge
Challonge is the source of truth for bracket wiring: capture it once, then replay thousands of tournaments through both systems and diff every result.
- Capture once. 252 real Challonge bracket topologies, ~1,250 API calls.
- Replay offline. 4,440 differential playouts against an emulator built from the captured wiring. 0 unexplained mismatches.
- Live samples. 15 elimination tournaments compared result by result, 15 Swiss checked structurally. 0 divergences in champion or podium.
- Historical replay. 192 real brackets, 2019-2025, up to 266 players. 0 violations.
Replay caught what tests missed: the double-bye deadlock above, a 2-player bracket that froze half the time, dead code that granted repeat byes. One known difference: at 9+ players, losers-bracket pairings diverge from Challonge (different rotation tables, both valid). Champion and podium always match.
The engine in production
The bot and the site call the same in-process API; no separate tournament service. It loads match rows, runs the bracket engine, writes rows back under a per-phase lock.
The open-source bracket engine
The bracket engine ships on its own, with a playground: github.com/nedhmn/bracket-engine. Pick a player count, report results, watch byes propagate. It pairs and advances one phase; phases, participants, and storage are the app's job. No runtime dependencies, no database required, typed end to end.
References
-
About Goat Format: the format, the community, and the events it runs. ↩
-
Challonge: hosted brackets with a REST API; the scene ran on it from 2019 to 2025. ↩
-
Duelist Codex, "Tie-breakers": the Mantis tiebreaker system as Upper Deck ran it. ↩
-
Challonge API docs: plan-based monthly request caps; hobbyist tier is 10,000 requests per month. ↩
-
FIDE Handbook C.04.3, the Dutch System: Swiss pairing specified as a deterministic procedure over scores and pairing history. ↩ ↩2
-
start.gg's reportBracketSet mutation and Challonge's match update both take winner and score, nothing else. ↩
-
Seeding on Wikipedia: the standard fold, 1 against 8, 4 against 5, 2 against 7, 3 against 6 in an eight-slot bracket. Challonge and start.gg place seeds the same way. ↩
-
Drarig29/brackets-manager.js: the double-elimination routing the engine ports. For 2^k players: k winners rounds, 2(k-1) losers rounds, drops placed by per-size rotation tables so nobody immediately rematches the player who beat them, and a grand final with no match, one match, or a reset. ↩
-
dambrisco/swiss-pairing: Swiss pairing on top of edmonds-blossom. ↩
-
Blossom algorithm: Edmonds' maximum matching over general graphs, 1965. ↩
-
Joris van Rantwijk's maximum matching: the reference implementation, via Matt Krick's edmonds-blossom JS port. Each pairing weighs minus (standing difference squared plus 1,000,000 times prior meetings); a bye edge costs the player's standing plus 1,000,000 per bye already taken. ↩