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.)