9
\$\begingroup\$

I'm attempting to find the damage distribution (given a target with stats) of a series of two 5e attacks, mind sliver and then two melf's minute meteors. But it seems I'm having trouble modeling dice rolls that have other dice rolls that they are dependent on. For instance:

function: MOD saving throw dc DC {
 result: d20+MOD < DC
} 

function: true or half SUCCESS DAMAGE {
 result: DAMAGE / (2 - SUCCESS)
}

function: true or none SUCCESS DAMAGE {
 result: SUCCESSdDAMAGE
}

TARGET_INT:1
TARGET_DEX:1
SPELL_SAVE:15
CANTRIP_DICE:2
INT_FAIL_CHANCE:[TARGET_INT saving throw dc SPELL_SAVE]
DEX_FAIL_CHANCE:[TARGET_DEX saving throw dc SPELL_SAVE]
SLIVER_DEX_FAIL_CHANCE:[TARGET_DEX-1d4 saving throw dc SPELL_SAVE]

ONE_METEOR: [true or half DEX_FAIL_CHANCE 2d6]
TWO_METEOR: ONE_METEOR + ONE_METEOR
ONE_SLIVERED_METEOR:[true or half SLIVER_DEX_FAIL_CHANCE 2d6]
TWO_METEOR_SLIVERED:ONE_SLIVERED_METEOR + ONE_METEOR
MIND_SLIVER: [true or none INT_FAIL_CHANCE CANTRIP_DICEd6]

function: sliver meteor {
 if INT_FAIL_CHANCE > 0 {
  result: CANTRIP_DICEd6 + TWO_METEOR_SLIVERED
 } else {
  result: TWO_METEOR
 }
}

output [sliver meteor]

At the very end, I need to use a slightly different "hit chance" if mind sliver "hit" the target before the first meteor. The only way I could imagine this working is if it branched on success or fail of the first roll, or mapped different outcomes (0 or 1) to different dice roll values. But it seems anydice is unable to do this. Although I might be missing some contrived solution that technically makes this possible.

The above code results in this error:

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

So it seems like the function conditional code doesn't simulate dice rolls and retabulate in the way I was expecting. Though this type of calculation would seem to fit right in with anydice, I just can't seem to figure out how to implement it with the language.

It's hard to know exactly how things are implemented under the hood, but it seems like maybe this would work as a language extension:

CONDITIONAL ? EXPRESSION_TRUE : EXPRESSION_FALSE

Where the conditional needs to be a boolean value or a 0-1 dice roll, and the result would be similar to an existing valid anydice expression in the dice case:

CONDITIONALdEXPR_ONE + (1-CONDITIONAL)dEXPR_TWO

Except it would take into account that the rolls are dependent rather than independent. Although to be fair, these might be equivalent by some statistical reasoning that currently escapes me.

So, how do I do this with the current version of anydice?

EDIT: Now that I know the answer, I can say with confidence that the independent and dependent versions of the resulting distribution vary a lot, most notably with the fact that treating the first roll as two independent rolls has a decent chance of being 0, whereas the dependent version has no chance of ever being zero. Which makes sense, if no term gets "selected".

\$\endgroup\$
3

4 Answers 4

7
\$\begingroup\$

You're trying to do a conditional on a value which is a die, which anydice doesn't like. In order to do a conditional comparison on the result of a die, you have to instead cast it to a number in the function definition. Try changing the last part of your program:

function: sliver meteor FAIL:n {
 if FAIL > 0 {
  result: CANTRIP_DICEd6 + TWO_METEOR_SLIVERED
 } else {
  result: TWO_METEOR
 }
}

output [sliver meteor INT_FAIL_CHANCE]

The FAIL:n parameter in the definition indicates that a number value is expected, and as per the documentation on functions:

If a sequence is provided, then the sequence will be summed. If a die is provided, then the function will be invoked for all numbers on the die – or the sums of a collection of dice – and the result will be a new die.

So rather than referring to the INT_FAIL_CHANCE external variable, provide it as a parameter to the function converted to a number, which anydice will then accept.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Ah, there's the tidbit of functionality I wasn't privy to. I guess my previous code only would've worked if there was a bit more static analysis to determine the best type for the argument when left unspecified. The error message makes sense in retrospect, but doesn't seem sufficient to lead one to the exact knowledge needed. \$\endgroup\$
    – brubsby
    Apr 24, 2022 at 18:15
5
\$\begingroup\$

If you care about the specific roll, pass it to a function

INT_FAIL_CHANCE is a die when it is being used in a boolean expression, which is what AnyDice is complaining about. The fix is to pass that die to a function as a number. That function will then be run for each possible outcome on the die (which is what we want to get a distribution). We force a function parameter to be a number by appending :n. Applying that change to the sliver meteor function we get:

