# How can I write a formula for this houseruled roll in AnyDice?

So here it goes:

1. The roll is a pool of dice of d6s, d8s, and d10s. The minimum dice pool is 1d6 and the maximum dice pool is 10 dice. It could be 2d6 + 2d8 + 1d10, for example.
2. Rolling 5+ is a success.
3. A number of successes is necessary equal or higher than the Difficulty (that ranges from 1 to 10) to have a successful check.
4. The maximum value of a die explodes: 6 in a d6, 8 in a d8, 10 in a d10. But there's a limit by the character's Protagonism (ranges 1 to 10). It's like the level of the character, so one with Protagonism 3 could not explode any dice more than 3 times. The eventual 4th time counts only as a normal success, even if it has the maximum value again.

I see it can be very difficult to do this, so I thank anyone who may come up with something. Thanks so much!

• What have you tried yourself and where did you get stuck? Commented Feb 25, 2021 at 23:06
• Oh and Welcome to RPG.SE! Take the tour if you haven't already and see the help center or ask us here in the comments (use @ to ping someone) if you need more guidance. Good Luck and Happy Gaming! Commented Feb 25, 2021 at 23:07
• How are kinds of dice decided? Commented Feb 25, 2021 at 23:10
• Are the dice all rolled one at a time? With a pool of 1d6+2d8 would "5, 1, 6" count as two successes while "5, 1, 4" counts as one success? If they are not rolled one at a time, how do you track how many times each die has exploded? Are you looking for a formula that accounts for any set of dice in the pool, ranging from 1d6 to 10d10 and everything in-between? Commented Feb 25, 2021 at 23:14
• Medix2, they are rolled together. Then, you count successes and then reroll dice that may have exploded one at a time, until it stops to explode or the limit is reached. Commented Feb 27, 2021 at 17:55

The trick to modelling these kinds of dice pool mechanics efficiently in AnyDice is to make use of the fact that each die in the pool is rolled and counted (and possibly rerolled) independently of the others.

Correction: That isn't actually the case here, since apparently the reroll limit is supposed to be applied to the entire pool, not to each individual die as I originally interpreted it. I've included a second program implementing the corrected mechanic in an addendum below, but I'll present my original answer here for educational value first.

This allows us to first calculate the probability distribution of successes for a single die of each size, and save it as an AnyDice "custom die". We can then add together any combination of these custom dice to get the distribution of successes for any dice pool.

So, let's see how to actually do that. First, we need a function that will calculate the success distribution for one die. If we didn't have the explosion mechanic, this would be really easy to do — just something like:

function: roll ROLL:n target TARGET:n {
result: ROLL >= TARGET
}

output [roll d6 target 5] named "single d6, no explosions"

The trick here is that, when you call a function expecting a numeric parameter (i.e. one marked with :n) but actually pass in a die (like d6) instead, AnyDice will automatically call your function for every possible result of rolling the die and collect the results into a new custom die.

Actually, for the simple example above we could've just done output d6 >= 5 and skipped the function entirely. But we're going to need a function to implement the explosion mechanic anyway. The new trick there is that we can have the function call itself recursively when an explosion occurs, e.g. like this:

function: roll ROLL:n target TARGET:n explode MAX:n limit LIMIT:n {
if ROLL = MAX & LIMIT > 0 {
result: (ROLL >= TARGET) + [roll dMAX target TARGET explode MAX limit LIMIT-1]
} else {
result: (ROLL >= TARGET)
}
}

output [roll d6 target 5 explode 6 limit 3] named "single d6, max 3 explosions"

Note how I'm reducing the LIMIT parameter by one for every nested function call, thus ensuring that the rerolls will stop when the limit goes down to 0. Also note that I'm using the MAX parameter for two different purposes: both to specify the roll on which a reroll should happen and to define the size of the die used for the reroll. I could've used separate parameters for those, but that would've made the function call even more verbose.

Anyway, now that we have a function that lets us roll a single die of any size and count the number of successes, we can do the for each of the die sizes and save the results as custom dice:

PROTAGONISM: 2

SIX: [roll d6 target 5 explode 6 limit PROTAGONISM]
EIGHT: [roll d8 target 5 explode 8 limit PROTAGONISM]
TEN: [roll d10 target 5 explode 10 limit PROTAGONISM]

And then we can roll any combination of those custom dice that we want to get the distribution of success counts:

output 2dSIX + 2dEIGHT + 1dTEN named "2d6 + 2d8 + 1d10, protagonism [PROTAGONISM]"

And there we have it. Here's what the output of the complete program looks like:

One minor detail worth noting in the complete program linked above is that I also added in the line:

set "maximum function depth" to PROTAGONISM + 1

Normally AnyDice limits the number of nested function calls to 10, and it turns out that this isn't quite enough if you set PROTAGONISM to 10. Setting the limit to one more than the protagonism value is sufficient for our purposes.

However, it also turns out that the protagonism value really doesn't have all that much effect is practice. For example, if we run the code above in a loop for all protagonism values from 1 to 5 and plot the results as graphs, they look like this:

Notice how all the graphs are basically drawn right on top of each other, and that the mean success count shown in the legend is also almost the same for all of them (approximately 2.56 for protagonism 1, 2.60 for protagonism 2 and 2.61 for all higher values).

Basically this is because getting more than one reroll in a row is very unlikely — a one in 36 chance for two consecutive rerolls on a d6, less for bigger dice and much less for longer chains of rerolls. So basically, while it might feel really awesome to get the rare triple explosion on a roll, it happens so rarely that it doesn't really make any difference to the averages. And with your success counting mechanic, the reward isn't even all that much — all a reroll gives you is a (chance for) a single extra success point, which may not even make any difference if you're not just under the difficulty target.

Honestly, I'd recommend either rethinking the protagonism mechanic to give better rewards, or get rid of it completely and just allow unlimited rerolls.

Addendum: I made a new program that applies the reroll limit to the entire pool, rather than separately to each individual die:

TARGET: 5

function: custom die N:n {
result: d{0:(TARGET-1), TARGET:(N-TARGET), N}
}

SIX: [custom die 6]
EIGHT: [custom die 8]
TEN: [custom die 10]

function: roll SIXES:s EIGHTS:s TENS:s protagonism P:n {
SUCCESSES: {SIXES, EIGHTS, TENS} >= TARGET
C: [lowest of (TENS = 10) and P]
B: [lowest of (EIGHTS = 8) and P - C]
A: [lowest of (SIXES = 6) and P - B - C]
if A + B + C > 0 {
SUCCESSES: SUCCESSES + [roll AdSIX BdEIGHT CdTEN protagonism P - A - B - C]
}
result: SUCCESSES
}

loop P over {1..5} {
output [roll 2dSIX 2dEIGHT 1dTEN protagonism P] named "2d6, 2d8, 1d10, protagonism [P]"
}

It works a little differently from the first program:

• First, it starts by constructing a bunch of custom relabeled $$\N\$$-sided dice. The way those dice are relabeled is so that each side below the target value of 5 is replaced with 0 (= no success), each side meeting or exceeding the target (but below the highest possible roll) is replaced with the target value (= normal success) and the highest numbered side on the die is left unchanged (= success and reroll).

Technically, those custom dice aren't necessary; the code below would runs just fine using normal d6's, d8's and d10's too — just a low slower. By relabeling the dice so that equivalent sides have the same number, we save time by reducing the number of possible dice rolls that AnyDice needs to examine. But if you want to double-check that the relabeling trick doesn't introduce any bugs, you can replace the custom dice with just SIX: d6, EIGHT: d8 and TEN: d10 and compare the results. Just probably with a smaller pool size and/or reroll limit, to avoid timeouts.

• Next is the roll SIXES EIGHTS TENS protagonism P function, which takes as its parameters the results of rolling each group of 6, 8 and 10 sided dice (as three separate parameters, since that's to only way to prevent AnyDice from summing the rolls up outside the function) and the number of allowed rerolls $$\P\$$. The roll parameters are typed as sequences (with :s), making AnyDice call the function for each possible combination of rolled values. (This is why we wanted to relabel the dice above to minimize the number of such combinations.)

• Inside the function, the code first counts the number of successes without rerolls by comparing each of the rolled values with the target threshold. (I arranged the relabeling scheme so that all successful rolls in fact still pass the comparison.)

• Then we calculate the number of each kind of dice that we can reroll based on the number of max-value rolls on each type of dice. The code assumes that, given a choice, the player will always reroll the biggest dice they can — this should be the optimal strategy, since the expected number of successes will always be higher for bigger dice.

• Finally, if the number of possible rerolls is greater than zero, the function calls itself recursively to perform the rerolls. Note that I'm subtracting the number of rerolled dice from the limit $$\P\$$ so that the function can't keep rerolling the same dice forever.

Anyway, running the code above and graphing the results produces the following chart:

The lines for different reroll limits aren't quite as closely overlaid as before, showing that the per-roll limit indeed makes a bigger difference than the per-die limit. But it's still not a huge difference — we're still taking about an average difference of about 0.11 successes per roll between protagonism level 1 and 5 (and about 0.03 successes between protagonism 2 and 5).

(For large reroll limits the code will time out. For the 2d6 / 2d8 / 1d10 pool in the example, I managed to run it for up to protagonism 7, but it times out for protagonism 8. The graphs, however, are nearly identical at that point; all that extra time is spent examining very unlikely cases involving extremely lucky rerolls.)

• Ilmari, thanks very much! Commented Feb 27, 2021 at 18:25
• I don't know very much about programs and code in general, but I could understand what you did! Very nice! About the limit in exploding, I wanted a formula exactly to visualize this question, because in playtests it only makes a difference in extreme cases of luck. I'll rethink it, indeed :D I'm from Brazil, so sorry to ask, Ilmari Karonen is your true name? Pardon my ignorance, I'm asking so I can give you credit for the help with my game! Commented Feb 27, 2021 at 18:34
• Oh, one comment reading now: You set the protagonism for 1 reroll per die, if I understood right, when it's 1 reroll per check. So, if the protagonism is 2, it's 2 rerolls for that check, no matter the dice. But I think this makes your point even stronger, since if each given die could explode X times, it would be hard to track, but it would be more powerful (in theory) than a check explode X times. Commented Feb 27, 2021 at 19:03
• @GlaucoLessa: Yes, it's my real name. And yeah, I see I got the protagonism mechanic wrong. Making the limit per-pool instead of per-die actually seems like it should make the limit matter more, since the probability of actually hitting the limit would be higher. I'll see if I can figure out a good way to model the per-pool limit and see what it actually does. Commented Feb 27, 2021 at 19:46
• @GlaucoLessa: …and I did. It does make slightly more difference that way, but still not much. Commented Feb 28, 2021 at 1:50