Observed
4-player pod, Swiss (3 rounds — SWISS_ROUNDS applies to every pod size): a player faces the same opponent twice and never meets one of the other seats. Reproduced 2026-08-26 in a local bot pod (Premier / Swiss / Casual / size 4).
Cause
generate_swiss_pairings (crates/draft-core/src/session.rs:259) pairs greedily: it takes the pool head and picks the first non-rematch partner, but on failure it falls back to the first partner regardless (.unwrap_or(0)), and pairs already made are never revisited.
With records A=2, B=1, C=1, D=0 after two rounds (prior: A–B, C–D, A–C, B–D), the walk is: [A] carries into [A,B,C]; both B and C are prior opponents of A, so the fallback pairs A–B again; C carries into [C,D], prior again, fallback pairs C–D again. The rematch-free perfect matching A–D / B–C exists — round 3 of a 4-pod is always the round-robin completion — but the greedy cannot find it.
The existing test_swiss_rematch_avoidance only covers 8 players / round 2, where the head-first pick happens to succeed.
Expected
When a rematch-free perfect matching exists for the round, it is chosen. A rematch is admitted only when no such matching exists (e.g., a 2-player pod from round 2 on).
Fix direction
Replace the head-first greedy with a small backtracking search over the standings-ordered pool (bracket order preserved, partners tried in standings order, depth bounded by pod size ≤ 8): dead ends backtrack; only if the full search fails is a rematch admitted. Odd pods: pick the bye bottom-up such that the paired remainder still admits a rematch-free matching. PR follows.
Observed
4-player pod, Swiss (3 rounds —
SWISS_ROUNDSapplies to every pod size): a player faces the same opponent twice and never meets one of the other seats. Reproduced 2026-08-26 in a local bot pod (Premier / Swiss / Casual / size 4).Cause
generate_swiss_pairings(crates/draft-core/src/session.rs:259) pairs greedily: it takes the pool head and picks the first non-rematch partner, but on failure it falls back to the first partner regardless (.unwrap_or(0)), and pairs already made are never revisited.With records A=2, B=1, C=1, D=0 after two rounds (prior: A–B, C–D, A–C, B–D), the walk is: [A] carries into [A,B,C]; both B and C are prior opponents of A, so the fallback pairs A–B again; C carries into [C,D], prior again, fallback pairs C–D again. The rematch-free perfect matching A–D / B–C exists — round 3 of a 4-pod is always the round-robin completion — but the greedy cannot find it.
The existing
test_swiss_rematch_avoidanceonly covers 8 players / round 2, where the head-first pick happens to succeed.Expected
When a rematch-free perfect matching exists for the round, it is chosen. A rematch is admitted only when no such matching exists (e.g., a 2-player pod from round 2 on).
Fix direction
Replace the head-first greedy with a small backtracking search over the standings-ordered pool (bracket order preserved, partners tried in standings order, depth bounded by pod size ≤ 8): dead ends backtrack; only if the full search fails is a rematch admitted. Odd pods: pick the bye bottom-up such that the paired remainder still admits a rematch-free matching. PR follows.