Skip to content

fix(draft): credit a Swiss bye as a match win (odd pods)#6351

Open
boskodev790 wants to merge 1 commit into
phase-rs:mainfrom
boskodev790:fix/swiss-bye-match-win
Open

fix(draft): credit a Swiss bye as a match win (odd pods)#6351
boskodev790 wants to merge 1 commit into
phase-rs:mainfrom
boskodev790:fix/swiss-bye-match-win

Conversation

@boskodev790

@boskodev790 boskodev790 commented Jul 22, 2026

Copy link
Copy Markdown

Summary

Fixes #6349: in an odd-sized Swiss pod, generate_swiss_pairings (crates/draft-core/src/session.rs) leaves one player unpaired each round — they take a bye — but the bye was never credited. match_wins stayed 0, so Swiss standings (sorted by match_wins) undercounted the bye player. DraftSession::new accepts any seats.len() (Swiss has no even/8 guard, unlike SingleElimination), so odd pods are reachable.

generate_swiss_pairings now returns the round's pairings plus the bye player (Option<PlayerId>), and apply_generate_pairings credits the bye a match win via the same ensure_match_record(..).match_wins += 1 path a normal win uses. SingleElimination is unaffected.

Files changed

  • crates/draft-core/src/session.rs — return the bye from generate_swiss_pairings; credit it in apply_generate_pairings; regression test.

Implementation method

Method: not-applicable — this is draft-core tournament pairing/standings, not crates/engine/ game logic, so /engine-implementer does not apply. Targeted fix (return-type widened to report the bye) + one regression test; no new types or public API beyond the internal fn's return shape.

CR references

None — Swiss tournament structure is not part of the MTG Comprehensive Rules (CR governs game rules; pairing/standings are tournament policy).

Track

Developer — Rust nightly-2026-04-19 (the repo's pinned toolchain) is installed locally. I built and ran the test on the real workspace build (not just CI):

  • cargo test -p draft-core swiss_bye_in_odd_pod1 passed.
  • Test-first check: with the credit line disabled, the same test fails left: 0, right: 1 — so the test genuinely catches the bug.
  • Gate A run locally (below).
  • clippy deferred to CI: repeated local cargo clippy runs were cut short by environment build-lock/timeout contention; the change mirrors existing idioms in the same file, so no new lint surface is expected.

LLM

Model: claude-opus-4-8
Thinking: high

Verification

  • Gate A output below is for the current committed head.
  • Anchors cite existing analogous code at the same seam.
  • cargo test -p draft-core swiss_bye_in_odd_podok, 1 passed (and fails 0 != 1 with the credit disabled — test-first verified).
  • scripts/check-parser-combinators.shGate A PASS (no parser files touched).
  • clippy / full cargo test — deferred to CI (local build-lock contention).

Gate A

Gate A PASS head=4378726a9aa302acf1c3f80c63c2e3ad2268210b base=21fc5f3b476021ea78e38b712cbffe0ae417a5d9

Anchored on

  • crates/draft-core/src/session.rs:311apply_match_record_result credits a match win with ensure_match_record(records, winner_pid).match_wins += 1; the bye credit reuses that exact house pattern.
  • crates/draft-core/src/session.rs:118apply_generate_pairings already special-cases tournament pod size (SingleElimination's non-8 guard); crediting the odd-Swiss bye is the same class of pod-size edge handling in the same function.

Summary by CodeRabbit

  • Bug Fixes
    • Swiss tournament byes now count as a credited match win for the player receiving the bye.
    • Corrected bye handling for odd-sized pods so match-win totals remain accurate.
    • Added coverage to verify that exactly one win is recorded for a three-player pod.

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.
@superagent-security superagent-security Bot added the contributor:flagged Contributor flagged for review by trust analysis. label Jul 22, 2026
@superagent-security

Copy link
Copy Markdown

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Swiss pairing generation now returns the unpaired player as an optional bye, and pairing application credits that player with a match win. A new three-player Swiss test verifies one total match win after round-one pairings are generated.

Changes

Swiss bye match-win credit

Layer / File(s) Summary
Return the Swiss bye player
crates/draft-core/src/session.rs
generate_swiss_pairings returns generated pairings together with the carried player as an optional bye.
Credit and validate the bye
crates/draft-core/src/session.rs
apply_generate_pairings increments the bye player’s match_wins, and a three-player test verifies the credited result.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: matthewevans, lgray

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: crediting a Swiss bye as a match win for odd pods.
Linked Issues check ✅ Passed The changes credit the unpaired Swiss player with a win and add a regression test for a 3-player session.
Out of Scope Changes check ✅ Passed The diff stays focused on Swiss bye crediting and the related test, with no clear unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/draft-core/src/session.rs (1)

1749-1752: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the specific bye player’s win.

The aggregate total passes even if a paired player is credited. Derive the unpaired PlayerId from the generated pairing and assert that record has exactly one match_win, while paired players remain unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/draft-core/src/session.rs` around lines 1749 - 1752, Replace the
aggregate total_match_wins assertion in the bye-handling test with an assertion
targeting the specific unpaired PlayerId from the generated pairing. Derive that
bye player, verify its match record has exactly one match_win, and retain checks
that paired players’ records remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/draft-core/src/session.rs`:
- Around line 131-143: Make the GeneratePairings flow around
generate_swiss_pairings and the new_pairings insertion idempotent for repeated
calls on the same Swiss round. Detect whether pairings for round already exist
before appending or crediting swiss_bye, and ensure an existing round cannot
duplicate pairings or increment match_wins again while preserving normal
generation for new rounds.

---

Nitpick comments:
In `@crates/draft-core/src/session.rs`:
- Around line 1749-1752: Replace the aggregate total_match_wins assertion in the
bye-handling test with an assertion targeting the specific unpaired PlayerId
from the generated pairing. Derive that bye player, verify its match record has
exactly one match_win, and retain checks that paired players’ records remain
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e09f8399-2e0d-4689-9c83-f69ed57fd3f1

📥 Commits

Reviewing files that changed from the base of the PR and between 7195a8e and 4378726.

📒 Files selected for processing (1)
  • crates/draft-core/src/session.rs

Comment on lines +131 to +143
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make Swiss bye credit idempotent.

GeneratePairings is permitted after RoundComplete, but this path does not verify that round is new or that pairings for it are absent. Repeating generation for an odd Swiss round can append another pairing set and increment bye standings again, corrupting match totals. Validate round uniqueness/monotonicity or make pairing insertion and bye credit idempotent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/draft-core/src/session.rs` around lines 131 - 143, Make the
GeneratePairings flow around generate_swiss_pairings and the new_pairings
insertion idempotent for repeated calls on the same Swiss round. Detect whether
pairings for round already exist before appending or crediting swiss_bye, and
ensure an existing round cannot duplicate pairings or increment match_wins again
while preserving normal generation for new rounds.

@matthewevans matthewevans left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocked — replaying an already generated Swiss round duplicates pairings and the bye win.

🔴 Blocker

crates/draft-core/src/session.rs:91-143 permits GeneratePairings from RoundComplete and appends generated pairings plus swiss_bye without checking whether round already exists. Since round is supplied by the action, replaying an existing odd Swiss round appends a second set of pairings and increments the bye player’s match_wins again, corrupting standings. The CodeRabbit finding is confirmed against the current head.

crates/draft-core/src/session.rs:1749-1752 only asserts the aggregate win total, so it also passes if the credit goes to a paired player. Add a regression that attempts the same round again after RoundComplete, verifies no duplicate pairing/bye credit (or a clear transition error), and asserts the specific unpaired player has exactly one match win while the paired players remain unchanged.

Recommendation: request changes for round uniqueness/idempotence and a discriminating specific-bye test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:flagged Contributor flagged for review by trust analysis.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants