It’s a UNIX system! I know this! — A Highly Requested Tournament Report For F2F Toronto

Elaine Cao
17 min readSep 27, 2022

--

I think the average person has some intuitive understanding that programming isn’t quite the same as it is in the movies. In popular media, a genius hacker codes some solution from scratch in the space of an hour or two, solving a problem that would otherwise doom the other protagonists. But in reality, programming is slow. Features take a while to implement, and its not reasonable to expect a programmer, no matter how good, to solve a problem ad hoc.

From XKCD

Surely, “just whip up a solution in an hour” doesn’t ever happen in reality.

Right?

…right?

On Saturday, September 24, I had the opportunity to do something that most programmers only fantasize about. And it worked.

This is that story.

A bit of background.

On the weekends, I am a “Magic Judge”, which means that I am a person who helps in the organization and logistics of running Magic: The Gathering tournaments. Most Magic players know judges as the people who help answer rules questions and resolve disputes between players, and that is in fact a large part of the role, but judging also involves making sure that tournaments run smoothly and solving any logistical challenges that may come up, and this is the more difficult part.

I am a Level 2 judge, which means that I am certified as, in general, more experienced than the vast majority of judges, though I am not yet Level 3, which is the highest level. (I’m hoping to reach that level soon.) As part of my aspiration towards Level 3, I have been spending my weekends travelling to other cities to help run large events, in the hopes of gaining experience and rubbing shoulders with other experienced judges and learning from them. On this particular weekend, I was in Toronto, just a few hours away from my home base in Montreal. I road tripped to the event with my friend Maxime, who is another Level 2 judge with Level 3 aspirations.

In the Magic judging community, it is common, but not required, for judges to write “tournament reports” after tournaments. Some, like me, will only do it in exceptional circumstances where I feel like I have a lot of interesting things to say. Others do it for every event, and still others never write tournament reports- its a matter of personal taste. My goal here is to write something that has some aspects of a tournament report, but also has aspects relating to the implementation details of the actual fix I came up with, since I’ve been asked about it at length by dozens, if not hundreds, of curious community members who heard what happened. I’m also going a bit more in depth about Magic tournament logistics than the average tournament report would (as you can see from the previous two paragraphs), since I plan on also posting this in technology-related places so I do not want to assume knowledge.

Anyway, this is what happened in Toronto on the morning of September 24.

Calm before the storm

Max and I arrive at the venue at 7:50 AM, after the obligatory stop at Tim Hortons (we are Canadians, after all). This is fairly normal operating procedure; the tournament organizer, Face To Face Games, is an organization we’ve both worked with dozens of times in the past, and they always ask judges to be at the venue at 8AM so that we have plenty of time to do some last minute preparation before the doors open to players at 9 and the event starts at 10. We make small talk with the other judges, and I make fun of their choice of Starbucks drinks and tease another judge for “being old” after he mentions having judged a major event in 2004.

We head into the venue and two judges- Mike Hill, the tournament organizer representative, and Sierra, a friend and a local Toronto judge- are already hard at work solving some technical issue. Apparently, they’ve been having issues with the venue wifi, and they’re also having trouble properly connecting the printers. This, once again, is pretty standard. These sorts of small issues happen all the time, and are not super concerning. After some talks with venue staff and some minor troubleshooting, we’re up and running. As players start to trickle in, I grab the microphone, excited. “Magic players! Welcome to Face to Face Tour Toronto! Please be sure to check in at the registration desk!”

A selfie I took before the event begun, featuring a photobomb from a player

For this tournament, we are using Wizards Eventlink, a webapp developed by Wizards of the Coast, the designers of Magic. I’ve publicly criticized Eventlink multiple times in the past for its lack of features, but Wizards effectively mandates that we use this software to run events because they have internal metrics tracking to see attendance levels of particular tournament organizers.

We begin checking players into Eventlink, as is SOP for any Magic event nowadays. Soon enough, 10AM rolls around, and we press the button to start the event. 135 players. Quite a bit smaller than I had originally anticipated, but that just makes it a slightly easier day. Let’s do this.

“Magic players! Seatings for the player meeting have been pushed to your Companion [player-facing Eventlink] apps, and paper seatings are going up on the pairings boards.”

But there’s a problem. Seatings have not been pushed to the Companion apps.

Uhm.

Crisis

Most judges are no stranger to stability issues with Eventlink. Architects of the service state that the software has 99.999% uptime, a claim that was immediately laughed at by anyone who has ever had to work with the software and had it go down during a small in-store event. With this in mind, we hoped that we would be able to wait it out and the service would soon come back up.

This did not happen.

We weren’t even able to print the seatings. We managed to eventually seat the players since we had the seatings via a ~28 inch TV. but every new Eventlink tab immediately crashed upon loading, and it seemed that the only one we still had was the slowly-scrolling “mirror” tab that we had on the display, and we weren’t able to print that tab due to the way it was formatted. And, of course, Companion seemed to be down for all players. We tried to wait it out, but there wasn’t any ETA for a fix.

At 10:35, the decision is made to move the tournament to MTGMelee, another webapp solution, originally designed as a third-party competitor to Eventlink which has many more features and generally higher stability. Unfortunately, we couldn’t get this audible to work either. MTGMelee seemed to require players to register individually with their emails, and our players seemed to be having issues confirming their accounts.

Further, it seemed that every other event around the world was also experiencing the same issue with Eventlink, so MTGMelee’s metaphorical server hamsters eventually died.

We attempted to move the event back to Eventlink, creating a brand new event, since it seemed to be showing a bit more life, but that didn’t work out.

We also investigated using other tournament services, like Challonge, but we weren’t familiar with it and we weren’t confident that it had the operational requirements that we would require. We even tried downloading YuGiOh’s tournament software, since we had heard that that game follows a similar tournament format, but we couldn’t get that to work.

I should probably note something here. Some people are wondering why we don’t start by pairing the tournament manually in Excel or a similar program, since pairing the first round should be relatively simple, and then later import the results into Eventlink. Well, as mentioned in my tweet above, Eventlink doesn’t allow “manual” pairings. There is no way to just import an arbitrary tournament in a given state and have the service take over once it comes back online.

Anyway, at this point, our only concern is to get some sort of solution where we can get the players playing, as long as there is some confidence that we can figure out the rest later.

Its 11:12 AM.

A Possible Solution

72 minutes into this crisis. Players are unclear what’s happening, and the judge staff is directionless. We have a crisis with no solution, and an unbounded end time, since we have no idea when Eventlink (or any other service) is going to come back up. I start thinking about solutions, no matter how insane.

What if I just generated pairings with Python?

“Hey Mike, what if I wrote up some code to pair the tournament?”

“How long would that take to set up?”

“Maybe an hour.” I leave out that this is an incredibly optimistic estimate that requires everything to go right, and I haven’t even begun to sort through the operational requirements.

Mike weighs this for a second. “Get started.”

I run out of the venue to get my laptop bag from the car. My planned judging duties are taken over by someone else and all standby judges- judges who were planning to play in the event but are available to help if something happens- are activated.

This laptop is barely set up to code. About a month ago, I had quit my programming job to go back to university for a non-technical field, and the only reason why there’s any coding software on this at all is because I decided I might want to mess with a random personal project. So I recently installed VS Code, Python 3.10.7, and a MINGW terminal. It has none of my personal projects and no pre-written code that I can copy/paste. I try to sign into Github, hoping to get some helper functions from my private repositories. For whatever reason, I’m locked out.

Shit.

“Hey Elaine, if we just generate pairings in Excel, can you use those for later rounds?”

“Uh, sure.” Why am I agreeing to this? I have no idea what the solution is going to be and I just hit a major snag.

Okay. Deep breath. In an hour, I have to have something that can take in the Round 1 results and generate Round 2 pairings.

I ask for a mouse (why didn’t I bring my own? I’m such a mess), headphones to drown out noise (seriously! I always have earbuds in my bag, why were they not with me this time?), something with caffeine and sugar, and someone to stand in front of my station and intercept people trying to ask me questions. I am provided these things.

At 11:25, with no other solution in sight, Round 1 pairings are generated in Google Sheets and posted. The players are finally seated and play begins. At this point, I’m on a clock until the round ends and I must produce pairings for the next round. We’ve already delayed this tournament an hour and a half, and players will be pissed if I’m still coding- not that I could properly do it with the pressure of an entire convention centre room waiting on me.

Let’s go.

Implementation Requirements

From having been a judge for several years, I thankfully have quite a bit of familiarity with the tournament algorithm.

  1. Pairings are determined according to the Swiss system.
  2. Players are given 3 points for a match win, 1 for a draw, and 0 for a loss.
  3. Each player is paired against a random player with the same number of points. The only exception is when there is an odd number of people with the same number of points, which may result in a “pair-down”.
  4. Players may never play the same opponent twice within the same event. This overrides the previous rule and the next rule, and may create additional “pair-downs”.
  5. If there is an odd number of players, one random player with the least number of points will be issued a bye and automatically win their round.
  6. The final round pairing is slightly different from #3, but that’s definitely not a problem right now.
  7. The first tiebreaker is the average of the match win percentage of all your opponents (OMW%). The second tiebreaker is your personal game win percentage (GW%)- each match is best of three, and can be won 2–1 or 2–0 (most of the time; there are some edge cases). The third tiebreaker is the average of the game win percentage of all the opponents (OGW%).
  8. Players are able to drop and no longer be in the event, but these players should still be counted in the tiebreaker math.
  9. I should be able to output not only pairings, but also standings according to tiebreakers as previous determined.

It should be noted here that there are multiple valid pairings for any given tournament state. Unlike pairings for other games, like in Chess, where seeding is determined by some pre-determined factor, any pairing within a “bucket” (as I decide to call them) of players with the same number of points is valid, as long as it doesn’t break rule #4. This is very good for me.

I have another advantage working for me, which is that I will be here to run the scripts myself, and there are plenty of experienced tournament people around me. This means that I can manually fix any corner cases that may come up, if these corner cases would be too complex to code properly.

I decide to use this advantage in two ways: Firstly, there is not a trivial solution to preventing players from playing each other again, especially in the situation where the pairing algorithm would have to create additional pair-downs in order to make it work, so I decide to ignore that. I decide my solution will be to run the algorithm multiple times and print a list of collisions each time, with the idea that if a particular collision is forced then I will see it over and over and I can fix it manually by creating more pair-downs, and if a collision is not forced then I will simply continue to repeatedly run the script until no collision exists. This is dirty, but it works and I don’t have a lot of time.

I also decide to use this advantage to simplify the tiebreakers, and only plan to code the first tiebreaker. I figure that tiebreakers almost never go past the first, and I’m not going to worry about the unlikely corner case where the second or third become relevant- if needed, we can easily determine the second tiebreaker manually from paper records with some time. I pray to the event gods that we won’t have to go to the third tiebreaker. Further, this also would mean that I wouldn’t have to record individual match records, which would drastically simplify the record of the tournament state.

A final operational requirement is that I want to make sure is that I will always be able to roll back to a previous state. I’m fully aware that I’m writing this code with very little, if any, time for testing, and I do not want to have a bug permanently break everything. Further, I want to be able to manually fix any problems that might come up. So, I definitely want to save all tournament states from previous rounds, and I want these to be easily human-readable and editable.

I decide to work with csvs, since those can be easily imported and exported from Google Sheets, which seems to be what the Scorekeeper is using. I decide to have each row be a separate player, like such:

player_name,match_points,opponent_1,opponent_2...

Seems simple enough.

Note that I’m not saving the player’s record in any particular game, because I don’t need it. If I were to code in the additional tiebreakers, I would need more information, but I don’t have the time to come up with a more complex contract.

With this in mind, I begin to develop a plan. The plan will be to create buckets of players by match points, move players up as needed to create an even number in each bucket, add a dummy “BYE” player at the bottom if needed, and create pairings. I quickly type:

def pair():
# read state from file
# sort into buckets by points # create pairdowns # add dummy bye player # pair buckets # print pairings to file
if __name__ == "__main__":
pair()

Alright, now I have an idea of what to do.

“But, Elaine! You’re just reading the current tournament state, including all previous results, but how do you actually input results into this format?” you may ask. Well, that’s a problem for Future Elaine. I figure I may need to add more to this contract, and I don’t have the time to have to go back and change something afterwards if I do the results formatting first. I’m also not going to generate tiebreakers yet, because that’s not important right now, and I’m not going to deal with players dropping from the event, since players don’t typically drop until later rounds.

At around 12:25, I have approximately a functional pairing algorithm. I feel a lot better about getting this to work.

“Players in the Modern Open, that’s time in Round 1. Active player, finish your turn, and you have five additional turns. If you need assistance, please call for a judge.”

Ah, fuck. There’s an expectation that results will continue to trickle in over the next ten minutes or so, but showtime for me is very soon. Thankfully, there was apparently a few very long time extensions that bought me time.

Implementation, The Details

At this point, I still have to figure out a way to ingest results and add them to the csv that represents the tournament state. I look up from my workstation, tucked in the corner of the event stage. A box has been put in front of me, and result slips- hastily written on blank pieces of paper- are being put in it. I mumble some curses to myself.

“Alright, Sierra, here’s what we’re gonna do.” I say. I turn to my right, where she has Google sheets open to the list of pairings. I guess its time to come up with something, so I say the first thing that comes to mind. “With that pairings list, I want you to put a 1 in the column to the right if the first player won, a -1 if the second player won, and a 0 if the match was a draw.”

“Got it.” She grabs the box of slips and starts putting in results.

Well, I guess I have to make this happen, huh?

I decide that I’m basically going to use the existing tournament state to make a dictionary with the player name as the key, so for each pairing I’ll find the relevant players, update their match point total and the list of opponents they’ve had. I have a moment of panic when I think about “what if the player name isn’t in the dictionary”, but I don’t have a ton of time to think about that and I just have to get something written. Yet something else to put on the pile of possible errors I may have to deal with.

def record():
# open pairings and state
# for each pairing # find players # add to crosstable # adjust match points # write new state to file

I look up. Its 12:44. Thankfully my results haven’t come in yet. Evidently there was a long time extension? Usually, I’d be frustrated, but I’m glad to have the extra time.

Debugging In Prod

I finish my script about a minute before the last result comes in, at 12:59.

Oh, I guess I didn’t test this at all, huh? Well, let’s hope it works.

It does not, instead spitting out a pile of errors.

During my limited time in Montreal, the Quebecois have taught me some choice curse words, and I choose to utilize them for this situation.

I furiously start breakpointing through my code, and after a minute or two I can at least properly import the match results after Round 1. Something, at least.

I import a csv into our shared Google sheet. “This is the standings as I have them.” I instruct the judges standing by, waiting for me to continue the tournament. “Print these and make the players check to verify that they’re correct before I pair.” I leave out the part where I’m very obviously stalling for time to fix my code, and need to give the players something to do before they start to riot. A few minutes later, the script finally generates valid pairings. I ship these and lean back, doing a fist-pump.

Its 1:13, 14 minutes after I received the results for Round 1. Players are seated and begin playing. Took a bit longer than I thought, but I’ll take it as a victory.

I still have some work to do, but nothing has exploded and I have generated valid pairings. Mike demands that I take a break before fixing what needs to be fixed for the next round. I whole-heartedly agree.

I leave the tournament hall and scream into a pillow, taking the time to tweet this on my way out:

Cleanup

After a few minutes of rest, I get back into my chair. I still have to figure out how to handle drops. I instruct Sierra to make another column, and put a “1” if the first player is dropping, a “2” if the second player is dropping, and a “3” if both players are dropping. I decide that, rather than changing the structure of the csv, I’ll prepend the players’ names with an asterisk to indicate that they’re dropped. I make sure to filter out dropped players in the pairing script. This is ready in time for time to be called in Round 2, though, once again, I didn’t have the time to properly debug this and there was a few minutes of me frantically fixing things and rerunning the script.

I also calculate the first tiebreak, and write a few lines of code to output a standings table that displays that tiebreak. This is ready by the end of Round 3. I also add a few lines of code to output pairings sorted by player name as well as by table number, since that was previously a manual step that Sierra had to do herself with some extremely gross Google Sheets hacks. Starting in Round 4, I have round turnover times below a minute.

The next few rounds are spent making a few minor improvements and solving corner cases that did not come up in the first few rounds but are likely to come up in later rounds. The power goes out for about 30 minutes during Round 4, just adding to the farcical nature of the day; thankfully it comes back on.

I snapped this picture of a dark tournament venue right after putting my laptop into power saving mode.

Face to Face orders sushi for the team as recognition for the insane day that we’ve had, and I partake gratuitously. During the final few rounds I even find time to actually watch players play Magic! I had almost forgotten what I had come to do in the first place. Evidently, tales of what I had done had made their way to the players, and I was showered in praise, which I can always appreciate.

After taking a well-deserved proper break towards the back half of the event, I write up the pairing algorithm for the final round, which is slightly different. Rather than pairing each bucket of players randomly, the algorithm sorts all players by standing, including tiebreakers, and pairs 1v2, 3v4, etc, while also doing some massaging in order to ensure players don’t play against opponents they’ve already played against. I decide, once again, to do this massaging manually. This took a little longer, and I missed one small issue, but it was thankfully at the lower, not-in-top-8-contention tables, and it was resolved with assistance from the Head Judge.

Aftermath

I print standings after 8 rounds of Swiss at 10:45 PM; typically, 8 rounds of swiss starting at 10AM would be expected to finish around 7:30PM. Since I started at 8AM, this makes it a 14.75 hour day at the event, which is somehow not a personal record.

I have to be in Montreal for an event at 11AM the next morning which I committed to weeks ago. Maxime volunteers to take my keys and drive home. I’m very glad to allow this and I collapse in the passenger seat, falling asleep almost instantaneously. I arrive home at 5:30AM and collapse for a well-deserved four hours of sleep in my own bed.

Face to Face Games promises to reward me heavily for saving their event and gives me promises of even more in the future.

Acknowledgements

I have to acknowledge Sierra, for her flexibility with this new system that I made up on the fly, Maxime, for being extremely supportive this entire day and also volunteering to drive me home, Face to Face Games, for rewarding me handsomely for coming up this solution, and the rest of the judge staff for running the event without further issues and helping me figure things out when required.

Also, please, Wizards of the Coast, I’m just saying: This could have been prevented if we just had manual pairings in Eventlink, because we could have started the event in a spreadsheet and imported into Eventlink when it came back up. If Face to Face Games didn’t happen to have a judge who is also a competent programmer that works well under high stress, this event literally would not have happened and you would have been cost six figures or more in bad press. Let me know where to send my invoice.

Update (2022/29/09)

Some of you have asked why I didn’t just do X. Here is my answer:

https://twitter.com/Oritart/status/1575177486056718337

--

--

Elaine Cao

I’m a Level 2 Magic judge who plays a lot of blue cards. she/her/hers, www.twitter.com/Oritart