This paper will explore the pricing model of ‘gacha games’, a genre of predominantly mobile games who’s primary monetization method is the sale of ‘gacha’ draws. These draws allow players to trade for a random in-game item, with associated rarities corresponding to their probabilities. something, something… lotteries, something, something… technically not gambling, something, something… ethics?
2 Introduction
Whilst ‘Gacha’ is generally understood to be a form of gambling, this is not strictly true. Both gacha and gambling involve exchanging money for the probability of wining a ‘prize’, the key difference is in the payoff: gambling wins you money (more often than not a negative amount); but gacha wins you in-game items. Being lucky when gambling gives you the means to gamble more: you increase the money you have meaning you have more money to place on the next bet. Whereas being lucky in gacha gives you the means to gacha less.
3 Literature Review
I hope no one has done this before.
4 Theory
Gacha is a series of lotteries: each pull has a set of possible outcomes, and each outcome has a probability of being drawn. Let us call each pull \(L_k\), where \(k\in \mathbb{N}_{++}\) is the number of the pull. Let the set of outcomes (i.e. the in-game items) be a fixed, finite set \(X=[x_1, x_2, x_3, ..., x_n]\). Each outcome \(x_i\in X\) has a probability of being drawn \(p_i\). The discrete probability distribution over the outcomes is given by \(P(x)\). To illustrate, consider the following example:
Imagine that you want a new sword for your character, so let us assume the items are all swords. Next, assume swords come in 3 different rarities: common (call it a Short Sword), which is drawn at a probability of 90%; rare (call it an Uchigatana), which is drawn with a probability of 9%; and legendary (call it the Sword of Night), which is drawn with a probability of 1%.
Clearly, the likelihood of obtaining the rarest, and often best, items is very low, thus players generally need to pull many times before obtaining the Sword of Night which they desire. Furthermore, pulling more times only increases the probability of obtaining the Sword of Night, it does not guarantee it. This unfortunate fact lead to the next section, which addresses this issue.
4.1 Pity
If you really wanted the Sword of Night, it would really suck if you pulled over and over, 100+ times, and still didn’t get it. Whilst this is still a possibility in some gacha games, nearly all implement a ‘Pity system’, which effectively sets the maximum limit on the number of unsuccessful pulls. This ‘hard pity’ is determined by the probability of the rarest items, the cost of a pull, and the desired profit margin of the game developers. A typical number for ‘hard pity’ is between 80-100 pulls, so for our example let us choose 90.
The so called ‘hard pity’ is not the only pity system in place, as the likelihood of the rarest items will increase the closer to the ‘hard pity’ you get. The number of pulls required to start the increase in probability is called the ‘soft pity’. This is generally around 70-80 pulls, so let us choose 75. By law, game developers must disclose the probabilities of each item, but they are not required to disclose the ‘soft pity’ numbers, nor the probabilities of the items after this point. Thus, let us assume some linear increase in probability, from 1% at 74 pulls to 100% at 90 pulls. Such a probability function, conditional on the pity \(k\), where the event \(x\) is success, would be:
Using this we can graph the probabilities (conditional on no successes) for the Sword of Night in Figure 1 below. Note that the x-axis is the number of pity \((given by k)\), which will always be 1 less than the number of pulls \((given by n)\).
The second rarest items generally also have a pity system, where you are guaranteed to receive at least 1 of them every 10 pulls or so. The number of pulls to be guaranteed a rare item is openly stated, and is usually 10. Further, this guarantee is inclusive of rarer items as well, meaning that every 10 pulls you are guaranteed at least 1 rare item, but it could be a legendary item. The probability that this rare item will be of legendary rarity is usually unchanged (1%), implying that the probability of a common item is transferred to the rare item (90%+9%=99%).
From the above distribution we can also calculate the expected number of pulls required to obtain the Sword of Night, first by finding the expected pity, and inferring that the expected number of pulls is 1 more than this. Call the number of pulls for a success \(\bar{n}\). The expected pity, conditional on success, is given by the sum of the probability of \(k\) being the first success, weighted by k:
\[
E(k+1|x) = 1 + \sum_{i=1}^{k} k P(k|x)
\tag{2}\]
Note that the probability of success \(x\) after \(k+1\) pulls is dependant the probability of \(k\) previous failures. Thus, the probability of success in pull \(k+1\) is given by:
In the case where the pity is zero, the probability of success is simply the conditional probability of success in the first pull, as the probability of zero pity is 1. All subsequent probabilities of success are conditional on the previous failures, and a thus defined slightly differently. In every case, as each pull is independent, we can deduce that \(P(k)=P(k-1)(1-P(x|k-1))\) for \(k>0\).
As the probability of success is 1% below 75 pulls, and follows equation (number) from 75 pulls upwards, we can input these into the probability equation:
The joint probability of success and pity can be seen to fall slowly, at a factor of 0.99, until the soft pity of 74, where it rises sharply, due to an increased (conditional) probability of success, and then falls back down due to a decreasing probability of reaching such high levels of pity. This decreasing probability is precisely due to the fact that the (conditional) probability of success is rising, and thus the (conditional) probability of failure is decreasing.
We can make some further inferences from this join probability distribution. The probability to obtain the Sword of Night in the first 74 pulls is 52.47%. This can be calculated in two ways, either by subtracting the probability of no successes \((0.99^{74})\) from 100%, or one can sum the joint probabilities from the above figure from 0 to 73 pity:
All but 1 term from each sum cancels each other out, only leaving \(0.99^{0}-0.99^{74}\), and hence 5.
From the joint probability it is a simple step to get to the probability of pity \(k\), conditional on success (thanks Bayes):
\[
P(k|x) = \frac{P(x\cap k)}{P(x)}
\]
But what is the probability of success \(P(x)\)? This probability can be difficult to find in practice, but luckily it’s rather simple: If you pull 90 times, you are guaranteed to succeed, thus \(P(x)=1\). I made a slightly sneaky assumption in the above calculations, and that is that the probability of success 100%, and I did this by assuming that one would pull until they succeeded. This reason for this, is that the domain for the pity should be complete, which requires enough pulls to happen to allow for all values of pity to be possible: if you only do 50 pulls then it’s impossible to have a pity above 50. More on this later.
Assuming still that we will keep pulling until success (and no more), we can calculate the expected number of pulls required to obtain the Sword of Night as:
\[
E(\bar{n}|x) = 1 + \sum_{i=0}^{k} k P(k|x) = 1 + \sum_{i=0}^{k} k P(x\cap k)
\tag{6}\]
These densities can be seen in Figure 3, the sum of which (plus 1) will yield our expected number of pulls for success. This value for our example is 54.65. This is the number of pulls you should expect to make in order to get the Sword of Night, and therefore the basis for the price of the item.
The mean does not tell us the entire story, as the distribution is by no means symmetric: it is heavily weighted above a pity of 74. Using Figure 2, the median can be seen to be 68, with 25% and 75% percentiles of 28 and 77 respectively, and the mode is also 77.
A brief aside on the number of pulls
In the previous section, I assumed that the number of pulls is sufficiently large to guarantee success. This sets the probability of success to 1, and allows for the full range of pity to be possible. However, I want to consider the case where this is not true; where the number of pulls may not be sufficient to guarantee success.
This can still be reflected, but requires truncation of the distributions depending on the number of pulls, and therefore a more complex probability of success. Instead of a unconditional probability of success, it will be conditional on the number of pulls, but just not conditional on the pity. This above probabilities will be the same, but also conditioned on the number of pulls, with the exception of when the pity is above the number of pulls, in which case the probability of success is 0. The conditional probability of success does not change, when also conditioning on n, the probability that is changing is the probability of reaching a pity of \(k\). Therefore, \(P(x|k, n)\) is the same as \(P(x|k)\), and given by 1. This implies that \(x\) is independent of \(n\).
The joint probability of success and pity, conditional on the number of pulls, is given by 7. This is the same as 3, but with the added condition that \(k<n\). As the probability of \(k\) being equal to or larger than the number of pulls \(n\) is 0, the joint probability of success and pity is 0 for \(k\geq n\):
This can be seen in Figure 4, where the x-axis is still the pity, like in Figure 2, but one can use the slider to see the effect of changing the number of pulls.
viewof n = Inputs.range( [1,90], {step:1,value:90,label:'Number of pulls',id:'n'})
One may notice that if \(k\ge n\), then the cumulative joint probability, conditional on \(n\), no longer adds to 1. This number is the probability of success, conditional on n, and is given by the sum over the joint probabilities, conditional on \(n\), for \(k<n\):
\[
P(x|n) = \sum_{i=0}^{n-1} P(x\cap i|n)
\]
Note that this is increasing with \(n\), reflecting that the probability of success increases the more pulls that you do, until it reaches 1 at hard pity. From this we can find the expected number of pulls to get a success \(\bar{n}\), conditional on only \(n\) pulls:
\[
E(\bar{n}|x, n) = 1 + \sum_{i=0}^{k} k P(k|x, n) = 1 + \sum_{i=0}^{k} k \frac{P(x\cap k|n)}{P(x|n)} = 1 + \frac{1}{\sum_{j=0}^{n-1} P(x\cap j|n)}\sum_{i=0}^{k} k P(x\cap k|n)
\]
These joint densities can be seen in Figure 5, where the x-axis is still the pity, like in Figure 3, but one can use same the slider above to see the effect of changing the number of pulls.
The distribution in unchanged from Figure 3 when \(n=90\), but as \(n\) falls the rest of the distribution grows. This happens because as the probability of success decreases with fewer pulls, the densities are scaled up.
Although it is important to consider the number of pulls as a possible constraint, for the remainder of the paper I will re-assume the previous case, where the number of pulls will be sufficient to guarantee success. When success is not guaranteed, the concept of a price for the items becomes murky, hence the wish to avoid it.
4.2 Simulation
To illustrate the above, we can simulate the process of pulling 10,000 times, and record the number of times each rarity of item is obtained.The simulation can be played, paused, and restarted using the buttons below, as well as manually sliding the bar to view the results at a specific pull number.
I have also included some statistics from this simulation above. The first table shows the total number for each rarity as a moving total as the number of pulls increase. It also shows how the pity changes over time, to illustrate how the pity system works. The second table shows final totals, as well as the minimum, maximum, and average number of pulls required to obtain a rare and legendary item. In addition, the distribution of the pity for legendary items is graphed in Figure 6, against the predicted counts based on Figure 3.
The below table also shows legendary pity statistics from the simulation and compares them to the predictions.
Mean Pity
Modal Pity
25% Percentile Pity
Median Pity
75% Percentile Pity
Simulation
57.43
77
38
67
78
Prediction
54.65
77
28
68
77
The simulated data and predictions are very well matched, with the exception of the 25% percentile, which is in the middle of the region which has the most variance due to such a low probability of success1. Looking at the simulated data, there is fewer successes between 0 and 33 than between 34 and the median of 67, which is why the 25% percentile is 10 pity higher in the simulation than the prediction.
To test if this 25% percentile difference is a one-off or a consistent bias, I will run the simulation 100 times and compare the 25% percentile of the pity for each run to the prediction. The distribution for the 25% percentile can be seen in Figure 7.
One can see that the range of values for the 25th percentile includes both the expected value (28), towards the middle, and the value from the simulation (38) towards the tail. This suggests that it was indeed an outlier, and that the simulation is consistent with the predictions. Further, the variance among the 25th percentile and median is larger than the 75th percentile as hypothesised. One can explain this by pointing to the variance of the geometrically distributed section of the probability distribution, of which the 25th percentile and median lie on. If this geometric distribution was continued to infinity (as it properly would be), the variance would be \(\frac{1-p}{p^2}\), which is larger for small probabilities. This gives reason for the larger variance in the 25th percentile and median, and the smaller variance in the 75th percentile, if not mathematically exact, or rigorous.
4.3 Sparks
Not all gacha systems include a pity system, and instead rely on a ‘spark’ system. This system is similar to the pity system, in the fact that it restricts the maximum number of pulls you need to do before acquiring the desired item. The difference is that the ‘spark’ system does not manipulate probabilities, but rather guarantees the choice of the item after a certain number of pulls. A common number for this is 200 pulls, and if the (pre-chosen) desired item has not been obtained by this point, one can choose to exchange ‘sparks’ for it. A single spark is obtained for every failed pull.
This system is more simple to calculated expected values for the number of pulls, as the probability of success is a simple geometric distribution. The system under pity is also geometrically distributed, but the probability of success is not constant, and is dependent on the number of pulls, whereas the probability of success in the spark system is constant.
\[
P(n=\bar{n}) = (1-p)^{\bar{n}-1} p
\]
The expected number of pulls to obtain the desired item is given by 8:
This expected value is based on the assumption that one will do 200 pulls (if necessary), so success is guaranteed. Unsurprisingly, with the same probability of success, the expected number of pulls is larger than the pity system. I have also simulated the spark system for 100000 pulls (p=0.01 and number of sparks for legendary is 200), and the distribution of the number of pulls can be seen in Figure 8. The statistics from these simulations, and the predicted amounts, can be seen in the table below.
Note, there are many observations above the spark threshold of 200, which means that it took more than 200 pulls to get a legendary item naturally, but one can assume that the player would choose the legendary item after 200 pulls, and would only continue to pull if they want further copies, or if a new limited-time item is released (in which case the spark system would reset, but their previous pull count would not). Therefore, we can view these observations as cases where the player would select a number of legendary items through the spark system and then obtains a legendary item through chance after the observed pulls.
4.4 50/50
Apologies, I should have included a trigger warning here for the gacha gamers reading this. The dreaded ‘50/50’ is something that has turned great excitement into extreme disappointment and anger. The reason for why is that, in most gacha systems, when you get a legendary item, you’re not always guaranteed that it’s the legendary item you want. In fact it can be legendary item you really don’t want.
Until now, i’ve assumed a single item of the highest rarity (in fact for any rarity). However, usually there is a set of legendary items, of which 1 will be chosen. This set will include a single limited-time item: normally more powerful, better looking, more fun; and also a subset of ‘standard’ items. When you obtain a legendary item, there is a 50% probability that it is the limited-time item, but also a 50% probability that it is any one of the ‘standard’ items. This is known as the 50/50.
Typically, losing the 50% chance of the legendary item of choice, will guarantee it for the next legendary item obtained, limiting the number of times one can lose this 50/50. Some systems will have variations where there is more than one limited-time item and the 50/50 is split across them, with a separate probability determining whether one obtains a limited legendary item or a ‘standard’ legendary item. For example, assume that there are 2 limited-time legendary items; on pulling a legendary, there is a random draw to determine whether ths legendary will be one of the 2 limited-time items or a standard item, and say these probabilities are 75% to 25%; then, if a limited-time item is chosen, there is a 50% chance that it will be either of the 2 limited-time items; if a standard item is chosen, the probabilities are also split uniformly across however many standard items are in the set.
Knowing that your chances of getting the limited-time item is not certain, even when you get a legendary item, will change the expected number of pulls, but it’s not obviously clear by how much it would increase this number. Instinct may tell us that the expected number of pulls will be half way between winning on the first legendary item, and losing the 50/50 and being guaranteed it on the next legendary. The expected value will, by definition, be between these two values, but the expectation is not the sum of the expectations of 2 legendary pull sequences. This is because the latter sequence is conditional on the first sequence, as 50% of the time (on average) you win on the first legendary, thus erasing the need to keep pulling.
Let us continue with the example used previously, but add a 50/50 mechanic. The range of pulls needed to the a single limited-time legendary, is now between 1 pull (if you’re lucky) and 180 pulls (if you’re really unlucky). Again, assume for simplicity that one will do enough pulls to guarantee the desired item, such that \(p(x)=1\). We can then calculate the expected number of pulls needed to get the limited-time legendary using the same conditional probability distribution in Figure 1, but changing the probability of observing each pity number to reflect the 50/50. The key difference here is that some of the possible values for the number of pulls for a success have (broadly) 2 different paths to them. For example, a successful pull of the limited-time legendary after 10 pulls can be achieved by either winning the 50/50 on the 10th pull, or losing the 50/50 before this, but getting another legendary on the 10th pull. These are the 2 broadly different paths, but actually there is 10 different possibilities here: winning the 50/50 on the 10th pull is one, but there are 9 different ways to lose the 50/50 before the 10th pull, and then get the limited-time legendary on the 10th pull. Therefore, the probability of getting the limited-time legendary on the 10th pull is the sum of the probabilities of these 10 different paths. For this example, the probability is:
Note that I have used the probability of success of 0.01 as we are well before the soft pity. If we use the general terms for the probability conditional on the pity \(p(x|k)\), we would have:
\[
p(\bar{n}=10) = \underbrace{[\Pi_{i=0}^{8}(1-p(x|k=i))]}_{\text{1st 9 are losses}} \underbrace{p(x|k=9)}_{\text{1 win in 10}} \underbrace{(0.5)}_{\text{win 50/50}} + \text{Pr(8 losses and 2 wins)} \underbrace{(0.5)}_{\text{lose 50/50}}
\]
This term I have not calculated is more complex as it needs to contain the multiple different ways of achieving \(n-2\) losses and 2 wins, specifically with the second win at \(n\). To calculate this, see the below table of probabilities. Each row represents when the first win occurs, and then the probability of this sequence happening.
First Win
Probability
\(m\)
\(Pr(\text{m-1 losses})\)
\(Pr(\text{Win in pull m})\)
\(Pr(\text{Loss until pull 9})\)
\(Pr(\text{Win in pull 10})\)
1
1
\(\times\)
\(p(x|k=0)\)
\(\times\)
\([\Pi_{i=0}^{7}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=8)\)
2
\((1-p(x|k=0))\)
\(\times\)
\(p(x|k=1)\)
\(\times\)
\([\Pi_{i=0}^{6}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=7)\)
3
\([\Pi_{i=0}^{1}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=2)\)
\(\times\)
\([\Pi_{i=0}^{5}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=6)\)
4
\([\Pi_{i=0}^{2}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=3)\)
\(\times\)
\([\Pi_{i=0}^{4}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=5)\)
5
\([\Pi_{i=0}^{3}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=4)\)
\(\times\)
\([\Pi_{i=0}^{3}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=4)\)
6
\([\Pi_{i=0}^{4}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=5)\)
\(\times\)
\([\Pi_{i=0}^{2}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=3)\)
7
\([\Pi_{i=0}^{5}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=6)\)
\(\times\)
\([\Pi_{i=0}^{1}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=2)\)
8
\([\Pi_{i=0}^{6}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=7)\)
\(\times\)
\((1-p(x|k=0))\)
\(\times\)
\(p(x|k=1)\)
9
\([\Pi_{i=0}^{7}(1-p(x|k=i))]\)
\(\times\)
\(p(x|k=8)\)
\(\times\)
1
\(\times\)
\(p(x|k=0)\)
The first term and 3rd term are the same, but the probabilities are realised in a different order, so will give the same probability. In fact, looking at the table, one can see that the probabilities are the same for m=1+i and m=9-i, so we can simplify the formula. This will mean that the formula will be different depending on whether n is even or odd:
Somehow my simplifications make this look more complex. Surely it simplifies… right? One way we could simplify the calculation of this is to separate the equation before n=74 (i.e. soft-pity) and after. Before soft-pity, all marginal probabilities are the same \((p(x|k)=p=0.01)\), so the equation simplifies to:
Typically, as economists, when face with uncertain outcomes, we use the expected values to inform us of what the price should be. In the case of gacha, we don’t want to focus on the expected value of a single pull, but rather the expected number of pulls one would need to obtain the desired items.
4.6 Special Cases
Here I will talk about Gacha without Pity, and loot boxes (which are pretty similar). The probability of success is easier to calculate here as it’s 1 - Probability of infinite failure. Wait, that’s 1?
5 Conclusion
6 References
Footnotes
Not 100% about this, but my gut tells me that the low probability of these events causes very large variance, and can lead to the 25% percentile being more volatile than other percentiles. In fact, this carries through for any percentile below the median, which is itself very close to the soft pity, and the higher likelihood events, and therefore less volatile.↩︎
The quantiles were calculated using \(\frac{\ln(1-q)}{\ln(1-p)}\), where \(q\) is the quantile, and \(p\) is the probability of success. These were then rounded up to the nearest integer.↩︎
Source Code
---title: "Pricing in Gacha Games"author: "Alex Ralphs"toc: truenumber-sections: truebibliography: references.biblink-citations: truelink-bibliography: truecrossref: eq-prefix: ""highlight-style: pygmentsexecute: echo: falseformat: html: code-tools: true code-fold: true html-math-method: mathjax smooth-scroll: true page-layout: full embed-resources: true # pdf: # geometry: # - top=30mm # - left=20mm # docx: defaultjupyter: python3editor: render-on-save: trueeditor_options: chunk_output_type: inline---<head><!-- Remote stylesheet --> <link rel="stylesheet" href="styles.css"></head>## AbstractThis paper will explore the pricing model of 'gacha games', a genre of predominantly mobile games who's primary monetization method is the sale of 'gacha' draws. These draws allow players to trade for a random in-game item, with associated rarities corresponding to their probabilities. something, something... lotteries, something, something... technically not gambling, something, something... ethics?## IntroductionWhilst 'Gacha' is generally understood to be a form of gambling, this is not strictly true. Both gacha and gambling involve exchanging money for the probability of wining a 'prize', the key difference is in the payoff: gambling wins you money (more often than not a negative amount); but gacha wins you in-game items. Being lucky when gambling gives you the means to gamble more: you increase the money you have meaning you have more money to place on the next bet. Whereas being lucky in gacha gives you the means to gacha less. ## Literature ReviewI hope no one has done this before.## TheoryGacha is a series of lotteries: each pull has a set of possible outcomes, and each outcome has a probability of being drawn. Let us call each pull $L_k$, where $k\in \mathbb{N}_{++}$ is the number of the pull. Let the set of outcomes (i.e. the in-game items) be a fixed, finite set $X=[x_1, x_2, x_3, ..., x_n]$. Each outcome $x_i\in X$ has a probability of being drawn $p_i$. The discrete probability distribution over the outcomes is given by $P(x)$. To illustrate, consider the following example: ::: {style="margin-left: 2em;"}*Imagine that you want a new sword for your character, so let us assume the items are all swords. Next, assume swords come in 3 different rarities: common (call it a **Short Sword**), which is drawn at a probability of 90%; rare (call it an **Uchigatana**), which is drawn with a probability of 9%; and legendary (call it the **Sword of Night**), which is drawn with a probability of 1%.*:::Clearly, the likelihood of obtaining the rarest, and often best, items is very low, thus players generally need to pull many times before obtaining the *Sword of Night* which they desire. Furthermore, pulling more times only increases the probability of obtaining the *Sword of Night*, it does not guarantee it. This unfortunate fact lead to the next section, which addresses this issue.### PityIf you really wanted the *Sword of Night*, it would really suck if you pulled over and over, 100+ times, and still didn't get it. Whilst this is still a possibility in some gacha games, nearly all implement a 'Pity system', which effectively sets the maximum limit on the number of unsuccessful pulls. This 'hard pity' is determined by the probability of the rarest items, the cost of a pull, and the desired profit margin of the game developers. A typical number for 'hard pity' is between 80-100 pulls, so for our example let us choose 90. The so called 'hard pity' is not the only pity system in place, as the likelihood of the rarest items will increase the closer to the 'hard pity' you get. The number of pulls required to start the increase in probability is called the 'soft pity'. This is generally around 70-80 pulls, so let us choose 75. By law, game developers must disclose the probabilities of each item, but they are not required to disclose the 'soft pity' numbers, nor the probabilities of the items after this point. Thus, let us assume some linear increase in probability, from 1% at 74 pulls to 100% at 90 pulls. Such a probability function, conditional on the pity $k$, where the event $x$ is success, would be:$$P(x|k) = \begin{cases} 0.01 \quad \text{for } 0\leq k\lt74 \\ 0.01 + \frac{0.99}{16} (k+1-74) \quad \text{for } 74\leq k\leq 89 \quad \end{cases}$$ {#eq-cond-prob}Using this we can graph the probabilities (conditional on no successes) for the *Sword of Night* in @fig-cond-prob below. Note that the x-axis is the number of pity $(given by k)$, which will always be 1 less than the number of pulls $(given by n)$. ```{ojs}//| label: fig-cond-prob//| fig-cap: 'Conditional Probability of Success, P(x|k)'{ let Freq_data = [] for (var i=0; i<=89; i++){ if (i<74){ Freq_data.push({'Pity': i, 'Probability': 0.01}) } else { Freq_data.push({'Pity': i, 'Probability': 0.01 + 0.99*(i-73)/16}) } } const plot = Plot.plot({ x: { label: 'Pity, (k)', domain: [-2, 92], ticks: d3.ticks(0, 90, 20) }, y: { label: 'Probability, P(x|k)', }, marks: [ Plot.ruleX(Freq_data, {x: 'Pity', y: 'Probability', stroke: 'black', strokeWidth: 8}), Plot.tip(Freq_data, Plot.pointerX({x: 'Pity', y: 'Probability'})), Plot.frame(), ], width: width, }) return plot}```The second rarest items generally also have a pity system, where you are guaranteed to receive at least 1 of them every 10 pulls or so. The number of pulls to be guaranteed a rare item is openly stated, and is usually 10. Further, this guarantee is inclusive of rarer items as well, meaning that every 10 pulls you are guaranteed at least 1 rare item, but it could be a legendary item. The probability that this rare item will be of legendary rarity is usually unchanged (1%), implying that the probability of a common item is transferred to the rare item (90%+9%=99%). From the above distribution we can also calculate the expected number of pulls required to obtain the *Sword of Night*, first by finding the expected pity, and inferring that the expected number of pulls is 1 more than this. Call the number of pulls for a success $\bar{n}$. The expected pity, conditional on success, is given by the sum of the probability of $k$ being the first success, weighted by k:$$E(k+1|x) = 1 + \sum_{i=1}^{k} k P(k|x)$$ {#eq-exp-pity-plus-one}Note that the probability of success $x$ after $k+1$ pulls is dependant the probability of $k$ previous failures. Thus, the probability of success in pull $k+1$ is given by:$$P(x\cap k) = P(x|k)P(k) = \begin{cases} P(x|0) \qquad\qquad\qquad\qquad \text{ for } k=0 \\ P(x|k)\Pi_{i=0}^{k-1} (1-P(x|i)) \quad \text{for } k>0 \end{cases}$$ {#eq-joint-prob}In the case where the pity is zero, the probability of success is simply the conditional probability of success in the first pull, as the probability of zero pity is 1. All subsequent probabilities of success are conditional on the previous failures, and a thus defined slightly differently. In every case, as each pull is independent, we can deduce that $P(k)=P(k-1)(1-P(x|k-1))$ for $k>0$.As the probability of success is 1% below 75 pulls, and follows equation (number) from 75 pulls upwards, we can input these into the probability equation:$$P(x\cap k) = P(x|k)P(k) = \begin{cases} 0.01*(0.99)^k \qquad\qquad \text{for } 0\leq k\lt 74 \\ (0.01+\frac{0.99(k+1-74)}{16})*(0.99)^{73}*\Pi_{i=74}^{k}(1-0.01-\frac{0.99(i-74)}{16}) \quad \text{for } 74\leq k\leq 89 \end{cases}$$ {#eq-joint-prob-2}This can be shown graphically in @fig-joint-prob.```{ojs}//| label: fig-joint-prob//| fig-cap: 'Joint Probability of Success and Pity, P(x ∩ k)'{ let Prob_data = [] let Prob_data_sum = [] let prob_x_k = 0 let prob_x_k_sum = 0 for (var k=0; k<=89; k++){ if (k<74) { prob_x_k = 0.01*(0.99**k) prob_x_k_sum += prob_x_k Prob_data.push({'Pity': k, 'Probability': prob_x_k}) Prob_data_sum.push({'Pity': k, 'Probability': prob_x_k_sum}) } else { let prob_failure_k_74 = 0.99**73 for (var i=74; i<=k; i++){ prob_failure_k_74 *= (0.99-0.99*(i-74)/16) } prob_x_k = (0.01+0.99*(k-73)/16)*prob_failure_k_74 prob_x_k_sum += prob_x_k Prob_data.push({'Pity': k, 'Probability': prob_x_k}) Prob_data_sum.push({'Pity': k, 'Probability': prob_x_k_sum}) } } const plot_1 = Plot.plot({ x: { label: 'Pity, (k)', domain: [-2, 92], ticks: d3.ticks(0, 90, 20) }, y: { label: 'Probability, P(x ∩ k)', domain: [0, 0.08] }, marks: [ Plot.ruleX(Prob_data, {x: 'Pity', y: 'Probability', stroke: 'black', strokeWidth: 8}), Plot.tip(Prob_data, Plot.pointerX({x: 'Pity', y: 'Probability'})), Plot.frame(), ], width: width, }) const plot_2 = Plot.plot({ x: { label: 'Pity, (k)', domain: [-2, 92], ticks: d3.ticks(0, 90, 20) }, y: { label: 'Cumulative Probability, P(x ∩ k)', domain: [0, 1] }, marks: [ Plot.ruleX(Prob_data_sum, {x: 'Pity', y: 'Probability', stroke: 'black', strokeWidth: 8}), Plot.tip(Prob_data_sum, Plot.pointerX({x: 'Pity', y: 'Probability'})), Plot.frame(), Plot.ruleY([0.25], {stroke: 'red', strokeDasharray: '5,5'}), Plot.ruleY([0.5], {stroke: 'red', strokeDasharray: '5,5'}), Plot.ruleY([0.75], {stroke: 'red', strokeDasharray: '5,5'}), ], width: width, }) return html`${plot_1}${plot_2}`}```The joint probability of success and pity can be seen to fall slowly, at a factor of 0.99, until the soft pity of 74, where it rises sharply, due to an increased (conditional) probability of success, and then falls back down due to a decreasing probability of reaching such high levels of pity. This decreasing probability is precisely due to the fact that the (conditional) probability of success is rising, and thus the (conditional) probability of failure is decreasing.We can make some further inferences from this join probability distribution. The probability to obtain the *Sword of Night* in the first 74 pulls is `{python} round(100*sum([0.01*(0.99**k) for k in range(74)]), 2)`%. This can be calculated in two ways, either by subtracting the probability of no successes $(0.99^{74})$ from 100%, or one can sum the joint probabilities from the above figure from 0 to 73 pity:$$P(x|k<74) = 1-0.99^{74} = \sum_{i=0}^{73} 0.01(0.99)^i$$ {#eq-prob-74}To prove this is true, note that 0.01 = 1-0.99 thus:$$\sum_{i=0}^{73} 0.01(0.99)^i = \sum_{i=0}^{73} (1-0.99)(0.99)^i = \sum_{i=0}^{73} 0.99^i - \sum_{i=0}^{73} 0.99^{i+1}$$All but 1 term from each sum cancels each other out, only leaving $0.99^{0}-0.99^{74}$, and hence @eq-prob-74. From the joint probability it is a simple step to get to the probability of pity $k$, conditional on success (thanks Bayes):$$P(k|x) = \frac{P(x\cap k)}{P(x)}$$But what is the probability of success $P(x)$? This probability can be difficult to find in practice, but luckily it's rather simple: If you pull 90 times, you are guaranteed to succeed, thus $P(x)=1$. I made a slightly sneaky assumption in the above calculations, and that is that the probability of success 100%, and I did this by assuming that one would pull until they succeeded. This reason for this, is that the domain for the pity should be complete, which requires enough pulls to happen to allow for all values of pity to be possible: if you only do 50 pulls then it's impossible to have a pity above 50. More on this later. Assuming still that we will keep pulling until success (and no more), we can calculate the expected number of pulls required to obtain the *Sword of Night* as:$$E(\bar{n}|x) = 1 + \sum_{i=0}^{k} k P(k|x) = 1 + \sum_{i=0}^{k} k P(x\cap k)$$ {#eq-exp-pulls}```{python}import numpy as npexp_n_x =round(sum([k*0.01*(0.99**k) for k inrange(74)]) +sum([k*(0.01+0.99*(k-73)/16)*(0.99**73)*np.prod([1-0.01-0.99*(i-74)/16for i inrange(74, k+1)]) for k inrange(74, 90)]) +1, 2)```These densities can be seen in @fig-joint-density, the sum of which (plus 1) will yield our expected number of pulls for success. This value for our example is `{python} exp_n_x`. This is the number of pulls you should expect to make in order to get the *Sword of Night*, and therefore the basis for the price of the item. ```{ojs}//| label: fig-joint-density//| fig-cap: 'Density, P(x ∩ k)*k'{ let Density_data = [] let prob_x_k = 0 let den_k = 0 let den_k_sum = 0 for (var k=0; k<=89; k++){ if (k<74) { prob_x_k = 0.01*(0.99**k) den_k = prob_x_k*k den_k_sum += den_k Density_data.push({'Pity': k, 'Density': den_k}) } else { let prob_failure_k_74 = 0.99**73 for (var i=74; i<=k; i++){ prob_failure_k_74 *= (0.99-0.99*(i-74)/16) } prob_x_k = (0.01+0.99*(k-73)/16)*prob_failure_k_74 den_k = prob_x_k*k den_k_sum += den_k Density_data.push({'Pity': k, 'Density': den_k}) } } const plot = Plot.plot({ x: { label: 'Pity', domain: [-2, 92], ticks: d3.ticks(0, 90, 20) }, y: { label: 'Density, P(x ∩ k)*k', }, marks: [ Plot.ruleX(Density_data, {x: 'Pity', y: 'Density', stroke: 'black', strokeWidth: 8}), Plot.tip(Density_data, Plot.pointerX({x: 'Pity', y: 'Density'})), Plot.frame(), ], width: width, }) return plot}```The mean does not tell us the entire story, as the distribution is by no means symmetric: it is heavily weighted above a pity of 74. Using @fig-joint-prob, the median can be seen to be 68, with 25% and 75% percentiles of 28 and 77 respectively, and the mode is also 77. **A brief aside on the number of pulls**In the previous section, I assumed that the number of pulls is sufficiently large to guarantee success. This sets the probability of success to 1, and allows for the full range of pity to be possible. However, I want to consider the case where this is not true; where the number of pulls may not be sufficient to guarantee success. This can still be reflected, but requires truncation of the distributions depending on the number of pulls, and therefore a more complex probability of success. Instead of a unconditional probability of success, it will be conditional on the number of pulls, but just not conditional on the pity. This above probabilities will be the same, but also conditioned on the number of pulls, with the exception of when the pity is above the number of pulls, in which case the probability of success is 0. The conditional probability of success does not change, when also conditioning on n, the probability that is changing is the probability of reaching a pity of $k$. Therefore, $P(x|k, n)$ is the same as $P(x|k)$, and given by @eq-cond-prob. This implies that $x$ is independent of $n$.The joint probability of success and pity, conditional on the number of pulls, is given by @eq-joint-prob-n. This is the same as @eq-joint-prob, but with the added condition that $k<n$. As the probability of $k$ being equal to or larger than the number of pulls $n$ is 0, the joint probability of success and pity is 0 for $k\geq n$: $$P(x\cap k|n) = P(x|k, n)P(k|n) = $$$$P(x\cap k|n)=\begin{cases} \begin{cases} 0.01*(0.99)^k \qquad\qquad \text{for } 0\leq k\lt 74\\ (0.01+\frac{0.99(k+1-74)}{16})*(0.99)^{73}*\Pi_{i=74}^{k}(0.99-\frac{0.99(i-74)}{16}) \quad \text{for } 74\leq k\leq 89 \end{cases} \quad \text{for } k\lt n \\ 0 \qquad \text{for } k\geq n \end{cases}$${#eq-joint-prob-n}This can be seen in @fig-joint-prob-n, where the x-axis is still the pity, like in @fig-joint-prob, but one can use the slider to see the effect of changing the number of pulls.```{ojs}//| label: fig-joint-prob-n//| fig-cap: 'Joint Probability of Success and Pity, P(x ∩ k|n)'{ let Prob_data_n = [] let Prob_data_n_sum = [] let prob_x_k_n = 0 let prob_x_k_n_sum = 0 for (var k=0; k<=89; k++){ if (k<74 && k<n) { prob_x_k_n = 0.01*(0.99**k) prob_x_k_n_sum += prob_x_k_n Prob_data_n.push({'Pity': k, 'Probability': prob_x_k_n}) Prob_data_n_sum.push({'Pity': k, 'Probability': prob_x_k_n_sum}) } else if (k>=74 && k<n) { let prob_failure_k_n_74 = 0.99**73 for (var i=74; i<=k; i++){ prob_failure_k_n_74 *= (0.99-0.99*(i-74)/16) } prob_x_k_n = (0.01+0.99*(k-73)/16)*prob_failure_k_n_74 prob_x_k_n_sum += prob_x_k_n Prob_data_n.push({'Pity': k, 'Probability': prob_x_k_n}) Prob_data_n_sum.push({'Pity': k, 'Probability': prob_x_k_n_sum}) } else { Prob_data_n.push({'Pity': k, 'Probability': 0}) Prob_data_n_sum.push({'Pity': k, 'Probability': prob_x_k_n_sum}) } } let median = 0.5*Prob_data_n_sum[n-1]['Probability'] let lower_quartile = 0.25*Prob_data_n_sum[n-1]['Probability'] let upper_quartile = 0.75*Prob_data_n_sum[n-1]['Probability'] const plot_1 = Plot.plot({ x: { label: 'Pity, (k)', domain: [-2, 92], ticks: d3.ticks(0, 90, 20) }, y: { label: 'Probability, P(x ∩ k)', domain: [0, 0.08] }, marks: [ Plot.ruleX(Prob_data_n, {x: 'Pity', y: 'Probability', stroke: 'black', strokeWidth: 8}), Plot.tip(Prob_data_n, Plot.pointerX({x: 'Pity', y: 'Probability'})), Plot.frame(), ], width: width, }) const plot_2 = Plot.plot({ x: { label: 'Pity, (k)', domain: [-2, 92], ticks: d3.ticks(0, 90, 20) }, y: { label: 'Cumulative Probability, P(x ∩ k)', domain: [0, 1] }, marks: [ Plot.ruleX(Prob_data_n_sum, {x: 'Pity', y: 'Probability', stroke: 'black', strokeWidth: 8}), Plot.tip(Prob_data_n_sum, Plot.pointerX({x: 'Pity', y: 'Probability'})), Plot.frame(), Plot.ruleY([lower_quartile], {stroke: 'red', strokeDasharray: '5,5'}), Plot.ruleY([median], {stroke: 'red', strokeDasharray: '5,5'}), Plot.ruleY([upper_quartile], {stroke: 'red', strokeDasharray: '5,5'}), ], width: width, }) return html`${plot_1}${plot_2}`}``````{ojs}viewof n = Inputs.range( [1, 90], { step: 1, value: 90, label: 'Number of pulls', id: 'n'})```One may notice that if $k\ge n$, then the cumulative joint probability, conditional on $n$, no longer adds to 1. This number is the probability of success, conditional on n, and is given by the sum over the joint probabilities, conditional on $n$, for $k<n$:$$P(x|n) = \sum_{i=0}^{n-1} P(x\cap i|n)$$Note that this is increasing with $n$, reflecting that the probability of success increases the more pulls that you do, until it reaches 1 at hard pity. From this we can find the expected number of pulls to get a success $\bar{n}$, conditional on only $n$ pulls: $$E(\bar{n}|x, n) = 1 + \sum_{i=0}^{k} k P(k|x, n) = 1 + \sum_{i=0}^{k} k \frac{P(x\cap k|n)}{P(x|n)} = 1 + \frac{1}{\sum_{j=0}^{n-1} P(x\cap j|n)}\sum_{i=0}^{k} k P(x\cap k|n)$$These joint densities can be seen in @fig-joint-density-n, where the x-axis is still the pity, like in @fig-joint-density, but one can use same the slider above to see the effect of changing the number of pulls. ```{ojs}//| label: fig-joint-density-n//| fig-cap: 'Density, P(x ∩ k|n)*k'{ let Density_data_n = [] let prob_x_k_n = 0 let den_k_n = 0 let den_k_sum_n = 0 let prob_x_j_n = 0 let prob_x_j_n_sum = 0 for (var j=0; j<=89; j++){ if (j<74 && j<n) { prob_x_j_n = 0.01*(0.99**j) prob_x_j_n_sum += prob_x_j_n } else if (j>=74 && j<n) { let prob_failure_j_n_74 = 0.99**73 for (var l=74; l<=j; l++){ prob_failure_j_n_74 *= (0.99-0.99*(l-74)/16) } prob_x_j_n = (0.01+0.99*(j-73)/16)*prob_failure_j_n_74 prob_x_j_n_sum += prob_x_j_n } } for (var k=0; k<=89; k++){ if (k<74 && k<n) { prob_x_k_n = 0.01*(0.99**k) den_k_n = prob_x_k_n*k den_k_sum_n += den_k_n Density_data_n.push({'Pity': k, 'Density': den_k_n/prob_x_j_n_sum}) } else if (k>=74 && k<n) { let prob_failure_k_n_74 = 0.99**73 for (var i=74; i<=k; i++){ prob_failure_k_n_74 *= (0.99-0.99*(i-74)/16) } prob_x_k_n = (0.01+0.99*(k-73)/16)*prob_failure_k_n_74 den_k_n = prob_x_k_n*k den_k_sum_n += den_k_n Density_data_n.push({'Pity': k, 'Density': den_k_n/prob_x_j_n_sum}) } else { Density_data_n.push({'Pity': k, 'Density': 0}) } } const plot = Plot.plot({ x: { label: 'Pity', domain: [-2, 92], ticks: d3.ticks(0, 90, 20) }, y: { label: 'Density, P(x ∩ k|n)*k', domain: [0, 8.5] }, marks: [ Plot.ruleX(Density_data_n, {x: 'Pity', y: 'Density', stroke: 'black', strokeWidth: 8}), Plot.tip(Density_data_n, Plot.pointerX({x: 'Pity', y: 'Density'})), Plot.frame(), ], width: width, }) return plot}```The distribution in unchanged from @fig-joint-density when $n=90$, but as $n$ falls the rest of the distribution grows. This happens because as the probability of success decreases with fewer pulls, the densities are scaled up. Although it is important to consider the number of pulls as a possible constraint, for the remainder of the paper I will re-assume the previous case, where the number of pulls will be sufficient to guarantee success. When success is not guaranteed, the concept of a price for the items becomes murky, hence the wish to avoid it. ### SimulationTo illustrate the above, we can simulate the process of pulling 10,000 times, and record the number of times each rarity of item is obtained.The simulation can be played, paused, and restarted using the buttons below, as well as manually sliding the bar to view the results at a specific pull number.```{python}import randomimport jsonimport numpy as npimport pandas as pdpity_lists = pd.read_csv('simple_pity_lists.csv')rare_pity_list = pity_lists['Rare'].tolist()legendary_pity_list = pity_lists['Legendary'].tolist()rarity_counts = pd.read_csv('simple_rarity_count_lists.csv')Common_count_list = rarity_counts['Common'].tolist()Rare_count_list = rarity_counts['Rare'].tolist()Legendary_count_list = rarity_counts['Legendary'].tolist()obtained_pity_lists = pd.read_csv('simple_obtained_pity_lists.csv')rare_obtained_pity_list = obtained_pity_lists['Rare'].tolist()legendary_obtained_pity_list = obtained_pity_lists['Legendary'].tolist()legendary_obtained_pity_list = [int(x) for x in legendary_obtained_pity_list ifstr(x) !='nan']pull_totals = json.loads(open('simple_pull_totals.json').read())legendary_pity_sims = pd.read_csv('legendary_pity_sim_stats.csv')legendary_pity_sims_25 = legendary_pity_sims['25th Percentile'].tolist()legendary_pity_sims_50 = legendary_pity_sims['50th Percentile'].tolist()legendary_pity_sims_75 = legendary_pity_sims['75th Percentile'].tolist()legendary_pity_sims_mean = legendary_pity_sims['Mean'].tolist()legendary_pity_sims_mode = legendary_pity_sims['Mode'].tolist()ojs_define(Common_count_list = Common_count_list)ojs_define(Rare_count_list = Rare_count_list)ojs_define(Legendary_count_list = Legendary_count_list)ojs_define(rare_pity_list = rare_pity_list)ojs_define(legendary_pity_list = legendary_pity_list)ojs_define(legendary_obtained_pity_list = legendary_obtained_pity_list)``````{ojs}simple_simulation_data = FileAttachment('simple_simulation_data.csv').csv({typed: true})simulation_data = []empty_data = []filter_sim_data = { for (var i=0; i<simple_simulation_data.length; i++){ simulation_data.push({'pull': simple_simulation_data[i].pull, 'x': simple_simulation_data[i].x, 'y': simple_simulation_data[i].y, 'rarity': simple_simulation_data[i].rarity}) empty_data.push({'pull': simple_simulation_data[i].pull, 'x': simple_simulation_data[i].x, 'y': simple_simulation_data[i].y, 'rarity': ''}) }}``````{ojs}Plot.plot({ padding: 0, grid: true, x: {axis: 'top', label: 'Pulls', ticks: d3.ticks(0, 100, 10)}, y: {label: "100's of pulls",ticks: d3.ticks(0, 100, 10)}, color: { legend: true, domain: ['Common', 'Rare', 'Legendary', ''], range: ['cornflowerblue', 'blueviolet', 'gold', 'white'], }, marks: [ Plot.cell(simulation_data.slice(0, pull_number).concat(empty_data.slice(pull_number, 10000)), {x: 'x', y: 'y', fill: 'rarity', stroke: 'black', strokeWidth: 0.5}), Plot.frame() ], width: width, height: width})```<p style="text-align: center; font-weight: bold">Moving Totals</p><div style="display: inline-flex"><div style="margin-right: 5px"><table> <tr> <td colspan=3 style="text-align: center">Pull Totals</td> </tr> <tr> <td style="width: 100px; padding-left: 5px; background-color: cornflowerblue;">Common</td> <td style="width: 100px; padding-left: 5px; background-color: blueviolet; color: white;">Rare</td> <td style="width: 100px; padding-left: 5px; background-color: gold;">Legendary</td> </tr> <tr> <td style="padding-left: 5px"> ${Common_count_list[pull_number-1]} </td> <td style="padding-left: 5px"> ${Rare_count_list[pull_number-1]} </td> <td style="padding-left: 5px"> ${Legendary_count_list[pull_number-1]} </td> </tr></table></div><div style="margin-right: 5px"><table> <tr> <td colspan=2 style="text-align: center">Pity Counters</td> </tr> <tr> <td style="width: 100px; padding-left: 5px; background-color: blueviolet; color: white;">Rare</td> <td style="width: 100px; padding-left: 5px; background-color: gold;">Legendary</td> </tr> <tr> <td style="padding-left: 5px"> ${rare_pity_list[pull_number]} </td> <td style="padding-left: 5px"> ${legendary_pity_list[pull_number]} </td> </tr></table></div><div><div style="display: inline-flex; padding-left: 193px">```{ojs}viewof simulation_restart = html`<form class="Restart_Simulation">${Object.assign(html`<button type=button><i class="bi bi-skip-start"></i>`, {onclick: event => event.currentTarget.dispatchEvent(new CustomEvent("input", {bubbles: true}))})}```````{ojs}viewof simulation_play = html`<form class="Play_Simulation" id="sim-play">${Object.assign(html`<button type=button><i class="bi bi-play"></i>`, {onclick: event => event.currentTarget.dispatchEvent(play())})}```````{ojs}viewof simulation_pause = html`<form class="Pause_Simulation" id="sim-pause" style="display: none">${Object.assign(html`<button type=button onclick="clearInterval(play)"><i class="bi bi-pause"></i>`, {onclick: event => event.currentTarget.dispatchEvent(pause())})}```````{ojs}viewof simulation_end = html`<form class="End_Simulation">${Object.assign(html`<button type=button><i class="bi bi-skip-end"></i>`, {onclick: event => event.currentTarget.dispatchEvent(new CustomEvent("input", {bubbles: true}))})}````</div>```{ojs}viewof pull_number = Inputs.range( [1, 10000], { step: 1, value: 1, label: 'Number of pulls', id: 'pull_number'})```</div></div>```{ojs}restart = { simulation_restart; const pull_number = document.getElementById('oi-3a86ea-1'); pull_number.value = 1 pull_number.dispatchEvent(new CustomEvent("input", {bubbles: true}))}end = { simulation_end; const pull_number = document.getElementById('oi-3a86ea-1'); pull_number.value = 10000 pull_number.dispatchEvent(new CustomEvent("input", {bubbles: true}))}```<script>let nIntervId;functionplay() {const play =document.getElementById('sim-play');const pause =document.getElementById('sim-pause'); play.style.display='none' pause.style.display='inline-block' nIntervId =setInterval(start_sim,67)}functionstart_sim() {const pull_number =document.getElementById('oi-3a86ea-1');const play =document.getElementById('sim-play');const pause =document.getElementById('sim-pause');if (pull_number.value==10000){clearInterval(nIntervId) play.style.display='inline-block' pause.style.display='none' } else { pull_number.value=parseInt(pull_number.value) +1 pull_number.dispatchEvent(newCustomEvent("input", {bubbles:true})) }}functionpause() {const play =document.getElementById('sim-play');const pause =document.getElementById('sim-pause'); play.style.display='inline-block' pause.style.display='none'clearInterval(nIntervId) nIntervId =null}</script><p style="text-align: center; font-weight: bold">Statistics</p><div style="display: inline-flex"><div style="margin-right: 5px"><table> <tr> <td colspan=3 style="text-align: center">Pull Totals</td> </tr> <tr> <td style="width: 100px; padding-left: 5px; background-color: cornflowerblue;">Common</td> <td style="width: 100px; padding-left: 5px; background-color: blueviolet; color: white;">Rare</td> <td style="width: 100px; padding-left: 5px; background-color: gold;">Legendary</td> </tr> <tr> <td style="padding-left: 5px">`{python} pull_totals['Common']`</td> <td style="padding-left: 5px">`{python} pull_totals['Rare']`</td> <td style="padding-left: 5px">`{python} pull_totals['Legendary']`</td> </tr></table></div><div style="margin-right: 5px"><table> <tr> <td colspan=2 style="text-align: center">Min Pity</td> </tr> <tr> <td style="width: 100px; padding-left: 5px; background-color: blueviolet; color: white;">Rare</td> <td style="width: 100px; padding-left: 5px; background-color: gold;">Legendary</td> </tr> <tr> <td style="padding-left: 5px">`{python} np.min(rare_obtained_pity_list)`</td> <td style="padding-left: 5px">`{python} np.min(legendary_obtained_pity_list)`</td> </tr></table></div><div style="margin-right: 5px"><table> <tr> <td colspan=2 style="text-align: center">Max Pity</td> </tr> <tr> <td style="width: 100px; padding-left: 5px; background-color: blueviolet; color: white;">Rare</td> <td style="width: 100px; padding-left: 5px; background-color: gold;">Legendary</td> </tr> <tr> <td style="padding-left: 5px">`{python} np.max(rare_obtained_pity_list)`</td> <td style="padding-left: 5px">`{python} np.max(legendary_obtained_pity_list)`</td> </tr></table></div><div><table> <tr> <td colspan=2 style="text-align: center">Average Pity</td> </tr> <tr> <td style="width: 100px; padding-left: 5px; background-color: blueviolet; color: white;">Rare</td> <td style="width: 100px; padding-left: 5px; background-color: gold;">Legendary</td> </tr> <tr> <td style="padding-left: 5px">`{python} round(np.mean(rare_obtained_pity_list), 2)`</td> <td style="padding-left: 5px">`{python} round(np.mean(legendary_obtained_pity_list), 2)`</td> </tr></table></div></div>I have also included some statistics from this simulation above. The first table shows the total number for each rarity as a moving total as the number of pulls increase. It also shows how the pity changes over time, to illustrate how the pity system works. The second table shows final totals, as well as the minimum, maximum, and average number of pulls required to obtain a rare and legendary item. In addition, the distribution of the pity for legendary items is graphed in @fig-simulation-pity-legendary, against the predicted counts based on @fig-joint-density. ```{ojs}//| label: fig-simulation-pity-legendary//| fig-cap: 'Pity Distribution for Legendary Items'{ let Pity_data = [] let Pity_predicted = [] let prob_x_k = 0 let pity_list = legendary_obtained_pity_list for (var i=0; i<=89; i++){ let count = pity_list.filter(x => x == i).length Pity_data.push({'Pity': i, 'Count': count}) } for (var k=0; k<=89; k++){ if (k<74) { prob_x_k = 0.01*(0.99**k) Pity_predicted.push({'Pity': k, 'Count': prob_x_k*Legendary_count_list[9999]}) } else { let prob_failure_k_74 = 0.99**73 for (var i=74; i<=k; i++){ prob_failure_k_74 *= (0.99-0.99*(i-74)/16) } prob_x_k = (0.01+0.99*(k-73)/16)*prob_failure_k_74 Pity_predicted.push({'Pity': k, 'Count': prob_x_k*Legendary_count_list[9999]}) } } const plot = Plot.plot({ x: { label: 'Pity', domain: [-2, 92], ticks: d3.ticks(0, 90, 19) }, y: { label: 'Count', domain: [0, 14] }, color: { legend: true, domain: ['Simulation Data', 'Prediction'], range: ['black', 'red'], }, marks: [ Plot.ruleX(Pity_data, {x: 'Pity', y: 'Count', strokeWidth: 8}), Plot.line(Pity_predicted, {x: 'Pity', y: 'Count', stroke: 'red'}), Plot.tip(Pity_data, Plot.pointerX({x: 'Pity', y: 'Count'})), Plot.frame(), ], width: width, }) return plot}```The below table also shows legendary pity statistics from the simulation and compares them to the predictions. <table style="width: 100%"> <tr> <th></th> <th>Mean Pity</th> <th>Modal Pity</th> <th>25% Percentile Pity</th> <th>Median Pity</th> <th>75% Percentile Pity</th> </tr> <tr> <td>Simulation</td> <td>`{python} round(np.mean(legendary_obtained_pity_list), 2)`</td> <td>`{python} np.argmax(np.bincount(legendary_obtained_pity_list))`</td> <td>`{python} int(np.percentile(legendary_obtained_pity_list, 25))`</td> <td>`{python} int(np.median(legendary_obtained_pity_list))`</td> <td>`{python} int(np.percentile(legendary_obtained_pity_list, 75))`</td> </tr> <tr> <td>Prediction</td> <td>`{python} round(exp_n_x, 2)`</td> <td>77</td> <td>28</td> <td>68</td> <td>77</td> </tr></table>The simulated data and predictions are very well matched, with the exception of the 25% percentile, which is in the middle of the region which has the most variance due to such a low probability of success[^1]. Looking at the simulated data, there is fewer successes between 0 and 33 than between 34 and the median of 67, which is why the 25% percentile is 10 pity higher in the simulation than the prediction. [^1]: Not 100% about this, but my gut tells me that the low probability of these events causes very large variance, and can lead to the 25% percentile being more volatile than other percentiles. In fact, this carries through for any percentile below the median, which is itself very close to the soft pity, and the higher likelihood events, and therefore less volatile. To test if this 25% percentile difference is a one-off or a consistent bias, I will run the simulation 100 times and compare the 25% percentile of the pity for each run to the prediction. The distribution for the 25% percentile can be seen in @fig-simulation-pity-by-percentile.```{ojs}legendary_pity_sims = FileAttachment('legendary_pity_sim_stats.csv').csv({typed:true})``````{ojs}//| label: fig-simulation-pity-by-percentile//| fig-cap: 'Pity Distributions for Legendary Items'{ let percentile_data_25 = [] let percentile_data_50 = [] let percentile_data_75 = [] let Pity_data = [] for (var i=0; i<legendary_pity_sims.length; i++){ percentile_data_25.push(legendary_pity_sims[i]['25th Percentile']) percentile_data_50.push(legendary_pity_sims[i]['50th Percentile']) percentile_data_75.push(legendary_pity_sims[i]['75th Percentile']) } for (var i=0; i<=89; i++){ let count_25 = percentile_data_25.filter(x => x == i).length if (count_25>0) { Pity_data.push({'Pity': i, 'Count': count_25, 'Percentile': '25'}) } let count_50 = percentile_data_50.filter(x => x == i).length if (count_50>0) { Pity_data.push({'Pity': i, 'Count': count_50, 'Percentile': '50'}) } let count_75 = percentile_data_75.filter(x => x == i).length if (count_75>0) { Pity_data.push({'Pity': i, 'Count': count_75, 'Percentile': '75'}) } } const plot = Plot.plot({ x: { label: 'Pity', domain: [-2, 92], }, y: { label: 'Count', domain: [0, 65] }, color: { legend: true, domain: ['25', '50', '75'], label: 'Percentile', }, marks: [ Plot.ruleX(Pity_data, {y: 'Count', x: 'Pity', stroke: 'Percentile', strokeWidth: 8}), Plot.tip(Pity_data, Plot.pointerX({x: 'Pity', y: 'Count', fill: 'Percentile'})), Plot.frame(), ], width: width, }) return plot}```One can see that the range of values for the 25th percentile includes both the expected value (28), towards the middle, and the value from the simulation (38) towards the tail. This suggests that it was indeed an outlier, and that the simulation is consistent with the predictions. Further, the variance among the 25th percentile and median is larger than the 75th percentile as hypothesised. One can explain this by pointing to the variance of the geometrically distributed section of the probability distribution, of which the 25th percentile and median lie on. If this geometric distribution was continued to infinity (as it properly would be), the variance would be $\frac{1-p}{p^2}$, which is larger for small probabilities. This gives reason for the larger variance in the 25th percentile and median, and the smaller variance in the 75th percentile, if not mathematically exact, or rigorous.### SparksNot all gacha systems include a pity system, and instead rely on a 'spark' system. This system is similar to the pity system, in the fact that it restricts the maximum number of pulls you need to do before acquiring the desired item. The difference is that the 'spark' system does not manipulate probabilities, but rather guarantees the choice of the item after a certain number of pulls. A common number for this is 200 pulls, and if the (pre-chosen) desired item has not been obtained by this point, one can choose to exchange 'sparks' for it. A single spark is obtained for every failed pull. This system is more simple to calculated expected values for the number of pulls, as the probability of success is a simple geometric distribution. The system under pity is also geometrically distributed, but the probability of success is not constant, and is dependent on the number of pulls, whereas the probability of success in the spark system is constant.$$P(n=\bar{n}) = (1-p)^{\bar{n}-1} p$$The expected number of pulls to obtain the desired item is given by @eq-exp-pulls-spark:$$E(\bar{n}) = \frac{1}{p} = \frac{1}{0.01} = 100$$ {#eq-exp-pulls-spark}This expected value is based on the assumption that one will do 200 pulls (if necessary), so success is guaranteed. Unsurprisingly, with the same probability of success, the expected number of pulls is larger than the pity system. I have also simulated the spark system for 100000 pulls (p=0.01 and number of sparks for legendary is 200), and the distribution of the number of pulls can be seen in @fig-simulation-spark. The statistics from these simulations, and the predicted amounts, can be seen in the table below.```{python}import pandas as pdimport jsonlegendary_obtained_pity_list_spark = pd.read_csv('simple_obtained_pity_list_spark.csv')['pity'].to_list()legendary_obtained_pity_stats_spark = json.loads(open('simple_sim_spark.json').read())legendary_count_spark = legendary_obtained_pity_stats_spark['legendary_count']min_legendary_pity_spark = legendary_obtained_pity_stats_spark['min_pity']max_legendary_pity_spark = legendary_obtained_pity_stats_spark['max_pity']ojs_define(legendary_obtained_pity_list_spark = legendary_obtained_pity_list_spark)ojs_define(legendary_count_spark = legendary_count_spark)ojs_define(legendary_min_pity_spark = min_legendary_pity_spark)ojs_define(legendary_max_pity_spark = max_legendary_pity_spark)``````{ojs}//| label: fig-simulation-spark//| fig-cap: 'Pity Distribution for Legendary Items under Spark System'{ let Pity_data_spark = [] let Pity_predicted_spark = [] let prob_x_k_spark = 0 let pity_list_spark = legendary_obtained_pity_list_spark for (var i=0; i<=legendary_max_pity_spark; i++){ let count = pity_list_spark.filter(x => x == i).length Pity_data_spark.push({'Pity': i, 'Count': count}) } for (var k=0; k<=legendary_max_pity_spark; k++){ prob_x_k_spark = 0.01*(0.99**k) Pity_predicted_spark.push({'Pity': k, 'Count': prob_x_k_spark*legendary_count_spark}) } const plot = Plot.plot({ x: { label: 'Pity', domain: [0, 800] }, y: { label: 'Count', }, color: { legend: true, domain: ['Simulation Data', 'Prediction'], range: ['black', 'red'], }, marks: [ Plot.dot(Pity_data_spark, {x: 'Pity', y: 'Count', r: 0}), Plot.ruleX(Pity_data_spark, {y: 'Count', x: 'Pity', stroke: 'black', strokeWidth: 1}), Plot.tip(Pity_data_spark, Plot.pointerX({x: 'Pity', y: 'Count'})), Plot.line(Pity_predicted_spark, {x: 'Pity', y: 'Count', stroke: 'red'}), Plot.frame(), ], width: width, }) return plot}```<table style="width: 100%"> <tr> <th></th> <th>Mean Pity</th> <th>Modal Pity</th> <th>25% Percentile Pity</th> <th>Median Pity</th> <th>75% Percentile Pity</th> </tr> <tr> <td>Simulation</td> <td>`{python} round(legendary_obtained_pity_stats_spark['mean_pity'], 2)`</td> <td>`{python} legendary_obtained_pity_stats_spark['modal_pity']`</td> <td>`{python} legendary_obtained_pity_stats_spark['percentile_25']`</td> <td>`{python} legendary_obtained_pity_stats_spark['median_pity']`</td> <td>`{python} legendary_obtained_pity_stats_spark['percentile_75']`</td> </tr> <tr> <td>Prediction [^2]</td> <td>100</td> <td>1</td> <td>`{python} np.ceil(np.log(0.75)/np.log(0.99))`</td> <td>`{python} np.ceil(np.log(0.50)/np.log(0.99))`</td> <td>`{python} np.ceil(np.log(0.25)/np.log(0.99))`</td> </tr></table>[^2]: The quantiles were calculated using $\frac{\ln(1-q)}{\ln(1-p)}$, where $q$ is the quantile, and $p$ is the probability of success. These were then rounded up to the nearest integer.Note, there are many observations above the spark threshold of 200, which means that it took more than 200 pulls to get a legendary item *naturally*, but one can assume that the player would choose the legendary item after 200 pulls, and would only continue to pull if they want further copies, or if a new limited-time item is released (in which case the spark system would reset, but their previous pull count would not). Therefore, we can view these observations as cases where the player would select a number of legendary items through the spark system and then obtains a legendary item through chance after the observed pulls. ### 50/50Apologies, I should have included a trigger warning here for the gacha gamers reading this. The dreaded '50/50' is something that has turned great excitement into extreme disappointment and anger. The reason for why is that, in most gacha systems, when you get a legendary item, you're not always guaranteed that it's the legendary item you want. In fact it can be legendary item you really don't want. Until now, i've assumed a single item of the highest rarity (in fact for any rarity). However, usually there is a set of legendary items, of which 1 will be chosen. This set will include a single limited-time item: normally more powerful, better looking, more fun; and also a subset of 'standard' items. When you obtain a legendary item, there is a 50% probability that it is the limited-time item, but also a 50% probability that it is any one of the 'standard' items. This is known as the 50/50. Typically, losing the 50% chance of the legendary item of choice, will guarantee it for the next legendary item obtained, limiting the number of times one can lose this 50/50. Some systems will have variations where there is more than one limited-time item and the 50/50 is split across them, with a separate probability determining whether one obtains a limited legendary item or a 'standard' legendary item. For example, assume that there are 2 limited-time legendary items; on pulling a legendary, there is a random draw to determine whether ths legendary will be one of the 2 limited-time items or a standard item, and say these probabilities are 75% to 25%; then, if a limited-time item is chosen, there is a 50% chance that it will be either of the 2 limited-time items; if a standard item is chosen, the probabilities are also split uniformly across however many standard items are in the set. Knowing that your chances of getting the limited-time item is not certain, even when you get a legendary item, will change the expected number of pulls, but it's not obviously clear by how much it would increase this number. Instinct may tell us that the expected number of pulls will be half way between winning on the first legendary item, and losing the 50/50 and being guaranteed it on the next legendary. The expected value will, by definition, be between these two values, but the expectation is not the sum of the expectations of 2 legendary pull sequences. This is because the latter sequence is conditional on the first sequence, as 50% of the time (on average) you win on the first legendary, thus erasing the need to keep pulling. Let us continue with the example used previously, but add a 50/50 mechanic. The range of pulls needed to the a single limited-time legendary, is now between 1 pull (if you're lucky) and 180 pulls (if you're really unlucky). Again, assume for simplicity that one will do enough pulls to guarantee the desired item, such that $p(x)=1$. We can then calculate the expected number of pulls needed to get the limited-time legendary using the same conditional probability distribution in @fig-cond-prob, but changing the probability of observing each pity number to reflect the 50/50. The key difference here is that some of the possible values for the number of pulls for a success have (broadly) 2 different paths to them. For example, a successful pull of the limited-time legendary after 10 pulls can be achieved by either winning the 50/50 on the 10th pull, or losing the 50/50 before this, but getting another legendary on the 10th pull. These are the 2 broadly different paths, but actually there is 10 different possibilities here: winning the 50/50 on the 10th pull is one, but there are 9 different ways to lose the 50/50 before the 10th pull, and then get the limited-time legendary on the 10th pull. Therefore, the probability of getting the limited-time legendary on the 10th pull is the sum of the probabilities of these 10 different paths. For this example, the probability is:$$p(\bar{n}=10) = \underbrace{(0.99^9)}_{\text{9 losses}}\underbrace{(0.01)}_{\text{1 win}}\underbrace{(0.5)}_{\text{win 50/50}} + 9\underbrace{(0.99^8)}_{\text{8 losses}}\underbrace{(0.01^2)}_{\text{2 wins}}\underbrace{(0.5)}_{\text{lose 50/50}} = (0.99^8)(0.01)(0.5)(0.99+0.01) \approx 0.00046$$Note that I have used the probability of success of 0.01 as we are well before the soft pity. If we use the general terms for the probability conditional on the pity $p(x|k)$, we would have:$$p(\bar{n}=10) = \underbrace{[\Pi_{i=0}^{8}(1-p(x|k=i))]}_{\text{1st 9 are losses}} \underbrace{p(x|k=9)}_{\text{1 win in 10}} \underbrace{(0.5)}_{\text{win 50/50}} + \text{Pr(8 losses and 2 wins)} \underbrace{(0.5)}_{\text{lose 50/50}}$$This term I have not calculated is more complex as it needs to contain the multiple different ways of achieving $n-2$ losses and 2 wins, specifically with the second win at $n$. To calculate this, see the below table of probabilities. Each row represents when the first win occurs, and then the probability of this sequence happening. <table style="width: 100%"> <tr> <th style="width: 75px">First Win</th> <th colspan="7" style="text-align: center">Probability</th> </tr> <tr> <td>$m$</td> <td>$Pr(\text{m-1 losses})$</td> <td></td> <td>$Pr(\text{Win in pull m})$</td> <td></td> <td>$Pr(\text{Loss until pull 9})$</td> <td></td> <td>$Pr(\text{Win in pull 10})$</td> </tr> <tr> <td>1</td> <td>1</td> <td>$\times$</td> <td>$p(x|k=0)$</td> <td>$\times$</td> <td>$[\Pi_{i=0}^{7}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=8)$</td> </tr> <tr> <td>2</td> <td>$(1-p(x|k=0))$</td> <td>$\times$</td> <td>$p(x|k=1)$</td> <td>$\times$</td> <td>$[\Pi_{i=0}^{6}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=7)$</td> </tr> <tr> <td>3</td> <td>$[\Pi_{i=0}^{1}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=2)$</td> <td>$\times$</td> <td>$[\Pi_{i=0}^{5}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=6)$</td> </tr> <tr> <td>4</td> <td>$[\Pi_{i=0}^{2}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=3)$</td> <td>$\times$</td> <td>$[\Pi_{i=0}^{4}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=5)$</td> </tr> <tr> <td>5</td> <td>$[\Pi_{i=0}^{3}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=4)$</td> <td>$\times$</td> <td>$[\Pi_{i=0}^{3}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=4)$</td> </tr> <tr> <td>6</td> <td>$[\Pi_{i=0}^{4}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=5)$</td> <td>$\times$</td> <td>$[\Pi_{i=0}^{2}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=3)$</td> </tr> <tr> <td>7</td> <td>$[\Pi_{i=0}^{5}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=6)$</td> <td>$\times$</td> <td>$[\Pi_{i=0}^{1}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=2)$</td> </tr> <tr> <td>8</td> <td>$[\Pi_{i=0}^{6}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=7)$</td> <td>$\times$</td> <td>$(1-p(x|k=0))$</td> <td>$\times$</td> <td>$p(x|k=1)$</td> </tr> <tr> <td>9</td> <td>$[\Pi_{i=0}^{7}(1-p(x|k=i))]$</td> <td>$\times$</td> <td>$p(x|k=8)$</td> <td>$\times$</td> <td>1</td> <td>$\times$</td> <td>$p(x|k=0)$</td></table>The first term and 3rd term are the same, but the probabilities are realised in a different order, so will give the same probability. In fact, looking at the table, one can see that the probabilities are the same for m=1+i and m=9-i, so we can simplify the formula. This will mean that the formula will be different depending on whether n is even or odd:If n is even:$$p(n-2\text{ losses and }2\text{ wins}) = 2p(x|k=0)\left( \Pi_{i=0}^{7}(1-p(x|k=i)) \right)p(x|k=8)$$$$+ 2\sum_{j=2}^{\frac{n}{2}-1} \left[ \left( \Pi_{i=0}^{j-2}(1-p(x|k=i)) \right) p(x|k=j-1) \left( \Pi_{i=0}^{n-j-2}(1-p(x|k=i)) \right) p(x|k=n-j-1) \right]$$$$+ \left[\left( \Pi_{i=0}^{\frac{n}{2}-2}(1-p(x|k=i)) \right) p(x|k=\frac{n}{2}-1) \right]^2$$If n is odd:$$p(n-2\text{ losses and }2\text{ wins}) = 2p(x|k=0)\left( \Pi_{i=0}^{7}(1-p(x|k=i)) \right)p(x|k=8)$$$$+ 2\sum_{j=2}^{\frac{n-1}{2}} \left[ \left( \Pi_{i=0}^{j-2}(1-p(x|k=i)) \right) p(x|k=j-1) \left( \Pi_{i=0}^{n-j-2}(1-p(x|k=i)) \right) p(x|k=n-j-1) \right]$$Combining with the previous equations, and generalising for $n$ gives us the complete formulas: If n is even:$$p(\bar{n}=n) = \frac{1}{2}[\Pi_{i=0}^{n-2}(1-p(x|k=i))]p(x|k=n-1)$$$$+ [\Pi_{i=0}^{n-3}(1-p(x|k=i))]p(x|k=n-2)p(x|k=0)$$$$+\sum_{j=2}^{\frac{n-2}{2}} \left[ \left( \Pi_{i=0}^{j-2}(1-p(x|k=i)) \right) p(x|k=j-1) \left( \Pi_{i=0}^{n-j-2}(1-p(x|k=i)) \right) p(x|k=n-j-1) \right]$$$$+\frac{1}{2}\left[\left( \Pi_{i=0}^{\frac{n}{2}-2}(1-p(x|k=i)) \right) p(x|k=\frac{n}{2}-1) \right]^2$$If n is odd:$$p(\bar{n}=n) = \frac{1}{2}[\Pi_{i=0}^{n-2}(1-p(x|k=i))]p(x|k=n-1)$$$$+ [\Pi_{i=0}^{n-3}(1-p(x|k=i))]p(x|k=n-2)p(x|k=0)$$$$+ \sum_{j=2}^{\frac{n-1}{2}} \left[ \left( \Pi_{i=0}^{j-2}(1-p(x|k=i)) \right) p(x|k=j-1) \left( \Pi_{i=0}^{n-j-2}(1-p(x|k=i)) \right) p(x|k=n-j-1) \right]$$Somehow my simplifications make this look more complex. Surely it simplifies... right? One way we could simplify the calculation of this is to separate the equation before n=74 (i.e. soft-pity) and after. Before soft-pity, all marginal probabilities are the same $(p(x|k)=p=0.01)$, so the equation simplifies to:If n is even: $$p(\bar{n}=n<75) = \frac{1}{2}(1-p)^{n-2}p+ (1-p)^{n-3}p^2$$$$+ (\frac{n-6}{2})(1-p)^{n-4}p^2$$$$p(\bar{n}=n<75) = (1-p)^{n-4}p\left[ \frac{1}{2}(1-p)^2+(1-p)p+(n-6)\frac{1}{2}p+ \frac{1}{2}p \right]$$$$p(\bar{n}=n<75) = \frac{1}{2}(1-p)^{n-4}p\left[1+(n-5)p-p^2 \right]$$If n is odd:$$p(\bar{n}=n<75) = \frac{1}{2}(1-p)^{n-2}p+ (1-p)^{n-3}p^2$$$$+(\frac{n-5}{2})(1-p)^{n-4}p^2+\frac{1}{2}(1-p)^{n-4} p^2$$$$p(\bar{n}=n<75) = (1-p)^{n-4}p\left[ \frac{1}{2}(1-p)^2+(1-p)p+(n-5)\frac{1}{2}p+ \frac{1}{2}p \right]$$$$p(\bar{n}=n<75) = \frac{1}{2}(1-p)^{n-4}p\left[1+(n-4)p-p^2 \right]$$### PricingTypically, as economists, when face with uncertain outcomes, we use the expected values to inform us of what the price *should* be. In the case of gacha, we don't want to focus on the expected value of a single pull, but rather the expected number of pulls one would need to obtain the desired items. ### Special CasesHere I will talk about Gacha without Pity, and loot boxes (which are pretty similar). The probability of success is easier to calculate here as it's 1 - Probability of infinite failure. Wait, that's 1? ## Conclusion## References