Using Python to Improve A Magic: The Gathering Deck

ยท 923 words ยท 5 minute read

Intro ๐Ÿ”—

I have been playing quite a bit of MTG: Arena lately. It is some good fun, and you can get away with spending very little (or even zero) money by focusing on just one or two core decks.

Currently I am building a deck in the Explorer category, a mono-white life gain deck. Having risen to the Diamond league I just cannot seem to broach into Mythic. The core loop of this deck is that the creatures gain +1/+1 counters on them whenever I gain life, and I have several ways of gaining life – such as when creatures enter, when they attack, or when my opponent plays a creature.

Here is what my deck consisted of when I started this endeavor:

Deck
3 Celestial Unicorn (AFR) 5
4 Ajani's Welcome (M19) 6
2 Ajani's Pridemate (WAR) 4
4 Lunarch Veteran (MID) 27
4 Ajani, Strength of the Pride (M20) 2
4 Voice of the Blessed (VOW) 44
4 Linden, the Steadfast Queen (ELD) 20
2 Ajani's Pridemate (M19) 5
2 Hopeful Initiate (VOW) 20
4 Valorous Stance (VOW) 42
3 Authority of the Consuls (KLR) 9
4 Tocasia's Welcome (BRO) 30
2 Grafdigger's Cage (M20) 227
21 Plains (DAR) 253

Taking A Look ๐Ÿ”—

My main concern at first was this: I felt as if I always have either too few mana/land (dry) cards or too many mana/land (flooded) cards. My porridge was never just right.

In this state my deck consisted of 63 cards with 21 of them being Plains (mana/land).

The first thing I decided to look at was the average probability of drawing a land on each given turn. After writing the code for that and running the simulation multiple times I found that it pretty much stays the same value. With 1/3 of cards in a deck being Plains it makes sense that the chance to draw that card is pretty much always 1/3. In retrospect this should have been obvious but this is why I love doing this kind of stuff – learning.

Here is what one of those charts looked like for posterity:

A line chart of average probability of drawing a Plains over 30 games

A line chart of average probability of drawing a Plains over 30 games

Next I decided what would be much more useful is to determine my probability of having X lands in my hand by turn Y. This part is explained in the following section.

The Code ๐Ÿ”—

First I needed a simple way to represent the deck of cards as well as the “hand” I was constantly drawing into. I decided to ignore maximum hand-size and would assume I would cast zero spells per turn – I just wanted to get some raw draw data.

Python’s dataclasses to the rescue!

from dataclass import dataclass

@dataclass
class Deck:
    total_cards: int
    cards_remaining: int
    original_cards: dict  # {"Card Name": count}
    cards: dict  # {"Card Name": count}, but decremented on draw
    pile: list  # An actual list of cards to draw from


@dataclass
class Hand:
    total_cards: int
    cards: list

    def show_hand(self) -> None:  # Prints the hand to the terminal
        print("Hand:")
        for card in self.cards:
            print("-", card)

    def count_card(self, cardname: str) -> int:
        return self.cards.count(cardname)

Sure, it could have been easier to just use arbitrary cards and just count how many Plains I drew over time, but I want to be able to reuse this code for other deck purposes.

Next I wrote a messy function for parsing the deck-list above, as well as a function for drawing cards from the deck into the hand. These are omitted here for brevity but all of the code can be found at the URL in the Resources section.

Now it was time to simulate the drawing of cards over X turns in Y games:

def cardCountGameSimulate(games: int, turns: int, cardname: str):
    card_count = []

    for _ in range(games):
        deck = load_deck()
        hand = Hand(0, [])
        random.shuffle(deck.pile)

        for _ in range(7):
            draw_card(deck, hand)

        for _ in range(turns):
            draw_card(deck, hand)

        card_count.append(hand.count_card("Plains"))

    plt.figure(figsize=(8, 3))
    plt.hist(card_count)
    plt.title(f"Copies of {cardname} in hand on turn {turns} over {games} games")

    plt.show()

Histogram of number of Plains on turn 4 over 10000 games

Histogram of number of Plains on turn 4 over 10000 games

Now that looks like useful information. It seems that more than half of the games have me having seen less than four Plains by turn four. Mind you this is after we draw our hand, so we have seen a total of 11 cards by turn 4. 4/11 cards (or less!) are Plains by turn 4. This could be okay or this could be terrible depending on what the opening hand was and how long it took to get these Plains.

After thinking about it for a hot minute I decided to increase the amount of hands in my deck by three, up to 24/66. I did this for multiple reasons:

  • I needed to ensure that I was getting lands to play my three and four mana cards by turns four
  • I needed to be able to possibly play multiple lower mana cards by turn four
  • I have Tocasia’s Welcome which allows me card draw when I play a creature with mana value three or less. This give me a mostly-reliable way to filter extra lands if they come up

Having been playing for a day or so after doing this I do feel like my deck is performing at least marginally better. I may reduce the land count by one soon, but we will see.

Resources ๐Ÿ”—