7
\$\begingroup\$

I was asked by a pal to help him model a dice mechanic in AnyDice. I must admit I am an absolute neophyte with it, and I offered to solve it using software I'm better versed in. I'd like to be able to help them do this in AnyDice.

The mechanic is as follows:

  • The player and their opponent are each assigned a pool of dice. This is done via other mechanics of the game, of which details are not germane. Suffice to say, the player will have some set of dice (say 2D6, 1D8, and 1D12) that will face off against the opponents pool (which will generally be different from the player's, say 3D6, 2D8 and 1D12).
  • The player and their oppoent roll their pools.
  • The opponent notes their highest value die. This is the target.
  • The player counts the number of their dice that have a higher value than the target, if any.
  • The count of the dice exceeding the target, if any, is the number of success points.

I searched the AnyDice tag here for questions that might be similar, the closest I found was "Modelling an opposed dice pool mechanic in AnyDice", specifically the answer by Ilmari Karonen.

That question and answer, however, deals with only a single die type.

Can a question like "What are the probabilities of N successes when rolling 4D6 and 6D20 as the player against 6D6 and 4D20 for the opponent?", be handled in AnyDice and produce a chart similar to that below?

enter image description here

\$\endgroup\$
0
12
\$\begingroup\$

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.

\$\endgroup\$
5
  • \$\begingroup\$ Thanks much for the reply. I have learned more from a quick scan of your code than I had from muddling through the AnyDice "documentation". 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. \$\endgroup\$ – rasher Oct 7 '20 at 4:43
  • \$\begingroup\$ Yeah, that last program (which wasn't my code; it was edited in by @DaleM) was broken. I've removed it. \$\endgroup\$ – Ilmari Karonen Oct 7 '20 at 5:40
  • \$\begingroup\$ Thanks for the clarification. Accepted and already +1'd. I 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! \$\endgroup\$ – rasher Oct 7 '20 at 5:44
  • 1
    \$\begingroup\$ I ran some tests, and the slowest part of the code is actually calculating the target number, and specifically calculating 1@XdY, which AnyDice apparently handles inefficiently for large X. 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@100d6 will time out, but the equivalent output 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. \$\endgroup\$ – Ilmari Karonen Oct 7 '20 at 5:58
  • 1
    \$\begingroup\$ … Also, for pool sizes that don't factor so nicely, you can use e.g. [highest of 1@10d(1@10d6) and 1@7d6] to calculate the equivalent of 1@107d6. Alas, AnyDice doesn't seem to be smart enough to apply such optimizations automatically. \$\endgroup\$ – Ilmari Karonen Oct 7 '20 at 6:00
3
\$\begingroup\$

Do it? Sure. Do it well? Not so much

The easiest way to do mixed dice pools in AnyDice is to cast them as seperate sequence inputs into a function, and then stitch the sequences together. Note that you'll have to build the function with a maximum different dice possible, but it is possible to give a 0 or {} if the function has more inputs than your pool.

We can find the maximum by sorting it (putting the highest first in default mode) and taking the first. We can then find the number of values in the other pool higher than it by comparing that number to that sequence. The resulting function is fairly short:

function: highest of pool A:s B:s C:s vs X:s Y:s Z:s{
  result: {X, Y, Z} > 1@[sort {A, B, C}]
}

output [highest of pool 2d6 1d8 0 vs 2d6 1d8 0]

The problem we're running into is that AnyDice has runtime limited to 5 seconds and so this times out with more than 6 dice in the pools total. Given the examples, you're probably better off using a different tool (with higher efficiency/allowed runtime) at least for the larger pools. (Unless of course there is some, more efficient AnyDice method I'm unaware of.)

\$\endgroup\$
1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.