Created
May 10, 2019 00:02
-
-
Save hexparrot/75ef8c47217175859b56769cde2ced0f to your computer and use it in GitHub Desktop.
MTG:A Opening Hand Land Simulator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
__author__ = "Will Dizon" | |
__license__ = "Public Domain" | |
__version__ = "0.0.1" | |
__email__ = "[email protected]" | |
import numpy as np | |
from scipy.stats import hypergeom | |
from collections import Counter | |
# variables | |
TRIALS = 100000 | |
LANDS_IN_DECK = 17 | |
LIBRARY_SIZE = 40 | |
INITIAL_HAND_SIZE = 7 | |
# calculations | |
land_composition = float(LANDS_IN_DECK)/float(LIBRARY_SIZE) | |
expected_land = (land_composition * INITIAL_HAND_SIZE) | |
[M, n, N] = [LIBRARY_SIZE, LANDS_IN_DECK, INITIAL_HAND_SIZE] | |
rv = hypergeom(M, n, N) | |
x = np.arange(0, n+1) | |
pmf_cards = rv.pmf(x) | |
std_deviation = hypergeom.std(M, n, N) | |
print('--') | |
print('Theoretical distribution of hand in initial hand size of %i' % INITIAL_HAND_SIZE) | |
print("Expected land composition: %.2f" % expected_land) | |
for i in range(0, INITIAL_HAND_SIZE+1): | |
print("%i land: %7.3f%%" % (i, pmf_cards[i] * 100)) | |
#prb = hypergeom.cdf(x, M, n, N) | |
#print(prb) | |
print('--') | |
print('Simulated distribution over %i trials, single-hand draw ("true random")' % (TRIALS)) | |
R = hypergeom.rvs(M, n, N, size=TRIALS) | |
unique, counts = np.unique(R, return_counts=True) | |
dist = dict(zip(unique, counts)) | |
print(dist) | |
print('') | |
for i in range(0, INITIAL_HAND_SIZE+1): | |
pct = float(dist[i]/float(TRIALS)) | |
try: | |
difference_pct = (pct - pmf_cards[i])*100 | |
print("%i land: %7.3f%% (diff: %7.3f%%)" % (i, pct * 100, difference_pct)) | |
except KeyError: | |
print("%i land: %7.3f%%" % (i, 0)) | |
print('--') | |
print('Simulated distribution over %i trials, double-hand draw' % (TRIALS)) | |
print('Deterministically choosing hand closer to expected land composition') | |
print("Selects based on min distance from expected land composition (%.2f land)" % expected_land) | |
R_1 = hypergeom.rvs(M, n, N, size=TRIALS) | |
R_2 = hypergeom.rvs(M, n, N, size=TRIALS) | |
unique_1, counts_1 = np.unique(R_1, return_counts=True) | |
unique_2, counts_2 = np.unique(R_2, return_counts=True) | |
dist_1 = dict(zip(unique_1, counts_1)) | |
dist_2 = dict(zip(unique_2, counts_2)) | |
print(dist_1) | |
print(dist_2) | |
print('') | |
cnt = Counter() | |
for pair in zip(R_1,R_2): | |
diff_1 = abs(expected_land - pair[0]) | |
diff_2 = abs(expected_land - pair[1]) | |
if diff_1 <= diff_2: | |
cnt[pair[0]] += 1 | |
else: | |
cnt[pair[1]] += 1 | |
for i in range(0, INITIAL_HAND_SIZE+1): | |
pct = float(cnt[i]/float(TRIALS)) | |
try: | |
difference_pct = (pct - pmf_cards[i])*100 | |
print("%i land: %7.3f%% (diff: %7.3f%%)" % (i, pct * 100, difference_pct)) | |
except KeyError: | |
print("%i land: %7.3f%%" % (i, 0)) | |
print('--') | |
print('Simulated distribution over %i trials, double-hand draw' % (TRIALS)) | |
print("Random selection of hand WEIGHTED TOWARD expected land composition (%.2f land)" % expected_land) | |
R_1 = hypergeom.rvs(M, n, N, size=TRIALS) | |
R_2 = hypergeom.rvs(M, n, N, size=TRIALS) | |
unique_1, counts_1 = np.unique(R_1, return_counts=True) | |
unique_2, counts_2 = np.unique(R_2, return_counts=True) | |
dist_1 = dict(zip(unique_1, counts_1)) | |
dist_2 = dict(zip(unique_2, counts_2)) | |
print(dist_1) | |
print(dist_2) | |
print('') | |
cnt = Counter() | |
for pair in zip(R_1,R_2): | |
''' | |
how this works- | |
Disclaimer: I am not asserting that this is the methodology used | |
by MTG:A for the hand-draw algorithm, but rather this is a | |
demonstration that with very little effort, opening hands can be | |
tweaked to slightly reduce unforgivingly bad hands and to add | |
that probabilities toward the DESIRED land composition. This | |
method does not--in any way--favor giving the player "exactly 3" | |
or any precise number. This method works specifically so that | |
no matter how many lands you include in your deck, that proportion | |
is more-likely represented by your opening hand at the start | |
of the game. As stated by the WotC mod Godot, this simultaneously | |
accomplishes the following goals: | |
- reduce the frequency of mulligans without incentivizing mana-base | |
construction outside the strategic norms of the game | |
- leans towards giving the player the hand with the mix of spells and | |
lands (without regard for color) closest to average for that deck. | |
https://forums.mtgarena.com/forums/threads/347?page=1 | |
''' | |
# these variables determine how far from the expected land | |
# composition the randomly-generated hand is. 17/40=2.98 | |
pct_off_1 = abs(float(expected_land) - float(pair[0])) / float(expected_land) | |
pct_off_2 = abs(float(expected_land) - float(pair[1])) / float(expected_land) | |
''' if the first and second hands drawn give you 1 and 3 lands, | |
respectively, then the pct_off would be: | |
1 land: 0.6638655462184874 | |
3 land: 0.008403361344537785 | |
That is, 1 land is 66% less than the expected land of 2.98 and | |
3 lands is ridiculously close at .008% off. | |
1 - 2.98 = -1.98, turn that into an absolute value (distance) -> 1.98 | |
3 - 2.98 = -0.02, turn that into an absolute value (distance) -> 0.02 | |
~1.98/2.98 = 0.6638655462184874 | |
~0.02/2.98 = 0.008403361344537785 | |
Since this is a percentage, we can subtract it from 100% to | |
get a meaningful inverse number representing proportion. | |
The means for weighting is the closer the number, the more likely | |
it should be picked, which isn't possible with just distance. | |
1 - 0.6638655462184874 = 0.33613445378151263 (3 land) | |
1 - 0.008403361344537785 = 0.9915966386554622 (1 land) | |
Now add these two together for the full range of the random to choose from. | |
.33613 + .99159 = 1.327731092436975 | |
A random floating point number between 0 and 1.327731092436975 is chosen | |
from a uniform random function which decides the hand: | |
If the number is < .33613, hand 1 is chosen, with 1 land offered. | |
If the number is >= .33613, hand 2 is chosen, with 3 lands offered. | |
This means that it is still POSSIBLE to get the bad hand, but it is far more | |
LIKELY that you would get the 3-land hand. In this specific case, | |
the 3-land is 74.683% likely, and the 1-land hand is the remainder. | |
Naturally, two hands of equal lands (3,3) or even (5,5) would have the same | |
distance from the mean, so both would be equally likely (50/50)--although | |
by natural probabilities, 5-land hands are already particularly unlikely. | |
''' | |
diff_1 = (1 - pct_off_1) | |
diff_2 = (1 - pct_off_2) | |
selection = np.random.uniform(low=0, high=diff_1+diff_2) | |
if selection < diff_1: | |
cnt[pair[0]] += 1 | |
else: | |
cnt[pair[1]] += 1 | |
for i in range(0, INITIAL_HAND_SIZE+1): | |
pct = float(cnt[i]/float(TRIALS)) | |
try: | |
difference_pct = (pct - pmf_cards[i])*100 | |
print("%i land: %7.3f%% (diff: %7.3f%%)" % (i, pct * 100, difference_pct)) | |
except KeyError: | |
print("%i land: %7.3f%%" % (i, 0)) | |
''' | |
UNDERSTANDING THE OUTPUT: | |
Theoretical distribution of hand in initial hand size of 7 | |
Expected land composition: 2.98 | |
[based on land:library size, 2.98 reflects the average amount of land | |
an opening hand will have. Averages do not need to adhere to discrete | |
numbers, e.g., 3, 4, 5.] | |
0 land: 1.315% | |
1 land: 9.205% | |
2 land: 24.546% | |
3 land: 32.297% | |
4 land: 22.608% | |
5 land: 8.397% | |
6 land: 1.527% | |
7 land: 0.104% | |
[Assuming true random, generate a number between 0 and 100 and you'll | |
find out which of these you'd get, most likely 3, 2, then 4 in that order.] | |
Simulated distribution over 100000 trials, double-hand draw | |
Random selection of hand WEIGHTED TOWARD expected land composition (2.98 land) | |
{0: 1222, 1: 9027, 2: 24759, 3: 32095, 4: 22878, 5: 8373, 6: 1541, 7: 105} | |
{0: 1290, 1: 9246, 2: 24574, 3: 32048, 4: 22919, 5: 8280, 6: 1542, 7: 101} | |
[These are the current run's actual generated numbers for lands-in-opening hand] | |
0 land: 0.046% (diff: -1.269%) | |
1 land: 6.523% (diff: -2.682%) | |
2 land: 25.456% (diff: 0.910%) | |
3 land: 38.795% (diff: 6.498%) | |
4 land: 23.268% (diff: 0.660%) | |
5 land: 5.881% (diff: -2.516%) | |
6 land: 0.031% (diff: -1.496%) | |
7 land: 0.000% (diff: -0.104%) | |
[Diff indicates how much impact from the theoretical distribution the adjustment made. | |
Here, over 100000 draws, the player received 1.269% fewer 0-land hands, as in | |
"here's the hand, do you wish to mulligan?" | |
The player received .104% fewer 7 land hands, and received 6.498% MORE 3-land hands. | |
Note, this does not favor ANY meta or land composition: if you adjust the 17/40 lands | |
to 12/40 lands, you'll see ALL THESE NUMBERS GRAVITATE TOWARD THE NEW "EXPECTED LAND" | |
value which would be lower at 2.10 land.] Example run: | |
Simulated distribution over 100000 trials, double-hand draw | |
Random selection of hand WEIGHTED TOWARD expected land composition (2.10 land) | |
{0: 6333, 1: 23843, 2: 34758, 3: 24543, 4: 8719, 5: 1669, 6: 130, 7: 5} | |
{0: 6446, 1: 24265, 2: 34562, 3: 24143, 4: 8777, 5: 1657, 6: 148, 7: 2} | |
0 land: 0.645% (diff: -5.706%) | |
1 land: 24.167% (diff: -0.082%) | |
2 land: 44.870% (diff: 10.078%) | |
3 land: 26.095% (diff: 1.934%) | |
4 land: 4.193% (diff: -4.505%) | |
5 land: 0.030% (diff: -1.576%) | |
6 land: 0.000% (diff: -0.139%) | |
7 land: 0.000% (diff: -0.004%) | |
This is one such approach that would improve best-of-one playability for all players, | |
regardless of the meta. Main takeaway: all players would more likely get exactly | |
the number of lands in their opening hand to match the proportion in their deck. | |
''' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment