#!/usr/bin/env python3
import numpy as np
import math
import random
from scipy.stats import truncnorm
import matplotlib.pyplot as plt
## Adjust these as desired:
# enable mulligans
mulligans_enabled = True
# number of simulations to run for each deck
num_simulations = 1000000
# cards in our deck
deck_size = 99
# simulate a range of decks with X lands in them
land_counts = [30,31,32,33,34,35,36,37,38,39,40,41,42]
# condition of success: play this many lands by simulation end
desired_land_drops = 4
# if we draw/play this many lands or less, we consider ourselves screwed
screw_threshold = 3
# if we draw X cards, and X*flood_rate or more are lands, we've flooded out
flood_threshold_rate = 0.50
## Adjusting number of sim turns requires adjusting the card draw distribution as well:
# number of turns we're simulating
sim_num_turns = 4
# get a normal-ish dist of integers that represent how likely we are to draw X cards in four turns
# you will need to manually adjust this by trial and error plotting if you change the number of turns you're simulating or how much card draw you want
loc=0.4
scale = 1.0
dist_range = 3
size = 100000
incidental_draw_dist = truncnorm(loc=loc, a=-dist_range/scale, b=+dist_range/scale, scale=scale).rvs(size=size)
incidental_draw_dist = incidental_draw_dist.round().astype(int)
def put_hand_back_and_shuffle_and_draw_seven(deck, hand):
for x in range(hand['Land']):
deck.append('Land')
for x in range(hand['Nonland']):
deck.append('Nonland')
hand['Land'] = 0
hand['Nonland'] = 0
random.shuffle(deck)
# deal 7
for x in range(7):
card = deck.pop(0)
hand[card] += 1
def mulligan(deck, hand, prev_mulligans):
match prev_mulligans:
case 0:
if (hand['Land'] == 3 or hand['Land'] == 4):
return 0
else:
# free mulligan
put_hand_back_and_shuffle_and_draw_seven(deck, hand)
return mulligan(deck, hand, prev_mulligans + 1)
case 1:
if (hand['Land'] >= 3 and hand['Land'] <= 5):
return 1
else:
# go to to 6
put_hand_back_and_shuffle_and_draw_seven(deck, hand)
return mulligan(deck, hand, prev_mulligans + 1)
case 2:
if (hand['Land'] >= 2 and hand['Land'] <= 5):
match hand['Land']:
case 2 | 3:
hand['Nonland'] -= 1
deck.append('Nonland')
case 4 | 5:
hand['Land'] -= 1
deck.append('Land')
case _:
print('error, no match')
return 2
else:
# go to 5
put_hand_back_and_shuffle_and_draw_seven(deck, hand)
return mulligan(deck, hand, prev_mulligans + 1)
case 3:
if (hand['Land'] >= 1 and hand['Land'] <= 6):
match hand['Land']:
case 1 | 2 | 3:
hand['Nonland'] -= 2
deck.append('Nonland')
deck.append('Nonland')
case 4:
hand['Land'] -= 1
hand['Nonland'] -= 1
deck.append('Land')
deck.append('Nonland')
case 5 | 6:
hand['Land'] -= 2
deck.append('Land')
deck.append('Land')
case _:
print('error, no match')
return 3
else:
# go to 4
put_hand_back_and_shuffle_and_draw_seven(deck, hand)
return mulligan(deck, hand, prev_mulligans + 1)
case 4:
# bottom as best we can and keep whatever
match hand['Land']:
case 0 | 1 | 2 | 3:
hand['Nonland'] -= 3
deck.append('Nonland')
deck.append('Nonland')
deck.append('Nonland')
case 4:
hand['Nonland'] -= 2
hand['Land'] -= 1
deck.append('Nonland')
deck.append('Nonland')
deck.append('Land')
case 5:
hand['Land'] -= 2
hand['Nonland'] -= 1
deck.append('Land')
deck.append('Land')
deck.append('Nonland')
case 6 | 7:
hand['Land'] -= 3
deck.append('Land')
deck.append('Land')
deck.append('Land')
case _:
print('error, no match')
return 4
case _:
print('error, no match')
return -1
# run sim
print('-------------------------------------------------------------------------------------------')
print('Info')
print(f'Simulating each deck for {sim_num_turns} turns, {num_simulations} times:')
print(f'Each simulation will draw {sim_num_turns + 7} cards + an extra ({max(min(incidental_draw_dist), 0)} - {max(incidental_draw_dist)}) cards (to represent additional draw)')
print(f'Condition of success is to have {desired_land_drops} total land drops by turn {sim_num_turns}')
print(f'The screwed threshold is {screw_threshold} lands and the flooded rate is {flood_threshold_rate*100}%')
for land_count in land_counts:
results = []
decklist = {
'Land': land_count,
'Nonland': deck_size - land_count
}
for i in range(num_simulations):
deck = []
num_mulligans = 0
num_incidental_draw = random.choice(incidental_draw_dist)
if (num_incidental_draw < 0):
num_incidental_draw = 0
#spread our incidental draw over number of turns
incidental_draw = []
draw = num_incidental_draw
for x in range(sim_num_turns):
if (draw > 0):
incidental_draw.append(1)
draw -= 1
else:
incidental_draw.append(0)
random.shuffle(incidental_draw)
# construct a deck and shuffle
for card in decklist.keys():
deck += [card] * decklist[card]
random.shuffle(deck)
hand = {
'Land': 0,
'Nonland': 0
}
# deal 7
for x in range(7):
card = deck.pop(0)
hand[card] += 1
if (mulligans_enabled and (hand['Land'] < 3 or hand['Land'] > 4)):
num_mulligans = mulligan(deck, hand, 0)
played_land = 0
for x in range(sim_num_turns):
# draw for turn
card = deck.pop(0)
hand[card] += 1
# first turn, we have to play a land before we can cantrip
if (x == 0):
#play a land?
if (hand['Land'] > 0):
played_land += 1
hand['Land'] -= 1
if (played_land > 0 and incidental_draw[x] == 1):
# draw an extra card
card = deck.pop(0)
hand[card] += 1
# we can use our cantrip to try and fix
else:
if (played_land > 0 and incidental_draw[x] == 1):
# draw an extra card
card = deck.pop(0)
hand[card] += 1
#play a land?
if (hand['Land'] > 0):
played_land += 1
hand['Land'] -= 1
result = [played_land, hand['Land'], num_mulligans, num_incidental_draw]
results.append(result)
total_played_land = 0
total_drawn_land = 0
total_mulligans = 0
total_extra_cards_drawn = 0
screw_count = 0
flood_count = 0
success_count = 0
success_count_no_flood = 0
cards_drawn_bins = [[0]*5 for j in range(max(incidental_draw_dist) + 1)]
for result in results:
total_played_land += result[0]
total_drawn_land += (result[0] + result[1])
total_mulligans += result[2]
total_extra_cards_drawn += result[3]
cards_drawn_bins[result[3]][0] += 1
cards_drawn_in_this_result = sim_num_turns + 7 + result[3]
if (result[0] >= desired_land_drops):
success_count += 1
cards_drawn_bins[result[3]][1] += 1
if (result[0] >= desired_land_drops and (result[0] + result[1]) < math.ceil(cards_drawn_in_this_result * flood_threshold_rate)):
success_count_no_flood += 1
cards_drawn_bins[result[3]][2] += 1
elif ((result[0] + result[1]) >= math.ceil(cards_drawn_in_this_result * flood_threshold_rate)):
flood_count += 1
cards_drawn_bins[result[3]][3] += 1
elif (result[0] <= screw_threshold ):
screw_count += 1
cards_drawn_bins[result[3]][4] += 1
average_played_land = total_played_land / len(results)
average_drawn_land = total_drawn_land / len(results)
average_mulligans = total_mulligans / len(results)
average_extra_cards_drawn = total_extra_cards_drawn / len(results)
success_rate = success_count / len(results)
flood_rate = flood_count / len(results)
screw_rate = screw_count / len(results)
success_rate_no_flood = success_count_no_flood / len(results)
print('-------------------------------------------------------------------------------------------')
print('-------------------------------------------------------------------------------------------')
print(f'LANDS: {land_count}')
print('-------------------------------------------------------------------------------------------')
print('Averages:')
print(f'Average number of lands played: {average_played_land:>58.3f}')
print(f'Average number of lands drawn: {average_drawn_land:>59.3f}')
print(f'Average number of mulligans required: {average_mulligans:>52.3f}')
print(f'Average number of extra cards drawn: {average_extra_cards_drawn:>53.3f}')
print('-------------------------------------------------------------------------------------------')
print('Overall:')
print()
print(f'Rate at which we hit our desired number of land drops: {success_rate * 100:>35.3f}%')
print(f'Rate at which we, without flooding, hit our desired number of land drops: {success_rate_no_flood * 100:>16.3f}%')
print(f'Rate at which we flooded out: {flood_rate * 100:>60.3f}%')
print(f'Rate at which we got mana screwed: {screw_rate * 100:>55.3f}%')
print('-------------------------------------------------------------------------------------------')
print(f'When we drew no extra cards: {cards_drawn_bins[0][0] / len(results) * 100:>59.3}%')
print()
print(f'Rate at which we hit our desired number of land drops: {(cards_drawn_bins[0][1] / cards_drawn_bins[0][0]) * 100:>35.3f}%')
print(f'Rate at which we, without flooding, hit our desired number of land drops: {(cards_drawn_bins[0][2] / cards_drawn_bins[0][0]) * 100:>16.3f}%')
print(f'Rate at which we flooded out: {(cards_drawn_bins[0][3] / cards_drawn_bins[0][0]) * 100:>60.3f}%')
print(f'Rate at which we got mana screwed: {(cards_drawn_bins[0][4] / cards_drawn_bins[0][0]) * 100:>55.3f}%')
print('-------------------------------------------------------------------------------------------')
print(f'When we drew one extra card: {cards_drawn_bins[1][0] / len(results) * 100:>59.3}%')
print()
print(f'Rate at which we hit our desired number of land drops: {(cards_drawn_bins[1][1] / cards_drawn_bins[1][0]) * 100:>35.3f}%')
print(f'Rate at which we, without flooding, hit our desired number of land drops: {(cards_drawn_bins[1][2] / cards_drawn_bins[1][0]) * 100:>16.3f}%')
print(f'Rate at which we flooded out: {(cards_drawn_bins[1][3] / cards_drawn_bins[1][0]) * 100:>60.3f}%')
print(f'Rate at which we got mana screwed: {(cards_drawn_bins[1][4] / cards_drawn_bins[1][0]) * 100:>55.3f}%')
print('-------------------------------------------------------------------------------------------')
print(f'When we drew two extra cards: {cards_drawn_bins[2][0] / len(results) * 100:>60.3f}%')
print()
print(f'Rate at which we hit our desired number of land drops: {(cards_drawn_bins[2][1] / cards_drawn_bins[2][0]) * 100:>35.3f}%')
print(f'Rate at which we, without flooding, hit our desired number of land drops: {(cards_drawn_bins[2][2] / cards_drawn_bins[2][0]) * 100:>16.3f}%')
print(f'Rate at which we flooded out: {(cards_drawn_bins[2][3] / cards_drawn_bins[2][0]) * 100:>60.3f}%')
print(f'Rate at which we got mana screwed: {(cards_drawn_bins[2][4] / cards_drawn_bins[2][0]) * 100:>55.3f}%')
print('-------------------------------------------------------------------------------------------')
print(f'When we drew three extra cards: {cards_drawn_bins[3][0] / len(results) * 100:>58.3f}%')
print()
print(f'Rate at which we hit our desired number of land drops: {(cards_drawn_bins[3][1] / cards_drawn_bins[3][0]) * 100:>35.3f}%')
print(f'Rate at which we, without flooding, hit our desired number of land drops: {(cards_drawn_bins[3][2] / cards_drawn_bins[3][0]) * 100:>16.3f}%')
print(f'Rate at which we flooded out: {(cards_drawn_bins[3][3] / cards_drawn_bins[3][0]) * 100:>60.3f}%')
print(f'Rate at which we got mana screwed: {(cards_drawn_bins[3][4] / cards_drawn_bins[3][0]) * 100:>55.3f}%')
print('-------------------------------------------------------------------------------------------')
print('-------------------------------------------------------------------------------------------')
print()
print()
#print(f'When we drew four extra cards: {cards_drawn_bins[4][0] / len(results) * 100:>58.3f}%')
#print()
#print(f'Rate at which we hit our desired number of land drops: {(cards_drawn_bins[4][1] / cards_drawn_bins[4][0]) * 100:>35.3f}%')
#print(f'Rate at which we, without flooding, hit our desired number of land drops: {(cards_drawn_bins[4][2] / cards_drawn_bins[4][0]) * 100:>16.3f}%')
#print(f'Rate at which we flooded out: {(cards_drawn_bins[4][3] / cards_drawn_bins[4][0]) * 100:>60.3f}%')
#print(f'Rate at which we got mana screwed: {(cards_drawn_bins[4][4] / cards_drawn_bins[4][0]) * 100:>55.3f}%')