Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

mtg: tournament planner

Open
#174 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
18/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
typescript

Research direction

Start by reading the Tournaments section of the mtg glossary, ADR 0002, and the tournament pairing research note named in the issue. Then trace the existing Account, deck, and tournament related entry points in the repository; the issue does not name specific files or tests. Done means implementing the broad tournament lifecycle described here, including participants, registered decks, rounds, results, standings, and Top cut.

Written by the indexing model from the issue text.

Description

ready-for-agent

Problem Statement

People who play Magic together — a Commander night among friends, a Friday Night Magic at the local store, a small cEDH event — run their tournaments on paper, in spreadsheets or in third-party tools that know nothing about their mtg Accounts and decks. Pairing Swiss rounds by hand is error-prone (rematches, who gets the Bye, who sat first last time), standings with Tiebreakers are tedious to compute, and afterwards nobody remembers which list was played. mtg already holds the players' Accounts and decks, but offers nothing to run a Tournament with them.

Solution

A tournament planner in mtg. Any Account can create a Tournament and run it as its Owner, together with Co-organizers. Participants are Accounts or Guests. A Tournament has a Table size (2 for one-on-one, 3–5 for multiplayer), a Ruleset (MTR one-on-one; TopDeck or Multiplayer Addendum multiplayer) and is played as Swiss Rounds, optionally followed by a Top cut. mtg pairs every Round (no rematches, balanced Seats in multiplayer), shows Matches, the round timer and the standings live on every Participant's phone, takes Results from the Participants themselves and lets Organizers settle disputes. Participants may register one of their decks; when the first Round starts it becomes a Registered deck — a copy that outlives the deck — and a Deck checkpoint named after the Tournament marks that point in the deck's Deck history. A Tournament can also be entered afterwards, with every pairing and Result set by its Organizers. Leagues are out of scope.

Vocabulary: the Tournaments section of the mtg glossary. Decisions: mtg ADR 0002 (a Registered deck is a copy). Pairing background: the mtg research note on tournament pairing.

User Stories

Creating and organizing
  1. As an Account, I want to create a Tournament, so that I can run an event for my group or store.
  2. As an Owner, I want to give the Tournament a name, date and start time, free-text location and description, so that Participants know when and where to show up.
  3. As an Owner, I want to choose the Table size (2–5), so that the same planner serves one-on-one events and Commander pods.
  4. As an Owner, I want to choose a Ruleset fitting the Table size (MTR for one-on-one; TopDeck or Multiplayer Addendum for multiplayer), so that scoring, Byes and Tiebreakers follow rules my players know.
  5. As an Owner, I want to optionally set a format, so that registered decks are checked against it.
  6. As an Owner of a one-on-one Tournament, I want to choose best of one or best of three for the Swiss Rounds, so that the event fits the time available.
  7. As an Owner, I want to choose the Top cut (none, Top 4 or Top 8 one-on-one; a final Match multiplayer) and its own Match length, so that the playoff can differ from the Swiss Rounds.
  8. As an Organizer, I want to change the Top cut size and Match length until the Swiss Rounds are over, so that I can react to the turnout.
  9. As an Owner, I want to set an optional round timer length, so that Rounds don't drag on.
  10. As an Owner, I want to set an optional participant limit, so that the event fits the venue.
  11. As an Owner, I want to set an optional entry fee (amount and currency), so that players know what it costs.
  12. As an Owner, I want to choose whether Accounts may sign up themselves, so that I decide between an open event and a closed list.
  13. As an Owner, I want to choose the Tournament's visibility (public, unlisted, private), so that I control who can find and see it.
  14. As an Owner, I want to choose between running the Tournament live and entering it afterwards, so that I can also record an event that was run on paper.
  15. As an Owner, I want to add other Accounts as Co-organizers, so that someone can run the event with me or in my place.
  16. As a Co-organizer, I want to do everything the Owner can except deleting the Tournament, removing Co-organizers and handing it on, so that I can actually run the event.
  17. As a Co-organizer, I want to remove myself, so that I'm not stuck with an event I no longer run.
  18. As an Owner, I want to remove a Co-organizer, so that only the people I trust run my event.
  19. As an Owner, I want to hand the Tournament on to any Account, so that someone else can take over.
  20. As the receiving Account, I want to accept or decline a handover, so that nobody ends up with a Tournament against their will.
  21. As a former Owner, I want to stay a Co-organizer after a handover, so that I can still help.
  22. As an Owner, I want to delete a Tournament, so that a mistaken or abandoned event disappears.
  23. As an Organizer, I want to cancel a Tournament at any point, so that an event that couldn't finish is marked as such while its Rounds so far stay visible.
  24. As an Organizer, I want to also be a Participant in my own Tournament, so that I can play at my own kitchen table.
Participants
  1. As an Organizer, I want to add Accounts as Participants, so that I can build the player list myself.
  2. As an Organizer, I want to add Guests by name, so that friends without an Account can play.
  3. As an Account, I want to sign up for a public or unlisted Tournament myself when sign-up is open, so that I don't need to ask the Organizer.
  4. As an Account, I want my sign-up to take effect at once, without confirmation, so that I know I'm in.
  5. As an Account, I want sign-up to be refused when the participant limit is reached, so that the limit holds.
  6. As an Organizer, I want to remove Participants before the first Round, so that I can fix the list.
  7. As an Organizer of a Tournament with an entry fee, I want every Participant to start as unpaid and to mark them paid, so that only people who paid get paired.
  8. As an Organizer, I want unpaid Participants to count towards the participant limit, so that nobody loses their place at the door.
  9. As an Organizer, I want only Organizers to see who has paid, so that it stays between me and the player.
  10. As an Organizer, I want unpaid Participants left out of the pairings when a Round starts, so that I can collect fees as people arrive and add them later.
  11. As a Participant, I want to Drop myself at any time, so that I can leave early without bothering the Organizer.
  12. As an Organizer, I want to Drop a Participant, so that a no-show isn't paired again.
  13. As a Participant who Dropped, I want to stay in the standings with my results so far, so that my games still count for my opponents' Tiebreakers.
  14. As an Organizer, I want to add a late Participant up to any Round, starting with no Match points, so that latecomers can still play.
  15. As an Organizer, I want sign-up to close when the first Round starts, so that only I can add late Participants.
Decks
  1. As a Participant with an Account, I want to register one of my decks, so that what I played is recorded.
  2. As a Participant, I want to play without registering a deck, so that a casual night stays casual.
  3. As a Participant, I want my Registered deck to follow my deck until the first Round starts, so that last-minute tuning counts.
  4. As a Participant, I want to swap or remove my Registered deck until the first Round starts, so that I can change my mind.
  5. As an Organizer, I want to swap or remove a Registered deck after the first Round has started, so that I can fix mistakes.
  6. As a Participant, I want my Registered deck to become a copy when the first Round starts (or when I join late), so that later edits to my deck don't change what I played.
  7. As a deck owner, I want a Deck checkpoint named after the Tournament set in my Deck history at that point, so that I can find the version I played.
  8. As a deck owner, I want to rename or remove that Deck checkpoint without affecting the Tournament, so that my Deck history stays mine.
  9. As an Organizer, I want a warning when a Registered deck's format differs from the Tournament's or breaks its rules (legality, bracket), so that I can talk to the player; rule-zero exceptions mean it never blocks.
  10. As a Participant, I want to decide when registering whether others may see my list after the Tournament, defaulting to shared for public decks, so that I control my private decks.
  11. As a Participant, I want other Participants' lists hidden until the Tournament is Finished, so that nobody gets scouted mid-event.
  12. As a viewer of a Finished Tournament, I want to see the shared Registered decks next to the standings, so that I learn what did well.
  13. As a viewer, I want a Registered deck to stay visible even after its deck was deleted, so that the record stays complete.
Running Rounds
  1. As an Organizer, I want mtg to suggest the number of Swiss Rounds from the player count and Ruleset, so that I don't have to look it up.
  2. As an Organizer, I want to set and later change the number of Swiss Rounds, so that I can adapt to time and turnout.
  3. As an Organizer, I want to have the next Round paired and see it as a preview, so that I can check it before anyone sees it.
  4. As an Organizer, I want to edit any Match in the preview, so that I can handle special requests or mistakes.
  5. As an Organizer, I want the first Round paired at random, so that it's fair.
  6. As an Organizer, I want later Rounds paired by Match points without rematches, so that the Swiss system works.
  7. As an Organizer of a one-on-one Tournament, I want the Bye to go to the lowest-ranked Participant without a Bye so far, counted as a 2-0 win, so that it follows the MTR.
  8. As an Organizer of a multiplayer Tournament, I want uneven player counts handled as the Ruleset says (smaller Matches or Byes), so that nobody waits without reason.
  9. As an Organizer of a multiplayer Tournament, I want repeat opponents avoided wherever possible, so that players meet new people.
  10. As a Participant in a multiplayer Tournament, I want Seats in Swiss Rounds to favour whoever has sat late most often, so that the first-player advantage evens out.
  11. As an Organizer, I want to start a Round, which starts the timer, so that everyone plays at the same time.
  12. As a Participant, I want to see my Match (table number, opponents, my Seat) and the timer live on my phone, so that I know where to sit and how long is left.
  13. As a Participant, I want the timer to show when time is up, with the extra turns shown as information only, so that we know how to finish.
  14. As an Organizer, I want a Round to end when every Match has a Result and none is disputed, so that the next Round can be paired.
  15. As an Organizer, I want to take back a Round as long as the next one hasn't started, so that I can recover from mistakes.
Results
  1. As a Participant, I want to report my Match's Result myself, so that the Organizer doesn't have to walk around.
  2. As a Participant in a one-on-one Match, I want to report the game score (e.g. 2-1 or 1-1-1), so that game-based Tiebreakers work.
  3. As a Participant in a multiplayer Match, I want to report the winner or a draw, so that the single Game is recorded.
  4. As a Participant, I want a reported Result to count at once, so that four people at a Commander table don't all have to confirm.
  5. As a Participant, I want to dispute a Result reported by someone else at my Match until the Round ends, so that a wrong report gets fixed.
  6. As an Organizer, I want a disputed Result to hold the Round open and to be flagged to me, so that I settle it.
  7. As an Organizer, I want to set or correct any Result at any time, also after the Tournament is Finished, so that the standings are right.
  8. As an Organizer, I want the standings recomputed after a correction while the pairings stay as they were, so that history isn't rewritten.
  9. As an Organizer, I want to report Results for Guests and for locked Accounts, so that they can still play.
  10. As a locked Account, I can no longer report or dispute anything myself.
Standings and Top cut
  1. As a Participant, I want live standings with Match points and Tiebreakers as the Ruleset defines them, so that I know where I stand.
  2. As a Participant, I want the Top cut seeded by standings (Top 8 one-on-one as 1v8, 4v5, 2v7, 3v6), so that it's fair.
  3. As a Participant in a multiplayer final, I want Seats by seed with the top seed going first, so that a better Swiss result pays off.
  4. As an Organizer, I want a Participant who Drops before the Top cut starts to be replaced by the next in the standings, so that the cut stays full.
  5. As an Organizer, I want a Participant who Drops during the Top cut to lose their current Match without replacement, and the rest of a multiplayer Match to play on, so that the cut continues.
  6. As an Organizer, I want the Tournament Finished when the Top cut (or the last Swiss Round without one) is over, so that it's final.
Entering afterwards
  1. As an Organizer, I want to enter a Tournament afterwards with a past date, setting every pairing and Result myself, so that an event run on paper is recorded.
  2. As a Participant, I don't want pushes or report prompts for a Tournament entered afterwards.
Visibility and discovery
  1. As anyone, I want an overview of public Tournaments filterable by upcoming/past, format and location text, so that I find events.
  2. As an Account, I want a "my Tournaments" view, as Organizer and as Participant, so that I find mine quickly.
  3. As an Account, I want public Tournaments I own or played in listed on my profile, so that my tournament history is visible.
  4. As anyone with the link, I want to see an unlisted Tournament, so that a group can share it privately.
  5. As an Organizer, I want a private Tournament visible only to its Organizers and Participants.
  6. As an Organizer, I want self sign-up only possible for public or unlisted Tournaments.
Live updates and Web Push
  1. As a Participant, I want Matches, timer, Results and standings to update live in the app without reloading, so that I can follow the event.
  2. As a Participant with an Account, I want a Web Push when a Round is paired, naming my table and Seat, so that I don't miss the start.
  3. As a Participant, I want a Web Push when my Result is disputed, so that I can react.
  4. As a Participant, I want a Web Push when I reach the Top cut, so that I stay around.
  5. As an Organizer, I want a Web Push when a Result is disputed, so that I settle it quickly.
  6. As an Account, I want to subscribe each of my devices to Web Push and turn tournament pushes off in my account settings, so that I control notifications.
  7. As anyone, I don't want any mails from the tournament planner.
Interplay with Accounts
  1. As a deleted Account's opponent, I want the deleted Account's Participants to turn into Guests named by its last username, so that standings stay intact.
  2. As a Co-organizer, I want to become Owner when the Owner's Account is deleted, so that the event survives.
  3. As a viewer, I want a public or unlisted Tournament whose Owner was deleted without Co-organizers to go to the Tombstone, so that links keep working; a private one is deleted.
  4. As a Participant, I want standings to show current usernames after a Rename.
  5. As an Admin, I want Statistics snapshots to count Tournaments, all and Finished, so that I see the feature's use.

Implementation Decisions

  • Scope of the service: everything lives in the existing mtg service and its frontend; no new service.
  • Domain model (rorm, with migrations): Tournament (settings, state, Owner, live or entered afterwards, visibility with share token as for decks, entry fee amount and currency), Co-organizer, pending handover, Participant (Account or Guest name, unpaid flag, dropped-after-Round, late-from-Round, list sharing choice), Registered deck (link to the deck while it exists plus its own copy of the Deck entries once pinned), Round (number, stage Swiss or Top cut, state previewed/running/ended, timer start), Match (table number, Match length), Match seat (Participant, Seat number), Result (game score or winner/draw, reported by, state reported/disputed/confirmed), Web Push subscription per device.
  • Tournament states: Registration → Swiss → Top cut → Finished; Cancelled from any state. Rounds: previewed (editable, invisible to Participants) → running → ended; the latest ended Round can be taken back while no later Round is running.
  • Pairing and standings engine as a deep, pure module with no database access: given a Ruleset, the Participants (with Drops, Byes so far, Seat history) and all Results so far, it returns the next Round's Matches with Seats and Bye, the standings with Tiebreakers, the suggested round count and the Top cut seeding. Rulesets are a closed set chosen per Tournament, never edited:
    • MTR (Table size 2): match points 3/1/0, game points per game, Bye = 2-0 win, OMW%/GW%/OGW% with the 0.33 floor, Byes left out of opponent averages; pairing as maximum-weight matching within point groups with rematches forbidden and pair-downs minimized; Bye to the lowest-ranked Participant without one; round counts and Top cut sizes from MTR Appendix E; Top 8 seeding 1v8, 4v5, 2v7, 3v6.
    • TopDeck (Table size 3–5): 5/1/0, Byes allowed (at most one smaller Match, then Byes), its Tiebreakers and floor.
    • Multiplayer Addendum (Table size 3–5): 7/1/0, no Byes, fewest smaller Matches and each Participant in a smaller one at most once, its Tiebreakers and floor; filling Matches from the top and swapping leftovers as in the Addendum (reference implementation canivete).
    • Seats in Swiss Rounds: whoever has had early Seats least often gets them first (canivete's weighted seating), ties by rank, then random. Top cut: Seats by seed, top seed first.
    • Repeat opponents in multiplayer are minimized, not forbidden.
  • Registered deck pinning: when the first Round starts (or a late Participant joins), each Registered deck copies its deck's Deck entries and sets a Deck checkpoint named after the Tournament (ADR 0002). Afterwards the copy never changes with the deck.
  • Result workflow: a report counts at once; any other Participant of the Match may dispute until the Round ends; a dispute blocks ending the Round; Organizers set or correct any Result at any time; corrections after Finished recompute standings but never re-pair.
  • Unpaid Participants: only exist when the Tournament has an entry fee; count towards the limit; are skipped by pairing; the paid flag is visible to Organizers only.
  • Account lifecycle hooks: Account Deletion turns its Participants into Guests named by its last username, passes owned Tournaments to a Co-organizer, else to the Tombstone when public or unlisted, else deletes them; Registered deck copies survive. Locked Accounts can't report or dispute; Organizers act for them. Statistics snapshots gain counts of all and of Finished Tournaments.
  • HTTP API: new handler domain for tournaments in the frontend API, grouped by auth level as usual: public read routes (overview, a Tournament by id or share token, standings, Matches, shared Registered decks after Finished), Account routes (sign up, register/swap/remove deck, report/dispute, Drop, accept/decline handover, my Tournaments), Organizer routes (settings, Participants, paid flag, Co-organizers, pairing preview and edits, start/end/take back Round, set Results, Top cut, cancel), Owner routes (delete, remove Co-organizers, hand on). Regenerate the frontend client with just gen-api mtg.
  • Live updates: a Server-Sent Events endpoint per Tournament that emits change events (Round paired/started/ended, Result reported/disputed/set, Participant changes, standings changed); the client refetches what changed. Viewing rights as for the Tournament itself.
  • Web Push: VAPID keys from env config (added to the dev env), subscriptions stored per device and Account, a global per-Account opt-out for tournament pushes; sending goes through a small push-sender interface so tests can record instead of sending. Triggers: Round paired (to each Account Participant, with table and Seat), Result disputed (to the Match's Participants and to Organizers), reached Top cut. Tournaments entered afterwards send no pushes. The PWA needs a service worker that handles push events, while /api stays NetworkOnly.
  • Frontend: tournament overview with filters, my Tournaments, profile list, create/edit settings, Organizer run view (Participants with paid flags, pairing preview editor, round control, timer, disputes), Participant view (my Match, Seat, timer, report/dispute, Drop, deck registration), standings and Top cut bracket, shared lists after Finished; i18n in de and en per the coding standards.

Testing Decisions

  • Good tests drive external behaviour only: requests and responses at the HTTP seam, inputs and outputs at the engine seam, recorded pushes at the push seam. No assertions on table rows or internal helpers.
  • HTTP seam (main seam): in-process router against a throwaway Postgres via the mtg test harness, like the existing rename, deletion, lock and statistics tests. Covers creation and settings, sign-up modes and limit, unpaid Participants, Co-organizers and handover, Round lifecycle and take-back, report/dispute/correct, Drop and late Participants, Top cut replacement, Finished/Cancelled, Registered deck pinning with its Deck checkpoint and survival after deck deletion, list visibility before/after Finished, visibility levels and share links, Account Deletion/Lock/Rename effects, statistics counts, and the SSE stream emitting events on changes.
  • Engine seam: pure unit tests of the pairing and standings module, per Ruleset: no rematches in one-on-one, Bye assignment, pair-downs, MTR tiebreaker examples including the floor and Byes, multiplayer remainders (e.g. 9, 10, 11 players under each Ruleset), repeat-opponent minimization, Seat balancing over several Rounds, round count suggestions, Top cut seeding. Property-style checks where cheap (every non-dropped, paid Participant appears exactly once per Round).
  • Push seam: the recording push sender asserts who gets which push when, including the opt-out and no pushes for Tournaments entered afterwards. Real delivery to push services is not tested.
  • Frontend: vitest for pure client-side helpers where any arise (prior art: the deck rules tests).

Out of Scope

  • Leagues.
  • Multi-stage Top cuts for multiplayer, e.g. semifinal pods (#171).
  • Carrying a Guest's results over to an Account (#172).
  • Geo coordinates, a tournament calendar and reminders for tournaments nearby (#173).
  • Configurable points or custom Rulesets; Round robin and single elimination without Swiss.
  • Payment through the app; prizes.
  • Mails of any kind from the tournament planner.
  • Admin tools for Tournaments; Tournaments are not part of the Admin log.

Further Notes

  • The pairing research note in the mtg docs lists the sources and where the two multiplayer rule sets disagree; the TopDeck and Multiplayer Addendum Rulesets follow it, and its open questions (e.g. round count tables) should be resolved while implementing the engine.
  • The Registered deck relies on Deck entries, Deck history and Deck checkpoints as defined in the mtg glossary.
Dominant language
TypeScript
Stars
0
Forks
1
Avg merge
10h 20m
Merged PRs (30d)
36

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from myOmikron/code

All issues in myOmikron/code

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.