Here's a fairly efficient solution:
TARGET_DIST: [highest of 1@3d6 and [highest of 1@2d8 and 1@1d12]]
output TARGET_DIST named "highest of 3d6, 2d8 and 1d12"
function: roll versus TARGET:n {
P: d6 > TARGET
Q: d8 > TARGET
R: d12 > TARGET
result: 2dP + 1dQ + 1dR
}
output [roll versus TARGET_DIST] named "2d6, 1d8 and 1d12 vs. highest of 3d6, 2d8 and 1d12"
First, we calculate the distribution of the target number and save it in a custom die named TARGET_DIST
. We can do that efficiently by taking the highest roll of each type of dice rolled by the opponent (which we can get with either [highest of XdY]
or simply 1@XdY
) and then take the highest of those using the built-in [highest of NUMBER and NUMBER]
function. (If we wanted, we could also define a custom function with more parameters to calculate the highest of multiple numbers with just one function call.)
Once we have the target number as a custom die, we pass it into a function as a numeric parameter (i.e. with :n
after the parameter name) to "freeze" it. The reason we need to freeze the target number is because we'll be comparing multiple different sized dice against it, and the probabilities of those comparisons succeeding will not be independent.
Inside the function, where TARGET
is now a fixed number instead of a custom die, we can then calculate the distribution of successes for the player's roll against the target number. The most efficient way to do that is to first define, for each die size in the pool, a corresponding custom die with the successful sides (i.e. those above the target number) relabeled as 1 and the rest as 0. We can then just roll the desired number of each of those custom dice and sum the results.
(We could skip the custom die definitions and just write the body of the function more concisely as result: 2d(d6 > TARGET) + 1d(d8 > TARGET) + 1d(d12 > TARGET)
, but that syntax looks kind of weird and ugly.)
It's also possible to make the function take the dice counts and sizes as parameters, but the syntax gets a bit verbose:
function: roll X x D and Y x E and Z x F versus TARGET:n {
P: D > TARGET
Q: E > TARGET
R: F > TARGET
result: XdP + YdQ + ZdR
}
output [roll 2 x d6 and 1 x d8 and 1 x d12 versus TARGET_DIST]
named "2d6, 1d8 and 1d12 vs. highest of 3d6, 2d8 and 1d12"
(The reason for including the x
's in the function name is that without them, AnyDice with parse e.g. 2 d6
as a single parameter, ignoring the space. And AFAIK there's no easy way in AnyDice to extract the underlying d6
die out of a dice pool like 2d6
, so we need to pass the count and the die as separate parameters. Alternatively you could remove the x
's and write the parameters e.g. as 2 1d6
or 2 (d6)
to resolve the ambiguity, but I don't really think that looks any better.)
And yes, this method works fine e.g. for your 4d6 and 6d20 vs. 6d6 and 4d20 example, with no risk of timeouts.
Alternatively, you can make functions that accept all dice types and just pass in '0' for those you do not want.
function: target A:n dfour B:n dsix C:n deight D:n dten E:n dtwelve F:n dtwenty {
result: [highest of 1@Ad4 and [highest of 1@Bd6 and [highest of 1@Cd8 and [highest of 1@Dd10 and [highest of 1@Ed12 and 1@Fd20]]]]]
}
TARGET_DIST: [target 0 dfour 3 dsix 2 deight 0 dten 1 dtwelve 0 dtwenty]
output TARGET_DIST named "highest of 3d6, 2d8 and 1d12"
function: roll A:n dfour B:n dsix C:n deight D:n dten E:n dtwelve F:n dtwenty versus TARGET:n {
P: d4 > TARGET
Q: d6 > TARGET
R: d8 > TARGET
S: d10 > TARGET
T: d12 > TARGET
U: d20 > TARGET
result: AdP + BdQ + CdR + DdS + EdT + FdU
}
output [roll 0 dfour 2 dsix 1 deight 0 dten 1 dtwelve 0 dtwenty versus TARGET_DIST]
named "2d6, 1d8 and 1d12 vs. highest of 3d6, 2d8 and 1d12"
(Credit for this variant should go to Dale M, who added it in an edit.)
Ps. Based on discussion in comments below, it turns out that the slowest part of the programs above is calculating the target value, and specifically the calculation 1@XdY
, for which AnyDice apparently uses an inefficient algorithm whose runtime grows exponentially with X
.
If you want to use this code with very large opponent dice pools (say, more than about 20 dice of any particular size), it's possible to write a custom function that computes the distribution of the highest roll in the pool more efficiently:
function: highest of N:n x D:d {
if N <= 10 { result: 1@NdD }
Q: N / 10
R: N - Q * 10
result: [highest of 1@RdD and 1@10d[highest of Q x D]]
}
For pools with more than 10 dice, this code automatically splits the pool into ten sub-pools with N
/ 10 dice, plus an extra pool with any remaining dice left over after the division, and calls itself recursively for the sub-pool in case it still has more than 10 dice. (The constant divisor 10 in the code above is somewhat arbitrary; any number between 2 and about 20 should work about equally well, as long as you remember to replace all four occurrences of 10 with it.)
You can use this helper function e.g. like this:
A: [highest of 3 x d6]
B: [highest of 2 x d8]
C: [highest of 1 x d12]
TARGET_DIST: [highest of A and [highest of B and C]]
output TARGET_DIST named "highest of 3d6, 2d8 and 1d12"
With this modification, the code can easily handle literally millions of dice in the opponent's pool. At this point the player's dice pool size becomes the next bottleneck, but even the unmodified original code above will handle player pools with up to hundreds of dice just fine.