4
\$\begingroup\$

Anydice is a useful online tool that can help answer questions about dice probabilities. It allows the user to define functions. A special effect of functions is that when they are given a die or dice collection as input, the function is called on every possible outcome of the dice separately, and the results are collected into a new die. For example, the following will output an equal distribution of the values 4, 6, 8, 10:

function: critical DICE { result: DICE * 2 }
output [critical d4+1]

Functions have a special feature in that allow you to cast the parameters to one of the types (number, die, seqeunce). With DICE:s, the parameter will first be turned into a sequence, if it is not already one, and "If dice are provided, then the function will be invoked for all possible sequences that can be made by rolling those dice. In that case the result will be a new die.", so it behaves just the same as the code above when given a die. With DICE:d, the parameter will first be turned into a die if it isn't already a die.

For the above code, there is really no difference in output, if I cast the DICE parameter to a die (as there is no change) or to a sequence.

What would be an example situation or use case, where it would make sense to use a sequence parameter, rather than a die parameter?

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

You need to use a function with a sequence (or number) parameter in AnyDice when…

…you want to use the result of a die roll in an if condition.

AnyDice doesn't let you use a dice roll in the condition part of an if statement. For example, the following function will trigger an error if called with a die roll:

function: clamp NUMBER between MIN and MAX {
  if NUMBER < MIN { result: MIN }
  if NUMBER > MAX { result: MAX }
  result: NUMBER
}

output [clamp 2d6 between 4 and 10]  \ <-- this triggers a calculation error \

If you try to run the code above, all AnyDice will output is a red error message box saying:

calculation error
Boolean values can only be numbers, but you provided "d{?}".
Depending on what you want, you might need to create a function.

To make this code work as expected, you need to tell AnyDice that all these parameters should be converted to numbers when the function is called (and, if necessary, the function should be called multiple times with all the possible values the parameters may have), like this:

function: clamp NUMBER:n between MIN:n and MAX:n {
  if NUMBER < MIN { result: MIN }
  if NUMBER > MAX { result: MAX }
  result: NUMBER
}

output [clamp 2d6 between 4 and 10]  \ <-- this works! \

Of course you can sometimes work around this problem by using combinations of AnyDice built-in functions and/or operators, such as:

function: clamp NUMBER between MIN and MAX {
  result: [highest of MIN and [lowest of MAX and NUMBER]]
}

…but this is really just doing the same thing "under the hood". In particular, if you wanted to reimplement the built-in functions [highest of NUMBER and NUMBER] and [lowest of NUMBER and NUMBER] yourself, you'd have to tag their parameters with :n to make them work like the AnyDice built-ins work.

…you want to "freeze" the roll so it has a fixed, definite result.

In AnyDice, each dice roll is treated as independent. For example, the following code does not always output zero, but rather outputs the difference between two separate 2d6 rolls:

output 2d6 - 2d6 named "not always zero"

What might be less obvious is that this is also true for dice-valued variables; every time the variable is evaluated, AnyDice treats it as a separate roll:

DICE: 2d6
output DICE - DICE named "still not always zero"

And the same even also true of function parameter that are of the "die" type:

function: diff DICE { result: DICE - DICE }
output [diff 2d6] named "also not always zero"

However, variables and function parameters with numeric or sequence values do have definite values. If you tell AnyDice that your function parameters should be numbers (or sequences), AnyDice will convert any dice rolls to the required type, possibly calling the function multiple times for each possible result of rolling the dice:

function: diff DICE:n { result: DICE - DICE }
output [diff 2d6] named "this is always zero"

…you want to inspect or loop over the specific numbers rolled.

There's only a limited number of (useful) things that AnyDice will let you do with a multi-dice pool such as 3d6 without passing it to a function:

  • You can sum it into a single weighted die. This is what AnyDice automatically does when you output such a value or use it in an arithmetic expression or in a comparison.
  • You can extract the distribution of the N-th highest die (or even the sum of the M-th highest, N-th highest, etc.) in the pool with the @ operator.
  • You can get the number of dice in the pool with the # operator (which is occasionally useful, but not very often).

Meanwhile, things you cannot do include:

  • Counting the number of times a specific result or results was rolled (although there's a built-in function for that).
  • Comparing e.g. the highest and the second-highest result in the roll. (Using e.g. 1@DICE - 2@DICE does not work for that, since as noted above, the two evaluations of DICE are treated as separate rolls!)
  • Looping over the numbers rolled. (If you try to do e.g. loop N over 2d6 { ... }, you'll just get a calculation error saying "A variable must loop over a sequence, but you provided "2d6".")

However, if you pass the dice into a (custom or built-in) function that is defined to take a sequence parameter, AnyDice will call the function once for each possible sequence of results the dice can roll (sorted in descending order by default) and collect the results returned by the function into a custom die weighted by their probability.

And with sequences, you can do a lot more than with dice, including all the things listed above!

For example, let's say you wanted to model a mechanic where the player rolls a number of 6-sided dice and counts the number of sixes plus the number of pairs of dice that sum to six. How would you do that in AnyDice?

Well, the only way is to write a function that takes a sequence parameter, e.g. like this:

function: count ROLL:s {
  result: (ROLL = 6)
    + [lowest of (ROLL = 5) and (ROLL = 1)]
    + [lowest of (ROLL = 4) and (ROLL = 2)]
    + (ROLL = 3) / 2
}

output [count 3d6] named "sixes and pairs summing to six"

Or what about a mechanic where you roll two or more dice and your result is the difference between the lowest and the highest number rolled? Like this:

function: delta ROLL:s {
  result: 1@ROLL - (#ROLL)@ROLL
}

loop N over {2..6} {
  output [delta Nd6] named "[N]d6 highest - lowest"
}

Or how about one where you roll two pools of dice and count the number of matching pairs between the two pools (using each die in only one pair). You can do that like this:

function: match ROLL_A:s and ROLL_B:s {
  COUNT: 0
  DISTINCT: {d ROLL_A}  \ <-- a quick way to remove duplicates from a sequence! \ 
  loop X over DISTINCT {
    COUNT: COUNT + [lowest of (ROLL_A = X) and (ROLL_B = X)]
  }
  result: COUNT
}

output [match 3d6 and 4d6]

None of these mechanics can be modelled in AnyDice, except by using a function that takes (one or more) sequence parameters and passing dice rolls into them, because they all involve either inspecting the results of the roll in ways that AnyDice only supports for sequences, or using the result of a single multiple times in the code (without having it be interpreted as multiple independent rolls), or both.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for a fantastic answer that did all I hoped for and more. \$\endgroup\$ Commented Oct 18, 2023 at 19:22
2
\$\begingroup\$

Dice pools

The major use case for parameters of type sequence are s. For simple success-counting systems you may be able to get away with just relabeling dice and adding them up, but more complex mechanics may benefit from the sequence expansion.

Examples

Such systems include:

What gets expanded into a sorted sequence in AnyDice?

In short, anything in the form of NdX provided as an argument to a parameter of type sequence. N must be an integer, but X is more flexible.

These examples are compatible with sequence expansion:

  • 4d6
  • 6d(3d6): This produces sorted sequences of length 6, where each element is determined using 3d6.
  • 2d[explode d10]: Function calls work too. The length of the sequence is exactly 2; the exploded dice add to the existing dice rather than being counted as separate dice. Note that using more than a few large dice will cause a timeout due to the large number of sequences.

These examples are not compatible with sequence expansion and will behave as if the function was called with a single die representing the sum of the argument, expanding to only a single-element sequence inside the function.

  • (3d6)d6: The left side must be an integer, not a die.
  • [highest 3 of 4d6]: You can't carry around probability distributions over sequences.
  • {1, 2, 3} @ 4d6: Same as previous.
  • 2d6 + 2d8: You can't combine two dice pools together (at least not directly).

How many such sorted sequences are there?

The number of such sorted sequences is

$$ \binom{n + s - 1}{n} $$

where \$n\$ is the number of dice and \$s\$ is the number of sides (unique outcomes) per die. For example, 10d6 has \$\binom{10 + 6 - 1}{10} = 3003\$ possible sequences. AnyDice will take care of weighting the results properly, even if the input distribution is not uniformly weighted. If there are multiple parameters, AnyDice evaluates the Cartesian product, which makes the number of evaluations equal to the product of the number of possibilities for each argument.

Judging from AnyDice's performance characteristics, sequence expansion appears to be the algorithm underpinning much of AnyDice's built-in functionality, such as contains, count, highest, lowest, middle, and the @ operator.

Are there more efficient methods?

The sequence expansion is convenient but potentially computationally expensive. In many cases it's possible to go considerably faster. There are simple algorithms for contains and count (relabel each die to a boolean, then add) and for highest 1 and lowest 1 (exponentiate the cumulative distribution function or its complement) that can easily handle a hundred dice and/or sides.

Even more complicated cases have the potential for greater efficiency. Here's Neon City Overdrive in my own Icepool Python package:

from icepool import multiset_function, d6

@multiset_function
def nco(action, danger):
    highest = (action - danger).highest(1).sum()
    sixes = (action - danger).keep_outcomes([6]).count()
    return highest, sixes

print(nco(d6.pool(6), d6.pool(6)))

Here - is the multiset difference operator, which does the cancellation between the two pools. Thanks to a dynamic programming algorithm, this script can handle much larger pools than AnyDice.

You can try this in your browser here.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Nice answer, and kind of complementary to mine, too. I'd maybe like to quibble a bit about the phrasing of the "what gets expanded into a sorted sequence" section (since the problem with the "not compatible" examples isn't really with "sequence expansion" as such, but simply with the fact that AnyDice has no way to evaluate those expressions in the first place, except by summing them into a single die) but I really can't think of a better way to phrase it either. \$\endgroup\$ Commented Oct 15, 2023 at 20:36
  • \$\begingroup\$ I'm impressed that we managed to address different aspects of the question without seeing each other's answers first; I think your answer is great too. \$\endgroup\$ Commented Oct 15, 2023 at 21:00
  • \$\begingroup\$ As for the phrasing: when I wrote that, I was imagining a hypothetical version of AnyDice that did allow distributions over sequences to be carried around, in which case at least some of the "not compatible" expressions would become compatible. If I understand it correctly, Troll does this, emphasizing multisets ("collections"). For my own Icepool I decided to make dice and pools explicitly separate types. \$\endgroup\$ Commented Oct 15, 2023 at 21:09
  • 1
    \$\begingroup\$ Thank you for an enriching answer. I went with Ilmari's for acceptance, as it addressed more directly what I was looking for, but there is a lot of interesting things to learn here, too. \$\endgroup\$ Commented Oct 18, 2023 at 19:24

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .