Latest Legends Pulling Simulator

kbmartin
kbmartin Posts: 13 Just Dropped In
edited January 2017 in MPQ General Discussion
Hey everyone, I wanted to see how many Latest Legends I would have to pull to fully cover all the new 5*. SO I wrote some Python code to simulate pulling latest legends. A Monte Carlo simulation with 50,000 runs yields rather stable results.

The code tells you how many latest legends you need to pull, to have any given percentage chance of getting fully covered five-stars. It gives you results for the current situation, where customer service is swapping duplicate-color (>5) Latest Legends covers for a different color of the same toon; it also gives results for the future, when, I gather, such swaps will no longer be available.

Here are some examples of the code in action. Caveat: I'm an amateur and I very easily could have screwed up something and the numbers could all be wrong. If you see an error in my code, please do tell me! OK? Good.

If you have NO latest legends covers and want to fully cover all three five-stars:
50%: with swaps: 315, without swaps: 377
55%: with swaps: 323, without swaps: 388
60%: with swaps: 331, without swaps: 398
65%: with swaps: 339, without swaps: 410
70%: with swaps: 348, without swaps: 423
75%: with swaps: 358, without swaps: 438
80%: with swaps: 369, without swaps: 456
85%: with swaps: 383, without swaps: 478
90%: with swaps: 402, without swaps: 508
95%: with swaps: 431, without swaps: 556

By the way, if you're really unlucky (not unlucky though, ha ha), there is around a 0.1% chance that you'll have to pull more than 600 times to fully cover with customer service swaps, or more than 800 times without the swaps. I've seen the odd simulation go 1100 pulls. Random chance can be brutal.


If you have no latest legends, but only care about getting two five-stars fully covered:
50%: with swaps: 293, without swaps: 347
55%: with swaps: 301, without swaps: 358
60%: with swaps: 309, without swaps: 369
65%: with swaps: 318, without swaps: 381
70%: with swaps: 327, without swaps: 394
75%: with swaps: 337, without swaps: 409
80%: with swaps: 349, without swaps: 427
85%: with swaps: 363, without swaps: 449
90%: with swaps: 383, without swaps: 477
95%: with swaps: 413, without swaps: 525

Hmm. That's an improvement of a little more than twenty pulls. Probably if you're going to do this, you may as well shoot for three.


Let's say you're me. I already have a 0/2/0 Natasha and a 0/0/1 Thanos, and I want to hoard until you can pull LTs and have all three latest fully-covered.
50%: with swaps: 295, without swaps: 359
55%: with swaps: 302, without swaps: 369
60%: with swaps: 310, without swaps: 380
65%: with swaps: 318, without swaps: 391
70%: with swaps: 326, without swaps: 405
75%: with swaps: 336, without swaps: 420
80%: with swaps: 348, without swaps: 437
85%: with swaps: 362, without swaps: 458
90%: with swaps: 380, without swaps: 488
95%: with swaps: 408, without swaps: 539


Let's say you're ._-KENSH-_. (!!!) A much better situation. You already have a 4/7/2 Natasha, a 4/6/1 Strange and a 1/4/2 Thanos. Here's what you need to do to fully cover that Thanos:
50%: with swaps: 114, without swaps: 186
55%: with swaps: 120, without swaps: 195
60%: with swaps: 126, without swaps: 204
65%: with swaps: 133, without swaps: 215
70%: with swaps: 140, without swaps: 227
75%: with swaps: 148, without swaps: 240
80%: with swaps: 157, without swaps: 256
85%: with swaps: 168, without swaps: 276
90%: with swaps: 183, without swaps: 303
95%: with swaps: 206, without swaps: 349


The Python (3.6) code follows. I know there are some inefficiencies, but 50,000 simulations wasn't a big enough problem to worry about optimizing the time I used. I also know that a real programmer could probably make this all into an awesome online calculator, provide graphical analysis of the data, etc., etc. Maybe someone on the forums has already done that and this was, "a waste of time" (although it was a labor of love and worth it in its own right). I only taught myself Python over the last two weeks. Before that I hadn't written anything in ten years, since I was writing stuff for fun in C++. Programming is not my day job. So go easy on me. Please attribute me if you share or improve. And, enjoy.


Yours,
thankyoumrdata
MPQU commander


"""copyright Kevin Martin 2017-01-14"""
"""Simulate pulling Latest Legends covers from MPQ toons until you have three
fully-covered Latest Legends."""

simsize = 50000
cutoffs_range = range(50, 100, 5)
"""50% - 95% by 5% increments"""

from random import randrange, seed

class Toon:

"""Defines the number of each of three covers you have in an MPQ toon"""

def __init__(self):
self.coverage = [0, 0, 0]

def reset(self):
self.coverage = [0, 0, 0]

def increment(self):
"""Adds a random cover to a toon"""
color = randrange(3)
self.coverage[color] = self.coverage[color] + 1

def fully_covered(self):
"""Returns boolean of whether you have at least enough covers
to fully cover a toon by MPQ definitions"""
sortedtoon = sorted(self.coverage)
if sortedtoon[0] < 3:
return False
elif sortedtoon[0] == 3:
return all((sortedtoon[1] >= 5, sortedtoon[2] >= 5))
else:
return all((sortedtoon[1] >= 4, sortedtoon[2] >= 5))

def covered_with_swaps(self):
"""Returns boolean of whether you have enough covers if
customer service will swap covers for you"""
if self.coverage[0] + self.coverage[1] + self.coverage[2] >= 13:
return True
else:
return False

"""initialization"""
seed()
Natasha = Toon()
Strange = Toon()
Thanos = Toon()
noswap_tally = []
swap_tally = []

"""Run the simulation, simsize number of times"""
for sim in range(simsize):

"""initialize this particular simulation"""
pullcount = 0
all_covered_with_swaps = False

"""For baseline scenario:"""
Natasha.reset()
Strange.reset()
Thanos.reset()

"""A variety of simulations I've been asked to run.
If you are interested in fewer than all three toons, just set
toons not interested in to fully covered, e.g. [5, 5, 5]"""

"""Simulation for Kensh's coverage as of 2017-01-14
Natasha.coverage = [4, 7, 2]
Strange.coverage = [4, 6, 1]
Thanos.coverage = [1, 4, 2]"""

"""Simulation for Ian Shen's coverage as of 2017-01-14. he wanted natasha
data only, others are arbitratily fully covered
Natasha.coverage = [4, 5, 2]
Strange.coverage = [5, 5, 5]
Thanos.coverage = [5, 5, 5]"""

"""Simulation for if you only care about finishing with two
of the three five-stars maxed:
Natasha.coverage = [5, 5, 5]
Strange.coverage = [0, 0, 0]
Thanos.coverage = [0, 0, 0]"""

"""Simuation for my personal situation 2017-01-14
Natasha.coverage = [0, 2, 0]
Strange.coverage = [0, 0, 0]
Thanos.coverage = [0, 0, 1]"""

"""Run the simulation until all toons are fully covered,
assuming no customer service swaps."""
while not all((Natasha.fully_covered(),
Strange.fully_covered(),
Thanos.fully_covered())):

pull = randrange(20)
"""5% chance of pulling each of the latest legends."""
if pull == 0:
Natasha.increment()
elif pull == 1:
Strange.increment()
elif pull == 2:
Thanos.increment()

"""If customer service is still swapping duplicate covers,
note that the simulation would have ended here and add
how many pulls that took to the swap tally"""
if not all_covered_with_swaps:
if all((Natasha.covered_with_swaps(),
Strange.covered_with_swaps(),
Thanos.covered_with_swaps())):
swap_tally.append(pullcount)
all_covered_with_swaps = True

pullcount = pullcount + 1;

"""progress bar"""
if (sim % 1000) == 0:
print('Sims: {:d} / {:d}'.format(sim, simsize))

"""the simulation finished, add how many pulls it took to the noswap
tally"""
noswap_tally.append(pullcount)


"""sort the tallies of simulations with a given number of
pulls needed for full coverage, from fewest to most"""
swap_tally.sort()
noswap_tally.sort()

"""dictionaries of how many pulls it took to reach
given cutoff percentages of simulations"""
swap_cutoff_tally = dict()
noswap_cutoff_tally = dict()

swap_currenttotal, noswap_currenttotal = 0, 0

"""print a list of all numbers of pulls,
where a simulation reached full coverage"""
for num_pulls in range(swap_tally[0], noswap_tally[len(noswap_tally)-1]+1):

"""set tally counts to the number of simulations that took
this number of pulls to complete"""
swap_tallycount = swap_tally.count(num_pulls)
noswap_tallycount = noswap_tally.count(num_pulls)

"""increment current total number of simulations reviewed"""
new_swap_currenttotal = swap_currenttotal + swap_tallycount
new_noswap_currenttotal = noswap_currenttotal + noswap_tallycount

"""if we've crossed a cutoff, record it"""
for cutoff in cutoffs_range:
if all((swap_currenttotal/simsize < cutoff / 100,
new_swap_currenttotal/simsize >= cutoff / 100)):
swap_cutoff_tally.setdefault(cutoff, num_pulls)
if all((noswap_currenttotal/simsize < cutoff / 100,
new_noswap_currenttotal/simsize >= cutoff / 100)):
noswap_cutoff_tally.setdefault(cutoff, num_pulls)

"""finalize increments"""
swap_currenttotal = new_swap_currenttotal
noswap_currenttotal = new_noswap_currenttotal

"""print raw data"""
if any ((swap_tallycount, noswap_tallycount)):
print('{:4d}: With swaps: {:3d} {:4.1%}, No swaps: {:3d} {:4.1%}'
.format(num_pulls+1,
swap_tallycount, swap_currenttotal/simsize,
noswap_tallycount, noswap_currenttotal/simsize))

"""print summary"""
print("Summary:")
for cutoffs in cutoffs_range:
print('{:3.0%}: with swaps: {:4d}, without swaps: {:4d}'
.format(cutoffs / 100,
swap_cutoff_tally.get(cutoffs),
noswap_cutoff_tally.get(cutoffs)))

Comments

  • kbmartin
    kbmartin Posts: 13 Just Dropped In
    I reposted this in the right place. viewtopic.php?f=34&t=57271
  • kbmartin
    kbmartin Posts: 13 Just Dropped In
    First error discovered -- I simulated pulling until fully covered when one of the three is already fully covered and assumed that was the same as "how long does it take to cover two.". But it's not, because, combinatorics. Will fix and update
  • Ducky
    Ducky Posts: 2,255 Community Moderator
    howard_icon.pngIf you wish to comment on this thread, do so here. Thanks! howard_icon.png
This discussion has been closed.