Swiss-pairing tournament organizer — design discussion (following up on #4612 / #4615) #5314
rykerwilliams
started this conversation in
Ideas
Replies: 1 comment
-
|
Wrote up a verification pass against the current repo state for this — full research/design docs (with file:line citations confirming the |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Swiss-pairing tournament organizer — design discussion (following up on #4612 / #4615)
Picking this up per @matthewevans's note on #4615 — posting the design discussion here before any new implementation work, rather than jumping straight to a PR.
Starting point
#4612's proposal is the right architecture and I'm not proposing an alternative to it: a
TournamentManagerliving incrates/lobby-brokeras a sibling toLobbyManager, sharing its functional-core/imperative-shell pattern, dispatch (Broker::handle), and existinglobby_subscribersbroadcast channel so it runs byte-identically on the native server and the Cloudflare Worker. Matches are self-reported by players who start their own table via the existingCreateGameWithSettings/JoinGameWithPasswordflow — the broker organizes, it doesn't superviseGameSessions. This is the same shape #4615 implemented; the review on that PR found real bugs in the implementation, not a problem with this direction.Grounding the rules in the actual Magic Tournament Rules (MTR), not invented convention
Rather than inventing tiebreaker/bye/round-count conventions from memory, I pulled the current official MTR (effective 2026-02-27, PDF, HTML) and verified the exact text. This should be the default ruleset the feature ships with — a tournament tool that gets tiebreakers subtly wrong is worse than not having one.
ExcludeFromStandingsvariant needed.TOs need override knobs, not hardcoded MTR constants
The MTR is the right default, but it's explicitly not universal practice — casual and format-specific communities commonly run variants. The concrete example that came up: some Old School/93-94-adjacent groups score draws as 0 match points instead of MTR's 1, specifically to discourage intentional draws. This repo is also already moving toward a general custom-format engine (per an internal backlog note reframing formats as presets rather than a fixed list), so tournament scoring policy should follow the same shape: MTR values as the shipped default, exposed as an explicit, TO-configurable
ScoringPolicyat tournament-creation time — not a constant baked intoTournamentManager. Concretely:ScoringPolicy { win_points: u8, draw_points: u8, loss_points: u8 }(default{3, 1, 0}), threaded through match-point accumulation and the tiebreaker calculations the same way. The tiebreaker order and floor value are less likely to need per-TO overrides than the point values are, but the scoring policy should be a first-class config field either way rather than assumed fixed.What #4615's review already surfaced (baking these in as day-one design decisions, not follow-up fixes)
ConnState, so closing the tab (which the UI did immediately after creating a tournament) triggeredon_disconnectand unregistered the tournament; joined players lostjoined_tournamentstoo, orphaning later report/drop calls. Proposal: mint an explicit organizer token and per-player tokens at creation/join time (mirroring how draft-pod sessions already do reconnect via token, not socket identity), so authority survives a socket bounce. The tournament page keeps its socket open for the page lifetime as an optimization (live updates), not as the source of authority.ReportMatchResultwhenwinner_player_keydoesn't match the player with the higher game-win count.last_activity_at, bumped on every tournament mutation (join, pairing, report, round advance); the native reaper and any Cloudflare Worker cleanup key off that, notcreated_at. Restrict staleness reaping to abandoned registration state — an active in-progress round should never be reaped by a fixed timeout.is_empty()inbroker-wasmcurrently only checks lobby games; it needs to also checktournaments.is_empty()so a Durable Object holding only tournament state keeps rescheduling its cleanup alarm.t()from day one, perclient/src/i18n/README.md— no English-only strings landing then getting retrofitted.Proposed rollout — split into small, independently reviewable PRs
Per the ask on #4615 to split this up rather than one large cross-stack PR:
crates/lobby-broker/src/tournament.rs:TournamentManager, Swiss pairing (score-group + backtracking with the fixed base case, float-down, bye assignment preferring players who haven't had one), standings using the MTR-cited tiebreak order/floors above,ScoringPolicyas a first-class config. Exhaustively unit tested with a deterministicFakeEnv, same style aslobby.rs/broker.rs— including test vectors pinned against the MTR's own worked examples where practical. No protocol changes, no shell wiring — reviewable purely as an algorithm + state machine.protocol.rsnew message variants and view types (TournamentView,TournamentSummary,TournamentStanding,PairingView), protocol version bump,broker.rsgains thetournaments: TournamentManagerfield and theConnStateorganizer/joined-tournament fields (as tokens per the auth fix above, not bare socket-bound state), native server dispatch arms inphase-server/src/main.rs, reaper keyed offlast_activity_at.lobby-worker/broker-wasmmutates_lobbymatch extension,lobby-do.tsConnAttachmentextension, the correctedis_empty()predicate.adapter/types.tsmirrors,tournamentClient.ts,TournamentLandingPage.tsx/TournamentPage.tsx+ presentational components, i18n keys, routes/nav entry, explicit socket-close-on-unmount.Each PR is reviewable independently and PR 1 in particular has no cross-stack surface at all — it's just an algorithm with tests.
Remaining open questions for maintainer alignment before PR 1
Most of the open questions from the first draft of this post are now resolved directly by the MTR citations above (bye handling, tiebreaker order/floors). What's left:
{3,1,0}should be the shipped default with a TO override, per the section above — is a simple three-number config sufficient for v1, or does this want to hook into the custom-format system if/when that lands?GameSessionper pairing) as Add a Swiss-pairing tournament organizer to the lobby broker #4612 proposes, with full-mode integration explicitly out of scope as a separate fast-follow.Happy to start on PR 1 once there's rough agreement here — it's the smallest, lowest-risk piece and doesn't block on the open questions above.
Beta Was this translation helpful? Give feedback.
All reactions