Event-Driven Programming

Summarized using AI

Magic is Ruby, Ruby is Magic!

Ryan Bigg • April 11, 2024 • Sydney, Australia

In the talk titled "Magic is Ruby, Ruby is Magic!" by Ryan Bigg at RubyConf AU 2024, the speaker explores the intersection of programming in Ruby and the card game Magic: The Gathering. He shares his passion for Ruby, which he has used for 18 years, and explains his journey into building a simulation of Magic: The Gathering using this programming language.

Key Points:

  • Introduction to Ruby and Magic:

    • Ryan describes how Ruby's simplicity and the extensive community keep him engaged.
    • He recounts moving to Warrnambool and discovering Magic: The Gathering, especially the Commander format.
  • Game Mechanics and Complexity:

    • An overview of the game's mechanics is given, emphasizing the complexity and various states within the game.
    • Ryan highlights the extensive rulebooks and the fundamental Golden Rule of Magic, which states that card text takes precedence over the rules.
  • Building the Card Game in Ruby:

    • Ryan set out to create a simulation of Magic using Ruby to practice his programming skills in a lower-stakes environment than his fintech job.
    • The speaker introduces coding aspects through Domain-Specific Languages (DSLs) in Ruby to represent various cards, beginning with Basic Lands, followed by creature cards with power, toughness, and abilities.
  • The Game Loop Concept:

    • He explains the concept of the game loop where actions by players cause effects that change the game state.
    • Ryan outlines specific player actions (e.g., conceding, playing a land, tapping a land, casting a spell) and how they interact within the game loop.
  • Events and Triggers:

    • Ryan introduces events generated from actions and effects, discussing triggered abilities that respond to these events.
    • He explains both triggered abilities and replacement effects, demonstrating how various cards can interact, sometimes leading to complex situations.
  • Programming Challenges:

    • Ryan describes challenges in implementing the game, like ensuring creations won’t lead to infinite loops during gameplay.
    • He emphasizes the iterative learning process and suggests that practicing faced challenges enhances Ruby skills.

Conclusion:

  • This project's primary goal is practicing Ruby programming, not achieving a perfect simulation of Magic, encouraging others to engage in deliberate practice.
  • Ryan invites developers to explore their Ruby skills, considering projects that push their boundaries.

In closing, Ryan offers his GitHub repo for those interested in the code behind his project, inviting collaboration and discussion about improving Ruby programming through engaging projects.

Magic is Ruby, Ruby is Magic!
Ryan Bigg • April 11, 2024 • Sydney, Australia

Magic: The Gathering is a popular card game with over 20,000 unique cards where the cards themselves define the rules of the game.

Can we write a Ruby app to simulate games of Magic: The Gathering? This is the story of one man's attempt to do just that.

RubyConf AU 2024

