Requirement already satisfied: symbulate in /usr/local/lib/python3.7/dist-packages (0.5.7)
Requirement already satisfied: numpy in /usr/local/lib/python3.7/dist-packages (from symbulate) (1.19.5)
Requirement already satisfied: matplotlib in /usr/local/lib/python3.7/dist-packages (from symbulate) (3.2.2)
Requirement already satisfied: scipy in /usr/local/lib/python3.7/dist-packages (from symbulate) (1.4.1)
Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->symbulate) (3.0.6)
Requirement already satisfied: python-dateutil>=2.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->symbulate) (2.8.2)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.7/dist-packages (from matplotlib->symbulate) (0.11.0)
Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.7/dist-packages (from matplotlib->symbulate) (1.3.2)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.1->matplotlib->symbulate) (1.15.0)
Name(s): Andrew Kerr
Introduction
One of my favoirte games to play is Team Fight Tactics, TFT for short. The goal of TFT is to be the last player standing in an 8-player free for all. Throughout the game you manage resources, items and gold, and create the strongest team of units, each with different strengths, weaknesses, and abilities. One key skill used throughout the game is knowing when to spend your gold looking for units, and when to save your gold for later rounds. Below I have described the relevant game mechanics I will reference during this investigation.
###Units
Units are separated into five tiers: 1-cost, 2-cost, 3-cost, 4-cost, and 5-cost. There is a fixed unit pool size for each tier which is shared among all the players:
Tier 1: 29
Tier 2: 22
Tier 3: 18
Tier 4: 12
Tier 5: 10
This number represents the amount of copies of each unit in the tier. For example, there are 18 copies of each tier three unit.
If you have 3 of the same unit, then they will combine to be a 2-star version (stronger version) of said unit. Furthermore, if you have three 2-star versions of the same unit, then they will combine to be a 3-star version. In other words, if you want the strongest version of a unit you need to collect 9 of that unit.
###The Shop
Units are mainly bought from the shop using gold you have accumulated. As you might expect, each tier costs the amount of gold in its name and every unit bought from the shop will be 1-star. For example, a 3-cost unit will cost you 3 gold. The shop will offer 5 units at a time. If you buy a unit, another unit will not fill the unoccupied spot until the shop is refreshed. You can use gold to “reroll” the shop, refreshing it with 5 new units (which can be the same as those in the previous reroll).
Every time the shop is created, for each spot, first the the unit tier is decided then the specific unit is decided.
###Levels
Another use of gold is to level-up. You passively gain 2 experience at the start of each round, but can buy 4 experience for 4 gold at any time. Your level corresponds to how many units you may use at once as well as the odds of seeing each tier of champion in the shop.
image.png
For example, lets say there are 2 different 3-cost units (Joe and Bob), you are level 4, and no 3-cost units have been removed from the unit pool. Then there is approximetly a 56% chance of seeing at least one 3-cost unit in your shop, and approximetly a 32% chance of seeing at least one Joe in your shop.
###The Economy
At the start of each round, you passively gain 5 gold. You gain bonus gold if you are on either a win or lose streak:
2-3 Rounds: +1 Gold
4 Rounds: +2 Gold
5+ Rounds: +3 Gold
At the end of each round, your interest is calculated. For every 10 gold you have, you get +1 Gold up to a maximum of +5 Gold.
10-19 Gold: +1 Gold
20-29 Gold: +2 Gold
30-39 Gold: +3 Gold
40-49 Gold: +4 Gold
50+ Gold: +5 Gold
This means when calculating your gold for the next round, your interest is calculated before your passive gold and win/loss streak gold. For example, if you have 29 gold and are on a 6 game loss streak, you will have 39 gold, or gain 10 gold, next round.
Main Code
# -------------------- Shop Creation --------------------# -------------------- X number of shops --------------------def multi_shop(num, level, tier, taken1=0, taken2=0, sims=1):'''Parameters num : amount of shops in a row rolled level : your level tier : the desired units tier taken1 : the amount of units you are looking for which are out of the pool (of the desired unit) taken2 : the amount of units of the tier you are looking for which are out (not including that of the desired unit) # taken1 + taken2 = total number of units in the tier sims : amount of shop simulations Returns a list of the amount of the desired units bought in N shops.''' out = [ [] for x inrange(sims) ]for i inrange(sims): # For each simulation... temp_num = num temp_taken1 = taken1while temp_num !=0: # Roll the shop num times and assume you buy the desired unit whever possible (aka increase taken1) result = shop(level, tier, temp_taken1, taken2) out[i].append(result[0]) temp_taken1 += result[0] temp_num -=1return out# -------------------- The shop at level X --------------------def shop(level, tier, taken1=0, taken2=0, sims=1):'''Parameters level : your level tier : the desired units tier taken1 : the amount of units you are looking for which are out of the pool (of the desired unit) taken2 : the amount of units of the tier you are looking for which are out (not including that of the desired unit) # taken1 + taken2 = total number of units in the tier sims : amount of shop simulations Returns the amount of the desired unit in "sims" shops.''' shop_odds = [(1, 0, 0, 0, 0), (1, 0, 0, 0, 0), (.75, .25, 0, 0, 0), (.55, .30, .15, 0, 0), (.45, .33, .20, .02, 0), (.25, .40, .30, .05, 0), (.19, .30, .35, .15, .01), (.15, .20, .35, .25, .05), (.10, .15, .30, .30, .15)] units_per_tier = [13, 13, 13, 11, 8] # Amount of unique units in each tier (there are nine 5-costs, but 1 does not show up in the shop so we will ignore it) shop_outcomes = spot(shop_odds[level -1], 5, sims) # Simulates a refresh of the shop total_spots = []for i inrange(len(shop_outcomes)): # Counts the number of spots which could hold the desired unit for each simualtion spots =0for z inrange(len(shop_outcomes.get(i))):if shop_outcomes.get(i)[z] == tier: spots +=1 total_spots.append(spots) units_found = []for simulation in total_spots: # Simulates assigning each spot a unit and calculates the total number of desired units found for each simulationif spots !=0: num = unit(taken1, taken2, tier, units_per_tier[tier -1], spots).get(0) units_found.append(num)else: units_found.append(0)return units_found# -------------------- Rolling shop spot(s) --------------------def spot(odds, n=5, sims=1):'''Parameters odds : the shop odds n : amount of spots you want rolled (shop has 5) sims : amount of shop simulations Returns a tuple with a size equivalent to the number of spots wanted, and where each element denotes the unit tier assigned to said spot.''' P = BoxModel([1, 2, 3, 4, 5], probs=odds, size=n) X = RV(P)return X.sim(sims)# -------------------- Rolling units --------------------def unit(taken1, taken2, tier, unique, n):'''Parameters taken1 : the amount of units you are looking for which are out of the pool (of the desired unit) taken2 : the amount of units of the tier you are looking for which are out (including that of the desired unit) tier : the desired units tier unique : the number of unique units in the tier in the same tier that the unit you want is in n : the number of spots to roll Returns the amount of the desired unit rolled.''' units_in_tier = [29, 22, 18, 12, 10] # Amount of units for each unique unit in each tier (eg. There are 13 1-cost units, of which there are 29 of each in the pool) num_sucesses = units_in_tier[tier -1] - taken1 # Amount of units you want in the pool num_failures = (units_in_tier[tier -1] * (unique -1)) - taken2 # Amount of units you do not want in the same tier in the pool Q = Hypergeometric(n = n, N0 = num_failures, N1 = num_sucesses)return Q.sim(1)
# -------------------- Statistics --------------------# -------------------- Expected Value --------------------def average(total_spots, multi=False):'''Parameters total_spots : a list containing the amound of desired units in the shop in a large number of simulations multi : whether the shop was rolled more than once Returns the average number of desired units in the shop.''' total =0ifnot multi:for val in total_spots: total += valelse:for simulation in total_spots: sim_total =0for roll in simulation: sim_total += roll total += sim_totalreturn total /len(total_spots)# -------------------- Probability of at least ... --------------------def at_least_x(total_spots, multi=False, x=1):'''Parameters total_spots : a list containing the amound of desired units in the shop in a large number of simulations multi : whether the shop was rolled more than once x : how many minimum of the desired unit you want to see in the shop Returns the probability of seeing at least x of your desired unit in the shop.''' sims =len(total_spots) count =0ifnot multi: # For each simulation --> see how many desired units --> if >= desired amount, count++for val in total_spots:if val >= x: count +=1else: # For each simulation --> for each shop see how many desired units --> if >= desired amount, count++for simulation in total_spots: total =0for roll in simulation: total += rollif total >= x: count +=1return count / sims
#Scenario 1
##Scenario 1a
The game has just begun, and you want to have a strong start. One way to do this is to get an early 2-star 2-cost unit. Since the game has just begun, no one has bought any 2-cost units yet and I am only level 3. What is the probability that you can get 3 of the same 2-cost unit in only 3 shops?
Another option is to find an early 3-cost unit after leveling up to level 4. Because 3-cost units are inherently stronger than 2-cost units, you do not need to 2-star it. What is the probability of getting 1 of your desired 3-cost unit in 3 shops?
Lets say you waited until you are level 4 for better odds of rolling 2-cost units. Consequently, other players have bought some 2-cost units. Particularly they have bought 10 2-cost units that you don’t want and 2 of the unit that you do want. What is the probability of getting a 2-star 2-costs unit in 3 shops?
How about we try and get a clearer picture of how many of your desired 2-cost units you are seeing in each shop. What is the expected value of the unit seen in these 3 shops at for the level 3 scenario? The level 4?
I have chosen to look at these situations because TFT is split into an early and late game. Depending on what part of the game you are in, you need to look for different units in the shop. I wanted to know what my chances are of finding what I want in during both sections of the game.
In the early part of the game when you are low level and not many units are taken out of the pool, you have the best chances of finding a singular 3-cost unit that you want rather than getting a 2-star 2-cost unit that you want. In both cases, rolling at level 3 and waiting to roll at level 4, the probability of 2-staring a 2-cost unit is approximetly 2%. Meanwhile, finding just one 3-cost unit only happens approximetly 11% of the time. However, you would want to stop at a 1-star since 3-staring the unit will only happen 0.2% of the time.
Scenario 2
##Scenario 2a
Now lets look at the some late game rolling odds. When looking for a 4-cost unit, the main powerhouse of your team, some people roll at level 7 while others wait to roll at level 8.
At level 7, you will have more money, allowing you to see more shops. The drawback is you have lower odds of finding 4-cost units. What is the probability of finding a 2-star 4-cost in 10 rolls, assuming 1 of your desired unit is taken and 3 other 4-costs are taken?
Look at that! You got lucky and found 1 of your desired 4-cost units without having to roll at all. What are your odds in the level 7 case? The level 8?
The most powerful units in the game are the 5-cost units. You hope to get 1 while you are level 8 since it is rare anyone makes it to level 9. Just how lucky are those who get early 5-cost units?
What is the probability of getting any 5-cost unit at level 7 (yes, this IS possible)?
I have chosen to look at these situations because TFT is split into an early and late game. Depending on what part of the game you are in, you need to look for different units in the shop. I wanted to know what my chances are of finding what I want in during both sections of the game.
In the late game it is better to roll for a 4-cost unit early at level 7 when you can afford more rolls and less 4-cost units are taken out of the pool than waiting to roll at level 8. Rolling any 5-cost unit at level 7 has a slightly lower probability than I expected at roughly 4.8% per shop. However, at level 8 the probability of seeing any 5-cost unit in one shop is higher than I expected at 22%.
#Scenario 3
How does the probability of seeing at least one of your desired unit in 1 shop change depending on your level? For simplicity, we will assume no units are removed from the pool.
for i inrange(1, 6): shops = [multi_shop(num=1, level=n, tier=i, sims=10000) for n inrange(1, 10)] results = [at_least_x(n, True) for n in shops] plt.plot(results)plt.legend(['1-Cost Unit', '2-Cost Unit', '3-Cost Unit', '4-Cost Unit', '5-Cost Unit'])plt.xlabel('Player Level (add 1 to each number for the level)')plt.ylabel('Probability of at least 1')plt.title('Probability of at Least One X-Cost Unit')
Text(0.5, 1.0, 'Probability of at Least One X-Cost Unit')
What about the probability of seeing at least one desired unit in X amount of shops? We will look at level 7 since this is the first level all tiers of units are availible (and running this code take a long time).
for i inrange(1, 6): shops = [multi_shop(num=n, level=7, tier=i, sims=1000) for n inrange(1, 15)] results = [at_least_x(n, True) for n in shops] plt.plot(results)plt.legend(['1-Cost Unit', '2-Cost Unit', '3-Cost Unit', '4-Cost Unit', '5-Cost Unit'])plt.xlabel('Number of Shops (add 1 to each shop for the number of shops)')plt.ylabel('Probability of at least 1')plt.title('Probability of at Least One X-Cost Unit in N Shops')
Text(0.5, 1.0, 'Probability of at Least One X-Cost Unit in N Shops')
##Scenario 3 Conclusions
I constructed these graphs because I have always wondered what level is best to roll at for what tier unit as well as how many times I need to roll to find what unit I am looking for.
When looking for a specific unit, you should roll at level:
2 for a 1-cost unit
4 for a 2-cost unit
5 for a 3-cost unit
9 for a 4-cost unit
9 for a 5-cost unit
Some of these levels suprised me since they do not align with which level has the highest probability of seeing that tier unit in the shop. For example, at level 4 there is a 30% chance of a shop spot to be a 2-cost unit. According to the first graph, this is the best level to roll for a specific 2-cost unit even thought at level 6 there is a 40% chance of a shop spot being a 2-cost unit.
When looking for at least one unit at level 7, to have a 50% chance of seeing the unit you should roll:
10 times for a 1-cost unit
7 times for a 2-cost unit
5 times for a 3-cost unit
12 times for a 4-cost unit
way too many times for a 5-cost unit
If you remember the introduction, it costs 2 gold to refresh the shop. This means it costs 10 gold to have a 50% chance of seeing a desired 3-cost unit (spending 10 gold on rolling is a reasonable amount). This is assuming no other 3-cost units are out of the pool, including the one you are looking for, so the real number of shops may be slightly more or less depending on the situation.
#Appendex
Unit Pool Size and Rolling Odds: https://www.esportstales.com/teamfight-tactics/champion-pool-size-and-draw-chances
Help describing the basics of TFT: https://mobalytics.gg/blog/tft-guide/
# Code for example in Introductiondef is_Joe(x):for i inrange(5):if x[i] ==31:return1return0def is_Bob(x):for i inrange(5):if x[i] ==32:return1return0def is_3_cost(x): out =0for i inrange(5):if x[i] ==31or x[i] ==32: out +=1return outP = BoxModel([1,2,31,32], probs=[.55, .3, .15/2,.15/2], size=5)X = RV(P)x = X.sim(10000)xx = x.apply(is_Joe)xxx = x.apply(is_Bob)xxxx = x.apply(is_3_cost)print(xx.sum() /10000)print(xxx.sum() /10000)print(xxxx.count_geq(1) /10000)xxxx.plot()xx.plot()
0.3176
0.3201
0.5521
# This code was never used... Oh well (I couldn't bring myself to delete it)# -------------------- Gold --------------------class Gold:def__init__(self, gold, streak, level):self.gold = goldself.streak = streakdef__repr__(self):return"Gold: %d\nStreak: %d"% (self.gold, self.streak)def add_interest(self): # Adds interest (at the end of each round) i =self.gold //10if i <=5:self.gold += ielse:self.gold +=5def add_streak(self):ifself.streak >=5: # Streak gold gain (at the start of each round)self.gold +=3elifself.streak ==4:self.gold +=2elifself.streak >=2:self.gold +=1defround(self):self.add_interest()self.gold +=5# Passive gold gain (at the start of each round)self.add_streak()self.streak +=1def roll(self): # It costs 2 gold to refresh the shopifself.gold <1: # You can't have negative goldreturnFalseself.gold -=2returnTruedef buy_unit(self, tier, amount=1): # The tier of a unit is also its costifself.gold < tier * amount: # You can't have negative goldreturnFalseself.gold -= (tier * amount)returnTruedef predicted_gold(bal, rounds):'''Paramaters bal : current amout of gold rounds : number of rounds without rolling Returns amount of gold you would have in "rounds" rounds.''' temp_bal = Gold(bal.gold, bal.streak)for i inrange(rounds): temp_bal.round()return temp_bal