#!/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}%')