AnyDice handles exploding dice in a peculiar fashion that's poorly-suited for roll-and-count dice pools. I have cobbled together a somewhat inelegant solution, but it appears to work correctly.
Here's the link to the program.
Instead of using dice in the usual manner, I've created several functions that, given parameters of a die, return its output, i.e. the number of successes it has produced.
function: roll ROLL:n threshold T {
if ROLL >=T {result: 1}
result: 0
}
function: rollexploding ROLL:n threshold T{
if ROLL =6 {result: 1+[rollexploding 1d6 threshold T]}
result: [roll ROLL threshold T]
}
function: rerollfailures ROLL:n threshold T{
if ROLL < T {result: [roll 1d6 threshold T]}
result: [roll ROLL threshold T]
}
function: rerollthenexplode ROLL: n threshold T {
if ROLL < T {result: [rollexploding 1d6 threshold T]}
result: [rollexploding ROLL threshold T]
}
Then I made a wrapper function that would figure out which of these functions to call, and handle requests for multiple dice being rolled:
function: wrapper DICE:n threshold T explode E reroll R{
RES:0
loop N over {1..DICE} {
if E & R {RES: RES+[rerollthenexplode 1d6 threshold T]}
else {if E {RES:RES+[rollexploding 1d6 threshold T]}
else {if R {RES:RES+[rerollfailures 1d6 threshold T]}
else {RES:RES+[roll 1d6 threshold T]}
}
}
}
result:RES
}
Finally, I made a bunch of functions that simplify input by providing pre-determined combinations of parameters. I'll provide only 3 of them here:
function: b DICE:n{
result:[wrapper DICE threshold 4 explode 0 reroll 0]
}
function: be DICE:n{
result:[wrapper DICE threshold 4 explode 1 reroll 0]
}
function: gr DICE:n{
result:[wrapper DICE threshold 3 explode 0 reroll 1]
}
To use it, type things like output [b 4]
Doppelgreener's answer is good if the player must always reroll their lowest die, no matter what they originally rolled. However, if using the ability is optional, the player will most likely choose not to use it if they roll, say, two twelves on their first 2d12 roll.
In general, it's hard to model such optional decision-making processes mathematically, since the rationally optimal decision may depend on what the player's specific goal is (not to mention that players are human, and thus often don't act rationally!). However, in this case, a fairly reasonable class of decision-making rules to consider are those where the player rerolls the lowest die only when it's less than some fixed threshold. In fact, if the player's goal is simply to maximize the expected average result of their roll, their optimal strategy is to reroll a die only when the original value of that die is less than the expected average of the reroll (which, for a d12, is (1+12)/2 = 6.5).
Here's a basic AnyDice script to model that decision-making strategy:
function: reroll lowest of ROLL:s as REROLL:d if less than MIN:n {
LOWEST: (#ROLL)@ROLL \ the lowest die is sorted last \
if LOWEST >= MIN {
result: ROLL + 0
} else {
REST: {1..#ROLL-1}@ROLL \ all but the lowest die \
result: REST + REROLL
}
}
output [reroll lowest of 2d12 as d12 if less than 7] named "2d12 replace lowest if < 7"
Note that the function in the code above is generic enough to allow arbitrary initial dice pool sizes (although only the lowest die is ever rerolled) and thresholds, and even provides the option for rerolling with a different die than the original pool had, should that be desired.
Looking at the output of the script, we can see that this "reroll lowest if less than 7" strategy significantly outperforms both "always reroll" and "never reroll":
Of course, we could also consider thresholds other than 7 (≈ 6.5). However, the summary statistics do reveal that, at least as far as the expected average outcome is concerned, 7 is indeed the optimal threshold for rerolling a d12.
All that said, other decision-making rules can still do even better in specific circumstances. For example, if the player is trying to roll to meet or exceed a particular target number, the natural and likely optimal rule is simply to reroll if the sum of their original roll is less than the target, and let the original roll stand otherwise.
Of course, we can model that in AnyDice as well:
function: roll ROLL:s vs TARGET:n with optional REROLL:d reroll {
SUM: ROLL + 0 \ force the sequence to be summed into a single number! \
if SUM >= TARGET {
\ no need to reroll, since we've already met the target \
result: 1
} else {
\ discard and reroll the lowest die \
REST: {1..#ROLL-1}@ROLL
result: REST + REROLL >= TARGET
}
}
loop TARGET over {2 .. 24} {
output 2d12 >= TARGET named "2d12 vs [TARGET]"
output [roll 2d12 vs TARGET with optional d12 reroll] named "2d12 vs [TARGET] with optional reroll"
}
In this case, we unfortunately don't get such nice graphs out, since each comparison against the target number just outputs 0 if the roll fails and 1 if it succeeds. Still, looking at the "transposed" view (which I've linked directly to above), we can see that allowing the reroll is slightly better than granting the player +3 to their roll. For example, an unmodified 2d12 roll has a 61.81% chance of meeting a target of 12, whereas 2d12 with an optional reroll has a 64.53% chance of meeting a target of 15, and a 56.71% chance of meeting a target of 16.
Best Answer
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:
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 anif
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. Soif !(ROLL = 1)
executes the following block only ifROLL
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 aresult:
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 ofd4
and2@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 thatROLL
has only one element. In fact, it still works, though only by a bit of a coincidence. What happens in that case is that2@ROLL
evaluates to 0, since that what AnyDice gives you if you ask for an element past the end of a sequence. And sinced4
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:
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. as1@ROLL >= 3
or even — since having any number in the roll ≥ 3 is equivalent to the highest number being ≥ 3 — asROLL >= 3
. We can't write it asROLL = {3,4}
, though; that's a sequence-to-sequence comparison, and those work differently.)