forked from frankkarsten/MTG-Math
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathNicolBolasInDevotion.py
240 lines (208 loc) · 10 KB
/
NicolBolasInDevotion.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
import random
from collections import Counter
num_simulations = 10000000
def put_cards_on_bottom(hand, spells_remaining_to_bottom):
"""
Parameters:
hand - A dictionary, with the same cardnames as in decklist, with number drawn
spells_remaining_to_bottom - The number of spells to bottom after a mulligan (must be <= number of spells in hand)
Returns - nothing, it just adjusts the opening hand value
"""
#First bottom other spells
Bottomable_other_spell = min(hand['Other spell'], spells_remaining_to_bottom)
hand['Other spell'] -= Bottomable_other_spell
spells_remaining_to_bottom -= Bottomable_other_spell
#Then bottom excess Nicol Bolas beyond the first
if hand['Nicol Bolas'] >= 2:
Bottomable_nicol = min(hand['Nicol Bolas'] - 1, spells_remaining_to_bottom)
hand['Nicol Bolas'] -= Bottomable_nicol
spells_remaining_to_bottom -= Bottomable_nicol
#Then bottom excess Storm the Festivals beyond the first
if hand['Storm the Festival'] >= 2:
Bottomable_storm = min(hand['Storm the Festival'] - 1, spells_remaining_to_bottom)
hand['Storm the Festival'] -= Bottomable_storm
spells_remaining_to_bottom -= Bottomable_storm
#Then bottom excess lands beyond the fifth
if hand['Forest or Elf'] >= 6:
Bottomable_forest = min(hand['Forest or Elf'] - 5, spells_remaining_to_bottom)
hand['Forest or Elf'] -= Bottomable_forest
spells_remaining_to_bottom -= Bottomable_forest
#Then bottom excess Oath of Nissa beyond the first
if hand['Oath of Nissa'] >= 2:
Bottomable_oath = min(hand['Oath of Nissa'] - 1, spells_remaining_to_bottom)
hand['Oath of Nissa'] -= Bottomable_oath
spells_remaining_to_bottom -= Bottomable_oath
#In the unlikely situation that we still have to bottom more, bottom lands
Bottomable_forest = min(hand['Forest or Elf'], spells_remaining_to_bottom)
hand['Forest or Elf'] -= Bottomable_forest
spells_remaining_to_bottom -= Bottomable_forest
def run_one_sim(decklist):
keephand = False
for handsize in [7, 6, 5, 4]:
#We may mull 7, 6, or 5 cards and keep every 4-card hand
#Once we actually keep, the variable keephand will be set to True
if not keephand:
#Construct library as a list
library = []
for card in decklist.keys():
library += [card] * decklist[card]
random.shuffle(library)
#Construct a random opening hand
hand = {
'Forest or Elf': 0,
'Storm the Festival': 0,
'Nicol Bolas': 0,
'Oath of Nissa': 0,
'Other spell': 0
}
for _ in range(7):
card_drawn = library.pop(0)
hand[card_drawn] += 1
if handsize == 7:
#Do we keep?
if (hand['Forest or Elf'] >= 2 and hand['Forest or Elf'] <= 5):
keephand = True
if handsize == 6:
#We have to bottom.
put_cards_on_bottom(hand, 1)
#Do we keep?
if (hand['Forest or Elf'] >= 2 and hand['Forest or Elf'] <= 4):
keephand = True
if handsize == 5:
#We have to bottom.
put_cards_on_bottom(hand, 2)
#Do we keep?
if (hand['Forest or Elf'] >= 2 and hand['Forest or Elf'] <= 4):
keephand = True
if handsize == 4:
#We have to bottom.
put_cards_on_bottom(hand, 3)
#Do we keep?
keephand = True
oath_on_battlefield = False
lands_on_battlefield = 0
bolas_on_battlefield = False
six_mana_by_turn_5 = False
for turn in [1, 2, 3, 4, 5, 6]:
#Draw step
draw_a_card = True if (turn == 1 and random.random() < 0.5) or (turn > 1) else False
if (draw_a_card):
card_drawn = library.pop(0)
hand[card_drawn] += 1
#Play a land if possible
land_played = False
if hand['Forest or Elf'] > 0:
lands_on_battlefield += 1
hand['Forest or Elf'] -= 1
land_played = True
mana_available = lands_on_battlefield
#On turn 4, 5 and 6, we assume to always have an extra mana via, say, Wolfwillow Haven or Nykthos
if turn in [4, 5, 6]:
mana_available += 1
#Play Nicol Bolas if possible
if mana_available >= 5 and hand['Nicol Bolas'] > 0 and oath_on_battlefield:
bolas_on_battlefield = True
hand['Nicol Bolas'] -= 1
mana_available -= 5
#Play Oath of Nissa if possible, then dig for Bolas or land
if mana_available >= 1 and hand['Oath of Nissa'] > 0:
hand['Oath of Nissa'] -= 1
oath_on_battlefield = True
top_three_cards = []
for _ in range(3):
top_three_cards.append(library.pop(0))
#Take Nicol Bolas if possible
if "Nicol Bolas" in top_three_cards:
hand["Nicol Bolas"] += 1
elif "Forest or Elf" in top_three_cards:
hand['Forest or Elf'] += 1
#Play a land if we hit one and didn't have a land for the turn before
if not land_played and hand['Forest or Elf'] > 0:
lands_on_battlefield += 1
hand['Forest or Elf'] -= 1
land_played = True
mana_available += 1
#Cast Nicol Bolas if possible
if mana_available >= 5 and hand['Nicol Bolas'] > 0 and oath_on_battlefield:
bolas_on_battlefield = True
hand['Nicol Bolas'] -= 1
mana_available -= 5
#Play Storm the Festival if possible
if mana_available >= 6 and hand['Storm the Festival'] > 0:
hand['Storm the Festival'] -= 1
top_five_cards = []
for _ in range(5):
top_five_cards.append(library.pop(0))
#Take Nicol Bolas if possible
if "Nicol Bolas" in top_five_cards:
bolas_on_battlefield = True
elif "Oath of Nissa" in top_five_cards:
#Normally it'll just be one, but we might grab two Oaths
number_oaths = min(Counter(top_five_cards)['Oath of Nissa'], 2)
for _ in range(number_oaths):
#Yeah, yeah, literally copy-pasting code from earlier is a bad practice. I'm lazy today
oath_on_battlefield = True
top_three_cards = []
for _ in range(3):
top_three_cards.append(library.pop(0))
#Take Nicol Bolas if possible
if "Nicol Bolas" in top_three_cards:
hand["Nicol Bolas"] += 1
elif "Forest or Elf" in top_three_cards:
hand['Forest or Elf'] += 1
#Play a land if we hit one and didn't have a land for the turn before
if not land_played and hand['Forest or Elf'] > 0:
lands_on_battlefield += 1
hand['Forest or Elf'] -= 1
land_played = True
mana_available += 1
#Cast Nicol Bolas if possible
if mana_available >= 5 and hand['Nicol Bolas'] > 0 and oath_on_battlefield:
bolas_on_battlefield = True
hand['Nicol Bolas'] -= 1
mana_available -= 5
#While theoretically we could grab some lands, this is unlikely to happen in practice
#Realistically you'd take some expensive permanent, so I'll ignore the land option for now
#On turn 5, check if we hit all of our land drops so far
if turn == 5 and lands_on_battlefield == 5:
six_mana_by_turn_5 = True
if six_mana_by_turn_5 and bolas_on_battlefield:
Outcome = 'BOLAS IN PLAY'
elif six_mana_by_turn_5 and (not bolas_on_battlefield) and hand["Nicol Bolas"] > 0 and oath_on_battlefield:
#Should only happen if we awkwardly hit Bolas with Oath of Nissa grabbed off of a turn-6 Storm the Festival
#We'll count this as a success as well
Outcome = 'BOLAS IN PLAY'
elif six_mana_by_turn_5 and (not bolas_on_battlefield) and hand["Nicol Bolas"] > 0 and (not oath_on_battlefield):
Outcome = 'UNCASTABLE BOLAS'
elif six_mana_by_turn_5 and (not bolas_on_battlefield) and hand["Nicol Bolas"] == 0:
Outcome = 'IGNORE'
elif not six_mana_by_turn_5:
Outcome = 'IGNORE'
else:
print("This is literally impossible. The simulation is drunk.")
return Outcome
decklist = {
'Forest or Elf': 33,
'Storm the Festival': 4,
'Nicol Bolas': 2,
'Oath of Nissa': 4,
'Other spell': 17
}
print("-----------------")
print(decklist)
print('')
total_relevant_games = 0.0
total_favorable_games_bolas_on_battlefield = 0.0
total_unfavorable_games_stuck_in_hand = 0.0
for _ in range(num_simulations):
Outcome = run_one_sim(decklist)
if (Outcome == "BOLAS IN PLAY"):
total_relevant_games += 1
total_favorable_games_bolas_on_battlefield +=1
if (Outcome == "UNCASTABLE BOLAS"):
total_relevant_games += 1
total_unfavorable_games_stuck_in_hand += 1
if (Outcome == "IGNORE"):
pass
print("Conditional probability to control Nicol Bolas by turn 6:" +str(round(total_favorable_games_bolas_on_battlefield / total_relevant_games * 100.0 ,1))+"%.")
print("Conditional probability to be stuck with an uncastable Nicol Bolas in hand on turn 6:" +str(round(total_unfavorable_games_stuck_in_hand / total_relevant_games * 100.0 ,1))+"%.")