20
\$\begingroup\$

The Problem

Suppose we have a character using Animate Objects (D&D 5e) to animate 10 Tiny objects, and is attacking the same enemy with all of them. According to RAW, you'd make a separate attack roll for each of the objects - so 10 attack rolls, and a damage roll for each that hits. That's a lot, and it would be nice if there were a way to reduce it.

Trivial strategy
Roll once for all of them together. Only one roll: great! But then we have either all or none of the objects hitting, which together are literally the least likely possible outcomes. So not very representative of what we'd expect to get from rolling each attack individually.

Grouping strategy
Split the objects into groups and roll attack for each group. This is simply a middle ground between individual rolls and the one-roll-for-all, and a tradeoff between speed and accurate representation. Fewer groups = more speed and worse representation; more groups = better representation & less speed.

Map a d20 to a number of hits?
My intuition is telling me that there ought to be a way to roll a single d20 and, based on the outcome (and factoring in +to hit and AC), determine a number of the 10 objects that hit, which is properly representative of how lucky the roll is. My question is, how can we do this?

(I'm aware that there are many other good and creative ways to deal with this problem, but for this question, I'm interested specifically in trying to create the described mapping!)

Requirements

  1. For me to be happy with using this method in a game, the most important requirement is that the character using it is neither advantaged nor disadvantaged by its use. More specifically, if all possible outcomes of rolling attacks individually were enumerated and compared against all possible outcomes of using this method, the number of hits, misses, and crits should match between them.

  2. As implied by the first condition, this method must preserve the number of outcomes that represent critical hits. "Critical misses" must also be similarly accounted for inasmuch as they represent a miss against any armor class.

  3. The method should avoid generating remarkable outcomes with higher frequency than rolling individual attacks would. ("Remarkable" = players' eyebrows raised all around the table; if it happened twice there would be joking (or serious) accusations of weighted dice, etc.)

  4. The method should preserve the intuitive paradigm that rolling higher is better. Rolling 20 would represent the most hits; rolling 1 would represent the least; and no roll X could be considered better than Y if X is less than Y.

  5. Finally, the method should, as much as possible while respecting the other constraints, properly represent the distribution of hits one would expect to see when rolling attacks individually.

Work in Progress

I am currently attempting to work this out myself, and will (hopefully soon) post my best shot at it as an answer here. I believe the solution will involve setting up a sort of generic table of # of hits and # of crits for each of the outcomes of a d20 against an arbitrary base AC, coupled with a formula that modifies these results according to the + to hit of the attack and the actual AC of the target.

But I'm not especially practiced at crunching dice probabilities, and my planned solution is based in part on intuitive leaps that I'm not sure I can justify. So I'm hoping some experienced players here may have attempted something similar or just know how to manipulate these kinds of dice probabilities. And anyway, I'm finding this problem a super interesting challenge, and why should I have all the fun? Thanks for any help/insight!

\$\endgroup\$
5
  • \$\begingroup\$ @Eddymage Hello, and thank you! The target of this question is a method that employs just one roll of a d20, and then uses the result of that roll in a function to map it to a number of hits. \$\endgroup\$
    – Qami
    Mar 7, 2023 at 12:51
  • 2
    \$\begingroup\$ To use computer-science jargon, I'm hoping for a function that looks something like this: int numberOfHits = GetHitsForTenAttackers(d20Outcome, plusToHit, targetAC); , where "d20Outcome" is an int in [1 to 20], and plusToHit/targetAC are both positive integers. \$\endgroup\$
    – Qami
    Mar 7, 2023 at 12:54
  • 1
    \$\begingroup\$ (That said, I appreciate HighDiceRoller's solution of using 3d6, since it's still technically a single attack roll and it's not too unmanageable...) \$\endgroup\$
    – Qami
    Mar 7, 2023 at 12:55
  • 3
    \$\begingroup\$ Hi HNQ visitors! I would just like to remind you that comments are not for answering the question. If you have advice to solve the problem, please put it in an answer below along with the support to back it up. \$\endgroup\$
    – Someone_Evil
    Mar 7, 2023 at 15:49
  • \$\begingroup\$ Welcome to RPG.SE! Take the tour if you haven't already, and check out the help center for more guidance. \$\endgroup\$
    – V2Blast
    Mar 7, 2023 at 22:37

9 Answers 9

43
\$\begingroup\$

Use the DMG rules for handling mobs

The DMG (page 250) presents some rules for the situation depicted. It consists in computing the target number on the d20 for a successful hit, and then check the table below to see how many creatures hit.

d20 Roll Needed Attackers Needed for 1 hit
1-5 1
6-12 2
13-14 3
15-16 4
17-18 5
19 10
20 20

In the depicted situation, 10 Tiny Animated objects have an attack bonus of +8: suppose that the target's AC is 17, hence the roll needed on the d20 is 9 and the attackers needed for having one successful hit is 2. This means that in the group of Animated Objects there are 10/2=5 that hit the target.

This table comes from the geometric distribution, and follows the original probability distribution. If you need a 14 on the d20 roll for hit, then the probability of success is 7/20, then the number of trials that you have to wait before the 1st success is 20/7 \$\sim\$ 2.8571, rounded up to 3. This means that the first hit (may) happen after 3 attacks, the second hit after 6 and so on: the grand total is hence 6 (or 7, depending on approximation) hits among 20 attacks.

Another way to see it is looking at the binomial distribution, which counts the number of success on independent trials, when the probability of success is the same. The figure below shows the simulation of 100 000 scenarios of 20 attackers, against an AC of 22 and with an attack Bonus of +8. Plotted against this simulation, the solid line represents the binomial distribution, and the legend reports the expected value using such distribution. The green line refers to the number of successful hits given by the method presented in the DMG.

Histogram of simulation, plot of the binomial pdf and number suggested by the DMG


The table reported in the DMG is a rough approximation: a more accurate one should be the table below.

d20 Roll Needed Attackers Needed for 1 hit
1-6 1
7-11 2
12-14 3
15 4
16 5
17 7
18 10
19-20 20

But the player wants to roll a d20!

The above method does not require any d20 roll1 and does not take into crits:

This attack resolution system ignores critical hits in favor of reducing the number of die rolls.

whilst provides some techniques for dealing with monsters that deal different damage or have multiple attacks.

You may make the player still roll a d20: if the result is above the target, then add 1 successful hit, if the roll is below remove one. If the roll is 20, add a further critical hit, in addition to the further hit added. If the roll is 1, you have a critical failure, among removing one successful hit from the total.

In this case, the probability distribution is still close to the original one (recall that the above table refers to averages) and let players roll a d20, if they want to.


1 For some players and/or DM, this means to take out some fun: I am one of those people.

\$\endgroup\$
5
  • \$\begingroup\$ I'm confused. You say "The above method does not require any d20 roll," but the chart is mapping a d20 roll to "attackers needed for 1 hit." Can't you just let the player make that roll? They're the one making an attack. \$\endgroup\$
    – Brian
    Mar 8, 2023 at 18:52
  • 10
    \$\begingroup\$ @Brian The chart is mapping "Target AC - attackers' attack bonus" to number of hits, but referring to that difference as "The number each attacker would need to roll on a d20 in order to hit the target" shortened to "d20 roll needed". Rolling a d20 and consulting the chart would result in wildly incorrect probabilities. \$\endgroup\$ Mar 8, 2023 at 18:58
  • 2
    \$\begingroup\$ I knew of the official table, but yours modification with "one roll for a bonus/malus attack" is a really cool modification, I love it \$\endgroup\$
    – Hobbamok
    Mar 10, 2023 at 9:05
  • \$\begingroup\$ I think this is a correct answer to an alternative question like, 'How should/can I handle large groups of identical attackers, a mob or crowd?', especially RAW. But the question here specifically asks for a close approximate mapping from d20 roll to outcome: 'for this question, I'm interested specifically in trying to create the described mapping!' \$\endgroup\$
    – Oly
    Mar 11, 2023 at 15:38
  • 1
    \$\begingroup\$ @Oly This answer builds on the fact that there is no need for such approximation, because it is already in the rules. Mapping such situation in single d20 roll is nearly impossible and any result is quite imprecise. The last paragraph encompasses the original problem. \$\endgroup\$
    – Eddymage
    Mar 11, 2023 at 21:27
11
\$\begingroup\$

This is mathematically impossible given the framing of the question.

The problem is that you are asking too much out of a single 1d20, which only has 20 equally likely possibilities.

To begin with, just looking at the number of critical hits can range anywhere from 0 to 10, that's 11 possible outcomes, of which the tail ends are less likely but still possible. Given each of the 0 to 10 crit results, you then have anywhere from 0 to 10- that number possible numbers of regular hits (Assuming regular hits can occur, which given bounded accuracy is a pretty good assumption) So that gives for

10 crits 1 possibility, 9 crits 2 possibilities, 8 crits 3....etc, all the way down 0 crits 10 possibilities for number of norma hits, meaning we have 10 times 1+9 times 2+ 8 times 3+...1 times 10=220 different possible ways of distributing # of crits, # of hits, and # of misses

even ignoring caring if any of the misses are 1s.

The best possible outcome, 10 crits, would only occur in (.05)^10, which is on the order (Rounded) of 10 to the -13, so almost never, and certainly nothing that could be accomplished with something as crude as 1d20.

So, you will either need to A: Increase your number of dice, B: Modify your goals, or C: Both.

For A, you could model the expected number of crits and the distribution and try to chop that up into 5% intervals. That's a binomial (Bernoulli) distribution with p=.05 and 10 trials

enter image description here

Rounding, we get 60% no crit, 30% 1 crit, 7.5% 2 crits, 2.5% 3 or more crits. So, we could map it to:

20 to 2+ crits, 19 to 2 crit, 13-18 to 1 crits, and 1-12 no crits

then just have to figure out the 20 break down, having shifted 2.5% (or 1/3d) of the probability of the 2 crit possibility into the the 20. Looking at the conditional probability of how many crits you get given that you rolled 2+ crits, we have P(X crits| given 2+ crits)=p(X and 2+ crits)/P(2+crits). Since X will be 2 or higher, we can simplify this to p(X)/P(2+ crits), which is just the 1-the chance of 0 or 1 crits. This would normally get us 87% 2 crits, 12% 3 crit, 1% 4 crit, and negligible changes of 5+ (.07%) However, we this ignores the fact that 5 of our original % chance of 2% was used on the roll of the 19. Our fix there has to be to take away 5% from the 2 roll on both the numerator and the denominator side, so now our conditional (denominator) becomes We rolled 2+ and it wasn't the 5% of the 2 that we assigned to a roll of 19.

Things brings us down to our secondary d20 roll having 68% chance of being 2 crits, 29% chance of being 3, 2.7% chance of being 4, .2% of being 5+ If you really wanted to try to keep using d20s and preserve something of the original distribution, we could round to 65% 2 crits, 30% 3 crits, 5% 4 or more (Doubling in this chance the 4 or more, but best we can do with a crude d20, withoug going to a d100 or a sum of dice for a better distribution). This would lead to:

"If roll a 20, roll again, 1-13 2 crits, 14-19 3 crits, 20 4+, roll again"

Now we have to sub-break up the 4, ignoring the weird rounding that got us here I'll just do conditional probability of X successes given 4+ successes: This gets us down to 94% 4, 6% 5, negligible 6+. Here is where our limits of 1d20 go, we just can't get down to the really small chances of 6 or more crits, so we could finalize it to

If you roll a second 20, roll again, 1-19 4 crits, 20 5 crits

This gives us a "reasonable" crit distribution with no more than 3 d20 getting us, results of 0-5 crits relatively closely to the true distribution, ignoring the miniscule chance of 6+

Now the fun part...how many of the non-crits are hits? There is pretty much no way to do this on the same die as the # of crits without making the model be even more silly. It might be simpler to model # of hits first, and then a secondary roll of how many of them are crits.

I can work on math for that if you'd like, but just thought I'd show the reasons why you really can't get a good breakdown for what you want.

\$\endgroup\$
2
  • \$\begingroup\$ Hi Alan! Thank you for your response. I appreciate the level of detail you've gone into, and you're absolutely right that I'm asking a lot of a single d20 roll...maybe you're right that I'm setting my goals too high. But I did want to clarify what I was saying about crits being "preserved". I wasn't meaning that every possible distribution of crits needs to be represented in outcomes produced by this method. Continuing... \$\endgroup\$
    – Qami
    Mar 7, 2023 at 14:27
  • \$\begingroup\$ I was trying to say: if you sum up all possible outcomes from rolling attacks individually, you'd have a certain number of crits (1/20th of all the rolls, right?)...this method, then, should also, if you sum up all the possible outcomes of method (which should be 200, right? 10 attacks per d20 outcome, 20 possible outcomes), 1/20th of those should be crits. One twentieth of 200 is ten, so if you attack using this method twenty times and roll each side of the d20 once, you should have made 10 crits. Does that make sense? \$\endgroup\$
    – Qami
    Mar 7, 2023 at 14:35
10
\$\begingroup\$

Margin of success using 4d4 or 3d6

The number of hits converges to a normal distribution, which is fully characterized by its mean and standard deviation. The sum of dice also converges to a normal distribution. Therefore, we can try using the margin of success as the number of hits. For 20 attacks, 4d4 is reasonably close to matching the standard deviation.

We can perform the following procedure:

  • Roll one attack roll using attack bonus + 10 + 4d4 (no d20).
  • Subtract the target AC.
  • The result is the number of normal hits.
  • Add an additional crit for each 4 rolled on the d4s.

This does a reasonable job of matching the mean and standard deviation. Each +1 to attack rolls increases the margin of success by about 1 for hit chances near 50%, which results in 1 more mean hit as desired. This also avoids hard breakpoints.

For 10 attacks (e.g. your animated objects):

  • Roll one attack roll using attack bonus + 10 + 3d6 (no d20).
  • Subtract the target AC.
  • Divide by 2 (rounding down).
  • The result is the number of normal hits.
  • Add an additional crit for each 6 rolled on the d6s.

Example: You roll 1, 4, 6. The total attack roll is 8 (attack bonus) + 10 + (1 + 4 + 6) = 29. Supposing the target AC is 20, you score (29 - 20) / 2 = 4 hits. Since you rolled one 6, you also score 1 crit.

Other notes:

  • Advantage/disadvantage can be 8d4 keep the highest/lowest 4 or 6d6 keep the highest/lowest 3.
  • Critical miss is almost never relevant for animated objects since it only matters if the target AC is 9 or lower. In general, 5e's bounded accuracy means this should not introduce too much error.

Partial casualties, etc. may present additional questions; at that point you may consider going all the way to defining your own "swarm" template to treat the collective as a single creature, possibly including the ideas above.

How good is this approximation?

Thanks to @Eddymage for the plot idea.

20 attacks

10 attacks

The big number in each subplot is the number needed to-hit.

The red area shows the true distribution of the number of hits (counting crits as one); the blue bars are the results of this margin-of-success method. As long as the chance to hit is not too far from 50%, the approximation is decently close. 5e's doctrine of bounded accuracy means that this should be the case most of the time.

The green bar in each plot is the DMG method, which is deterministic, making it a degenerate distribution with zero standard deviation. Thus, it has just one skyscraper of a bar that goes to 100%. I've let it overflow the plot on the first row.

\$\endgroup\$
11
  • 1
    \$\begingroup\$ "If we figure that the typical hit chance is close to 50%": why? It depends on attack bonus and target's AC, doesn't it? \$\endgroup\$
    – Eddymage
    Mar 7, 2023 at 8:53
  • 3
    \$\begingroup\$ To first-order this is accounted for by the margin of success: every +1/-1 difference in attack bonus and AC produces 1 more or fewer hit per 20 attacks as desired. But as hit chance gets far from 50% then you start to see more higher-order error due to the target standard deviation and normality decreasing, going deeper into the tails which is more sensitive to imperfections in matching standard deviation, not being able to get below 0 hits, etc. \$\endgroup\$ Mar 7, 2023 at 9:10
  • 3
    \$\begingroup\$ @Eddymage: it continues to work fine as long as the hit probability doesn't move too far away from 50% \$\endgroup\$ Mar 7, 2023 at 12:55
  • 2
    \$\begingroup\$ @Eddymage: You can't reliably model the original distribution anyway (see Alan's answer), so at best you will have an approximation. \$\endgroup\$ Mar 7, 2023 at 13:04
  • 2
    \$\begingroup\$ For 20 attacks, assuming an attack bonus of +0 and an AC of 10 (to keep things simple), one would normally expect an average of 10 hits, including a crit. With this method you get 3d6 hits (average of 10.5) including one crit on average. \$\endgroup\$ Mar 7, 2023 at 13:11
6
\$\begingroup\$

This is untenable to perfectly model.

If you look at the number of possible results from rolling 10d20, you get:

$$C^R(20,10) = \frac{(n+r-1)!}{r! * (n-1)!} = 20,030,010$$

Mapping 20 million results to a single d20 roll is clearly not possible especially when some of the results are as few as 1 in 20 million odds.

Starting with outcomes rather than discrete results

To start with tackling the problem, we should boil it down to misses, hits, and critical hits. These probabilities will be different for each AC, so we will have different distributions as a result. Below is what you get for 10-25 AC. You'll see why I cut off 5 critical hits and greater in the next section.

enter image description here

As you can see, for high and low ACs, the probable results are extremely concentrated on a handful of outcomes whereas middling ACs are more widespread.

Regardless, in either case, a large number of outcomes are extremely unlikely. By pruning unlikely outcomes we may be able to actual develop some mappings.

Pruning unlikely outcomes

Where you draw the line at "unlikely" is arbitrary, but I chose to use 1 in 10,000. Over the course of a year long campaign, you will probably only be making around 500 animate objects attacks at most (assuming 2 combat encounters per session, 2-3 rounds per encounter, and 50 sessions).

As such, we can demonstrate the unlikeliness of these outcomes using 54 in 10,000 (for a maximum of 54 possible outcomes with these low percentages) with 500 attempts. This yields:

$$\frac{500 \text{ attempts}}{\frac{10,000}{54} \text{ attempts per occurrence}} = 2.7 \text{ expected occurrences}$$

If you are willing to sacrifice less than 3 dramatic results (which could be positive or negative) over the course of an entire campaign, we can prune out these results and get the following distribution (adjusting probabilities to account for the loss of some sections):

enter image description here

Many critical hits

The first step was to deal with the feasible but still relatively unlikely situation of more than a couple critical hits. To deal with this, I propose a new rule:

If you roll a natural 20 using the model, you can roll d20s to turn some of the normal hits that you gained into critical hits. This happens on another roll of 20, and continues until you stop rolling 20s.

This allows for those scenarios with similar odds and also makes the natural-20 with animate objects that much more exciting.

We can then remove the 3-critical hits and 4-critical hits sections, as well as the bulk of the 2-critical hits section to more easily fit the remaining options onto a single d20.

Combining similar outcomes

We still have some outcomes with less than a 1 in 20 chance which are difficult to map to a single d20 roll. In order to resolve this, I shunted them to the nearest result that was very close to an interval of X in 20.

Here I highlighted those sections where yellow represents the shunted probabilities and orange represents the destination (adjusting probabilities to account for the loss of some sections):

enter image description here

This gives the following distribution with probabilities all in the ball park of intervals of X in 20. Thus, we can follow these steps to get our models:

  • Multiply each cell by 20
  • Round to the nearest whole number
  • Add values on the low-end to create 20 total outcomes for each AC (highlighted in orange)
  • Make some judgment-based adjustments to account for the shunting (highlighted in yellow)

Doing this we get the following models:

enter image description here

Expected damage

To evaluate these models for their efficacy, I calculated the effective damage of rolling separately or using the model. Remember also that any 20s continue to roll for additional critical hits.

Here is the result:

enter image description here

As you can see, our models slightly overestimates our damage output (and no, it isn't just because of the natural-20 rule). I'm not 100% sure where this variance comes from and I even used the 20-total-outcomes and rounding-adjustments to try to reign in this variance.

Regardless, these models should work fairly effectively with an average variance of only +1.2 points of damage. You could always make slight adjustments to bring this in line, but those adjustments would be arbitrary just as mine were. I couldn't figure anything else obvious to adjust with any justification.

Ordering the models

From here, we just need to place the models in order so that the intuitive paradigm that rolling higher is better is preserved. To do this, simply match up the number of hits and place the critical hits higher.

Here is how that is done for AC 12:

enter image description here

Conclusion

Overall, modeling this many results on a single d20 roll is just not a good idea. Even filtering down and adding arbitrary adjustments with any sort of justification (such as rounding differences, average position of shunting, and removal of permutations), I couldn't conveniently fit my models to the expected outcomes of rolling individually, let alone the distribution of outcomes (which isn't particularly accurately modeled at all).

I would suggest using the proposed rules in the Dungeon Master's Guide as in Eddymage's answer.

\$\endgroup\$
2
  • \$\begingroup\$ I started a sheet calculating something similar (simpler), but noted that I'd probably order the results so that a higher roll always means >= #hits, and that 15-19 is 1 crit and 20 is 2 crits. A much more intuitive model, though slightly more inaccurate. I had trouble easily generating the distribution of #hits though \$\endgroup\$ Mar 8, 2023 at 20:50
  • \$\begingroup\$ @MooingDuck I was basically generating the probability tree. P1^a * P2^b * P3^C... Then you multiply each discrete outcome by the number of paths on the tree (given by n!/a!*b!*c!...) So for just hits vs misses you do [hit chance]^[number of hits] * [miss chance]^[number of misses] * 10!/([number of hits]!*[number of misses]!) \$\endgroup\$ Mar 9, 2023 at 13:45
3
\$\begingroup\$

I ran some simulations for 10 attackers, with 20 being crit, 1-14 missing and 15-19 hitting. This ran 10k times for some statistics. Results were c crits and h hits, for each run. These results were sorted as 2*c+h - crit is worth 2 hits.

In these results, d=1 and d=20 obviously correspond to 5% of worst/best results while say d=10 corresponds to results between 45%-50%. This approach (and my random run) tells that if we roll 12 to 14, we get exactly 4 hits, but rolling 11 either gives 3 or 4 hits, with average of 3.25. It would be obviously trivial to sort results by some other valuation of crits vs hits; and we could easily look at the underlying cells to see how many of those 3 "worths of hits" are actually 3 hits and how many are 1 crit and 1 hit. Looking at roll d=10 with previously mentioned average of 3.25, there were 143 crits in my run, no cases with 2 crits.

Now, the good thing is that for most rolls, how good or bad you did is simple - top and bottom value are the same, and if not the same they are at least close together so it doesn't matter much which value you pick. However, for the best roll of d=20, worst result in top 5% is 7, average is 7.5, but the bets result in my try was 12, and happened twice: 3 crits + 6 hits + 1 miss and 4 crits + 4 hits + 2 misses. So, how are you going to count this? Given stated goals of avoiding "wow, amazing roll" but keeping some sort of great/poor rolls, I suggest the following:

Generate table for any "roll X to hit". Yes, it will be a huge lookup table. Use average hits, rounded up/down. Except on 1 or 20, where you roll the die again, selecting how good/bad your final result is. This time, consult simply best/worst rolls and a linear scale between these two. This allows for fairly extreme outcomes, but those are very rare - requiring 2x20 in a row.

If you want to eliminate most of the lookup table - generate it first, then select one representative row around expected values to hit, and simply tweak values - if you need "X+1" to hit, simply shift the same table one step (but keep rolls of 1 and 20 special).

Code (in Matlab; it works in https://octave-online.net/, just paste function first and then the script):

N = 10e3;
ch = nan(N,2);
for i = 1 : N
    [ch(i,1),ch(i,2)] = critHit(15, 10); % 15 to hit, 10 attackers.
end
hitval = ch(:,1)*2 + ch(:,2); % 1 crit = 2 hits. Tweak as needed.
[hitval,I] = sort(hitval); % poor results are first, good results are last.
ch = ch(I,:); % sort ch in the same order.
for i = 1 : 20
    worst(i) = hitval(1+(i-1)*N/20);
    average(i) = mean(hitval(1+(i-1)*N/20 : i*N/20)); % arithmetic mean of this band.
    best(i) = hitval(i*N/20);
end

function [c, h] = critHit(rollToHit, Nattackers)
    r = floor(rand(Nattackers,1)*20) + 1;
    c = nnz(r==20);
    h = nnz(r>=rollToHit)-c; % subtract crits - h are normal hits.
end
\$\endgroup\$
3
  • 3
    \$\begingroup\$ I think that your code could be improved. Recall to try to avoid for loops in MATLAB as much as possible. For example, instead for 1e3 rolls you can use the function randi(20,1e3,1) for a vector with 1000 rolls of a d20. \$\endgroup\$
    – Eddymage
    Mar 7, 2023 at 15:54
  • \$\begingroup\$ @Eddymage I agree - the code could be improved quite a lot in both performance and clarity. But given nobody will use it in a way to notice, it doesn't really matter :P \$\endgroup\$ Mar 8, 2023 at 7:42
  • \$\begingroup\$ Well, it depends: usually I test codes like this with N = 1e6, so a for loop could take a while (several seconds, an eternity) to finish... \$\endgroup\$
    – Eddymage
    Mar 8, 2023 at 7:59
3
\$\begingroup\$

Simply convert d20 quantile to hits quantile

TL;DR: convert d20 outcome quantile to the true outcome quantile. Tables at the end...

If you're just looking for number of hits (ignoring crits), the outcome is a sum of independent coinflips (biased depending on AC and attack bonus) - that is, a Binomial distribution with n=10 (number of attackers = number of coinflips) and p (prob of individual hit = prob of individual coinflip) determined according to the AC and attack bonus.

What's p_hit?

We can compute p like this (examples in Python)

def get_p_hit(ac:int, bonus:int) -> float:
    # 1 is a definite miss, 20 is a definite hit, while [2,20) check vs ac
    return (sum([0 if i+bonus < ac else 1 for i in range(2,20)])+1) / 20.

(This is roughly (21 + bonus - ac) / 20 except for guaranteed success or failure on 20 or 1)

At this point we have a fully defined probability distribution over number of hits.

Converting d20 to outcome

The outcome of a d20 is also a fully defined probability distribution. We can always produce an approximate conversion between probability distributions via the quantile.

In this case, we map a roll of 1 to some outcome in the worst 1/20th of outcomes, a 2 to some outcome in the second worst 1/20th of outcomes, all the way to 20 which we map to some outcome in the best 1/20th of outcomes[^hitch].

def get_q_from_r(r:int) -> float:
    '''
    r should be a d20 roll expressed as an integer from 1 to 20 inclusive
    '''
    return (r - .5) / 20

[^hitch]: There is a small hitch: there might be multiple outcomes belonging to said worst/second worst/.../best 1/20th of outcomes. It seems reasonable to pick the middle outcome out of those, which is what my code does, by subtracting .5 from the roll before dividing.

Going from quantile back to hits outcome

With our binomial distribution in hand, and an outcome quantile, there are many ways we could compute an outcome. Probably the simplest (and most generalisable) is to recognise that with reasonably large n (10 is fine for these purposes) a Binomial is closely approximated by a Normal distribution with the same mean and standard deviation. So we just need to compute the mean and standard deviation of our Binomial using the standard formulae, and then use a Normal quantile.

Here's an example using some Python libraries, but most languages have numerics packages that will do the same

import numpy as np
import scipy.stats
from typing import Tuple

def get_mean_std(p_hit:float, n_attackers:int) -> Tuple[float, float]:
    mean = n_attackers * p_hit
    std = np.sqrt(mean * (1-p_hit))
    return mean, std

def get_hit_count_normal_approx(mean:float, std:float, q:float) -> int:
    return round(mean + std * scipy.stats.norm.ppf(q))

For our specific case (if p_hit is very low or very high, the very edge values can be invalid due to the Normal approximation - so we need to truncate just to be sure):

def get_hits_for_attackers(ac:int, bonus:int, n_attackers:int, r:int) -> int:
    '''
    r should be a d20 roll expressed as an integer from 1 to 20 inclusive
    '''
    p_hit = get_p_hit(ac, bonus)
    mean, std = get_mean_std(p_hit, n_attackers)
    q = get_q_from_r(r)
    return max(0, min(n_attackers, get_hit_count_normal_approx(mean, std, q)))

We can easily run this function as many times as we need and compute a table for faster lookup at playtime.

Bonus discussion on crits

Unfortunately all of the above glosses over critical hits! This approach is nice and modular though: we can think of crits given hits as just another sum of chancy outcomes - another binomial!

This time the n is the number of hits (that might or might not 'become' crits), and the p is the chance that a hit is a crit, given than it is a hit! i.e. if I rolled high enough to hit, what proportion of those rolls was a 20?

def get_p_crit_given_hit(p_hit:float) -> float:
    return 1/20 / p_hit

With this probability in hand, we can 'convert' a random number of hits into crits, if we are willing and able to roll another d20. It's exactly the same approach, except this time we need a variable rather than hardcoding 10.

def get_crits_for_hits(ac:int, bonus:int, n_hits:int, r:int) -> int:
    '''
    r should be a d20 roll expressed as an integer from 1 to 20 inclusive
    '''
    p_hit = get_p_hit(ac, bonus)
    p_crit_given_hit = get_p_crit_given_hit(p_hit)
    mean, std = get_mean_std(p_hit=p_crit_given_hit, n_attackers=n_hits)
    q = get_q_from_r(r)
    return max(0, min(n_hits, get_hit_count_normal_approx(mean, std, q)))

Observation on usefulness of tables

Obviously it would be cumbersome to have a table for every combination of ac and bonus! But thankfully those two variables can be encapsulated for our purposes in p_hit. p_hit can take one of exactly 19 values (0.05 through 0.95), so you only need 19 tables of d20 outcomes.

Unfortunately, the variance/deviation of actual outcomes depends on the number of attackers, so there's no escaping the need for a new table per number of attackers. But the code here can easily generate that.

In fact, for exactly 10 attackers, you can have one wide table like this, with p_hit along the top and the d20 roll r selecting the row:

p_hit 0.05 0.1 0.15 0.2 0.25 0.3 0.35 0.4 0.45 0.5 0.55 0.6 0.65 0.7 0.75 0.8 0.85 0.9 0.95
1: 0 0 0 0 0 0 1 1 1 2 2 3 4 4 5 6 6 7 8
2: 0 0 0 0 1 1 1 2 2 3 3 4 4 5 6 6 7 8 9
3: 0 0 0 1 1 1 2 2 3 3 4 4 5 5 6 7 7 8 9
4: 0 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 9
5: 0 0 1 1 1 2 2 3 3 4 4 5 5 6 6 7 8 8 9
6: 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9
7: 0 1 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 9 9
8: 0 1 1 2 2 3 3 4 4 4 5 6 6 7 7 8 8 9 9
9: 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
10: 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
11: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10
12: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10
13: 1 1 2 2 3 3 4 4 5 6 6 6 7 7 8 8 9 9 10
14: 1 1 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 9 10
15: 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10
16: 1 2 2 3 4 4 5 5 6 6 7 7 8 8 9 9 9 10 10
17: 1 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9 10 10 10
18: 1 2 3 3 4 5 5 6 6 7 7 8 8 9 9 9 10 10 10
19: 1 2 3 4 4 5 6 6 7 7 8 8 9 9 9 10 10 10 10
20: 2 3 4 4 5 6 6 7 8 8 9 9 9 10 10 10 10 10 10

Alternatively if you don't want to compute p_hit you can use the same table with (ac - bonus) header instead (since this determines p_hit)

(ac - bonus) 20 or more 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 or less
1: 0 0 0 0 0 0 1 1 1 2 2 3 4 4 5 6 6 7 8

etc.

If we want to use the bonus method for getting crits, we'd need a table for each 'number of attackers' from 1 to 10. Some other answers attempt to mix in the critical hit outcomes to a single roll, which might be preferable.

\$\endgroup\$
2
  • \$\begingroup\$ A principled way to mix criticals into a single roll outcome could be to initially generate a distribution, not for the number of hits (a binomial), but for the number of critical-weighted hits (e.g. number of hits + number of crits if crits count as double). This would simply inflate the mean and variance of the distribution to be slightly larger. We'd add 1/20 * n_attackers to the mean, and 1/20 * (19/20) * n_attackers to the variance (the bit inside np.sqrt(...) in the calculation of std). Everything else as before, except the result would be critical-weighted hit count. \$\endgroup\$
    – Oly
    Mar 8, 2023 at 22:14
  • \$\begingroup\$ In the case I mentioned of a 'critical weighted' calculation, you'd get a single number counting the number of hits plus number of those which crit. Whether '8' means '8 hits, no crits', or '7 hits, one of which is crit', or something else, would not be specified but could be glossed or estimated if desired based on p_hit_given_crit. \$\endgroup\$
    – Oly
    Mar 8, 2023 at 22:17
2
\$\begingroup\$

We can calculate the distribution of possible hits, and map those to dice. First, we use the attack bonus (+8) and AC (17) to calculate the accuracy (55%). Then, for each of the 20 possible die rolls representing the result with the odds 2.5%, 7.5%, 12.5%... 92.5%, 97.5%, we calculate the number of hits.

It turns out this is tricky to do in one step, so its easiest to do in two steps. First, for each number of hits, what are the odds of at least this many hits occurring? This can be calculated in Google sheets with the equation BINOMDIST(NumberOfHits,NumberOfAttackRolls,HitChance,true) For an attack bonus of +8 against an AC of 17, and 10 attacks, that generates this table

NumberOfHits Odds
0 0.01%
1 0.17%
2 1.23%
3 5.48%
4 16.62%
5 36.69%
6 61.77%
7 83.27%
8 95.36%
9 99.40%
10 100.00%

And then since each side of the dice represents a 5% range, we can map each side of the dice to a probability range, and therefore to the corresponding number of hits. For instance, we want a die roll of 7 to represent the 37.5-42.5% probability range. In the CDF, that probability range is within the 36.69-61.77% range that maps to 6 hits. A dice roll of 6 represents the 32.5-37.5% probability range. The CDF tells me that's usually 5... and possibly 6 hits. To avoid ambiguity, we'll map each dice roll to the center of its range. So a roll of 1 maps to the 2.5%, and a roll of 20 maps to the 97.5% odds.

The equation for this is VLookup((DiceRoll-0.5)/20,AttackOddsTable,DieRollIndex,true).

Roll NumberOfHits
1 2
2 3
3 3
4 4
5 4
6 4
7 4
8 5
9 5
10 5
11 5
12 5
13 6
14 6
15 6
16 6
17 6
18 7
19 7
20 8

It's worth noting that these values, though "accurate", are a little disappointing/unexpected around 1 and 20, so I would personally manually tweak 1 to miss all attacks and 20 to hit with all attacks.
We can also calculated the expected number of crits via the same formula, with "a 5% chance to hit".

I have made this spreadsheet to calculate all of this, and also average damage. If you don't want to have a sheet open to adjust the AC for each target, then can consider saving the results for each possible AC.

\$\endgroup\$
1
0
\$\begingroup\$

Average with noise

There are 10 monsters. Say you need to roll 13 to hit. On average 8/20 of the monsters hit which we multiply by 10 to get is 4 hits. You can rule exactly 4 always hit.

Or you can rule 4 plus some random mean zero number of them hit. For example rolls a d6 and get 4+n hits for n=-2,-1,0,0,1,2 depending on whether you roll 1,2,3,4,5,6 respectively.

Oh, and take average damage per hit.

\$\endgroup\$
-2
\$\begingroup\$

I like Eddymage's answer. This is based on that. If I was allowed, I would comment this as a suggested improvement to that answer.

If you want to make the result on the DMG table be based on an attack roll, you should flip it and subtract 10 like so: 20 -> 1 -> -9, 18 -> 3 -> -7, 10 -> 11 -> 1, etc

Difference to Target Attackers Needed for 1 hit
-9 20
-8 10
-7 to -6 5
-5 to -4 4
-3 to -2 3
-1 to +5 2
+6 to +10 1

Roll 1d20 to hit as normal, with all modifiers. Compare your roll to the AC of the target. For example, if you hit exactly, one in every two of your minions hit. If you roll 4 below the AC, one in every four of your minions hit. If you roll 8 above the AC, all of your minions hit.

I personally would prefer to use the more mathematically accurate table that Eddymage calculated:

Difference to Target Attackers Needed for 1 hit
-9 to -8 20
-7 10
-6 7
-5 5
-4 4
-3 to -1 3
0 to +4 2
+5 to +10 1

I'm not sure exactly how this would measure up statistically compared to just rolling for eveything. This does not preserve crits, but you could add some by either:

  • having a 20 on the dice generate a crit
  • every X hits produces one crit
  • Enough "partial" hits alongside at least one successful hit (i.e. 1/4 of 7 minions produces 1 and 3/4 hits) cause a single hit to crit
  • some combination of the above
\$\endgroup\$

You must log in to answer this question.

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