# How do I calculate probability for the dice mechanic in my custom indie ttrpg?

I am currently working on writing an indie TTRPG based on the "press your luck" style games instead of being based around miniature war games.
I am struggling to understand the probabilities in my game.

The dice mechanic itself is very simple. On your turn you roll 3d6 at a time, then if you roll under your character's ability score you count that as 1 success, you can spend successes on different class features and abilities to do things.
You can keep rolling 3d6 over and over again until you roll 3 dice that fail.

For example: I want to cast the chain lightning spell which requires 2 successes per target, with 3 targets I would need 6 successes to hit them all or I could end my turn after only 2 or 4 successes if I am afraid of failing. Since my Intelligence is 4 I need to roll a 1-4 on a d6 to get a success.

I would like some help to understand how to calculate the probabilities here so I can answer the following questions

• Based on different ability scores how many successes can I expect to get before 3 failures at different probabilities? (With an ability score of X there is a 50% chance of getting at least 5 successes before 3 failures)
• How do the probabilities change if I use d10s instead of d6s
• If the players crit fail after rolling at least 3 "6s" how does that likelihood change over time (What is the probability of rolling at least 3 6s on 3d6, 6d6, 9d6, etc.)
• How would class features that change the amount of dice rolled affect probabilities (What if I am reckless and roll 4d6 or 5d6 instead of 3d6)
• How does the ability to reroll individual dice affect the probabilities (I roll my 3d6 and use a special ability to immediately reroll a failure)
• How do you order the dice within a roll? That is: let's say I need one more success but already have two failures (both natural 6s) and I roll a 1, 5, 6. Do I get my last success (off of the 1), a normal failure (off of the 5), or a crit failure (off of the 6)? Do I get to choose or do I need to designate "die 1, die 2, die 3" before rolling? May 8, 2023 at 20:24
• They occur simultaniously. So let's say you want 6 successes with a 66.66% chance of success on each roll 1st round you roll 2 Successes 1 failure 2nd round you roll 3 Successes 0 failures 3rd round you roll 1 Success and 2 Failures. That totals 3 failures and you bust out and end your turn but you can still spend the 6 sucesses. Enemies activate extra stuff on your failures May 8, 2023 at 21:18
• Do you have access to a any programming environments, for example MSVisualStudio, or a Python shell? Weird stuff that might be super-hard in, say, Any-Dice, is generally simple for a 1st-year coder to close-enough simulate by blasting 10,000 tries for all combinations. May 8, 2023 at 22:44
• I do not and even if I did I have zero coding experience. May 10, 2023 at 12:03

As minnmass notes, AnyDice is a useful tool for these kinds of calculations. Here's a quick AnyDice program that should get you started:

ABILITY: 4
DIE: d6 <= ABILITY
DICE: 3dDIE

function: roll ROLL:s until X:n successes or Y:n failures {
S: ROLL = 1   \ count successes in roll \
F: ROLL = 0   \ count failures in roll \
if S >= X | F >= Y {
result: S
} else {
result: S + [roll DICE until X-S successes or Y-F failures]
}
}

set "maximum function depth" to 99

output [roll DICE until 30 successes or 3 failures] named "successes"


What we're doing is first defining a custom relabeled six-sided die named DIE that rolls a 1 on a success and 0 on a failure, and then a pool of three such dice named DICE. (This is mainly a performance optimization: the fewer differently numbered sides your dice have, the faster AnyDice runs.)

Then we define a function that takes a sequence parameter named ROLL (representing the result of rolling a bunch of the custom dice defined above) and two numeric parameters named X and Y (indicating when we want or must stop rolling more dice). The function counts the number of successes and failures in ROLL and then either just returns the success count (if we hit one of the thresholds and stop rolling) or calls itself (to model a subsequent roll) with adjusted thresholds (subtracting the number of successes and failures already rolled) and adds the result to the success count.

