My entry for “most readable”:
struct Roll{R} end
LowRoll = Union{Roll{1},Roll{2},Roll{3}}
HighRoll = Union{Roll{4},Roll{5},Roll{6}}
roll() = Roll{rand(1:6)}()
O6() = D6(roll())
O6(r0) = (r0 == 1 || r0 == 6) ? D6(r0, roll(), r0, "") : (r0, "")
O6(::LowRoll, ::HighRoll, result, mark) = (result, mark) # stop rolling
O6(::HighRoll, ::LowRoll, result, mark) = (result, mark) # stop rolling
O6(::Roll{1}, ::Roll{1}, result, mark) = D6(Roll{1}(), roll(), result-1, "fumble")
O6(::Roll{6}, ::Roll{6}, result, mark) = D6(Roll{6}(), roll(), result+1, "critical")
O6(::LowRoll, r::LowRoll, result, mark) = D6(r, roll(), result-1, mark)
O6(::HighRoll, r::HighRoll, result, mark) = D6(r, roll(), result+1, mark)
Obviously that’s painfully slow. Assuming external packages are allowed, this one is much faster while still being quite readable:
using MLStyle
roll() = rand(1:6)
@active LowRoll(roll) roll ≤ 3
@active HighRoll(roll) roll ≥ 4
O6() = O6(roll())
O6(first_roll) = @match first_roll begin
1 || 6 => O6(first_roll, roll(), first_roll)
_ => (first_roll, nothing)
end
O6(r1, r2, result, mark=nothing) = @match (r1, r2) begin
(1, 1) => O6(1, roll(), result-1, Some("fumble"))
(6, 6) => O6(6, roll(), result+1, Some("critical"))
(LowRoll(), LowRoll()) => O6(r2, roll(), result-1, mark)
(HighRoll(), HighRoll()) => O6(r2, roll(), result+1, mark)
_ => (result, mark)
end