https://github.com/maxhalford/zugzug
๐น Monte Carlo experiments for Hearthstone
https://github.com/maxhalford/zugzug
hearthstone monte-carlo theorycrafting
Last synced: 5 months ago
JSON representation
๐น Monte Carlo experiments for Hearthstone
- Host: GitHub
- URL: https://github.com/maxhalford/zugzug
- Owner: MaxHalford
- License: wtfpl
- Created: 2020-04-11T10:04:50.000Z (almost 6 years ago)
- Default Branch: master
- Last Pushed: 2020-04-23T14:51:41.000Z (over 5 years ago)
- Last Synced: 2025-02-08T16:30:48.837Z (11 months ago)
- Topics: hearthstone, monte-carlo, theorycrafting
- Language: Python
- Homepage:
- Size: 11.7 KB
- Stars: 0
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# zugzug
This is a little framework to answer statistical questions related to [Hearthstone](https://playhearthstone.com). Things started when I wrote an analysis that gathered some interest [on reddit](https://www.reddit.com/r/CompetitiveHS/comments/g1tqj0/analyzing_the_time_it_takes_to_summon_zixor_prime/). I then looked into doing some more analysis but noticed that I was doing a lot of copy/pasting. I then decided to write some helpers to avoid repeating myself and make less mistakes.
Many Hearthstone questions can probably be answered with exact formulas. However, that may involve brain power that you might want to invest elsewhere. An alternative solution is to approach the exact answer by running multiple simulations and aggregating the results. Essentially, `zugzug` is a tool that helps you perform so-called [Monte Carlo experiments](https://www.wikiwand.com/en/Monte_Carlo_method). The user provides a simulation function, which takes various parameters as inputs and outputs a number. Then, the user calls `zugzug`'s `run` function and specifies a grid of parameters. The results are then aggregated and displayed in a table.
Various utilities such as cards, conditions, and game mechanisms are implemented in `zugzug` to help out writing simulation functions. These utilities are implemented on an as-needed basis. Therefore, not all of them are available. However, new features and specific requests are more than welcome to be discussed. Likewise, the API is highly succeptible to change.
## Installation
```sh
pip install git+https://github.com/MaxHalford/zugzug
```
## Examples
**What is the probability of having a 1 mana card at the first turn?**
We first import `zugzug`.
```py
>>> import zugzug as zz
```
Reproducibility can be enforced by fixing the global random number generator.
```py
>>> import random
>>> random.seed(42)
```
For this analysis, we're not interested in any card in particular. Instead, we are interested by the mana cost of each cost. We can thus define two kinds of cards, one that costs 1 mana and 1 that costs 2 mana.
```py
>>> one_mana_card = zz.cards.Card(name='1 Mana Card', mana=1)
>>> two_mana_card = zz.cards.Card(name='2 Mana Card', mana=2)
```
Let us now define a simulation function. One parameter will determine one many 1 mana cards are in the deck, whilst the other will indicate if we're looking for 1 mana cards during the mulligan phase or not. The details of the simulation are influenced by these two parameters. We'll output a boolean value which tells whether or not a 1 mana card is in hand.
```py
>>> def sim(n_ones, mulligan):
...
... # Create a deck with 1 and 2 mana cards
... deck = [one_mana_card] * n_ones + [two_mana_card] * (30 - n_ones)
...
... # Indicate that we want to fish for 1 mana cards during the mulligan phase
... wishlist = [one_mana_card] * 3 if mulligan else []
... game = zz.Game(deck, wishlist=wishlist)
...
... # Go to the first turn
... game.next_turn()
...
... return one_mana_card in game.hand
```
We may now call the `run` function by providing it with the simulation function. We'll also choose how many repetitions we want to do and a set of values for each parameter. The rest is taken care by `zugzug`.
```py
>>> results = zz.run(sim, n=1000, n_ones=range(1, 7), mulligan=[False, True])
>>> print(results)
โโโโโโโโโโโโคโโโโโโโโโโโโโคโโโโโโโโโโโโคโโโโโโโโโคโโโโโโโโโโโ
โ n_ones โ mulligan โ median โ mean โ stdev โ
โโโโโโโโโโโโชโโโโโโโโโโโโโชโโโโโโโโโโโโชโโโโโโโโโชโโโโโโโโโโโก
โ 1 โ False โ 0.0733945 โ 0.128 โ 0.334257 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 1 โ True โ 0.15445 โ 0.236 โ 0.424835 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 2 โ False โ 0.171141 โ 0.255 โ 0.436079 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 2 โ True โ 0.33612 โ 0.402 โ 0.490547 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 3 โ False โ 0.293651 โ 0.37 โ 0.483046 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 3 โ True โ 0.595841 โ 0.553 โ 0.497432 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 4 โ False โ 0.402527 โ 0.446 โ 0.497324 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 4 โ True โ 0.760355 โ 0.676 โ 0.468234 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 5 โ False โ 0.589253 โ 0.549 โ 0.497842 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 5 โ True โ 0.826146 โ 0.742 โ 0.437753 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 6 โ False โ 0.702552 โ 0.627 โ 0.483844 โ
โโโโโโโโโโโโผโโโโโโโโโโโโโผโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค
โ 6 โ True โ 0.87578 โ 0.801 โ 0.399448 โ
โโโโโโโโโโโโงโโโโโโโโโโโโโงโโโโโโโโโโโโงโโโโโโโโโงโโโโโโโโโโโ
```
**How much mana does [Frizz Kindleroost](https://hearthstone.gamepedia.com/Frizz_Kindleroost) save?**
Summoning Frizz Kindleroot reduces the mana cost of each dragon in the deck by 2. To measure how much mana this saves on average, we can run a simulation where the turns go by as long as Frizz is not in hand. We can do this by calling `game.play_until` with the `zz.conditions.Playable(frizz)` condition.
```py
>>> import random
>>> import zugzug as zz
>>> random.seed(42)
>>> dragon = zz.cards.Minion(name='Dragon', mana=None, race=zz.races.DRAGON)
>>> frizz = zz.cards.FrizzKindleroost()
>>> def sim(mulligan, n_dragons):
...
... deck = [frizz] + [dragon] * n_dragons
... deck = deck + [zz.cards.Wisp()] * (30 - len(deck))
... game = zz.Game(deck, wishlist=[frizz] if mulligan else [])
...
... game.play_until(zz.conditions.Playable(frizz))
...
... return sum(2 for card in game.deck if card == dragon)
>>> results = zz.run(sim, n=1000, mulligan=[False, True], n_dragons=[2, 4, 6, 8, 10, 12])
>>> print(results)
โโโโโโโโโโโโโโคโโโโโโโโโโโโโโคโโโโโโโโโโโคโโโโโโโโโคโโโโโโโโโโ
โ mulligan โ n_dragons โ median โ mean โ stdev โ
โโโโโโโโโโโโโโชโโโโโโโโโโโโโโชโโโโโโโโโโโชโโโโโโโโโชโโโโโโโโโโก
โ False โ 2 โ 1.93236 โ 1.898 โ 1.5761 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ False โ 4 โ 3.90851 โ 3.7 โ 2.6178 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ False โ 6 โ 5.98276 โ 5.728 โ 3.76034 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ False โ 8 โ 8.03237 โ 7.678 โ 4.73653 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ False โ 10 โ 10.1261 โ 9.72 โ 5.68805 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ False โ 12 โ 12.0412 โ 11.724 โ 6.57381 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ True โ 2 โ 2.02603 โ 2.038 โ 1.59408 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ True โ 4 โ 4.07219 โ 3.948 โ 2.79376 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ True โ 6 โ 6.28 โ 6.12 โ 3.74802 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ True โ 8 โ 9.52564 โ 8.224 โ 4.74514 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ True โ 10 โ 11.5612 โ 10.298 โ 5.81214 โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ True โ 12 โ 13.8684 โ 12.224 โ 6.94997 โ
โโโโโโโโโโโโโโงโโโโโโโโโโโโโโงโโโโโโโโโโโงโโโโโโโโโงโโโโโโโโโโ
```
**How long does it take to get [Zixor Prime](https://hearthstone.gamepedia.com/Zixor_Prime) in hand?**
This was the first analysis I did. You can see the code I used in [this gist](https://gist.github.com/MaxHalford/0bf06078d2dd6ef3609f4e3cca5bc41c). Using `zugzug` reduces the amount of necessary of code and helps a bit with readability. It also helped me gain trust in my analysis by delegating the game mechanics to `zugzug`.
This simulation is a bit more verbose than the previous ones because their are two first phases involved. First of all we need to wait that Zixor is in hand. Then, once Zixor's deathrattle has triggered, we have to wait to draw Zixor Prime. The goal of this analysis is to study the impact of draw cards such as [Diving Gryphon](https://www.hearthpwn.com/cards/151350-diving-gryphon), [Scavenger's Ingenuity](https://www.hearthpwn.com/cards/210658-scavengers-ingenuity), and [Tracking](https://hearthstone.gamepedia.com/Tracking).
```py
>>> import random
>>> import zugzug as zz
>>> random.seed(42)
>>> zixor = zz.cards.Zixor()
>>> zixor_prime = zz.cards.ZixorPrime()
>>> gryphon = zz.cards.DivingGryphon()
>>> si = zz.cards.ScavengersIngenuity()
>>> tracking = zz.cards.Tracking(zixor_prime, zixor, si, gryphon)
>>> tracking.wishlist.append(tracking)
>>> wisp = zz.cards.Wisp()
>>> playlist = [si, gryphon, tracking]
>>> wishlist = [zixor, si, gryphon, tracking]
>>> def sim(n_gryphons, n_si, n_tracking):
...
... deck = (
... [zixor] +
... [gryphon] * n_gryphons +
... [si] * n_si +
... [tracking] * n_tracking +
... [wisp] * (30 - 1 - n_gryphons - n_si - n_tracking)
... )
... game = zz.Game(deck, wishlist=wishlist, playlist=playlist)
...
... # Play until Zixor is playable
... game.play_until(zz.conditions.Playable(zixor))
...
... # Assume it takes 0 to 2 turns to get Zixor killed
... for _ in range(random.randint(0, 2)):
... game.next_turn()
...
... # Insert Zixor Prime into the deck once Zixor's deathrattle triggers
... game.play_card(zixor)
...
... # Wait until Zixor Prime is playable
... game.play_until(zz.conditions.Playable(zixor_prime))
...
... return game.turn
>>> results = zz.run(
... sim, n=1000,
... n_gryphons=[0, 1, 2],
... n_si=[0, 1, 2],
... n_tracking=[0, 1, 2]
... )
>>> print(results)
โโโโโโโโโโโโโโโโคโโโโโโโโโคโโโโโโโโโโโโโโโคโโโโโโโโโโโคโโโโโโโโโคโโโโโโโโโโ
โ n_gryphons โ n_si โ n_tracking โ median โ mean โ stdev โ
โโโโโโโโโโโโโโโโชโโโโโโโโโชโโโโโโโโโโโโโโโชโโโโโโโโโโโชโโโโโโโโโชโโโโโโโโโโก
โ 0 โ 0 โ 0 โ 22.7881 โ 21 โ 6.45024 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 0 โ 0 โ 1 โ 19.7245 โ 18.687 โ 5.65376 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 0 โ 0 โ 2 โ 17.959 โ 16.835 โ 4.97056 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 0 โ 1 โ 0 โ 16.3727 โ 16.652 โ 6.19462 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 0 โ 1 โ 1 โ 15.2869 โ 15.328 โ 5.3125 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 0 โ 1 โ 2 โ 13.6 โ 13.836 โ 4.43517 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 0 โ 2 โ 0 โ 11.4167 โ 12.864 โ 4.98481 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 0 โ 2 โ 1 โ 10.7182 โ 12.204 โ 4.35814 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 0 โ 2 โ 2 โ 9.76364 โ 11.118 โ 3.61623 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 1 โ 0 โ 0 โ 16.5435 โ 16.481 โ 6.1929 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 1 โ 0 โ 1 โ 15.1866 โ 15.239 โ 5.34588 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 1 โ 0 โ 2 โ 13.0821 โ 13.556 โ 4.74694 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 1 โ 1 โ 0 โ 11.4 โ 13.165 โ 5.41434 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 1 โ 1 โ 1 โ 10.7222 โ 12.243 โ 4.57109 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 1 โ 1 โ 2 โ 9.48864 โ 11.186 โ 3.80866 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 1 โ 2 โ 0 โ 8.91096 โ 10.991 โ 4.01036 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 1 โ 2 โ 1 โ 8.47466 โ 10.254 โ 3.19683 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 1 โ 2 โ 2 โ 8.35616 โ 9.604 โ 2.60845 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 2 โ 0 โ 0 โ 11.8519 โ 13.515 โ 5.63556 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 2 โ 0 โ 1 โ 11.1833 โ 12.664 โ 4.79897 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 2 โ 0 โ 2 โ 9.51818 โ 11.342 โ 3.88692 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 2 โ 1 โ 0 โ 9.30172 โ 11.208 โ 4.12065 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 2 โ 1 โ 1 โ 8.75954 โ 10.536 โ 3.56559 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 2 โ 1 โ 2 โ 8.47466 โ 9.96 โ 2.95112 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 2 โ 2 โ 0 โ 8.4311 โ 9.948 โ 3.02795 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 2 โ 2 โ 1 โ 8.33056 โ 9.391 โ 2.44256 โ
โโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโค
โ 2 โ 2 โ 2 โ 8.28616 โ 9.138 โ 2.1353 โ
โโโโโโโโโโโโโโโโงโโโโโโโโโงโโโโโโโโโโโโโโโงโโโโโโโโโโโงโโโโโโโโโงโโโโโโโโโโ
```
**What is the likelihood of having [Scalerider](https://hearthstone.gamepedia.com/Scalerider) in hand without a dragon?**
To do.
## Development
I don't expect anyone else to use this code but you never know. Here are the steps you'll want to follow:
```sh
$ python3 -m venv .
$ source ./bin/activate
$ pip install -e ".[dev]"
$ python3 setup.py develop
```
You can run tests with `pytest` and `mypy`.
## License
Licensed under the [WTFPL](http://www.wtfpl.net/).