As requested in the comments, here's my optimized version of Magician's code, using the die-relabeling trick instead of simply counting:
\ roll AdX + BdY, target 4, where X is one size less than Y and A+B = 6 \
DICE: {4,6,8,10,12,20}
loop I over {2..#DICE} {
X: (I-1)@DICE
Y: I@DICE
output 6d(dX >= 4) named "6d[X]"
loop B over {1..5} {
A: 6-B
output Ad(dX >= 4) + Bd(dY >= 4) named "[A]d[X] and [B]d[Y]"
}
}
output 6d(dY >= 4) named "6d[Y]"
This code will handle arbitrarily large dice (yes, really, even something crazy like d1000), and can also be easily modified to use a larger dice pool.
Looking at the averages, it's pretty clear that going up from d4 to d6 in the beginning, and from d12 straight to d20 in the end, give the most rapid increases in the success rate, while there's relatively little difference between, say, a d10 and a d12 in the middle of the progression.
For some purposes this kind of a curve, with rapid progress in the beginning that gradually slows down, before speeding up again at the final levels, might be exactly what you want. That said, if you'd prefer something a bit more linear, one way to speed up the middle part of the curve would be to drop the d10 from the progression, or, alternatively, to keep the d10 but drop both d8 and d12.
Ps. What's "the die-relabeling trick", you ask? Well, it turns out that, if you're rolling NdX and counting successes against some target number T, by far the most efficient way to do that in AnyDice is to first create a custom X-sided die with all the sides less than T relabeled as "0" and the rest as "1", and then roll that die N times.
The easiest way to do that is to take a single normal X-sided die and let AnyDice relabel it for you by applying a function or a comparison operator to it.
That is, instead of doing something like this:
output [count {4..20} in 6d20] named "6d20, target 4"
you should instead do this:
output 6d[count {4..20} in d20] named "6d20, target 4"
or just this:
output 6d(d20 >= 4) named "6d20, target 4"
Here, [count {4..20} in d20]
(or, equivalently, d20 >= 4
) is a custom 20-sided die with the sides 1–3 relabeled as "0" and the sides 4–20 relabeled as "1".
The last example above works because AnyDice comparison operators like >=
return 1 for "true" and 0 for "false" and because comparing a die with a number applies the comparison to each roll of the die, relabeling the sides according to the result.
It's also possible to construct such relabeled dice "manually", without starting with a normal dX at all, e.g. like this:
output 6d{0:3, 1:17} named "6d20, target 4"
Here, d{0:3, 1:17}
is a die with three sides labeled "0" and 17 sides labeled "1", for a total of 20.
I used to recommend this method in older versions of this answer, but I now feel that it's uglier and more prone to mistakes than just starting with a normal die and letting AnyDice relabel it for you.
You can check for yourself that all of these programs produce the same results, but the first one takes up to several seconds to run (and might time out, if the server is busy), while the other three finish instantly. And the bigger you make the dice, or the more dice you add to the pool, the more obvious the difference becomes. Trying to calculate, say, 8d20 vs. 4 in the naïve way is pretty much guaranteed to time out, whereas with relabeled dice the code will run pretty much instantly even for, say, 100d20 vs. 4.
The reason for this is that, if you pass a large dice pool to a function like [count ... in ...]
, AnyDice will waste a lot of time rolling all the possible outcomes of, say, 6d20, and then counting the number of successes in each of them — it's not smart enough to realize that, say, (2, 7, 8, 14, 16, 18) is just as good a roll as (2, 7, 8, 14, 16, 19), and that those cases don't really need to be considered separately. Using relabeled dice tells AnyDice that all the sides of the die equal to or greater than the target number really are equivalent, and so are all those less than the target.
Pps. One situation where the "manual" relabeling method can be useful is if you need a more complex relabeling scheme like, say, having a natural 20 count as two successes:
output 6d{0:3, 1:16, 2:1} named "6d20, target 4, double success on nat 20"
However, you can also achieve the same result with a custom relabeling function, e.g. like this:
function: relabel SIDE:n target TARGET:n {
if SIDE = 20 { result: 2 }
if SIDE >= TARGET { result: 1 }
result: 0
}
output 6d[relabel d20 target 4] named "6d20, target 4, double success on nat 20"
Using a helper function is more verbose, but it can also be a lot more readable, especially if the relabeling involves a bunch of special cases like autofail on natural 1, autocrit on natural 20, etc. In particular, note that the relabeling function can itself return a (relabeled) die to simulate a reroll, or even the "empty die" d{)
to make AnyDice ignore that particular roll entirely, as if it never happened.
For even more control over the results (e.g. if certain rolls count as critical successes or fumbles that modify the outcome in some non-additive manner) you can also pass the results of rolling the relabeled dice into a function for further processing, like here. Often it's still useful to start with relabeling a single die, though, since letting AnyDice know that certain sides of the die are equivalent can make it handle large pools of such dice a lot faster.