A tool for mathematical analysis of Magic the Gathering
Project description
Ertai
Mathematical models of Magic the Gathering.
Tutorial
We will use ertai
to obtain the probability of being able to play a card in
our first turn.
>>> import ertai
>>> import random
>>> def build_deck(card_counts, number_of_lands):
... """
... This returns a list of `ertai.Card` objects with the
... specified number of cards
... """
... deck = [ertai.BasicLand() for _ in range(number_of_lands)]
... for cost, number in card_counts.items():
... cost = ertai.Mana(*(None for _ in range(cost)))
... deck += [ertai.Card(cost=cost) for _ in range(number)]
... return deck
>>> card_counts = {1: 10, 2: 16}
>>> number_of_lands = 14
>>> deck = build_deck(card_counts=card_counts, number_of_lands=number_of_lands)
>>> for card in deck:
... print(card)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
BasicLand(title=None, cost=0 Mana, tapped=False, color=None)
Card(title=None, cost=1 None Mana, tapped=False)
Card(title=None, cost=1 None Mana, tapped=False)
Card(title=None, cost=1 None Mana, tapped=False)
Card(title=None, cost=1 None Mana, tapped=False)
Card(title=None, cost=1 None Mana, tapped=False)
Card(title=None, cost=1 None Mana, tapped=False)
Card(title=None, cost=1 None Mana, tapped=False)
Card(title=None, cost=1 None Mana, tapped=False)
Card(title=None, cost=1 None Mana, tapped=False)
Card(title=None, cost=1 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
Card(title=None, cost=2 None Mana, tapped=False)
The above is code that uses the ertai
classes to construct a deck.
Now let us write code to find the probability of drawing a hand with a card that can be played.
>>> def play_land(hand):
... """
... If the hand contains a land, return it.
... """
... for card in hand:
... if isinstance(card, ertai.BasicLand):
... hand.remove(card)
... return card
... return None
>>> def play_card(hand, pool):
... """
... Given a pool of mana, play the first possible card in a hand.
... """
... for card in hand:
... if (card.cost <= pool) and (isinstance(card, ertai.BasicLand) is False):
... pool = card.cast(pool)
... hand.remove(card)
... return card
... return None
With the above we can now write code to draw a hand of card and see if we can play:
>>> def can_play_spell(card_counts, number_of_lands, seed):
... """
... Given a hand returns a boolean if we are able to play a spell
...
... This assumes we play on the first turn.
... """
... deck = build_deck(card_counts=card_counts, number_of_lands=number_of_lands)
... random.seed(seed)
... random.shuffle(deck)
... hand = deck[:7]
... if (land:=play_land(hand)) is not None:
... pool = land.generate_mana()
... return play_card(hand, pool) is not None
... return False
>>> can_play_spell(card_counts=card_counts, number_of_lands=number_of_lands, seed=0)
True
The above is a single simulation, let us compute the expected probability:
>>> import numpy as np
>>> def expected_probability_of_first_turn_play(card_counts, number_of_lands, number_of_repetitions=500):
... """Returns the expected probability of having a first turn play."""
... return np.mean([can_play_spell(card_counts=card_counts, number_of_lands=number_of_lands, seed=seed) for seed in range(number_of_repetitions)])
Let us see how this probability changes as we modify the number of cards with cost 1 in our deck. We will assume a 40 card deck with 16 lands and all other cards have cost 2.
>>> number_of_lands = 14
>>> for number_of_cards_with_cost_1 in range(1, 26 + 1):
... card_counts = {1: number_of_cards_with_cost_1, 2: 26 - number_of_cards_with_cost_1}
... probability = expected_probability_of_first_turn_play(card_counts=card_counts, number_of_lands=number_of_lands)
... print(number_of_cards_with_cost_1, round(probability, 3))
1 0.134
2 0.286
3 0.422
4 0.504
5 0.58
6 0.644
7 0.714
8 0.762
9 0.796
10 0.838
11 0.862
12 0.88
13 0.898
14 0.914
15 0.932
16 0.946
17 0.948
18 0.95
19 0.954
20 0.958
21 0.958
22 0.958
23 0.958
24 0.958
25 0.958
26 0.958
We see that as expected the probability of playing in the first turn goes up as the number of cards with cost 1 increases.
How to
How to create mana
ertai
has a mana class that can be used to create any amount of mana.
>>> spell_cost = ertai.Mana("Blue", "Blue")
>>> spell_cost
2 Blue Mana
How to add more mana
>>> spell_cost += ertai.Mana("Red")
>>> spell_cost
2 Blue Mana, 1 Red Mana
How to check if one collection of mana can be paid using another
>>> mana_pool = ertai.Mana("Blue", "Red", "Red", "Blue")
>>> spell_cost <= mana_pool
True
How to take one collection of mana away from another
>>> mana_pool -= spell_cost
>>> mana_pool
1 Red Mana
We see that we no longer have enough mana for the spell:
>>> spell_cost <= mana_pool
False
How to create a basic land
>>> island = ertai.BasicLand(title="Island", color="Blue")
>>> island
BasicLand(title='Island', cost=0 Mana, tapped=False, color='Blue')
### How to generate mana from a land
>>> island.tapped
False
>>> island.generate_mana()
1 Blue Mana
>>> island.tapped
True
When tapped the land will not generate mana.
>>> island.generate_mana()
How to untap or tap a card
All cards in ertai
have a untap
method.
>>> island.untap()
>>> island.tapped
False
They also have a tap
method.
>>> island.tap()
>>> island.tapped
True
How to cast a card
At present, casting a card does nothing but modify a given mana pool if the cost can be paid.
>>> mana_pool = ertai.Mana("Red", "Red", "Black")
>>> card_cost = ertai.Mana("Red")
>>> card = ertai.Card(title="Lightning bolt", cost=card_cost)
>>> mana_pool = card.cast(mana_pool)
>>> mana_pool
1 Red Mana, 1 Black Mana
Note that if a card cannot be case then the mana pool will stay unchanged but no warning is given.
>>> card_cost = ertai.Mana("Blue", "Blue")
>>> card = ertai.Card(title="Counterspell", cost=card_cost)
>>> mana_pool = card.cast(mana_pool)
>>> mana_pool
1 Red Mana, 1 Black Mana
How to create a ceature card
Pass a power
and toughness
to the Creature
class:
>>> creature = ertai.Creature(title="Selfless Savior",cost=ertai.Mana("White"),
... power=1,
... toughness=1)
>>> creature
Selfless Savior Cost:1 White Mana Power:1 Toughness:1
How to use a creature to fight
We can then use this creature to fight a target
create:
>>> target = ertai.Creature(title="Usher of the Fallen",cost=ertai.Mana("White"),
... power=2,
... toughness=1)
>>> creature.fight(target)
In this case the create is no longer alive after the fight:
>>> creature.is_alive()
False
How to contribute
Clone the repository and create a virtual environment:
$ git clone https://github.com/drvinceknight/ertai.git
$ cd ertai
$ python -m venv env
Activate the virtual environment and install tox
:
$ source env/bin/activate
$ python -m pip install tox
Make modifications.
To run the tests:
$ python -m tox
Pull requests are welcome.
Discussion
What is Magic The Gathering
Magic the Gathering is a collectable card game that was first released in 1993.
- Here is the official basic rules: https://magic.wizards.com/en/magic-gameplay
- Here is the wikipedia page entry: https://en.wikipedia.org/wiki/Magic:_The_Gathering
Why is it called Ertai
Alongside the game there is a lot of lore that exists. Ertai was a particular character. From [https://villains.fandom.com/wiki/Ertai]:
He is a brilliant but arrogant mage who learned from the head wizard of the Tolarian Academy Barrin and signed up to join the Weatherlight Crew.
What can this library be used for
At present this library offers data class that can be used to model aspects of Magic the Gathering.
The erta.Mana
class allows us to carry out Mana arithmetic:
>>> mana_pool = ertai.Mana("Blue", "Blue", "Red", "None", "Blue")
>>> ertai_the_adept_cost = ertai.Mana("Blue", "Blue", "None")
>>> mana_pool - ertai_the_adept_cost
1 Blue Mana, 1 Red Mana
The ertai.Card
class can be used to create new cards with given abilities.
Development stack
There are a number of continuous integration checks that are used:
- black and flake8 for code style.
- interrogate to ensure all code has docstrings.
- mypy for static type checking.
- coverage to ensure 100% coverage of source code by test.
- mccabe to measure code complexity.
tox is used to test the code in isolated environments.
Reference
Other Python magic the gathering libraries
Magic: The Gathering SDK: A wrapper around the MTG API of magicthegathering.io
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file ertai-0.0.3.tar.gz
.
File metadata
- Download URL: ertai-0.0.3.tar.gz
- Upload date:
- Size: 12.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: python-requests/2.26.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | aa590e14011c885bec4accd6e171e30140dc86babbb25ea5a25a304bbef91c28 |
|
MD5 | 29acca42cfc8df9fbb87b21fc8c34a64 |
|
BLAKE2b-256 | 469d5f3cbfaff6fca5384f33c315a5151f9733effa1113dfea3f6ab9154e3176 |
File details
Details for the file ertai-0.0.3-py3-none-any.whl
.
File metadata
- Download URL: ertai-0.0.3-py3-none-any.whl
- Upload date:
- Size: 7.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: python-requests/2.26.0
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | c9ec15f416ee602f0e55285cb9614877bf0a6279a82372cffcafd613ed39624a |
|
MD5 | 8fec0457fd0d5b33da38bd6f8bf7790f |
|
BLAKE2b-256 | 7b186cdc03bd094e2e5b82976e668f5c4e7d1896836fa81ffa37ca40e93124b5 |