24
\$\begingroup\$

As noted in David Coffron’s answer to another question, the moonblade (Dungeon Master’s Guide, pg. 217) has no fixed limit on how much damage it can do: there is no stated limit to the number of runes on the blade. It states:

A moonblade has one rune on its blade for each master it has served (typically 1d6 + 1). The first rune always grants a +1 bonus to attack and damage rolls made with this magic weapon. Each rune beyond the first grants the moonblade an additional property. The DM chooses each property or determines it randomly on the Moonblade Properties table.

Some of the items on the table include:

91–92: When you hit with an attack using the moonblade, the attack deals an extra 1d6 slashing damage.

93–94: When you hit a creature of a specific type (such as dragon, fiend, or undead) with the moonblade, the target takes an extra 1d6 damage of one of these types: acid, cold, fire, lightning, or thunder.

But as David notes in a comment, some runes have limits on how many times they may be gained. That means, once the moonblade has gained these runes, it cannot gain them again, and the odds of gaining the remaining runes—including the quoted damage-improving runes—improves in the future.

So then, assuming that each rune is determined randomly per the table, rerolling any rolls that result in a rune that would not stack with the runes already on the moonblade, and that the creature-type-specific runes are distributed evenly among the creature types (feel free to assume fractional runes for that purpose), what is the expected value of the damage roll of a moonblade with 𝑛 runes? Assume that the hit has already taken place, so changes to the weapon’s accuracy, such as making it finessed, or that make it easier to target enemies in the first place, such as making it thrown, are irrelevant. Also, absent any information about the target, ignore the damage benefit that vorpal might have. The best answer would include the chance of a critical hit, including the possibility that the critical threat range has been doubled by the relevant rune.

Do not assume any GM intervention limiting the moonblade or otherwise making any particular ruling here: instead use a rules as written interpretation of the text.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ Why the provision to re-roll non-stackable runes? \$\endgroup\$
    – GcL
    Aug 20, 2019 at 15:02

3 Answers 3

45
\$\begingroup\$

I wrote a python script to calculate answers accurate to several decimal places. I created a Moonblade object, and added a specified number of runes just as the rules suggest (with the one alteration you made that you always re-roll non-stackable properties). It then calculates the expected damage from this moonblade taking into account all bonuses, bonus dice, critical hits, creatures, etc.... I made a couple of assumptions beyond what you have explicitly stated.

  1. I assumed that the creature on the receiving end has a uniformly random chance of being any of the 14 types. This is a bad assumption for most games (how often do you see a plant or an ooze compared to a humanoid?), but absent more data, I cannot do better.
  2. I assumed that the roll to hit was greater than 1. Though you did say to assume that the hit has been made, in order to calculate the chance of a hit being critical, we need to know the chance of the d20 showing a sufficiently high number given that we know that it is high enough to hit in the first place. The chance of a random d20 roll being a 20 is 1/20, but the chance of it being 20 given that it had to be able to hit an AC 18 creature could be significantly higher. Since we do not have an AC to measure against, the best we can do is say that we know the roll must be higher than 1 (as a natural 1 never hits).
  3. I assumed that the user never uses the property of the defender whereby he can transfer some of the sword's hit bonus to his AC.
  4. I assumed the user always uses one hand.
  5. I assumed the user has a damage modifier of 5 because you would expect this of the high-level character wielding the moonblade.

With these assumptions, I get that with 99% confidence, the true expected damage values given \$n\$ runes are these, \$\pm 0.05\$.

\begin{array}{ll} 0\text{ runes}&9.737\\ 1\text{ runes}&10.244\\ 2\text{ runes}&10.752\\ 3\text{ runes}&11.252\\ 4\text{ runes}&11.727\\ 5\text{ runes}&12.166\\ 6\text{ runes}&12.557\\ 7\text{ runes}&12.899\\ 8\text{ runes}&13.196\\ 9\text{ runes}&13.459\\ \end{array}

