# -*- coding: utf-8 -*- """ Created on Sun Oct 11 15:11:05 2020 @author: Alex Majlaton """ import random, copy import pandas as pd from statistics import mean from time import process_time ##### MULLIGAN ENGINES ##### # Here I'm writing a few functions that implement various mulligan strategies. # Note: The way these functions are implemented, the cards you remove from # your mulligan hands are never actually put on the bottom of your deck. # I don't expect this to matter for these simulations, but it's # important to note in case this code gets reused again somewhere. # This function is what it sounds like - you never mulligan. def neverMull(deck): starting_deck = copy.copy(deck) random.shuffle(starting_deck) opening_hand = starting_deck[:7] starting_deck = starting_deck[7:] return starting_deck, opening_hand # This function takes a starting deck, a starting hand size, and a starting # number of bolts as an input and generates that starting hand. # The purpose is to use it in a series of loops to simulate games with # every possible opening hand. def startingHandSize(deck, n, b): starting_deck = copy.copy(deck) opening_hand = [] for i in range(b): starting_deck.remove(2) opening_hand.append(2) for j in range(n-b): starting_deck.remove(1) opening_hand.append(1) random.shuffle(starting_deck) return starting_deck, opening_hand # This was the original policy I wrote, before I simulated every hand. # It was based only on my intuition after considering every hand. # A rough description is this: # # 7 cards: Keep 4, 5, or 6 bolts. # 6 cards: Keep 4, 5, or 6 bolts and start 4/2 if possible, then 5/1. # 5 cards: Keep 3, 4, 5, or 6 bolts and start 4/1 if possible, then 3/2. # 4 cards: Keep 2-6 bolts and start 3/1 if possible, then 2/2. # 3 cards: Keep 2-7 bolts and start 2/1 if possible, then 3/0. # 2 cards: Keep 1-7 bolts and start 1/1, if possible, then 2/0. # 1 card: Keep and start a bolt if you have one. def londonMull(deck): mulls = 0 keep = 0 while (keep == 0): starting_deck = copy.copy(deck) random.shuffle(starting_deck) opening_hand = starting_deck[:7] starting_deck = starting_deck[7:] OHB = opening_hand.count(2) if mulls == 0: if OHB in range(4,7): keep = 1 else: mulls += 1 elif mulls == 1: if OHB == 4: keep = 1 opening_hand = [2,2,2,2,1,1] elif OHB in range(5,7): keep = 1 opening_hand = [2,2,2,2,2,1] else: mulls += 1 elif mulls == 2: if OHB in range(4,7): keep = 1 opening_hand = [2,2,2,2,1] elif OHB == 3: keep = 1 opening_hand = [2,2,2,1,1] else: mulls += 1 elif mulls == 3: if OHB in range(3,7): keep = 1 opening_hand = [2,2,2,1] elif OHB == 2: keep = 1 opening_hand = [2,2,1,1] else: mulls += 1 elif mulls == 4: if OHB in range(2,7): keep = 1 opening_hand = [2,2,1] elif OHB == 7: keep = 1 opening_hand = [2,2,2] else: mulls += 1 elif mulls == 5: if OHB == 7: keep = 1 opening_hand = [2,2] elif OHB >= 1: keep = 1 opening_hand = [2,1] else: mulls += 1 elif mulls == 6: keep = 1 if OHB == 7: opening_hand = [2] else: opening_hand = [1] return starting_deck, opening_hand # This is the policy described in the blog post. def mullPolicyTest(deck): mulls = 0 keep = 0 while (keep == 0): starting_deck = copy.copy(deck) random.shuffle(starting_deck) opening_hand = starting_deck[:7] starting_deck = starting_deck[7:] OHB = opening_hand.count(2) if mulls == 0: # here is where you can adjust the range of 7 card keeps. # 4 and 5 represent number of bolts. # to keep 6 bolts, add a 6 to the list. if OHB in [4,5]: keep = 1 else: mulls += 1 elif mulls == 1: if OHB in [4,5,6]: keep = 1 if OHB == 6: opening_hand = [2,2,2,2,2,1] else: opening_hand = [2,2,2,2,1,1] else: mulls += 1 elif mulls == 2: if OHB in [3,4,5,6]: keep = 1 if OHB == 3: opening_hand = [2,2,2,1,1] else: opening_hand = [2,2,2,2,1] else: mulls += 1 elif mulls == 3: keep = 1 if OHB in [3,4,5,6]: opening_hand = [2,2,2,1] elif OHB == 2: opening_hand = [2,2,1,1] else: # even though you've kept 4 cards you're adding 1 to the # mulls counter to signify that you've kept a "bad" 4 card # hand as a way to track how often they occur mulls += 1 if OHB == 7: opening_hand = [2,2,2,2] elif OHB == 1: opening_hand = [2,1,1,1] elif OHB == 0: opening_hand = [1,1,1,1] return starting_deck, opening_hand # create empty lists for scorekeeping boltsInDeck = [] avgKillTurn = [] # use these if you're simulating every hand # numStartingCards = [] # numStartingBolts = [] # timekeeping total_time = float(0) # If you're simulating every hand, uncomment these lines and use them to # loop with the startingHandSize function. # for cards in range(8): # for spells in range(cards+1): # The big loop begins here. # Looping through desired values of lands and bolts in the deck. # These ranges can be adjusted. for i in range(8,25): t1_start = process_time() # create empty starting deck list deck = [] lands = i # adjust deck size here. 60 = regular constructed deck, 99 = EDH, etc. bolts = 60 - i # populate the deck with lands and bolts for l in range(lands): deck.append(1) for b in range(bolts): deck.append(2) # create empty list for scorekeeping scores = [] # choose number of iterations to run here for j in range(10000): # this is where you decide what mulligan policy you're testing starting_deck, opening_hand = mullPolicyTest(deck) #starting_deck, opening_hand = startingHandSize(deck, cards, spells) #starting_deck, opening_hand = londonMull(deck) #starting_deck, opening_hand = neverMull(deck) turn = 0 # adjustable to any life total oppLife = 20 # adjustable to any damage spell - Shock would be 2 for ex. boltDmg = 3 # using the lists to define the starting gameplay values landsInPlay = 0 landsInHand = opening_hand.count(1) boltsInHand = opening_hand.count(2) while oppLife > 0: turn += 1 # strategy for turn 1: # if you have a land, play it # if did and you have a bolt, cast it if turn == 1: if landsInHand >= 1: landsInPlay += 1 landsInHand -= 1 manaLeft = landsInPlay if boltsInHand >= 1 and manaLeft == 1: oppLife -= boltDmg manaLeft -= 1 boltsInHand -= 1 # general strategy after turn 1: # play a land if you have one # then play every single bolt you have mana for if turn > 1: # pop(0) pulls from the front of the list drawStep = starting_deck.pop(0) if drawStep == 1: landsInHand += 1 if drawStep == 2: boltsInHand += 1 if landsInHand > 0: landsInPlay += 1 landsInHand -= 1 manaLeft = landsInPlay if boltsInHand >= manaLeft: boltsInHand -= manaLeft oppLife -= (manaLeft * boltDmg) elif boltsInHand < manaLeft: oppLife -= (boltsInHand * boltDmg) boltsInHand = 0 # discard to hand size if boltsInHand > 7: boltsInHand = 7 # add the score (i.e. the turn you killed on) to the scores list scores.append(turn) # append the results to two lists, use to make a dataframe # if you want CSV output boltsInDeck.append(bolts) avgKillTurn.append(mean(scores)) # use these if you're simulating every hand #numStartingCards.append(cards) #numStartingBolts.append(spells) t1_stop = process_time() total_time += (t1_stop - t1_start) print("Split time:", t1_stop - t1_start) print("Elapsed time:", total_time) print("Total time:", total_time) # if you want a dataframe & CSV output results = pd.DataFrame( {'bolts in deck': boltsInDeck, 'average kill turn': avgKillTurn # use these if you're simulating every hand, don't forget the comma above #'starting hand size': numStartingCards, #'starting bolts': numStartingBolts }) results.to_csv("YOUR DIRECTORY HERE/bolt_sim_allhands_60_100k.csv", index=False)