Dev Diary: Zurgo, Thunder's Decree
Recently, I worked on implementing
Let's stop for a moment and consider how many different things "this can't be sacrificed" needs to do:
- If an effect would cause the object to be sacrificed, it is not sacrificed. This includes the end-step trigger from the mobilize ability but also various "sacrifice all creatures" effects, like
Emrakul, the World Anew orBringer of the Last Gift .
- The creature is not a valid choice to sacrifice while paying a sacrifice cost. So, if you activate
Goblin Bombardment , you can't choose that creature to sacrifice. If you are casting Fling, you can't sacrifice that creature. And if the creature hasMask of Immolation attached to it, its self-sacrifice ability can't be activated.- We want the game to recognize this, so if you only control creatures that can't be sacrificed, Fling should not even be highlighted in your hand as a castable spell, just as if you controlled no creatures at all.
- If an effect instructs you to sacrifice a creature of your choice, you should not be able to choose a creature with this ability (and thus would have to choose some other creature to sacrifice).
- If a spell or ability, while resolving, says "you may sacrifice a creature. If/When you do …" and the only creatures you control can't be sacrificed, you should just skip over that part of the resolution entirely. If you control some creatures that can be sacrificed and some that can't, you should only be able to choose to sacrifice the creatures without that ability.
Let me catch my breath …
- Various preexisting Magic abilities instruct the player to sacrifice an object. For instance, when the final chapter of a Saga resolves, the Saga is sacrificed. When the vanishing trigger resolves for a permanent with no time counters, that permanent is sacrificed. In all those cases, "can't be sacrificed" causes that permanent to remain on the battlefield.
Finally, my teammates with an encyclopedic knowledge of Magic and its rules identified a few specific cards with odd behaviors, most entertainingly:
If
So … that's pretty daunting. That seems like a lot of work.
Well, maybe? MTG Arena is very complicated because Magic is very complicated, but I was pretty confident that all of those cases should be handled by a single "CLIPS rule," which I've written about in the past.
It's easiest to understand what this CLIPS rule would do when you think about a spell or ability that tries to make a player sacrifice all their creatures.
So, a player casts a spell, say
To Do:
- Player 1 sacrifices
Serra Angel . - Player 2 sacrifices
Shivan Dragon . - Player 1 sacrifices Warrior token.
But the Warrior token can't be sacrificed, so the CLIPS rule for "this token can't be sacrificed" comes along and erases that third line from the whiteboard. When the spell finishes resolving, the Dragon and Angel are sacrificed, but the token isn't.
That seems vaguely straightforward. But how does that help with all the other cases? The way MTG Arena's engine is written, that one rule should (and the word "should" is carrying a lot of weight here) make all the cases work.
For instance, let's look at the next case. The player begins to cast Fling. They have to choose a creature to sacrifice. What happens?
Well, the engine sees that the player needs to sacrifice a creature. It sees that they have two creatures, a
To Do:
- Sacrifice
Serra Angel . - Sacrifice Warrior token.
Then it waits. While it's waiting, the CLIPS rule for the Warrior token comes along, sees the to-do list, and erases "Sacrifice Warrior token" from the whiteboard. Then the game engine wakes back up, sees that "Sacrifice Warrior token" is no longer on the list, and realizes that the Warrior token can't be sacrificed but the
Crucially, the engine can write that "to-do list" on a whiteboard, wait to see if any of the elements on the list get crossed out, and then not actually DO any of the things on the list. The initial list is there mostly to gather information.
So, back to our big should, I went ahead and generated the CLIPS rule for "this creature can't be sacrificed." It was time to run a test and see what worked and what didn't!
Let's briefly discuss the types of testing we do here. For this discussion, there are two kinds of tests that are important. First are "regression tests" which are basically fully scripted games of Magic. A text file controls both players, and we verify that the game is what is expected at all times. Once a regression test is written, it stays around forever and is continuously reverified. In theory, we'll be alerted if a card printed in five years suddenly breaks the behavior of Zurgo. As of this writing, we have nearly 5,000 regression tests that we run.
We also have a dedicated team of quality-assurance (QA) engineers who test all new cards in the actual graphical MTG Arena client on a real MTG Arena server. They typically get involved after a card passes all the regression tests that we write. As you will see, they will play a key role in this story.
The challenge before me involved how to test all those cases. Remember, Zurgo is (currently) the only card on MTG Arena with this ability, and it only applies to Warrior tokens in the end step. That doesn't mean it's impossible to test any of those cases, just difficult. I could, with some effort, script out a game where I cast an
Instead, I decided to add an ASUP card.
ASUP stands for "Arena supplemental." ASUP cards are a "set" of debug-only cards. These have card text that makes it easy to get a game of Magic to a state otherwise difficult or time consuming to reach. For instance, likely the most commonly used ASUP card is ASUP_AddLoyalty, which adds 50 loyalty counters to a planeswalker. When you want to write a script to test a planeswalker's ultimate, you don't want to spend five turns activating its abilities, particularly if it requires having a target or meeting some other requirement. ASUP cards are used both in regression test scripts and the debug client by our QA engineers.
My favorite ASUP card is ASUP_ShuffleCounter, which is an enchantment with "Whenever a player shuffles their library, they gain 1 life." This is useful when testing abilities like the one on
Target opponent chooses a permanent they control and returns it to its owner's hand. Then they shuffle each nonland permanent they control into its owner's library.
This should cause a player to shuffle zero to two libraries no more than once each. But it's hard just by looking at the state of a game to see how many times a library was just shuffled. You certainly can't just check that the top card changed, because sometimes, it won't! Gaining life helps make that clear.
So, I created a new ASUP card: ASUP_CantBeSacrificed, which is an Aura enchantment with "enchant permanent" and "enchanted permanent can't be sacrificed." This made it as straightforward as possible to write a regression test for each of the various cases.
So, I wrote all those regression tests, held my breath, crossed my fingers, and … they all worked!
I was frankly shocked. I was confident MTG Arena's architecture should properly let one and only one CLIPS rule handle all the cases, but I was sure that something would go wrong.
Actually, one thing did go wrong, but it was so trivial as to be almost comical. We have something called a "disqualified pulse," a little animation that plays when some object in the game stops something from happening. For instance, if Narset, Parter of Veils prevents you from drawing, the animation is played on Narset. On my first attempt, that was happening every time the "can't be sacrificed" CLIPS rule fired, including all the complicated predictive cases. So, if you had a creature you couldn't sacrifice in play and Fling in your hand, the creature would just be spamming disqualified pulses every second. But that was easily enough fixed.
So, I double checked all my tests with ASUP_CantBeSacrificed, threw in a very simple test for Zurgo itself which verified that, yes, the tokens it created with mobilize were not being sacrificed during the end step, and I figured it was job done.
A Happy Ending or an Unhappy End Step?
Of course, QA came back to me and said it didn't work. More specifically, it didn't work in a very weird and specific fashion. They'd cast
This baffled me. I had tested all the tricky cases with ASUP_CantBeSacrificed, and I knew that Zurgo's ability was working, at least partly, from my regression test and the weird results that QA was seeing. What was going on?
My first thought was that "this token can't be sacrificed" and "enchanted permanent can't be sacrificed" were somehow working differently. But that wasn't the case.
I started combing through the extremely verbose logs that CLIPS rules generate, and I finally came up with a theory that had to do with layered effects.
One of the most complicated parts of any Magic board state is the various layered effects that apply to it. It's not a coincidence that the answer to so many questions about Magic rules is "because layers." One of the difficult things about layered effects is that any time they change, we have to fully rebuild them from scratch. They can change basically any time anything changes. When any cards move between zones, there's at least a chance that some layered effect is now counting something differently, so we recalculate layered effects. It's one of the main things that the engine spends time doing. So, we want to avoid doing it whenever possible.
I realized we were not recalculating layered effects when moving between steps and phases. Because, somewhat to my surprise, Zurgo is not just the first card on MTG Arena with "can't be sacrificed," it's also the first card on MTG Arena that grants an ability only during a specific phase or step.
So, we'd be in the post-combat main phase. The Warrior tokens would not have "this token can't be sacrificed." Both players pass priority. We move into the end step. Now the Warrior tokens should have "this token can't be sacrificed." But they wouldn't actually get that ability until we recalculated layered effects, which we wouldn't do until something happened. That's why everything acted so weirdly.
In my test, by the time the tokens should have been sacrificed, the mobilize trigger had gone on the stack and started resolving. Somewhere in that process, layered effects were recalculated. But in the QA test, when they activated
You'd assume the fix would be to recalculate layered effects when moving between steps and phases. Well, no. That would add a ton of extra and unnecessary computing. Instead, I added a new CLIPS rule for any ability with the structure "during (phase/step), (object) gains (ability)." This new rule recalculates layered effects if that ability is active, and only at the beginning or end of the specified phase or step. I tested that in my regression test and could now no longer sacrifice Warrior tokens as the first action in my end step. Problem solved, right?
Hidden Agendas
Nope. QA tested my fix and reported that it basically made no difference at all.
I fired up a debug client and started poking around. After pouring over more CLIPS logs, I realized what was going on.
The MTG Arena engine does most of its work via CLIPS rules, and CLIPS rules are divided into "agendas." Agendas are categories of behavior. For instance, abilities triggering, replacement effects, or state-based actions. The order in which agendas happen is a carefully choreographed ballet that underlies basically everything happening in a game on MTG Arena. It's something that is so fundamental that if it didn't work nearly all the time, nothing would ever work. Since it "always" works, I've almost never thought about it in the years I've worked on MTG Arena.
But this time, it needed a bit of attention. The trouble comes from what the ability "this (object) can't be sacrificed" does. The ability itself doesn't stop creatures from being sacrificed. Rather, it creates an MTG Arena-specific object called a "qualification." Then the CLIPS rules say "that creature has a can't-be-sacrificed qualification, and the whiteboard says to sacrifice it. Erase that line!"
Why is a qualification involved? Why not just have the CLIPS rule say "That creature has the can't-be-sacrificed ability, and the whiteboard says to sacrifice it, erase that line!"? The reason is that, in general, we want to make rules for "this creature can't be sacrificed" and "this turn, creatures you control can't be sacrificed" as similar as possible. If there are qualifications involved, it becomes easier to make these two kinds of effects simpler.
But in this case, they were the effect's downfall. Because the recalculation of layered effects at the beginning of the end step was something novel, it was running its agendas in a slightly weird way. There's an agenda called "game actions" in which there is a rule that says "if a creature has this ability, create a qualification for it." As we entered the end step, the game actions agenda was coming before the agenda that granted the ability. The Warrior token had the ability, but the ability hadn't created a qualification and thus wasn't doing anything.
Once I fixed that, I sent it off to QA, and everyone was very happy that Zurgo finally appeared to be working.
Zurgo has been