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@NdY, for which AnyDice apparently uses an inefficient algorithm whose runtime grows exponentially with N.
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 recursive function that computes the distribution of the highest roll in the pool more efficiently:
function: highest of N:n x D:d {
if N = 0 { result: 0 }
if N = 1 { result: D }
RES: 1@2d[highest of N/2 x D]
if N - (N/2)*2 = 1 { RES: [highest of D and RES] }
result: RES
}
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. Note that for pools with 1024 = 210 or more dice you'll need to increase AnyDice's recursion limit above the default of 10, e.g. with:
set "maximum function depth" to 99
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.
However, trying your consolidated function (final entry in the post), it appears to give incorrect results. For your example linked, it gives for 0, 1, 2 successes 57.01, 37.89, and 4.94, when the correct results for same are 60.71, 31.77 and 6.25.
– rasher Oct 07 '20 at 04:43I presume AD uses some kind of tuples/sequence generation internally to construct empirical distributions, limiting it to cases not much larger than the example.
Should suffice for my pal's needs though, if they're using pools larger than that it's silly IMO.
Thanks again!
– rasher Oct 07 '20 at 05:441@XdY, which AnyDice apparently handles inefficiently for largeX. If you want to use this code with opponent pools larger than about 20 dice, it's actually possible to trick AnyDice into using a smarter algorithm: for example,output 1@100d6will time out, but the equivalentoutput 1@10d(1@10d6)(which calculates the highest roll in 10 pools of 10 dice instead of a single pool of 100 dice) runs quite fast. – Ilmari Karonen Oct 07 '20 at 05:58[highest of 1@10d(1@10d6) and 1@7d6]to calculate the equivalent of1@107d6. Alas, AnyDice doesn't seem to be smart enough to apply such optimizations automatically. – Ilmari Karonen Oct 07 '20 at 06:00