From 4378726a9aa302acf1c3f80c63c2e3ad2268210b Mon Sep 17 00:00:00 2001 From: boskodev790 <233739930+boskodev790@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:50:10 -0400 Subject: [PATCH] fix(draft): credit a Swiss bye as a match win MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An odd-pod Swiss round leaves one player unpaired (a bye), but the bye was never credited — match_wins stayed 0, so Swiss standings (sorted by match_wins) undercounted the bye player. generate_swiss_pairings now reports the bye alongside the pairings, and apply_generate_pairings credits it a match win via the same ensure_match_record(..).match_wins += 1 path used for a normal win. SingleElimination is unaffected (it already rejects non-8 pods). Added a regression test: a 3-seat Swiss round-1 credits exactly one match win (the bye); it fails on the pre-fix code (0 != 1) and passes after. --- crates/draft-core/src/session.rs | 44 +++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/crates/draft-core/src/session.rs b/crates/draft-core/src/session.rs index 0a1c16071c..72681b6134 100644 --- a/crates/draft-core/src/session.rs +++ b/crates/draft-core/src/session.rs @@ -128,14 +128,19 @@ fn apply_generate_pairings( let mut rng = ChaCha20Rng::seed_from_u64(session.config.rng_seed ^ (round as u64 * 0xDEAD_BEEF)); - let new_pairings = match session.config.tournament_format { + let (new_pairings, swiss_bye) = match session.config.tournament_format { TournamentFormat::Swiss => generate_swiss_pairings(session, round, &mut rng), - TournamentFormat::SingleElimination => generate_se_pairings(session, round), + TournamentFormat::SingleElimination => (generate_se_pairings(session, round), None), }; for p in &new_pairings { session.pairings.push(p.clone()); } + // A Swiss bye counts as a match win for the unpaired player; without this credit an + // odd-pod bye scores nothing and Swiss standings (sorted by match_wins) are wrong. + if let Some(bye) = swiss_bye { + ensure_match_record(&mut session.match_records, bye).match_wins += 1; + } session.status = DraftStatus::MatchInProgress; session.current_round = round; @@ -147,11 +152,14 @@ fn apply_generate_pairings( ]) } +/// Returns the round's pairings plus the bye player, if any. An odd pod leaves one player +/// unpaired; in Swiss that player takes a bye, which counts as a match win — the caller +/// credits it (`Some(pid)`). `None` when every player was paired. fn generate_swiss_pairings( session: &DraftSession, round: u8, rng: &mut ChaCha20Rng, -) -> Vec { +) -> (Vec, Option) { let seat_indices: Vec = session .seats .iter() @@ -222,11 +230,12 @@ fn generate_swiss_pairings( } } - // If there's still an unpaired player (odd count), they get a bye (no pairing generated) - // For 8-player pods this shouldn't happen. + // An unpaired player (odd pod) takes a bye — reported to the caller so the bye can be + // credited as a match win. Common only in non-8-player pods. + let bye = carry.map(|(pid, _)| pid); // Generate DraftPairing structs - paired + let pairings = paired .iter() .enumerate() .map(|(table, (p1, p2))| DraftPairing { @@ -237,7 +246,9 @@ fn generate_swiss_pairings( status: PairingStatus::Pending, winner: None, }) - .collect() + .collect(); + + (pairings, bye) } fn generate_se_pairings(session: &DraftSession, round: u8) -> Vec { @@ -1721,4 +1732,23 @@ mod tests { assert!(flags.get(2)); // new slot, default true assert!(flags.get(3)); // new slot, default true } + + #[test] + fn swiss_bye_in_odd_pod_is_credited_a_match_win() { + // Odd pod -> exactly one player takes a bye each round. + let (mut session, _) = test_session(3); + session.status = DraftStatus::Deckbuilding; // satisfy the pairing-generation guard + apply_generate_pairings(&mut session, 1).unwrap(); + + // Three players: one two-player pairing plus one bye. + assert_eq!( + session.pairings.iter().filter(|p| p.round == 1).count(), + 1, + "the two paired players get exactly one pairing", + ); + // The unpaired (bye) player is credited exactly one match win. Without the credit an + // odd-pod bye scores nothing and Swiss standings (sorted by match_wins) undercount it. + let total_match_wins: u8 = session.match_records.values().map(|r| r.match_wins).sum(); + assert_eq!(total_match_wins, 1, "the bye player earns a match win"); + } }