Swiss Pairing Desk: Rematches, Byes and Tiebreaks
HardAI-assisted coding · AI-Assisted Feature Work
Swiss Pairing Desk: Rematches, Byes and Tiebreaks
You've joined the platform team at the Northgate Chess & Esports League. Every Friday the league runs a blitz night on Swiss pairings: nobody is knocked out, and each round the desk pairs players who have similar scores. The pairing desk is a small Python service that the tournament directors use to pair rounds, record results and publish standings.
Last Friday it embarrassed itself in front of 60 people. The incident write-up (PAIR-412) is Part 1. Three follow-up tickets are queued behind it: tiebreaks (Part 2), withdrawals (Part 3) and a rehearsal mode for the 300-player Spring Open (Part 4).
Codebase
| File | What it does |
|---|---|
swiss/api.py | run(operations) entry point and the Dispatcher that routes each operation |
swiss/service.py | Tournament: registration, pairing a round, reporting results, standings |
swiss/pairing.py | Decides who plays whom and who gets the bye |
swiss/ranking.py | rank_key: the order players are ranked in when pairing |
swiss/rounds.py | RoundBook: every paired round, its games, byes, running points and who has met whom |
swiss/colors.py | Decides which player of a finished pair gets white (preference only, never changes a pairing) |
swiss/standings.py, swiss/tiebreaks.py | The standings table (tiebreaks are placeholders until Part 2) |
swiss/roster.py | Player registry and registration rules |
swiss/models.py, swiss/config.py, swiss/errors.py, swiss/scorefmt.py | Records, constants, error codes, number formatting |
tests/ | Existing unit and end-to-end tests (one currently fails) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh tournament and returns one result per operation. An operation is [name, *args]. Errors are returned as strings "error: <code>", never raised, so one bad request never aborts the batch.
| Operation | Arguments | Returns | Errors (checked in this order) |
|---|---|---|---|
register | player_id, rating | true | registration closed, invalid arguments, player exists |
register_many | [[player_id, rating], ...] | number registered | registration closed, invalid arguments, player exists |
pair_round | none | the round (see below) | round in progress, not enough players, no valid pairing |
report | round, board, result | true | unknown round, unknown board, invalid result, already reported |
report_round | round, results | number of results recorded | unknown round, already reported, result count mismatch, invalid result |
round | round | the round (see below) | unknown round |
standings | none (Part 2) | list of rows | |
withdraw | player_id (Part 3) | true | unknown player, already withdrawn |
auto_rounds | count, draw_every (Part 4) | summary | invalid arguments |
- An unknown operation name returns
"error: unknown operation". A wrong number of arguments, a non-list operation, or an argument of the wrong type (for example a non-integerround) returns"error: invalid arguments"; that check comes before every other error in the table. (The one exception: awithdrawid that is not a registered player, whatever its type, isunknown player.) - A player id is a non-empty string with a non-blank character (compared case-sensitively, and compared as plain strings whenever ids are sorted). A rating is an integer from 0 to 4000 (
trueandfalseare not integers).register_manyis all-or-nothing: if any entry is malformed or any id already exists (or appears twice in the list) nothing is registered. Registration closes when round 1 is first paired. - The round value returned by
pair_roundandroundis{"round": n, "boards": [[board, white_id, black_id, result], ...], "bye": id_or_null}. Boards are numbered from 1.resultis"*"while a board has no result, otherwise"W"(white won),"B"(black won),"D"(draw), or a forfeit code introduced in Part 3. reporttakes"W","B"or"D"and records it on a board that has no result yet.report_roundtakes a string with one such character per board of the round (for example"WDB"for a three-board round), in board order; it is all-or-nothing, and returnsalready reportedif any board of that round already has a result.- Points are kept in half-points: a win is 1 point, a draw is half a point, a loss is 0, and a bye is worth 1 point (the same as a win;
BYE_HALVESinconfig.py). Points shown to users look like3or3.5. - A round is *in progress* while any of its boards has no result.
Pairing order. Everywhere the desk needs to say who is "higher" it uses ranking.rank_key: more points first, then higher rating, then id ascending.
Worked example. Six players; the ratings are 1800, 1700, 1600, 1500, 1400, 1300.
[["register_many", [["ana",1800],["ben",1700],["cy",1600],["dee",1500],["eli",1400],["fay",1300]]],
["pair_round"],
["report_round", 1, "WWW"],
["pair_round"],
["report_round", 2, "BWW"],
["pair_round"]]returns
[6,
{"round":1,"boards":[[1,"ana","dee","*"],[2,"ben","eli","*"],[3,"cy","fay","*"]],"bye":null},
3,
{"round":2,"boards":[[1,"ben","ana","*"],[2,"dee","cy","*"],[3,"eli","fay","*"]],"bye":null},
3,
{"round":3,"boards":[[1,"ana","cy","*"],[2,"fay","ben","*"],[3,"eli","dee","*"]],"bye":null}]In round 3 ana (4 points) is paired first and cannot meet ben again, so she takes cy. Then ben cannot take dee, because that would leave eli and fay, who already met in round 2. The desk has to go back and give ben someone else (fay) so that everybody gets a game.
Part 1: The Friday blitz incident (PAIR-412)
Goal: make the pairing step follow the rules below. The directors reported, from one night:
- "Round 4: two players who had already played each other in round 2 were paired again."
- "Round 5: one player got no opponent and no bye. He just wasn't on any board and his tournament points stayed flat."
- "A player who received a bye in round 1 was listed at the bottom of the round-2 pairings as if he had no points."
The rules, which are what the desk should do. Everything here is about pair_round.
- Who plays. Every registered player takes part (Part 3 changes this). Order them by
rank_keyas of the start of the round (points include byes). Call this list the *ranking*. - No rematches. Two players who were paired against each other in any earlier round are never paired again. Which colors they had then is irrelevant.
- Byes. If the number of players is odd, exactly one player gets the bye: 1 point, awarded as soon as the round is paired, no color, and no board. Candidates for the bye are tried in this order: first the players who have never had a bye, lowest-ranked (last in the ranking) first; after all of those, the players who already had one, again lowest-ranked first. For each candidate in turn the desk tries to pair everyone else (step 4); the first candidate for whom that succeeds gets the bye. (So the bye is not always given to the lowest-ranked fresh player: if giving it to them would leave an unpairable field, the next one gets it.)
- Pairing a group of players: depth-first, in a fixed order. Take the *remaining* players, still in ranking order. The first of them is the lead. Let
mbe the number of players at the front of the remaining list (lead included) whose points equal the lead's. Index the remaining list from 0 (the lead is index 0). The lead's possible opponents are tried in this order, skipping anyone the lead has already played: - indices
m//2, m//2 + 1, ..., m - 1(integer division; whenmis 1 this range is just the lead itself and is skipped); - then indices
m//2 - 1, m//2 - 2, ..., 1; - then indices
m, m + 1, ..., len - 1(players with fewer points than the lead, best-ranked first).
Pair the lead with the first candidate and repeat on what is left. If the rest cannot be completed, the choice is undone and the lead's next candidate is tried; if the lead runs out of candidates, the problem is reported upward to the previous decision, which moves on to its next candidate (ordinary backtracking). The pairing found this way (the first complete one in this order) is the one the desk uses. Boards are numbered in the order pairs were made, i.e. by the ranking position of each pair's better-ranked player.
- When nothing works (no choice of bye, or no pairing for an even field)
pair_roundreturns"error: no valid pairing"and changes nothing: no round is created, no bye is awarded and registration stays as it was. - Colors. Once the pairs are known
colors.pydecides who is white. It is correct for this Part: a player is *due* the color they have had less of, or if equal the opposite of the color of their most recent game, or nothing if they have not played; if both are due the same color the one with the bigger imbalance (whites minus blacks, ignoring sign) gets it, ties going to the better-ranked player; if they are due different colors, or only one is due anything, each gets what they are due; if neither is due anything the better-ranked player gets white. Byes have no color. This never affects who is paired with whom. - Errors, in order.
round in progressif the latest round still has a board without a result;not enough playersif fewer than two players take part; thenno valid pairing.
tests/test_pairing.py has one test that currently fails.
Part 2: Standings and tiebreaks (PAIR-418)
Goal: ["standings"] returns every registered player as a row [rank, player_id, points, buchholz, sonneborn_berger, status], where rank is 1, 2, 3, ... down the table, the three numbers are strings ("3", "3.5", "7.25", no trailing zeros; see scorefmt.py), and status is "active" (Part 3 adds "withdrawn"). ["standings", k] returns only the first k rows (k an integer >= 1, otherwise "error: invalid arguments").
Rules
- Only results that have been recorded count. A board without a result adds nothing for anyone.
- A *played game* is a game with a result of
W,BorD. Byes are not played games, and neither are the forfeits that Part 3 introduces. - Buchholz of a player: the sum, over their played games, of the opponent's current points (the opponent's total including their own bye or forfeit points, in half-points).
- Sonneborn-Berger of a player: the sum, over their played games, of
result * opponent's points, whereresultis 1 for a win, 1/2 for a draw and 0 for a loss. (Internally it is quarter-points;scorefmt.format_quartersrenders it.) - A bye adds nothing to either tiebreak: it is not a game against anyone.
- Order of the table: points (more first), then Buchholz (more first), then Sonneborn-Berger (more first), then rating (higher first), then id ascending.
- Pairing is unchanged by this Part: it still uses the pairing order of Part 1 (
rank_key: points, rating, id), not the table order.
Example. Five players v, w, x, y, z rated 1900, 1800, 1700, 1600, 1500. In round 1 the field is odd, so z takes the bye; round 1 results are WB. Round 2 is y (white) vs v, z vs w, with x on the bye, and its results are WD.
[["register_many", [["v",1900],["w",1800],["x",1700],["y",1600],["z",1500]]],
["pair_round"], ["report_round", 1, "WB"],
["pair_round"], ["report_round", 2, "WD"],
["standings"]]The last result is
[[1,"y","2","1.5","1.5","active"],
[2,"z","1.5","0.5","0.25","active"],
[3,"v","1","3","1","active"],
[4,"x","1","1","0","active"],
[5,"w","0.5","3.5","0.75","active"]]v and x both have 1 point; v is ahead on Buchholz (opponents x and y have 1 + 2 points between them).
Part 3: Withdrawals mid-event (PAIR-431, a requirements change)
Goal: directors need to remove players who leave. ["withdraw", player_id] returns true.
Rules
- A withdrawn player is never paired again. Rule 1 of Part 1 now reads "every registered player who has not withdrawn takes part": the number of players for the bye rule, the ranking and the pairing all use only those players, while the rematch rule is unchanged. A player can be withdrawn before round 1 too.
- Withdrawn players stay in
standings, ranked by the same rules as everyone else, with status"withdrawn". Their past games still count, exactly as before: an opponent's Buchholz and Sonneborn-Berger use the withdrawn player's points as they stand, and the withdrawn player's own row is computed normally. - If the player has a board in the latest round that has no result yet, that game is forfeited: the opponent is awarded the win and the board's result becomes
"FW"(white awarded the point) or"FB"(black awarded it). The opponent gets 1 point. A board that already has a result is left alone, and so is a bye the withdrawing player holds. Because this can finish the round,pair_roundmay become available right after a withdrawal. - A forfeit is not a played game. It adds nothing to Buchholz or Sonneborn-Berger for either player and it does not count for color history (nobody sat at that board). It does count as a meeting for rematch purposes, though that can only matter to the withdrawn player.
- Changed rule:
report_roundnow takes one character for every board of the round that does not have a result yet, in board order (replacing "one per board of the round" and the "any board already has a result" error of Part 1). It returnsalready reportedonly if every board already has a result; the count of characters must equal the number of boards still waiting (result count mismatch).reporton a forfeited board returnsalready reported. ["withdraw", id]errors:unknown player, thenalready withdrawn.- With fewer than two non-withdrawn players
pair_roundreturnsnot enough players.
Example. Six players as in the worked example above. After round 1 is paired, only the first board is reported (WWW is not sent); eli (black on board 2) leaves.
[["register_many", [["ana",1800],["ben",1700],["cy",1600],["dee",1500],["eli",1400],["fay",1300]]],
["pair_round"],
["report", 1, 1, "W"],
["withdraw", "eli"],
["report_round", 1, "W"],
["round", 1]]returns
[6, {"round":1,"boards":[[1,"ana","dee","*"],[2,"ben","eli","*"],[3,"cy","fay","*"]],"bye":null},
true, true, 1,
{"round":1,"boards":[[1,"ana","dee","W"],[2,"ben","eli","FW"],[3,"cy","fay","W"]],"bye":null}]ben has 1 point from the forfeit; ben and eli will never be paired again.
Part 4: Rehearsal for the Spring Open (PAIR-440)
Goal: the directors want to rehearse a whole event (a 300-player open over 9 rounds, or a small club league playing a near round-robin) before the day. Add ["auto_rounds", count, draw_every], which pairs and scores up to count rounds in one call, and make the pairing fast enough that these rehearsals finish.
Rules
countmust be an integer>= 1anddraw_everyan integer>= 0, otherwise"error: invalid arguments". (Both bools are rejected.)- It repeats
counttimes: pair a round exactly aspair_rounddoes, then score every board of that round: if the two ratings are equal the board isD; otherwise the higher-rated player wins (Wif white is higher-rated, elseB). Then, ifdraw_every > 0and the board number is a multiple ofdraw_every, the board is aDinstead. - It stops early, without raising, the first time
pair_roundwould fail (for any reason, includinground in progressif the latest round was left unfinished). Rounds already paired stay. - It returns
{"played": rounds_completed_by_this_call, "stopped": null_or_the_error_code, "rounds": total_rounds_in_the_book}. The code is the text aftererror:, for example"no valid pairing". - Everything from Parts 1 to 3 still applies; rehearsals can be mixed with
withdraw,reportandstandingscalls. - Hidden tests rehearse fields of a few hundred players and 30-40 player events that run until the field is exhausted (a near round-robin). In the latter, naive backtracking never returns: be sure that whatever you do still returns exactly the pairing defined in Part 1, not merely a valid one. The whole suite has a few seconds.
Notes
models.py,config.py,scorefmt.py,colors.py(until Part 3) androster.pyare known-good.- You may use the AI assistant freely; you are expected to verify what it produces.
- Later parts must not break earlier ones; hidden tests re-check earlier behaviour after the later parts are in.
- Parts 1-3 is a strong result. Part 4 is a stretch.
Example 1
- Input:
- [["register_many",[["ana",1800],["ben",1700],["cy",1600],["dee",1500],["eli",1400],["fay",1300]]],["pair_round"],["report_round",1,"WWW"],["pair_round"],["report_round",2,"BWW"],["pair_round"]]
- Output:
- [6,{"round":1,"boards":[[1,"ana","dee","*"],[2,"ben","eli","*"],[3,"cy","fay","*"]],"bye":null},3,{"round":2,"boards":[[1,"ben","ana","*"],[2,"dee","cy","*"],[3,"eli","fay","*"]],"bye":null},3,{"round":3,"boards":[[1,"ana","cy","*"],[2,"fay","ben","*"],[3,"eli","dee","*"]],"bye":null}]
- Explanation
- In round 3 ana takes cy. ben cannot meet dee without leaving eli and fay, who already played, so the search backs up and ben plays fay.
Example 2
- Input:
- [["register_many",[["v",1900],["w",1800],["x",1700],["y",1600],["z",1500]]],["pair_round"],["report_round",1,"WB"],["pair_round"],["report_round",2,"WD"],["standings"]]
- Output:
- [5,{"round":1,"boards":[[1,"v","x","*"],[2,"w","y","*"]],"bye":"z"},2,{"round":2,"boards":[[1,"y","v","*"],[2,"z","w","*"]],"bye":"x"},2,[[1,"y","2","1.5","1.5","active"],[2,"z","1.5","0.5","0.25","active"],[3,"v","1","3","1","active"],[4,"x","1","1","0","active"],[5,"w","0.5","3.5","0.75","active"]]]
- Explanation
- z takes the round-1 bye and is worth a full point, so z is ranked with the 1-point players in round 2 and x, now the lowest-ranked player without a bye, sits out. v and x tie on points; v wins the tiebreak on Buchholz.
Guidelines
- Python 3 standard library only
- Points are kept as integer half-points; there is no floating-point arithmetic anywhere
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- A pairing is defined as the first complete rematch-free pairing in the stated depth-first order, so the output is unique
- Part 4 rehearses fields of hundreds of players and near round-robin events of 30-40 players