This plot demonstrates the almost linear relationship.

For the first 10 or so runes, the expected damage is about \$9.912 + 0.421 n\$. However, the linearity drops off at about \$n=10\$. This is because we expect to have reached a bonus of +3 by this point.

If we extend to a truly absurd number of runes, we see a transition to another linear behavior. I think this is because the dominant factor in the damage is the steadily growing number of d6's added to the roll. Because all other runes eventually stop stacking, in the end, all we can do is add another d6 for each rune, meaning we either add 1d6 to the damage, or add 1d6 to the damage for a particular type of creature. After adjusting to account for critical hits, this averages out to about 2.07 damage per rune, which is what we expect the slope to eventually be. Therefore, for large numbers of runes, the expected damage is calculated as \$\overline{d}\approx-93.24 + 2.07 n\$

\$\endgroup\$
6
  • 3
    \$\begingroup\$ Very nice! I would say that the intent was to leave the actual wielder out of the equation entirely, rather than assuming +5 or one hand or whatever else, so that you could just add your actual bonus to the numbers here, but that’s minor. Very cool. \$\endgroup\$
    – KRyan
    Jul 23, 2018 at 1:01
  • \$\begingroup\$ Thanks! You can just subtract 5 from each estimate. If we are ignoring the whole AC/nat 20 question, then it is the same. \$\endgroup\$
    – Alex S
    Jul 23, 2018 at 1:03
  • \$\begingroup\$ Do you account for critical hits, including the possibility of the doubled threat range? Also, can you expand a little more on how you handle the creature-specific damage? I had thought that by specifying that we assume that \$\frac{1}{14}\$ of each of those applies to each type, we’d achieve a uniform damage value regardless of the target’s type (since all types would be damaged in the same way). \$\endgroup\$
    – KRyan
    Jul 23, 2018 at 1:15
  • \$\begingroup\$ I do account for the fact that Moonblade may go critical at a 19 (there is even an option to specify a different number if the user is, say a high-level Champion Fighter). I think that the way I handle creatures is equivalent. Each moonblade has a list of creature types and the associated number of d6's for each type. It then calculates the average number of creature-specific d6's, and uses that to calculate expected damage. This is a little more work than is necessary to answer your question, but allows us to see details of the properties of each individual Moonblade, which is fun. \$\endgroup\$
    – Alex S
    Jul 23, 2018 at 1:19
  • 5
    \$\begingroup\$ @Pyrotechnical I did not. Despite being a math PhD candidate, my first impulse is to turn to a computer for problems like these. \$\endgroup\$
    – Alex S
    Jul 23, 2018 at 1:30
8
\$\begingroup\$

Put into a very simple equation, the average damage of a moonblade with n runes can be roughly calculated as base damage + 0.47n + 1.

This doesn't involve probability at all, I calculated the value 0.47 by using a calculator I wrote to roll runes on a moonblade over and over (anywhere from 5 to 5000 runes per moonblade), calculated the average damage increase per rune, and repeated this process tens of thousands of times to get as close to the statistical average as I could.

This calculator confirms that as the sample size (number of runes) increases the average damage increase per rune approaches 0.47.

The calculator operates on the following rules:

The first rune adds 1 to the damage with no extra effect.
58% of the time, a rune is gained that does not affect the damage output.
40% of the time, a rune is gained that increases damage by 1. However, after this rune is rolled 3 times it is treated as a reroll.
2% of the time, a rune is gained that increases the damage by 3.5 (1d6).

I rolled n runes on a moonblade and divided the damage increase total by the number of runes to get the average damage per rune on that moonblade. I then repeated this process x number of times, added the average damage per rune on each moonblade together, and divided by x to get the average damage per rune on an average moonblade over thousands of iterations.

My calculations do NOT take into account factors such as increased crit chance, the vorpal blade, the finesse property, different creature types, etc.

I understand this isn't quite what you were looking for, especially since it doesn't take into account the increased critical chance, but it's a baseline approach if you needed to do a rough calculation.

\$\endgroup\$
1
  • 3
    \$\begingroup\$ I like the approach, and I appreciate the first-pass approximation. I do hope you’ll continue to improve it to cover various cases you’re missing, but even as-is I find value here. +1. \$\endgroup\$
    – KRyan
    Jul 22, 2018 at 20:09
4
\$\begingroup\$

Closed-form solution using Markov chain

This can be computed in closed-form using a discrete-time Markov chain where each timestep corresponds to adding one rune. States correspond to the Cartesian product of:

  • The current "plus" level of the item.
  • The number of minor properties of the item (but see below).
  • The number of non-repeatable 2% properties (finesse, thrown, defender, flash, spell storing).
  • Whether the critical property has been rolled.
  • The number of non-repeatable 1% properties (shadow, vorpal).

The number of repeatable properties can be implicitly tracked by subtracting the total number of the above non-repeatable properties from the current total number of runes (= timestep counter). On average half of these will be the universal +1d6 damage and half the creature-specific +1d6 damage. If desired, the exact distribution between the two could be computed using the binomial distribution, but by linearity of expectation this doesn't matter in terms of the mean damage.

Here the state space is only in the hundreds, which is trivial on a modern PC.

Minor properties

There is one part of the problem that was not explicitly defined in the question nor Alex S's answer, namely how minor properties are handled:

  • Are any of the minor properties repeatable? Guardian, Hidden Message, Key, Language, and Sentinel might be argued to be so.
  • If a non-repeatable minor property is rolled, is the re-roll made on the minor property table, or is the entire rune rerolled?
  • Does rolling twice on a 20 apply?
  • Does rolling twice on a 20 apply on a reroll caused by rolling a duplicate non-repeatable minor property?

For simplicity, I decided that rolling 41-80 on the main rune table exactly nineteen times exhausts the minor property option. It's possible but more complicated to compute the probabilities for a more involved minor property generation process.

Other than this and not counting any bonus damage from the wielder's ability score, I made the same textual assumptions as Alex S's answer.

Results

Runes past first Mean damage from dice Mean flat damage Mean total damage
0 3.684 1.000 4.684
1 3.771 1.400 5.171
2 3.857 1.802 5.659
3 3.953 2.141 6.094
4 4.062 2.403 6.464
5 4.181 2.595 6.776
6 4.310 2.731 7.040
7 4.446 2.824 7.269
8 4.588 2.886 7.474
9 4.734 2.927 7.662
10 4.884 2.954 7.838

Graph of mean total damage:

Graph of mean total damage.

Like Alex S's answer, there's three regimes: a brief initial ramp as the moonblade gets promoted to +3, a shallow increase as the non-repeatable properties are exhausted, and finally a steeper increase once the repeatables are the only possibilities left with high probability.

Code

import numpy
import matplotlib as mpl
import matplotlib.pyplot as plt

max_pluses = 2 # 0-2 bonus to attack and damage (not counting the initial +1) (40%)
max_minors = 19 # minor properties (40%)
max_twos = 5 # non-damage 2% properties: finesse, thrown, defender, flash, spell storing
max_crit = 1 # enhanced critical range (4%)
max_ones = 2 # non-damage 1% properties: shadow, vorpal

max_values = (max_pluses, max_minors, max_twos, max_crit, max_ones)
state_size = tuple(x+1 for x in max_values)

