8
\$\begingroup\$

The system uses a dice pool of d4s of up to 3 dice (3d4 is the maximum number of dice you can roll). If at least a 4 comes up in the pool, that's a success. If the highest number is a 3, it's a partial success. If the highest is lower than 3 it's a failure.

Output [count {3..4} in 3d4]

I was able to calculate that with ease, but here's the part I am stuck with: If you get any number of 1s in your pool, the highest die is rerolled if it's a 3 or a 4. The second result is kept. Examples:

  • You roll 2d4 and get 4, 1. Since you got a 1, you reroll the 4 and keep the second result. We get a 3, which means the result is a partial success instead of a success.

  • You roll 3d4 and get 3, 1, 1. Since there's a 1, you reroll the 3 and get 4. Lucky you, you got a success instead of a partial.

How can I code this in AnyDice to accurately calculate a player's chance of success, partial success, or failure?

\$\endgroup\$
3
  • 2
    \$\begingroup\$ Welcome to RPG.SE! Take the tour if you haven't already and see the help center or ask us here in the comments (use @ to ping someone) if you need more guidance. Good Luck and Happy Gaming! \$\endgroup\$
    – Someone_Evil
    Sep 24 at 19:45
  • 1
    \$\begingroup\$ Are you rerolling these dice because it's fun to reroll on a crit fail or are you trying to model an actual set of probabilities? \$\endgroup\$
    – StuperUser
    Sep 24 at 22:37
  • 1
    \$\begingroup\$ @StuperUser I came up with that mechanic because the chances of succeeding when rolling 3d4 was too high (87.5%), and I needed to find a way to lower those chances down a bit. I must admit it is not an elegant solution. \$\endgroup\$ Sep 26 at 15:27
12
\$\begingroup\$

You need a function, but no loops :)

As Someone_Evil correctly notes, the way to do any non-trivial inspection and manipulation of the results of a roll in AnyDice is to pass that roll into a function expecting a sequence (i.e. a parameter tagged with :s). When you do that, what AnyDice does is it calls the function for every possible (sorted) outcome of the roll and collects the results, weighted by their probability, into a new custom die.

So what should your function look like? Something like this (modulo a misreading of the question; see correction below), for example:

function: test ROLL:s {
  if !(ROLL = 1) { result: 1@ROLL }
  result: [highest of d4 and 2@ROLL]
}

output [test 3d4]

OK, let's unpack that a bit.

First of all, ROLL = 1 compares a number (1) with a sequence (ROLL), returning the number of values in the sequence that match. Normally an if statement in AnyDice treats 0 as false and anything non-zero as true, but the ! operator negates that, turning 0 to 1 and anything else to 0. So if !(ROLL = 1) executes the following block only if ROLL contains no ones.

Inside the block, we just return the first element of the sequence ROLL. We know the sequence is sorted in descending order (because that's what AnyDice does when you convert a die to a sequence in a function call), so the first element is the highest. Note that AnyDice will stop running the function as soon as it sees a result: statement, so the second line of code in the function will not be run in this case.

If ROLL includes some ones, however, the !(ROLL = 1) expression will be false and the block after it will not run, so AnyDice instead continues on to the second line. In this case, we return the highest of d4 and 2@ROLL, which is the second-highest value originally rolled.

Why those? Well, imagine what you'd do when rolling the dice by hand. You'd first roll them all, and then observe that you indeed rolled a 1. Now you can safely set aside all but the highest two rolled dice, since those can never be the final result. Then you reroll the higher of the remaining two dice, and take the highest of the two after that.

And that's what the code does.

(BTW, you might be wondering what happens if you call the function above with 1d4, so that ROLL has only one element. In fact, it still works, though only by a bit of a coincidence. What happens in that case is that 2@ROLL evaluates to 0, since that what AnyDice gives you if you ask for an element past the end of a sequence. And since d4 is always higher than 0, the function just ends up rerolling the single die if it's originally a 1.)


Correction: When writing the answer above, I completely missed the latter part of the "the highest die is rerolled if it's a 3 or a 4" rule, so the code above doesn't implement it. We can fix it easily, though:

function: test ROLL:s {
  if !(ROLL = 1 & 1@ROLL = {3,4}) { result: 1@ROLL }
  result: [highest of d4 and 2@ROLL]
}

This version runs the { result: 1@ROLL } block unless the roll contains any ones and the first (i.e. highest) number in the roll is 3 or 4.

(1@ROLL = {3,4} is a fairly literal translation of the "it's a 3 or a 4" rule. Of course we could've euivalently written it e.g. as 1@ROLL >= 3 or even — since having any number in the roll ≥ 3 is equivalent to the highest number being ≥ 3 — as ROLL >= 3. We can't write it as ROLL = {3,4}, though; that's a sequence-to-sequence comparison, and those work differently.)

\$\endgroup\$
5
  • \$\begingroup\$ Thank you ilmari-karonen and @someone-evil for the taking the time to answer and provide a breakdown of the coding process. This will help me to better understand how to do complex functions in the future. \$\endgroup\$ Sep 25 at 2:44
  • 1
    \$\begingroup\$ The question mentioned 'the highest die is rerolled if it's a 3 or a 4' which I don't think is accounted for here. e.g. if you roll [2, 2, 1, 1] in the first roll, I think this function will effectively reroll one of the 2s, and might get a 3 or a 4, turning a fail into a (partial) success! But my reading of the description in the question suggests that you'd in fact keep the 2 (fail). Does that sound right? \$\endgroup\$
    – Oly
    Sep 25 at 9:43
  • 1
    \$\begingroup\$ Perhaps the first block should have altered condition? if (!(ROLL = 1) | !(ROLL = 3) | !(ROLL = 4)) { result: 1@ROLL } \$\endgroup\$
    – Oly
    Sep 25 at 9:45
  • 1
    \$\begingroup\$ @Oly: Thanks for pointing that out. I've added a correction to my answer. \$\endgroup\$ Sep 25 at 12:03
  • \$\begingroup\$ Thank you for the correction, good ser! \$\endgroup\$ Sep 26 at 15:28
5
\$\begingroup\$

You need functions and loops

Whenever you want to be doing manipulations based on individual rolls in a dice pool, you're gonna want to use a function and cast the roll to a sequence. The latter is done by appending :s on the variable definition.

We can then do a couple of different tricks. Firstly, dice pools cast to sequences will be sorted depending, so if there is a 1, it will be in the last position. So we can just test whether that's a one for whether to reroll. Anydice isn't a fan of cutting or making replacements in sequences, so we need to do a loop to build a new sequence as our resulting sequence. (You can do it simpler if you make the function simpler if you strictly built it for grabbing the highest and/or for only working with up to 3 dice. This version is more general.)

function: poolroll A:s with B:n {
   if #A@A = 1 {
      D: {}
      loop N over {2..#A} {
          D: {D, N@A}
      }
      D: [sort {D, B}]
   } else {D: A}
   result: 1@D
}
output [poolroll 3d4 with 1d4]

Since we only care about the highest result, we grab that as the result. We then get the distribution of result, where 4 is success, 3 is partial and 1 and 2 are failures.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ That code might be more readable if you wrapped the sequence manipulation in a helper function, or used something like the generic one from this answer. \$\endgroup\$ Sep 24 at 23:01

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.