# "Wild Magic Surges"
- categories: [dnd]
- image: images/dnd/rolls.png

### A Comparison of Two Homebrew Methods

In order to make Wild Magic Surges a more frequent occurence, we can tweak the rules to trigger them. Two such tweaks are:

1. The "Increasing Count" method. Start as usual with a Wild Magic Surge triggering when the player rolls a `1` on their Surge roll. Every time a Surge *does not occur*, increase the D.C. for avoiding the Surge by one: `1` $\rightarrow$ `2` $\rightarrow$ `3`, etc. When a Surge *does* occur, reset the D.C. to 1.

2. The "Decreasing Die" method. Start as usual with a Wild Magic Surge triggering when the player rolls a `1` on their `d20` Surge roll. Every time a surge *does not occur*, decrease the size of the die by one: `d20` $\rightarrow$ `d12` $\rightarrow$ `d10`, etc. When a Surge *does* occur, reset the die to a `d20`. If a player avoids triggering a Surge all the way down through a `d2` (a coin flip), their next Surge is automatic. Or, we can think of this as rolling a `1` on a "`d1`".

Below we calculate the probabilities of triggering a Wild Magic Surge under both of the above systems.


```python
import numpy as np
import pandas as pd
import altair as alt
dice = [20, 12, 10, 8, 6, 4, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
count = [*range(1,21)]

def pDiceSurge(d):
    return 1.0/d
def pDiceNoSurge(d):
    return 1.0-(1.0/d)
def pCountSurge(dc):
    return dc/20.0
def pCountNoSurge(dc):
    return 1-(dc/20.0)
```

First, we calculate the $PDF$ for the Decreasing Dice method &mdash; that is, the probability of rolling a Wild Magic surge on the $i^{th}$ roll exactly, no earlier and no later, for each $i$.

This is equal to the probability that we *don't* rolls a surge for the first $i-1$ rolls, times the probability that we *do* roll a surge on the $i^{th}$ roll:


```python
marginalP = list(map(pDiceNoSurge, dice))
dicePDF = []
for i in range(0,20):
    if (i == 0):
        P = pDiceSurge(dice[0])
    else:
        P = np.prod(marginalP[:i]) * pDiceSurge(dice[i])
    dicePDF.append(P)
```

We also calculate the $PDF$ for the Increasing Count method:


```python
marginalP = list(map(pCountNoSurge, count))
countPDF = []
for i in range(0,20):
    if (i == 0):
        P = pCountSurge(count[0])
    else:
        P = np.prod(marginalP[:i]) * pCountSurge(count[i])
    countPDF.append(P)
```

Then we calculate the $CDF$ &mdash; that is, the probability of encountering a Wild Magic Surge in $k$ rolls or fewer, for each $k$.  
This is just the sum from $i=1$ to $i=k$ of the probabilities of getting a surge in exactly $i$ rolls &mdash; a partial sum of the $PDF$ we calculated above:


```python
diceCDF = []
countCDF = []
for i in range(1,21):
    diceCDF.append(np.sum(dicePDF[:i]))
    countCDF.append(np.sum(countPDF[:i]))
```

And now, the fun part, we plot the results!


```python
#collapse
cData = []
pData = []
for i in range(0, len(diceCDF)):
    cData.append([i+1, diceCDF[i], 'Decreasing Die'])
    cData.append([i+1, countCDF[i], 'Increasing Count'])
    pData.append([i+1, dicePDF[i], 'Decreasing Die'])
    pData.append([i+1, countPDF[i], 'Increasing Count'])

df = pd.DataFrame(cData, columns=['Number of Rolls', 'Probability', 'Method'])
df.reset_index()

# Create a selection that chooses the nearest point & selects based on x-value
nearest = alt.selection(type='single', nearest=True, on='mouseover',
                        fields=['Number of Rolls'], empty='none')

points = alt.Chart(df).mark_circle().encode(
    x='Number of Rolls:O',
    y=alt.Y('Probability', title='Probability of a Surge'),
    color='Method',
    opacity=alt.condition(nearest, alt.value(1), alt.value(.6))
)

# Transparent selectors across the chart. This is what tells us
# the x-value of the cursor
selectors = alt.Chart(df).mark_point().encode(
    x='Number of Rolls:O',
    opacity=alt.value(0),
).add_selection(
    nearest
)

# Draw text labels near the points, and highlight based on selection
text = points.mark_text(align='left', dx=5, dy=-5).encode(
    text=alt.condition(nearest, 'Probability:Q',alt.value(' '))
)

# Draw a rule at the location of the selection
rules = alt.Chart(df).mark_rule(color='gray').encode(
    x='Number of Rolls:O',
).transform_filter(
    nearest
)

# Put the five layers into a chart and bind the data
alt.layer(
    selectors, points, rules, text
).properties(
    width=600, height=300
)
```





<div id="altair-viz-29ea6b60a555442ab8e119cb06a27c0b"></div>




As you can see, the probability of a Wild Magic Surge is generally higher with the Increasing Count method. This is somewhat expected, as the probability is the same initially for both methods, then at the second roll we have:

$$\begin{align}
P_{\text{Increasing Count}}(S) = \frac{2}{20} &= \frac{1}{10}\\
P_{\text{Decreasing Die}}(S) &= \frac{1}{12}
\end{align}$$

Similarly for the third roll, where the Increasing Count gives a $3/20$ probability of a surge vs. a $1/10$ for the Decreasing Die. So the Increasing Count takes an early lead which it maintains until the seventh roll, where the probability is about even between the methods. After this roll, the Decreasing Die method takes the lead because it gives an automatic Surge from here on out.

My personal preference would lean towards the Increasing Count method, since you could conceivably get a string of rolls that build to a fairly high D.C., which feels a little more dramatic. On the other hand, the Decreasing Die method gives you a guaranteed Surge a fair bit sooner, which is part of the point of these tweaks in the first place.

We can also compute the expected number of rolls to get a Wild Magic Surge for both methods:


```python
def E(pdf):
    ex = 0
    for i in range(0, len(pdf)):
        ex += (i+1)*pdf[i]
    return ex

print("Expectation for Dice  Method:", E(dicePDF))
print("Expectation for Count Method:", E(countPDF))
```

    Expectation for Dice  Method: 5.504768880208333
    Expectation for Count Method: 5.293584586000901


Which is to say, on average the Increasing Count method will give us a Wild Magic Surge slightly sooner. Perhaps another reason to favor it.

For completeness, here is the same data as in the graph, but in a table view:


```python
df.pivot(index='Number of Rolls', columns='Method')
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead tr th {
        text-align: left;
    }

    .dataframe thead tr:last-of-type th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr>
      <th></th>
      <th colspan="2" halign="left">Probability</th>
    </tr>
    <tr>
      <th>Method</th>
      <th>Decreasing Die</th>
      <th>Increasing Count</th>
    </tr>
    <tr>
      <th>Number of Rolls</th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>1</th>
      <td>0.050000</td>
      <td>0.050000</td>
    </tr>
    <tr>
      <th>2</th>
      <td>0.129167</td>
      <td>0.145000</td>
    </tr>
    <tr>
      <th>3</th>
      <td>0.216250</td>
      <td>0.273250</td>
    </tr>
    <tr>
      <th>4</th>
      <td>0.314219</td>
      <td>0.418600</td>
    </tr>
    <tr>
      <th>5</th>
      <td>0.428516</td>
      <td>0.563950</td>
    </tr>
    <tr>
      <th>6</th>
      <td>0.571387</td>
      <td>0.694765</td>
    </tr>
    <tr>
      <th>7</th>
      <td>0.785693</td>
      <td>0.801597</td>
    </tr>
    <tr>
      <th>8</th>
      <td>1.000000</td>
      <td>0.880958</td>
    </tr>
    <tr>
      <th>9</th>
      <td>1.000000</td>
      <td>0.934527</td>
    </tr>
    <tr>
      <th>10</th>
      <td>1.000000</td>
      <td>0.967264</td>
    </tr>
    <tr>
      <th>11</th>
      <td>1.000000</td>
      <td>0.985269</td>
    </tr>
    <tr>
      <th>12</th>
      <td>1.000000</td>
      <td>0.994107</td>
    </tr>
    <tr>
      <th>13</th>
      <td>1.000000</td>
      <td>0.997938</td>
    </tr>
    <tr>
      <th>14</th>
      <td>1.000000</td>
      <td>0.999381</td>
    </tr>
    <tr>
      <th>15</th>
      <td>1.000000</td>
      <td>0.999845</td>
    </tr>
    <tr>
      <th>16</th>
      <td>1.000000</td>
      <td>0.999969</td>
    </tr>
    <tr>
      <th>17</th>
      <td>1.000000</td>
      <td>0.999995</td>
    </tr>
    <tr>
      <th>18</th>
      <td>1.000000</td>
      <td>1.000000</td>
    </tr>
    <tr>
      <th>19</th>
      <td>1.000000</td>
      <td>1.000000</td>
    </tr>
    <tr>
      <th>20</th>
      <td>1.000000</td>
      <td>1.000000</td>
    </tr>
  </tbody>
</table>
</div>



0690bbe07a1416077b20b1e3cb718d64ded36d7f
ipynb
Jupyter Notebook
_notebooks/2020-05-03-Wild Magic Surges.ipynb
buggins333/mmnxi
8f322e7aae952bb18b4957cc1b62370e22e5f4f4
Apache-2.0
_notebooks/2020-05-03-Wild Magic Surges.ipynb
buggins333/mmnxi
8f322e7aae952bb18b4957cc1b62370e22e5f4f4
Apache-2.0
2020-05-03T23:08:57.000Z
2022-02-26T07:37:30.000Z
_notebooks/2020-05-03-Wild Magic Surges.ipynb
buggins333/mmnxi
8f322e7aae952bb18b4957cc1b62370e22e5f4f4
Apache-2.0
Qwen/Qwen-72B
1. YES
2. YES