This came up in my RSS feed today. The Julia code could do with a makeover for anyone interested.

Thanks, but to others looking at this, note, the Julia code is incorrect. You often donâ€™t see it until you do (and Iâ€™ve only seen a negative value from the Ruby code):

```
$ ~/julia-1.4.0/bin/julia O6.jl
ERROR: LoadError: UndefVarError: t not defined
Stacktrace:
[1] top-level scope at /home/pharaldsson_sym/O6.jl:18
[2] include(::Module, ::String) at ./Base.jl:377
[3] exec_options(::Base.JLOptions) at ./client.jl:288
[4] _start() at ./client.jl:484
in expression starting at /home/pharaldsson_sym/O6.jl:16
```

Maybe helpful to see why, in Julia 1.5 or:

```
$ julia-1.6-DEV O6.jl
â”Ś Warning: Assignment to `m` in soft scope is ambiguous because a global variable by the same name exists: `m` will be treated as a new local. Disambiguate by using `local m` to suppress this warning or `global m` to assign to the existing global variable.
â”” @ ~/O6.jl:8
â”Ś Warning: Assignment to `mark` in soft scope is ambiguous because a global variable by the same name exists: `mark` will be treated as a new local. Disambiguate by using `local mark` to suppress this warning or `global mark` to assign to the existing global variable.
â”” @ ~/O6.jl:9
â”Ś Warning: Assignment to `d` in soft scope is ambiguous because a global variable by the same name exists: `d` will be treated as a new local. Disambiguate by using `local d` to suppress this warning or `global d` to assign to the existing global variable.
â”” @ ~/O6.jl:12
[..]
```

Itâ€™s good to know about the new soft scope rules (that only apply in the REPL, as of 1.5), while Iâ€™ve been using 1.5 (and 1.6) for a while now, Iâ€™m first now seeing these warnings, that Iâ€™m not sure how to switch off (-q doesnâ€™t), nor non-REPL or include(â€śO6.jlâ€ť).

```
D(x) = rand(1:x)
function D(x, roll)
previous_roll = roll
roll = D(x)
return roll, previous_roll
end
function O(x)
main_roll, previous_roll = D(x, 0)
mark = ""
if main_roll == 1
roll, previous_roll = D(x, main_roll)
while roll <= (x Ă· 2)
main_roll -= 1
roll, previous_roll = D(x, roll)
if roll == 1 && previous_roll == 1
mark = " fumble"
end
end
elseif main_roll == x
roll, previous_roll = D(x, main_roll)
while roll >= (x Ă· 2 + 1)
main_roll += 1
roll, previous_roll = D(x, roll)
if roll == x && previous_roll == x
mark = " critical"
end
end
end
println("$(main_roll)$(mark)")
end
```

I think this correctly implements the O(6) roll, but how Julian it is, I donâ€™t really know (Iâ€™m a bit new to the language). Iâ€™m also adding a histogram of the results, ignoring fumbles and crits.

This seems a lot easier (the non-Julia codes are specialized to 6-sided die, which is the only case described in the problem, so it makes sense to specialize the Julia code as well), and you can unify the `6`

and `1`

loops:

```
function O6()
total = rand(1:6)
critical = false
if total == 6 || total == 1
Î” = total == 6 ? 1 : -1
prev = 6
while true
die = rand(1:6)
die â‰¤ 3 && break # 1-3 or 4-6 are equivalent for 6-sided die
critical = critical || (prev == die == 6)
prev = die
total += Î”
end
end
return total, critical ? (total > 6 ? "critical" : "fumble") : ""
end
```

With a little statistics knowledge, you could eliminate the loop entirely and write an expression (probably based on `randexp`

) to directly generate an integer equivalent to the number of consecutive `true`

values from a sequence of `rand(Bool)`

calls.

Yeah that last bit is what Iâ€™m working on now (though I think it might be easier to use rand(Geometric(0.5)). The trick is, I think, accounding for the marks (â€śfumbleâ€ť and â€ścritâ€ť)

Yes, I missed that we needed to track this in my first reading; I amended the code to check for that, but it makes that statistics a lot more complicated to model without a loop.

An entry for fewest lines:

```
julia> D(n) = rand(1:n)
D (generic function with 1 method)
julia> O6() = begin
mark = nothing
s = t = D(6)
s == 1 && while ((tâ€˛, t) = (t, D(6)); t in 1:3)
s -= 1
1 == tâ€˛ == t && (mark = :fumble)
end
s == 6 && while ((tâ€˛, t) = (t, D(6)); t in 4:6)
s += 1
6 == tâ€˛ == t && (mark = :critical)
end
return s, mark
end
O6 (generic function with 1 method)
```

Here is an abbreviated version of your code that merges the two loops:

```
function O6()
s, t, crit = rand(1:6), 1, false
s in (1,6) && while ((tâ€˛, t) = (t, rand(1:6)); t < 4)
s += s â‰Ą 6 ? 1 : -1
crit |= 1 == tâ€˛ == t
end
return s, crit ? (s > 6 ? :critical : :fumble) : nothing
end
```

which is shorter than the Forth code while being (I think) more readable.

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
```

I think I got the loopless version working. The mark probability can be seen as a function of the number of modifying steps. In this way, it is similar to the probability of getting two heads in a row after n throws of an unfair coin (in this case 1/3 heads, 2/3 tails). This probability I took from (https://elec424.fandom.com/wiki/Two_Heads_in_A_Row:_With_Unequal_Probability), although thereâ€™s not a derivation so Iâ€™m not entirely sure itâ€™s correct. Nonetheless, hereâ€™s my revised code.

```
using Distributions
D(x) = rand(1:x)
P(m) = m <= 1 ? 0 : P(m - 1) + (1 - P(m - 1)) / 3 - (1 - P(m - 2)) * (2 / 9)
function O(x)
roll, modifier = D(x), rand(Geometric(0.5))
if (roll == x || roll == 1) && rand() <= P(modifier)
println(roll == x ? "$(roll + modifier) critical" : "$(roll - modifier) fumble")
else
println(roll == x ? roll + modifier : roll == 1 ? roll - modifier : roll)
end
end
```

Unfortunately, itâ€™s still not smaller than the looped version, and it certainly makes it less readable, so not sure if thereâ€™s any real benefit to this version

Thanks. This was really instructive for me. I am new to Julia but attracted to it for various reasons. This thread pushed my motivation to go deeper into Julia. BTW (itâ€™s my GitHub repo - the O6 is a way for me to learn new languages).

Oh, also, just for kicks â€“ hereâ€™s one which is (i) more compact than the Forth example, (ii) arguably much more readable, and (iii) quite fast (BenchmarkTools.jl gives it a median runtime of ~21ns):

```
using MLStyle
O6() = O6(rand(1:6))
O6(r0) = (r0 == 1 || r0 == 6) ? O6(r0, rand(1:6), r0) : (r0, nothing)
O6(r1, r2, result, mark=nothing) = @match (r1, r2) begin
if r1 â‰¤ 3 && r2 â‰¤ 3 end => O6(r2, rand(1:6), result-1, r1 == r2 == 1 ? Some("fumble") : mark)
if r1 â‰Ą 4 && r2 â‰Ą 4 end => O6(r2, rand(1:6), result+1, r1 == r2 == 6 ? Some("critical") : mark)
_ => (result, mark)
end
```

My entry for most compact code. I combined everything into one giant ternary statement, and since that felt like cheating, I split the line for (marginally) easier to read code

```
using Distributions
P(m) = m <= 1 ? 0 : P(m - 1) + (1 - P(m - 1)) / 3 - (1 - P(m - 2)) * (2 / 9)
function O(x)
r, m = rand(1:x), rand(Geometric(0.5))
println(r == x ? (rand() <= P(m) ? "$(r + m) critical" : r + m) :
(r == 1 ? (rand() <= P(m) ? "$(r - m) fumble" : r - m) : r))
end
```

And another version of the same length, but not using any packages

```
rand_geometric(x = 0) = rand(Bool) ? rand_geometric(x + 1) : x
P(m) = m <= 1 ? 0 : P(m - 1) + (1 - P(m - 1)) / 3 - (1 - P(m - 2)) * (2 / 9)
function O(x)
r, m = rand(1:x), rand_geometric()
println(r == x ? (rand() <= P(m) ? "$(r + m) critical" : r + m) :
(r == 1 ? (rand() <= P(m) ? "$(r - m) fumble" : r - m) : r))
end
```

Wonâ€™t the *median* runtime be counting only the cases where the first throw is `2-5`

(because this accounts for more than 50% of the cases and they are the fastest cases)? If so, itâ€™s not a very meaningful metric to look at because it excludes almost all of the code.

On a general note, one can treat all rolls beyond 1 and 6 in the same loop, with a roll of 4-6 adding 1 to an accumulating â€śopenâ€ť score and only afterwards checking if that accumulated open score is to be treated as an addition or subtraction to the original dice roll (if the original dice roll was a 1, subtract the accumulated open score and add if the original roll was a 6). The same goes for Criticals and Fumbles, two consecutive 6s is a Critical if the original roll was a 6 and a Fumble if the original roll was a 1 (but remember to then treat the original roll of 1 as a â€ś6â€ť for this purpose). This is how I wrote the FOCAL program (for the HP-41 calculator).

I like this, this almost reads like Haskell

Conditional on the first draw being `1`

or `6`

, the subsequent sequence length has geometric distribution with p = 1/2, but no simple closed form solution occurs to me for the probability of â€ścriticalâ€ť or â€śfumbleâ€ť (eg conditional on the sequence length).

A standard trick would be formulating it as an absorbing Markov chain with the state being the current roll and whether critical/fumble has happened.

I know - just wanted to make it into a general note.