function: sliver meteor ROLL:n{
 if ROLL > 0 {
  result: CANTRIP_DICEd6 + TWO_METEOR_SLIVERED
 } else {
  result: TWO_METEOR
 }
}

output [sliver meteor INT_FAIL_CHANCE]

Which produces:

enter image description here

Which is identical to what I got with a different approach. That approach is perhaps more linear in a approach (and probably less efficient), but the agreement between methods is promising.

\$\endgroup\$
4
\$\begingroup\$

It's hard to know exactly how things are implemented under the hood, but it seems like maybe this would work as a language extension:

CONDITIONAL ? EXPRESSION_TRUE : EXPRESSION_FALSE

AnyDice doesn't currently support such a ternary conditional operator, but in theory a future version of AnyDice could add such an operator without any major difficulty.*

In the mean time however, as I noted in my answer to an earlier question here, you can define a custom function that works the same way:

function: if CONDITION:n then A else B {
  if CONDITION { result: A } else { result: B }
}

FOO: [if d2 = 1 then 42d6 else 23 + 17d8]

Note that, for the function to work as expected, the CONDITION argument must be coerced into a number by tagging it with :n in the function definition. If (an expression that evaluates into) a die is passed to the function as the condition, this causes AnyDice to evaluate the function for each possible value of the die and collect the returned results (which in this case are just the other two arguments) into a custom die.

On the other hand, the A and B arguments are untyped, so AnyDice will pass their values to the function unchanged. (However, since AnyDice doesn't support sequence-valued dice, combining the results of the function into a die may still cause any sequence values passed in as those arguments to be summed.)

Also note that, for optimal performance, the CONDITION argument should always be a "boolean die", i.e. a die that can roll at most one non-zero value. That's because AnyDice isn't smart enough to realize that all non-zero CONDITION values yield identical results for this function, and will waste time evaluating the function for each of them. Fortunately, these kinds of "boolean dice" are exactly what AnyDice's comparison and boolean operators (=, !=, <, <=, >, >=, &, |, !) return when applied to dice, so they'll work fine with this function.


*) How do I know this? Because my (alas still unfinished) reimplementation of AnyDice in JavaScript includes it as a syntax extension, using exactly the syntax suggested by the OP. Conveniently, it doesn't interfere with any existing AnyDice syntax in any way, and the implementation is straightforward — just like the example function above.

\$\endgroup\$
4
  • \$\begingroup\$ Hah, I too have an unfinished javascript implementation of anydice, although you seem to be further along than me (I only have dice arithmetic implemented fully). Care to share a link? \$\endgroup\$
    – brubsby
    Apr 24, 2022 at 21:36
  • \$\begingroup\$ I also (after seeing other solutions but before this one) implemented a similar function, but with the python ternary syntax: ``` function: TRUE_EXPR if COND:n else FALSE_EXPR { if COND > 0 { result: TRUE_EXPR } else { result: FALSE_EXPR } } SLIVER_METEOR: [SLIVER_SUCCESS_INTO_METEORS if INT_FAIL_CHANCE else TWO_METEOR] ``` \$\endgroup\$
    – brubsby
    Apr 24, 2022 at 21:38
  • \$\begingroup\$ @brubsby: It's all in a local git repo or two that I never uploaded anywhere, and the only part that actually works at all is the parser. Everything else is just full of TODOs and missing pieces. But I can try to tidy it up a bit and upload it to GitHub. Maybe we can combine our half-finished projects into something that's actually usable. :D \$\endgroup\$ Apr 24, 2022 at 21:42
  • \$\begingroup\$ Haha, well I wrote the calculation logic into the parser, so that'll have to be untangled first, but here's the most up to date version of my peggy grammar at the time of this writing: github.com/brubsby/brubsby.github.io/blob/… \$\endgroup\$
    – brubsby
    Apr 24, 2022 at 22:26
0
\$\begingroup\$

With the algorithm provided, the error is coming from the function sliver_meteor. The condition INT_FAIL_CHANCE > 0 is a sequence variable compared to a number. If I plug in 1 or 0 into that condition instead, I get results without a problem.

If I bypass the normal output using that function and output INT_FAIL_CHANCE, it gives my a graph of 0 with 35% and 1 with 65%.

Looks like you want a loop:

function: sliver meteor { loop N over {INT_FAIL_CHANCE}{ if N { result: CANTRIP_DICEd6 + TWO_METEOR_SLIVERED } else { result: TWO_METEOR } } }

\$\endgroup\$

You must log in to answer this question.

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