# compute tensors
# chance of going from state -> state
transition = numpy.zeros(state_size * 2)
# number of nonrepeatable runes
nonrepeatable_counts = numpy.zeros(state_size, dtype=int)
for state in numpy.ndindex(*state_size):
    plus, minors, twos, crit, ones = state

    # relative chances of rolling each result
    # accounting for nonrepeatables being eliminated from the table
    plus_weight = (plus < max_pluses) * 40
    minor_weight = (minors < max_minors) * 40
    twos_weight = (max_twos - twos) * 2
    crit_weight = (crit == 0) * 4
    ones_weight = max_ones - ones
    repeatable_weight = 4

    total_weight = plus_weight + minor_weight + twos_weight + crit_weight + ones_weight + repeatable_weight

    if plus_weight > 0:
        next_state = (plus+1, minors, twos, crit, ones)
        transition[state + next_state] += plus_weight / total_weight
    if minor_weight > 0:
        next_state = (plus, minors+1, twos, crit, ones)
        transition[state + next_state] += minor_weight / total_weight
    if twos_weight > 0:
        next_state = (plus, minors, twos+1, crit, ones)
        transition[state + next_state] += twos_weight / total_weight
    if crit_weight > 0:
        next_state = (plus, minors, twos, 1, ones)
        transition[state + next_state] += crit_weight / total_weight
    if ones_weight > 0:
        next_state = (plus, minors, twos, crit, ones+1)
        transition[state + next_state] += ones_weight / total_weight

    # default case: rolled a repeatable
    # repeatables are kept track implicitly by subtracting the number of nonrepeatable runes
    # from the total number of runes
    transition[state + state] += repeatable_weight / total_weight

    nonrepeatable_counts[state] = sum(state)
    
# initial state: 100% no properties
dist = numpy.zeros(state_size)
dist[tuple(0 for x in state_size)] = 1.0

flat_damage_by_plus = numpy.array([1.0, 2.0, 3.0])
mean_damage_per_repeatable = (3.5 + 3.5 / 14) / 2

max_runes = 50
mean_damages = numpy.zeros((max_runes+1,))

for rune_count in range(max_runes+1):
    repeatable_counts = rune_count - nonrepeatable_counts
    dice_damages = repeatable_counts * mean_damage_per_repeatable + 3.5
    dice_damages_weighted = dist * dice_damages

    # 20/19 and 21/19 are the multipliers to mean damage given by the critical range,
    # conditioned on a hit assuming hitting on a 2+.
    mean_dice_damage = (
        numpy.sum(dice_damages_weighted[:, :, :, 0, :]) * 20/19 +
        numpy.sum(dice_damages_weighted[:, :, :, 1, :]) * 21/19
        )

    # marginal distribution of plusses
    dist_plus = numpy.sum(dist, axis=tuple(range(1, len(state_size))))
    mean_flat_damage = numpy.dot(flat_damage_by_plus, dist_plus)
    
    mean_damage = mean_dice_damage + mean_flat_damage
    mean_damages[rune_count] = mean_damage

    line = '| %d | %0.3f | %0.3f | %0.3f |' % (rune_count, mean_dice_damage, mean_flat_damage, mean_damage)

    print(line)
    
    # transition to the next distribution
    dist = numpy.tensordot(dist, transition, axes=len(state_size))

figsize = (8, 4.5)
dpi = 120

fig = plt.figure(figsize=figsize)
ax = plt.subplot(111)
ax.grid(True)

ax.plot(numpy.arange(max_runes+1), mean_damages)
ax.set_xlim(0, max_runes)
ax.set_ylim(0)
ax.set_xlabel('Runes past first')
ax.set_ylabel('Mean damage')
plt.savefig('output/moonblade.png', dpi = dpi, bbox_inches = "tight")
\$\endgroup\$
2
  • \$\begingroup\$ Awesome—and great points about the remaining ambiguities. Very nice graph, too. But isn’t “closed form” some kind of algebraic equation? I don’t actually see any equation? \$\endgroup\$
    – KRyan
    Dec 10, 2021 at 14:18
  • \$\begingroup\$ The equation is implicitly defined in the code, which at the bottom level does not use anything other than the four elementary operations +, -, *, /. If you like, you can flatten the state space into a 1D vector, dump out the transition matrix (aka en.wikipedia.org/wiki/Stochastic_matrix), and use the standard Markov chain equation, which is just a vector-matrix multiplication at each timestep. \$\endgroup\$ Dec 10, 2021 at 20:33

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .