Swiss pairings: the algorithm behind a tournament's fairness

Running a tournament with 40 players poses a pairing problem before it even poses a chess problem. A round-robin (everyone plays everyone) needs 39 rounds. A single-elimination bracket knocks out half the field after a single game, often lost to bad luck more than skill level. The Swiss system, implemented in kotlin-chess-tournament and used by EloChessPlanner, solves that exact problem: keep everyone playing, in a reasonable number of rounds, while pairing players of comparable strength.
The principle: pairing by score group
After round one, players are split into groups by their cumulative score (players with 1 point, those with 0.5, those with 0). Pairing happens within each group, never between players with very different scores: that guarantees that mid-tournament, every game pits players with a comparable track record so far.
Score 3 ─┬─ Player A ── Player D
└─ Player B ── Player C
Score 2.5 ─┬─ Player E ── Player H
└─ Player F ── Player G
The Dutch method, in detail
Within each score group, the Dutch method (the most widespread, implemented by bbpPairings, which kotlin-chess-tournament references as a C++/Pascal alternative) follows a precise sequence:
- Sort the group by score, then by Elo rating (initial tiebreak).
- Split into two halves: the upper half (the group's top-rated players) and the lower half.
- Pair position by position: the first player in the upper half faces the first player in the lower half, the second faces the second, and so on.
- Avoid repeats: if a pairing would recreate a game already played in the tournament, swap with the next candidate in the lower half instead of forcing the repeat.
Upper half Lower half
Player A ──── Player C
Player B ──── Player D
If Player A has already faced Player C in an earlier round, the algorithm tries Player A against Player D instead, and readjusts the rest of the group's pairings accordingly.
What happens when a group has an odd number of players
A score group with an odd number of players can't be fully paired internally: one player must float down into the next lower score group to find an opponent. The FIDE rule specifies who floats down first (usually the lowest-rated player in the group), so it's never arbitrary and never always the same player type paying the price round after round.
The bye: a simple rule, a common trap
With an odd total number of players, someone doesn't play that round and receives a bye (often counted as a win, sometimes a half-point win depending on the regulations). The rule that seems obvious but that many homegrown implementations get wrong: the bye must rotate, the same player can't receive two byes while another eligible player hasn't received one yet. Without that explicit tracking, a player can end up receiving a bye twice while another never gets one, silently skewing the tournament's fairness without any pairing rule technically being violated.
Color balancing, the constraint that gets forgotten
A good Swiss pairing isn't limited to score and avoiding repeats: it also has to balance colors, avoiding a player getting white (or black) three times in a row. This constraint stacks on top of the previous ones, which means a naive pairing algorithm that only optimizes for score can produce a pairing that's valid on paper but unfair in how it plays out, one player consistently stuck with black while their opponent of the day strings together whites.
Tiebreaks: three metrics, not one
At equal score by the end of the tournament, the final ranking can't stop at the raw score. Three tiebreaks, applied in order, settle most ties:
| Tiebreak | Calculation | What it rewards |
|---|---|---|
| Buchholz | Sum of the final scores of every opponent faced | Having faced a tough field |
| Sonneborn-Berger | Sum of the scores of opponents beaten, plus half the scores of opponents drawn against | Beating strong opponents, not just accumulating points against weak ones |
| Average opponent Elo | Simple average of the ratings faced | A more direct measure, less sensitive to opponents' one-off performances |
Buchholz and Sonneborn-Berger answer different questions: the first rewards the difficulty of the schedule regardless of the outcome, the second specifically rewards wins against opponents who performed well. A player with a very tough schedule but few wins can have a good Buchholz and a weak Sonneborn-Berger; that's why a robust final ranking applies both in a defined order, never just one in isolation.
Why the ranking order must be deterministic
Once score and tiebreaks are applied, there's always a non-zero chance of a perfect tie across every criterion. The final ranking must still produce a stable, reproducible order, not one dependent on database insertion order or an unstable sort. It's a detail that seems minor, but becomes a real problem the day two runs of the same calculation, on the same data, produce a different ranking: in an officially rated tournament, that kind of inconsistency isn't acceptable, even to settle a position that has no impact on the podium anyway.