And finally we have an output statement that calls the function and outputs the result. (But before that we need to adjust AnyDice's setting a bit to stop it from complaining about too many nested function calls.)

There's a bit of AnyDice magic going on in the function calls, where we pass DICE in as the value of the ROLL parameter. Remember, DICE is a collection of (unrolled) dice, while the :s in the function definition indicates that ROLL is supposed to be a sequence of numbers. When AnyDice notices this, it does something special: it calls the function for every possible (sorted) outcome of rolling the dice and collects the results of each of these function calls into a custom die weighed by their probability. So, whereas outside the function DICE is just a pool of three dice with no fixed numeric value, inside the function ROLL is a fixed sequence of three numbers that we can examine and manipulate any way we want.

FWIW, this is what the output of the code above looks like in Graph mode:

That's an interesting looking graph! I wonder where those peaks at multiples of three successes come from. Let's see what happens if I adjust the maximum allowed number of failures while keeping the pool size fixed:

It looks like the spacing between the peaks is determined by the pool size, but the position of the peaks is also affected by the maximum number of failures — apparently allowing one more failure moves the peaks one success to the left.

BTW, note that we're still only outputting the total number of successes rolled. We can tweak the function to also return the total number of failures, but that requires a bit of trickery, since AnyDice can normally only output numbers. However, we can encode multiple numbers into one number, e.g. by mapping the numbers S and F into 100 × S + F, like this:

ABILITY: 4
DIE: d6 <= ABILITY
DICE: 3dDIE

function: roll ROLL:s until X:n successes or Y:n failures {
S: ROLL = 1   \ count successes in roll \
F: ROLL = 0   \ count failures in roll \
ENCODED: 100 * S + F
if S >= X | F >= Y {
result: ENCODED
} else {
result: ENCODED + [roll DICE until X-S successes or Y-F failures]
}
}

set "maximum function depth" to 99

output [roll DICE until 30 successes or 3 failures] named "100 * successes + failures"


As long as F can never be greater than 99, this encoding is unambiguous and easy to read: the last two digits of the encoded number correspond to F, and the digits to the left of them to S. So, for example, the output 603 corresponds to 6 successes and 3 failures.

Alas, the one feature we lose is the ability to (usefully) graph the output. But we can still look at the output in Table mode:

In particular, looking at this output and playing with different maximum failure counts and dice pool sizes, we can see what's going on with the peaks: the total sum of failures and successes must, of course, be a multiple of the pool size, and the peaks simply correspond to the cases where we roll the minimum number of failures needed to stop, as that's more likely than overshooting.

OK, that's interesting, but what about the other things you wanted to investigate?

The code above already lets you easily adjust the ability score, the number of dice rolled at a time, the type of dice used and the limits on how many failures you want to allow and/or how many successes you consider to be enough. That takes care of most of your questions, except for crits and rerolls.

Crit fails on 6s can be handled by relabeling the dice a bit differently: instead of just 1 for success and 0 for failure, we can also add e.g. -1 for critical failure. Probably the most elegant way to construct such dice is with a helper function:

function: relabel SIDE:n {
if SIDE <= ABILITY { result: 1 }  \ success \
else if SIDE < 6   { result: 0 }  \ normal failure \
else { result: -1 }  \ critical failure \
}
DIE: [relabel d6]


We'll also need to adjust our main function to deal with crits, of course, e.g. like this:

function: roll ROLL:s until X:n successes or Y:n failures or Z:n crit fails {
S: ROLL = 1   \ count successes in roll \
F: ROLL <= 0  \ count failures (including crits) in roll \
C: ROLL = -1  \ count critical failures in roll \
if S >= X | F >= Y | C >= Z {
result: S
} else {
result: S + [roll DICE until X-S successes or Y-F failures or Z-C crit fails]
}
}

set "maximum function depth" to 99

output [roll DICE until 30 successes or 3 failures or 2 crit fails] named "successes"


Of course, we can also still use the encoding trick to show the failure and crit failure counts as well:

function: roll ROLL:s until X:n successes or Y:n failures or Z:n crit fails {
S: ROLL = 1   \ count successes in roll \
F: ROLL <= 0  \ count failures (including crits) in roll \
C: ROLL = -1  \ count critical failures in roll \
ENCODED: 100 * S + 10 * F + C
if S >= X | F >= Y | C >= Z {
result: ENCODED
} else {
result: ENCODED + [roll DICE until X-S successes or Y-F failures or Z-C crit fails]
}
}

set "maximum function depth" to 99

output [roll DICE until 30 successes or 3 failures or 2 crit fails]
named "100 * successes + 10 * failures + crit fails"


I'll leave dealing with rerolls as an exercise for now. It should certainly be possible, but incorporating it into the code above feels non-trivial (unless I'm just missing some simple and obvious mathematical trick, which is quite possible).

• I think there is indeed a mathematical trick. If the R rerolls are per-stage, then it's the classic "roll R extra dice, keep the best" for boolean dice. If the R rerolls are for the whole sequence, then you can "pre-roll" the rerolls to determine how many failures you will convert to successes. Each successful "pre-reroll" then counts as both an extra max failure and an extra success. Finally you just run your existing algorithm for each possible number of successful pre-rerolls. (Of course, this assumes that you can spend multiple rerolls on the same die.) May 9, 2023 at 8:35

The easiest option is probably to play around with AnyDice to simulate different targets, die-pool sizes, and die-sizes. To start with, output [count {1, 2, 3, 4} in 3d6] will count successes in a single roll of 3d6; varying the numbers being counted will change the success target, and varying the "3d6" part will vary the die pool/size.

Since you always roll in groups of 3, you can just vary the die pool size to see how 3 vs. 6 vs. 9 (etc.) d6 (or d10s or whatever else) will change the odds of getting X successes. Note that failures are "die pool - successes", so you can see how rolling more groups of dice will affect the outcome.

Note: critically, this will not tell you how often you'd expect to roll (eg.) 3 failures before 6 successes on 9d6, just how often 9d6 would result in 6 successes.

What AnyDice won't easily show you are how critical failures affect the results or how individual re-rolls affect them.

Determining the odds of getting three 6s involves some "fun" math - this site concisely puts it:

The probability of rolling exactly X same values (equal to y) out of the set - imagine you have a set of seven 12-sided dice, and you want to know the chance of getting exactly two 9s. It's somehow different than previously because only a part of the whole set has to match the conditions. This is where the binomial probability comes in handy. The binomial probability formula is:

P(X=r) = nCr · pʳ · (1-p)ⁿ⁻ʳ,

where r is the number of successes, and nCr is the number of combinations (also known as "n choose r").

With AnyDice, you can fudge it by using output [count {6} in 3d6] then in 6d6, etc.. That said: it doesn't matter a whole lot if your target number isn't 6. Regardless, that'll show you how often some number of dice show a 6.

And, re-rolls are a bit tricky to model/calculate, too. Largely because it's unclear how many dice might be re-rolled out of a set. I don't know AnyDice well enough to try to model this, nor whether it really can if each die is re-rolled individually. But, you can get a rough idea by adding a die to the pool and discounting the extra successes row (eg., rolling 4d6 and discounting the "4 successes" row), then comparing that to rolling 3d6.

If you want to play around with numbers, I've put together a little C# program to start with, here. Lines 7-32 define the values you want to play around with (dice per roll, size of dice, etc.); you can modify any of them then hit the "run" button at the top to see the count of runs that end with successes, failures, critical failures, or some combination thereof. Note that it rolls until it either gets the target number of successes or failures (ie., it won't stop early), so it will tend to overestimate the number of runs that end with failures. It also doesn't display the actual number of successes/failures/crit-failures per run, since coding that would get hairy. On one test run, it got 9283 runs that ended because of 4 successes ("Successful Simulations"), 717 that ended because of failures ("Failure Simulations"), and 12 that ended because of a critical failure ("Critical Failure Simulations"); note that there were almost certainly some failures rolled in with the successful simulations and vice-versa, but that tells me that a player with a target number of 4 will be "forced" to stop rolling because they've hit 4 successes about 92% of the time; that particular simulation didn't have any runs that ended with the player having rolled both 4 successes and 3 failures, but the program will account for that.