1. 5

So far it seems to me that Forth is the most compact code, while Vimscript and Fortran are the most verbose, with Julia and Nim perhaps being the most humanly readable - although I prefer to program in Ruby and FOCAL.

  1. 3

    Here’s a solution for Lua.

    local function d6() return math.random(6) end
    
    local function o6()
      local function reroll(acc,target,low,high,delta,result,crit)
        local roll = d6()
        
        if roll == target then
          if crit then
            return result
          else
            return reroll(acc + delta,target,low,high,delta,result,true)
          end
          
        elseif roll >= low and roll <= high then
          return reroll(acc + delta,target,low,high,delta,result,false)
        
        else
          return acc
        end
      end
      
      local roll = d6()
      
      if roll == 1 then
        return reroll(roll,1,1,3,-1,'fumble',true)
      elseif roll == 6 then
        return reroll(roll,6,4,6, 1,'success',true)
      else
        return roll
      end
    end
    

    Since Lua does tail call optimization, the recursion won’t blow out the stack, and it allowed me to write this in a functional style. The function o6() will return either ‘success’ (for a critical success), ‘fumble’ (for a critical failure) or a number.

    1. 2

      I don’t know Lua, but looking at the code, does it return, let’s say “-2 Fumble”, “10” or “8 Critical” (Or ‘success’ as you call it - the reason why it is not called ‘success’ in the Amar RPG is that a successfull roll is when a character’s roll + skill value is equal to or higher than a Difficulty Rating).

      1. 2

        It currently returns either ‘success’ (if two consecutive 6s are rolled), ‘fumble’ (if two consecutive 1s are rolled) or a number otherwise. The description on both the github site, and on the Amar RGP site is ambiguous as to what this means, but I have played a number of other RPGs with the concept of an automatic success or fail upon a roll meeting some criteria. The code can be modified to return both the number and the text (if applicable), but it would be nice to know what the difference between rolling “6 6” and “6 4 4 4 1”—if I calculate correctly, one is a “critical” 12, the other just a 12. Is a “critical” not an automatic success? (and conversely, a “fumble” an automatic failure?)

    2. 3

      I haven’t heard of this problem before. Here’s a quick q-implementation (assuming I’m reading the description of the problem right). For reference, my macbook air i5 gets about 600,000 “open” rolls per second (my Xeon 6234 workstation does about 12 million per second). Single core performance only.

      o6:{$[not (x:1+rand 6) in 1 6;x;(x+count[r]*-1 1@x=6),(`,`fumble`critical x=6)any 6=r 1+where 6=r:ceiling 6*(0.5<)rand\1.]}
      

      Some things that might be interesting:

      not (x:1+rand 6) in 1 6
      

      I check if we’re a 2-5 roll first ,because that’s the shortest path. (return self). Otherwise I have to compute my rolls:

      r:ceiling 6*(0.5<)rand\1.
      

      The instructions were a little strange here. It says if I roll a six, I keep rolling while I get 4,5,6 and if I rolled a one, I keep rolling while I get 1,2,3, but then I only ever use the number of rolls, so I think they want a 50% chance of ending the sequence, so I generate random floats until I get one below 0.5. I then multiply by 6 so I get things that look like dice rolls. Because my sequence starts with 1, the sequence will go 6, roll, roll, roll, …

      any 6=r 1+where 6=r
      

      Did I get any 6s? That’s the 1/6th chance. 6=r returns a bitmap where 1b is the location of a 6. “where” returns the offsets of the 1-bits. I add one to this value and look up those indexes in r. Are any of those also 6?

      (`,`fumble`critical x=6)
      

      This is the two-symbol sequence (blank symbol) or the symbol “fumble” or “critical” if we initially rolled a 1 or a 6. I’m indicating how I’m intending to interpret my “doubles”. Remember in my rolls (r), 1s are 6s.

      (x+count[r]*-1 1@x=6)
      

      The “score” is the first roll, plus the number of rolls (if we first rolled a 6) or minus the number of rolls (if we didn’t).

      1. 1

        Now that was interesting. I updated the description in the Readme after an other reader here pointed out that it was a bit confusing. Check if you also think this is a better way of describing this.

        Now, the reason for the Open-Ended D6 is that when we simplified the MEGA RPG that we published in 1987, we decided to go strictly with D6s. But the range is a bit limiting and we needed criticals and fumbles - and then the O6 was born, and has been with us since the mid-90s.

        1. 2

          That updated README is much easier to understand.

          Sounds like it’s biased towrads 2-5 for the majority of rolls, with higher extents and lower extents being triggered in roughly 1/3rd of rolls skewing high or low.

          1. 1

            Yup. And thanks for the feedback on the readability.

          2. 2

            Showing examples would really help: A few sequences of rolls (and their outcomes) would be really illustrative for people who aren’t exactly sure what “criticals and fumbles” are.

            It might also be fun to think about an O(1) implementation. That is, computing the “score” from a single random number (e.g. chosen from between 0 and 1).

            Most of the decision tree is binary (roll 1 or 6 a certain number of times), so if x>1/3, then (6*x)-1, otherwise if x is halfway between 1/6th and 1/3rd, we choose between fumble or critical, leaving if x <1/6 for the “open-ended” part, which represents 1/6th of the scores – but of course, that “open-ended” part isn’t linear. It is easy to analyse, but in other problems it might not be, so looking at the rolling (off by one because the first roll was a 1 or a 6):

            q)x:(0#0)!(0#0);do[10000000;x[count (0.5<)rand\1.]+:1];{k!x k:asc key x}x
            2 | 5001097
            3 | 3464806
            4 | 1200716
            5 | 277905
            6 | 47839
            7 | 6748
            8 | 787
            9 | 94
            10| 7
            11| 1
            

            we can convert it directly into a function:

            f:12-asc[value 0.1667964*x%sum x] bin
            

            That’s using the same “x” as above, and you should see a perfectly normal distribution even though we’re sampling from a linear distribution. And that’s a real function; We can print it back out:

            q)f
            -[12]bin[`s#2.001557e-05 0.0001050817 0.0007088847 0.003824641 0.01503336 0.0..
            

            and we can use it in our complete, “O(1)” o6 roller:

            o1o6:{floor $[x>1%3;(6*x)-1;x<0.1667964;6-f[x]%2;x<0.25;`fumble;`critical]}
            

            which is about 25% faster on my laptop than the iterative solution.

        2. 2

          It would be interesting to see a Haskell implementation…

          1. 2

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

            1. 1

              Here’s the output of my Perl translation of the Nim code. I’ve run the simulation a number of times and discarded sequences that end after 1 roll (first throw is not a 1 or a 6). Do the results look correct (especially with regards to ‘fumble’ and ‘critical’)?

              https://gist.github.com/gustafe/2e0b6e32da235ce0f5979a3ff720a407