00:00:04.720 Hello everyone! This year marks my 18th year of writing Ruby code. Ruby has held my attention for 18 years, and I attribute that to one major thing: Ruby is magic. It has bewitched me with its simplicity of syntax and the ‘batteries included’ nature of its standard library and wealth of gems. Not to mention its awesome worldwide community. I absolutely love writing Ruby code, and it brings me so much joy. When I'm writing Ruby, it feels like I'm making magic.
00:00:19.279 I now write Ruby from a little coastal town called Warrnambool. At the end of 2020, my family decided that we didn’t want to pay over a million dollars for a 1.5 bedroom box in an outer suburb of Melbourne. Fortunately, around the same time, working from home became more acceptable due to the pandemic, which you might have heard of. So, we decided to move to my wife's hometown of Warrnambool, or as I affectionately call it, the outer west of Melbourne.
00:01:01.920 I work from here in the fintech space at a company called Fat Zebra. I’m not making that name up; it’s really the company’s name. I think it’s one of those randomly generated names, like Post Malone or Childish Gambino. After moving to Warrnambool, I didn’t know many people outside of my family, and I wanted to meet some local nerds to make some new friends. Long story short, I ended up going to one of the two local board game stores in town. Yes, Warrnambool is actually big enough to have two board game stores and a cinema, as well as a JB Hi-Fi.
00:01:37.919 I started to play a game called Magic: The Gathering. Magic is a trading card game where you can build decks of cards and play them with your friends—or enemies, depending on how you see them. The format I usually play is called Commander. It’s a four-player free-for-all format. The aim of the game is simple: players have a life total, and your goal is to reduce everyone else’s life total to zero before yours hits zero.
00:02:02.520 Here’s a little video showing just how ludicrous this game can get sometimes. Okay, snug, there are just a few new mechanics since you last played Magic, but I think you should be able to get the hang of it. Just let me know if there’s something you don’t understand. I’m going to pay seven mana to cast this adventure spell, which makes three bear tokens. But after I cast this spell, this other part goes into exile, and then I can cast that part later.
00:02:21.840 When I cast that, this Davolt Tower gives me two energy counters, and I can spend these to activate an ability later. It’s kind of like mana, but it’s a resource that’s different from mana. So anyway, I get my three bears now. I’m going to use this die to represent how many I have. This is the number of bears; these aren’t counters. Then I’m going to minus my planeswalker to put this creature into play from my library. This one enters the battlefield with a shield counter on it. Now this one is a counter, but it’s not a +1/+1 counter, and when this creature enters the battlefield, it becomes daytime.
00:03:43.759 This is important; we’ll have to remember this for the rest of the game. Okay, then I’ll attack you with this thing. Now, this doesn’t deal normal combat damage; it deals damage in the form of poison counters. So you get one of these, and you have to track that for the rest of the game as well. I should stress that Magic is not usually this complicated, but I use this as an example of two things: how Magic: The Gathering play sounds, and also what you sound like when you’re onboarding a new starter at your company.
00:04:06.279 Now, as much as I’d like to talk about bettering your onboarding practices, that’s a talk for another year. I enjoy talking about Magic much more, so let’s do that. But this isn’t Magic; it’s RubyConf, so why am I here talking about Magic: The Gathering at a Ruby conference? Because about 18 months ago, I decided to continue my streak of making magic with Ruby and create a simulation of Magic: The Gathering.
00:04:38.679 I absolutely love the mechanics of this game; it stimulates the same part of my brain that programming tickles, but in new and interesting ways. So I thought, why not combine my old love of Ruby with my new love of Magic? My day job in fintech doesn’t provide much room for experimentation or error; companies tend to get quite grumpy when you mess with their ability to transact real-world dollars while showing off your super awesome Ruby skills. So, I needed somewhere to practice where the stakes were lower.
00:05:10.440 Even after 18 years of writing Ruby, there are still things I can learn. Magic seemed like a good choice because it has all these different programming-like concepts: it’s got a stack, it’s got state machines, it’s got event-driven code, and what’s more flavor-of-the-week than event-driven code and microservices? So, I want to take you on this journey of figuring out how to build this wonderful card game in Ruby. Along the way, I’ll show you some of the cards and talk about the mechanics within the game that I find the most interesting.
00:05:36.920 And perhaps by the end of this, you might want to join me in building this or running away screaming. If you’re going to build any game in any language, the first step is to learn how to play that game. With most games, you can do that by familiarizing yourself with that game’s rule book. Here it is! This is what it feels like when someone says, 'Go read our employee handbook on Confluence.' The rulebook here outlines every rule of the game in meticulous and pedantic detail; it is a rules lawyer's dream. However, on the rules page of the official Magic site, there’s a clear warning: these rules are not meant to be read beginning to end. Here be dragons!
00:06:27.679 A brief aside: another card game you might be familiar with that also has a rulebook is Uno. Uno has a rule set that fits onto a double-sided A4 sheet of paper. These Magic rules, however, are 291 pages long! So when we build Magic in Ruby, we’ve got 291 pages of rules text that we need to turn into Ruby. To read all of these rules out loud would take about 21 hours. We would be here until the wee hours of tomorrow morning.
00:06:58.839 But what I will share with you is one particular rule that I think you’ll like, called the Golden Rule of Magic, straight from the rulebook itself. It says, rule 101.1: whenever a card’s text directly contradicts these rules, the card takes precedence. The cards themselves have rules written on them; that’s what makes this game interesting. The cards make the rules, too! So when building this card game in Ruby, not only do we need to take into account the 150,000 words in the rulebook, but then we need to completely disregard those rules whenever a card says the rule is otherwise.
00:07:34.000 As Caitlyn said, there are about 25,000 unique cards in Magic, and some of these cards change the rules of the game when played. If we count up all the rules text on all of these cards, we get about 834,000 words, or about another five and a half rule books' worth! And there are new cards coming out all the time at a rate that npm packages would be jealous of. Putting this together with the rulebook, we get about a million words describing the rules of this game, some of which, if printed on a card and played, will contradict and override the rules in the rulebook thanks to that Golden Rule of Magic.
00:08:09.000 Reading all of these rules will take us about five days. I have to get home before then! Not only this, but we also have something called the Oracle text, where the rules on the cards can sometimes be confusing or open to interpretation by the players of the game. When this happens, the creators of Magic come along to clarify those rules, publishing updates into what’s called the Oracle.
00:08:52.199 So, we have a card game that we want to model in Ruby that takes over a million English words to describe in its entirety, where those words can be found in one of three different places: the rulebook, on a card, or in the Oracle. Sounds like fun, right? Yes, enthusiastic agreement! I love it. So, I thought, why not give it a go as a way of practicing my Ruby skills? Fortunately, you don’t have to start at the first rule and work your way down.
00:09:32.680 Remember, the creators actively discouraged that. Instead, you can work just card by card, making sure that each card works as written as well as in the larger system. That’s where I began. Ruby lets us build DSLs (domain-specific languages) that can then be used to express Magic cards in a simple format. This card is called a Basic Land, and it’s a Plains. When we play this card, we can use it to create a resource in the game called White Mana, which you can think of as the in-game currency. There’s not much you can do in the game without mana.
00:10:06.360 Similarly, this is a Mountain, which works the same as a Plains, but instead of giving us White Mana, it gives us Red Mana. There are five different types of mana: white, blue, black, red, and green, each with corresponding basic land cards. We can express these very easily in Ruby. After implementing the basic lands in Ruby, I went looking for more of a challenge and found it in implementing creature cards. Creature cards have a type, they have a cost, they have power and toughness, and again, we can express all of this very easily in Ruby by constructing nice DSLs to abstract away the complexity behind the scenes.
00:10:51.840 This bit, italics at the bottom, is what we would call flavor text. It adds flavor to the game but doesn’t change the behavior of the game at all. Here’s a separate creature card with a little bit of a difference. Again, we can define the name, the cost, the creature type, power, and toughness the same as we did with the other card, but this card has an activated ability. An activated ability is an extra thing a card can do that usually costs extra resources.
00:11:09.720 In this case, we can define this ability by inheriting from ActivatedAbility and defining the cost and resolution method. The cards I’ve shown you so far sit at one end of that complexity spectrum. To get an idea of what the other end of that spectrum looks like, let’s take a peek by looking at Dance of the Dead. This card has the most text on it in the game, but I haven’t gotten to it yet. Now that we’ve taken that peek, let’s zoom out and talk about the game loop. I want to spend the rest of the talk talking about it, as it’s the glue that kind of pieces the whole thing together.
00:11:44.440 A game loop is something you’ll all be familiar with—central to everything I’m going to build our mental concept of the game loop bit by bit. Players take turns during the game; during those turns, players take actions. Actions then have effects, and when players take actions in the game, this causes the game to change, and that change is called an effect. The effects usually change the state of the game. The state of the game represents things like whose turn it is, what players’ life totals are, and what cards are currently in play.
00:12:43.600 And on it goes; players take more actions, which create more effects, which continue to modify the game. Maybe somebody wins. If you’re writing Ruby code to model actions, you do it this way: actions in a game are simple to model. We have a game and a player. We can then define a perform method on these actions to define what happens when actions are taken. This code is very generic. Now, I’d like to give four examples with Ruby code of concrete actions a player can take within Magic. I’ll tie these back to our loop as we go along.
00:13:30.080 The first action is that at any time, a player can concede the game—they can throw in the towel and give up. This is the simplest action a player can take, and they can do it literally anytime. The game then treats the player like they don’t exist, even if they are off in the corner, having a big sulk. Let’s talk about it in terms of the game loop: the action is to concede, the effect is that the player loses, and the state of the game is changed to decrease the number of players by one.
00:14:04.560 Action number two: playing a land. Players can put land cards onto the area in front of them called the battlefield to increase the amount of mana resources available to them. Playing a land starts off much the same as the conceding action, but this time we need to know the card for the action up front. The action’s object is the card, and we need to determine if, at the current time, this action can be performed by the player. Some actions in Magic have prerequisites that have to be checked to see if the action is valid.
00:14:39.200 A player can only play a land on their turn and if they haven’t already played a land this turn. Of course, there are exceptions to this printed on many cards. Lastly, we can resolve the card, which in this case means taking the card from the hand and putting it onto the battlefield. Now, again in terms of the gameplay loop, the action here is playing a land, the effect is that a card will be added to the battlefield, and the state is updated to reflect that.
00:15:05.280 Now that you have that land, you probably want to do something with it. The third action is tapping a land. 'Tapping' is used here in the sense of drawing a resource out of it. We’re drawing mana out of the land to use it. To indicate that a land is tapped, we turn it sideways. Tapping a Mountain gives you a single red mana resource, which you can then spend to perform another action in the game.
00:15:49.440 Before we move on to the next action, let's talk about tapping again. In the game loop, the tap action has the effect of tapping a card and adding a single red mana to the player’s pool of mana. The game state has changed in two ways: this time, the land’s state changes to indicate that it’s tapped, and the player's mana pool has grown by one red mana. Now, onto the fourth action. We’re going to cast a spell called Shock. Shock deals two damage to any target.
00:16:35.439 This is how we can represent casting Shock in Ruby. We’re casting the spell, paying its cost of one red mana, and defining the spell's target as player two. There’s a lot of code for the casting action. I tried to fit it on a slide, but it didn’t fit. It’s one of the more complex actions of the game, so you’ll just have to imagine it being very complex and scientific. This is the pretty face in front of all that code.
00:17:38.239 Now, back to the terms of the loop: the action here is to cast a spell, but this time the effect isn’t what’s written on the card. Instead, the card gets added to something called the stack. Let’s talk about the stack for a moment. Each new spell, when cast, goes onto the stack. When no other player has anything else to add, the stack of spells is processed from the top down. In the first case here, player one casts Shock targeting player two. If no other player in the game acts, Shock resolves, and player two loses two life.
00:18:23.039 The stack also lets players respond to each other's spells. Let’s look at a different example. This time, player one casts Shock again, targeting player two. In response to this spell being cast, player two casts another spell called Counterspell. It does what it says on the card: it counters target spell. This card can target any other spell that’s on the stack at the moment, not just the last one; it can be any spell deep in the stack. Player two wisely chooses to counter the Shock spell that’s underneath it on the stack.
00:19:18.440 This time, when the stack resolves, Counterspell counters the Shock spell, meaning that Shock never happens and the effect of Shock never happens. Player two's life total remains what it was. Using Ruby, we can write this interaction like this: Player one casts a spell, paying a red mana to do so, targeting player two. Player two casts Counterspell, which adds it on top of the stack. We then call game.stack.resolve to resolve the stack from the top card down, which makes Shock fizzle.
00:20:06.440 We’ve now covered four of the most common actions within the game: conceding, playing a land, tapping a land, and casting a spell, as well as a brief foray into the stack. Let’s go back to our game loop and keep building out our model. There are still unexplored places. We have actions, effects, and state, but we also have a fourth thing here that doesn’t have a concrete name yet.
00:20:58.679 Casting a spell, dealing damage, losing life, playing a land—all these actions sound like they should be in the same category. We’ve discussed these things as things that happen within the game, but we haven’t given them a collective name yet. I would call these things events, and that’s a good thing because that’s what the rulebook calls them as well—rule 7001 if you’re following along. Here’s where the events would sit within our game loop diagram: effects, once they happen, generate events that tell things in the game what has just transpired.
00:22:02.199 But why are they relevant to mention here? Because in Magic, cards within the game can respond to these events through a mechanism called triggered abilities. Triggered abilities happen when particular events occur during the game, such as whenever a player casts a spell or whenever a creature dies, or even at particular times within the game like the beginning or end of your turn. Everything in the game listens to these events and can respond to them by registering event handlers. You might recognize this as the Observer pattern.
00:23:01.639 Here’s an example of a triggered ability on a card and its event handler. The card is called a Johnny's Pride mate. It has a triggered ability that states: whenever you gain life, put a +1/+1 counter on this card. When this ability triggers, the creature gets a little stronger and tougher. In Ruby code, it looks like this: whenever a player gains life, the game notifies all observers in the game with an event, represented by the events LifeGain class. A Johnny's Pride mate sees this event and responds to it. When we trigger an ability, just like in earlier examples with actions, we sometimes have a prerequisite on that ability.
00:23:50.839 In this case, we only want to perform this ability if the player who gained life is the player who played a Johnny's Pride mate. We don’t want it triggered when opponents gain life, just one specific player. If the prerequisite is met, we can then perform that ability’s effect; in this case, a Johnny's Pride mate gets a little boost to its power and toughness in the form of a +1/+1 counter. Again, Ruby allows us to express this in a simple fashion. On top of triggered abilities that trigger with the words 'whenever,' 'when,' or 'at,' we also have another system in Magic called replacement effects.
00:24:47.159 Usually written with 'if,' these indicate that an effect is going to be replaced with a separate one. For example, there’s a card called Nine Lives. It says: whenever anything would deal damage to you, that damage is prevented. This card then gets an Incarnation counter—a different type of counter. Here’s how we implement the first sentence on this card: we check if the damage target is the same as the card’s controller. If it is, we replace that effect with another one that adds that counter instead. No damage is dealt.
00:25:38.639 There are also replacement effects that can boost existing effects rather than outright replacing them. This card, Doubling Season, says that whenever a counter would be put on a permanent you control, it puts twice that many counters on it instead. It boosts the effects of counters rather than replacing them. This applies to all counters, such as ones from a Johnny's Pride mate and Nine Lives. Here’s both a Johnny's Pride mate and Doubling Season next to each other. When a player gains life, a Johnny's Pride mate sees the event and goes to add that counter. Doubling Season sees that event; it replaces the event, and you get two counters.
00:26:46.800 Of course, when you have two Doubling Seasons on the field, when a player gains life, a Johnny's Pride mate sees the event and goes to add the counter. Doubling Seasons 1 and 2 both see the event, and you end up with four counters instead. I would recommend that setup. What I wouldn’t recommend is this: Nine Lives and Doubling Season. When the player is damaged, Nine Lives sees the event and goes to add counter 1. Both Doubling Seasons see the event, and you end up with four counters. Once Nine Lives receives nine counters, you lose the game.
00:27:35.319 I do not recommend replacement effects! I mention these because it took me so long to work out; I reckon it was at least two months before I figured out how to write this in Ruby. Until one day, I was looking at the rack middleware stack and realized that it’s essentially the same thing: a request goes through the middleware stack and can get modified by any of the middleware along the way. It’s like replacement effects—a new event comes in and goes through the cards in the game, and the response gets modified as it rolls through that stack.
00:28:06.200 So, I modeled replacement effects after that. All cards that care about an effect want to replace it, processing that effect one after the other, effectively forming a chain of middleware on the fly. Eventually, I’ll need to handle this differently, as players can choose the order of their replacement effects—rule 6116.1 if you’re still following. But this is fine for the current state of the engine. Triggered abilities and replacement effects connect the fourth pillar of our game loop back to actions.
00:29:22.840 Now, you might see a problem with this: an action can cause an effect, which can cause an event, which can cause further effects. There’s another loop inside our loop. As a concrete example of this, and my final example for this talk, we have a card called a Johnny's Chosen, which has a triggered ability that triggers whenever a card with the type enchantment (that’s the middle text bit) enters the battlefield. The effect this card then performs is to create a creature called a Cat onto the battlefield.
00:30:05.600 This means that when a player plays a card called Enchanted Evening, which itself is an enchantment, a Johnny's Chosen’s ability will trigger. When this ability triggers, it creates a Cat. The second card also has an effect that applies once this Cat has been created; it says all permanents are enchantments. This is known in the game as a static ability—it applies always. It’s not triggered; it’s not a replacement effect; it’s always happening. This means that this ability applies to our new Cat.
00:31:06.440 When our Cat is created, it starts out as a token creature (type Cat), but because of Enchanted Evening, it turns into a token enchantment creature (Cat) as soon as it’s created. This, in turn, triggers a Johnny's Chosen again, as the Cat is now considered an enchantment itself. Another Cat is created, and on and on it goes! Forever! What happens in this case? In real life, if you get this case in an actual game of Magic, we can consult the rulebook for guidance. It states that if a game somehow enters a loop of mandatory actions, repeating a sequence of events with nowhere to stop, the game ends in a draw.
00:32:05.719 This, of course, makes logical sense. But how do we do this in a programming language like Ruby? How do we detect if a player's action will cause the game to enter a state where it loops infinitely? Where it enters a state where the program never halts? If you’re able to figure this out, not only would I be grateful, but many others around the world would be grateful too. You’d probably win some big award, like a Nobel Prize or a Fields Medal.
00:32:38.519 It has been so much fun implementing the game loop and tying together all the different abilities for every card. This has been such a fun project to take on, and I really enjoy that the stakes are so much lower than in my day job, where I am constantly under pressure to produce results.
00:33:16.760 It’s a little Zen garden of a project that I’ve been tinkering away with these past few months. So far, the project has implemented a little over 200 cards and all of their interactions, and out of the 25,000-plus possible cards, I have roughly 25,000 more to go. Each new card changes the requirements of the system in new and interesting ways.
00:33:54.879 I get to consider how the system needs to change in order to accommodate a new card or a new game mechanic from that card. Sometimes, I can get away with writing just a card and its tests; other times, I realize I’ve made a huge mistake somewhere else, and it requires a grand re-architecting of one part or another.
00:34:34.720 Now, the goal of this project isn’t to implement all 25,000 cards and one million words of rules text and have a perfect simulation of Magic in Ruby; the goal is the work itself— to practice working within an application. One that isn’t an app that’s processing people’s payments. The goal is to further my Ruby programming skills. That’s what some would call deliberate practice—setting tasks that adjust at the cusp of my current ability and then trying them.
00:35:25.440 It’s okay if I fail at the task; the task isn’t the goal—the practice is the goal. I set these little goals of implementing one new card at a time or tidying up different parts of the system so that I can improve my Ruby programming abilities. I encourage you all to think about how you can deliberately practice your Ruby skills as well. You might think that practice is just for juniors or newbies, but it’s not; it’s for everyone.
00:36:14.119 Think about how you can push your Ruby skills further. Perhaps there’s that cool web framework called Hanami that you’ve been meaning to check out. Can you build something small in it to push your skills a little further? Or maybe you’d like to try game programming in Ruby? You’re still here! Help me out with my borderline Magic obsession. If you’d like to take a look at the full code for this project, it’s available on a GitHub repo. If you’d like some pointers on where to start, come and find me later on, and I can show you where to go.
00:37:09.840 Thanks for today!
Explore all talks recorded at RubyConf AU 2024
+14