[QUIZ] Dice Roller (#61)

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

···

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Matthew D Moss

Time to release your inner nerd.

The task for this Ruby Quiz is to write a dice roller. You should write a
program that takes two arguments: a dice expression followed by the number of
times to roll it (being optional, with a default of 1). So to calculate those
stats for your AD&D character, you would do this:

  > roll.rb "3d6" 6
  72 64 113 33 78 82

Or, for something more complicated:

  > roll.rb "(5d5-4)d(16/d4)+3"
  31

[NOTE: You'll usually want quotes around the dice expression to hide parenthesis
from the shell, but the quotes are not part of the expression.]

The main code of roll.rb should look something like this:

  d = Dice.new(ARGV[0])
  (ARGV[1] || 1).to_i.times { print "#{d.roll} " }

The meat of this quiz is going to be parsing the dice expression (i.e.,
implementing Dice.new). Let's first go over the grammar, which I present in a
simplified BNF notation with some notes:

  <expr> := <expr> + <expr>
         > <expr> - <expr>
         > <expr> * <expr>
         > <expr> / <expr>
         > ( <expr> )
         > [<expr>] d <expr>
         > integer
  
  * Integers are positive; never zero, never negative.
  * The "d" (dice) expression XdY rolls a Y-sided die (numbered
    from 1 to Y) X times, accumulating the results. X is optional
    and defaults to 1.
  * All binary operators are left-associative.
  * Operator precedence:
            ( ) highest
            d
            * /
            + - lowest

[NOTE: The BNF above is simplified here for clarity and space. If requested, I
will make available the full BNF description I've used in my own solution, which
incorporates the association and precedence rules.]

A few more things... Feel free to either craft this by hand or an available
lexing/parsing library. Handling whitespace between integers and operators is
nice. Some game systems use d100 quite often, and may abbreviate it as "d%"
(but note that '%' is only allowed immediately after a 'd').

[...]
} [NOTE: The BNF above is simplified here for clarity and space. If
} requested, I will make available the full BNF description I've used in my
} own solution, which incorporates the association and precedence rules.]

I would appreciate the full BNF, please.

--Greg

···

On Sat, Jan 07, 2006 at 03:56:47AM +0900, Ruby Quiz wrote:

I assume integer arithmetic? So if, for example, a 3 comes up on your
d4, 16/d4 would be 5?

Jacob Fugal

···

On 1/6/06, Ruby Quiz <james@grayproductions.net> wrote:

Or, for something more complicated:

        > roll.rb "(5d5-4)d(16/d4)+3"
        31

Please don't take this the wrong way, but I've never played D&D. Would someone mind explaining the math that went into the command below to generate it's result?

~ ryan ~

···

On Jan 6, 2006, at 1:56 PM, Ruby Quiz wrote:

Time to release your inner nerd.

The task for this Ruby Quiz is to write a dice roller...

> roll.rb "3d6" 6
72 64 113 33 78 82

Well, I'm no D&Der, but I think I'm gonna hand in my solution for this one as my first Ruby Quiz entry :slight_smile:

Cheers,

···

On Fri, 06 Jan 2006 18:56:47 -0000, Ruby Quiz <james@grayproductions.net> wrote:

Time to release your inner nerd.

The task for this Ruby Quiz is to write a dice roller.

--
Ross Bamford - rosco@roscopeco.remove.co.uk

The task for this Ruby Quiz is to write a dice roller. You should write a
program that takes two arguments: a dice expression followed by the number of
times to roll it (being optional, with a default of 1). So to calculate those
stats for your AD&D character, you would do this:

  > roll.rb "3d6" 6
  72 64 113 33 78 82

Ok, I'm still a little confused. This should have output something like:

   rand(16)+3 rand(16)+3 rand(16)+3

Or, for something more complicated:

  > roll.rb "(5d5-4)d(16/d4)+3"
  31

What is the -4 and the /d4 do?

Does the +3 apply to (5d5-4)d(16/d4) or to (16/d4) only, assuming it matters
since I don't know what this stuff does.

A few more things... Feel free to either craft this by hand or an available
lexing/parsing library. Handling whitespace between integers and operators is
nice. Some game systems use d100 quite often, and may abbreviate it as "d%"
(but note that '%' is only allowed immediately after a 'd').

So d100 == d% == d00

and

100 == 00

correct?

···

On Jan 6, 2006, at 12:56 PM, Ruby Quiz wrote:

Ruby Quiz schrieb:

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Huhu.
How do you parse 5d6d7?
As (5d6)d7 or 5d(6d7) since there is no "Assoziativgesetz" like (AdB)dC == Ad(BdC).

···

-
aTdHvAaNnKcSe

In article <20060106185354.LPAB613.centrmmtao03.cox.net@localhost.localdomain>,
  Ruby Quiz <james@grayproductions.net> writes:

Or, for something more complicated:

> roll.rb "(5d5-4)d(16/d4)+3"
  31

What's the execution order in this case?
Do 5d5-4 rolls with 5d5-4 probably different dices having 16/d4 sides
(number of sides calculated for each roll individually) or should one
choose the number of sides once for all rolls?
I guess it doesn't make much difference but it should be specified...

Morus

Hehe, sorry, it's too early. I'm just happy because I finally
finished a ruby quiz.

Cya in 40 hours or so... (and hope I'm really computing non-fubar'd
results... :slight_smile:

Regards,

Bill

Wow... You guys are just having too much fun with
"(5d5-4)d(16/d4)+3", I think. Heck, if y'all know Ruby so well (more
than me, cause I'm still such a n00b), you'd be able to swap in loaded
dice for random dice and get your min's and max's. =)

Anyway, I just wanted to add a couple of notes. It was made aware to
me that the simplified BNF (in the original post) is slightly in
error, in that it allows expressions like this:

   3dddd6

which is invalid.

A couple possible fixes:

1. Use the expanded BNF I posted, which doesn't have this fault.
2. Implement your dice parser using right-associativity for 'd'
operators (but maintain left-assoc for the other binary operators).

Feel free to do what you like.

Attached is my submission. It looks pretty cool to me, but then this is only my second-ever Ruby program.

Meta-comment: if [QUIZ] opens the quiz, then surely [/QUIZ] should close it.

Luke Blanshard

roll.rb (4.29 KB)

Hi,

I finally finished a Ruby Quiz! Albeit by means of a goofy
method_missing hack. <grin> But it was fun.

Here 'tis:

61_dice_roller.rb (835 Bytes)

···

------------------------------------------------------------------

#!/usr/bin/env ruby

expr = ARGV[0] || abort('Please specify expression, such as "(5d5-4)d(16/d4)+3"')
expr = expr.dup # unfreeze

class Object
  def method_missing(name, *args)
    # Intercept dieroll-method calls, like :_5d5, and compute
    # their value:
    if name.to_s =~ /^_(\d*)d(\d+)$/
      rolls = [1, $1.to_i].max
      nsides = $2.to_i
      (1..rolls).inject(0) {|sum,n| sum + (rand(nsides) + 1)}
    else
      raise NameError, [name, *args].inspect
    end
  end
end

class String
  def die_to_meth
    # Prepend underscore to die specs, like (5d5-4) -> (_5d5-4)
    # making them grist for our method_missing mill:
    self.gsub(/\b([0-9]*d[0-9]*)\b/, '_\1')
  end
end

expr.gsub!(/d%/,"d100") # d% support
# inner->outer reduce
true while expr.gsub!(/\(([^()]*)\)/) {eval($1.die_to_meth)}
p eval(expr.die_to_meth)

Ruby Quiz wrote:

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Matthew D Moss

Time to release your inner nerd.

The task for this Ruby Quiz is to write a dice roller. You should write a
program that takes two arguments: a dice expression followed by the number of
times to roll it (being optional, with a default of 1). So to calculate those
stats for your AD&D character, you would do this:

  > roll.rb "3d6" 6
  72 64 113 33 78 82

Or, for something more complicated:

  > roll.rb "(5d5-4)d(16/d4)+3"
  31

My submission isn't going to win points for brevity - at 600+ lines it's
maybe a bit long to post here.

It's got a few extras in there, though:

$ ./dice.rb "(5d5-4)d(16/d4)+3"
45

$ ./dice.rb "3d6" 6
11 7 10 13 9 14

$ ./dice.rb -dist "2d5 + 1dd12"
Distribution:
3 0.0103440355940356
4 0.0276987734487735
5 0.0503975468975469
6 0.0773292448292448
7 0.107660533910534
8 0.120036676286676
9 0.120568783068783
10 0.112113997113997
11 0.096477873977874
12 0.07495670995671
13 0.0588945406445407
14 0.0457661135161135
15 0.0345793650793651
16 0.0250565175565176
17 0.0171049783549784
18 0.0107247474747475
19 0.00596632996632997
20 0.00290909090909091
21 0.00113636363636364
22 0.000277777777777778
Check total: 1.0
Mean 9.75 std. dev 3.37782803325187

$ ./dice.rb -cheat "2d5 + 1dd12" 19
19 : D5=2 D5=5 D12=12 D12=12 p=0.000277777777777778

$ ./dice.rb -cheat "2d5 + 1dd12" 25
Cannot get 25

I've shoved it on http://homepage.ntlworld.com/a.mcguinness/files/dice.rb

···

--
Andrew McGuinness
http://anomalyuk.blogspot.com/

Hi,

This is my quiz entry for Ruby Quiz 61 (Dice Roller). It's actually the
second idea I had, after starting out with Antlr (I still finished that
one, because I wanted to get to grips with Antlr anyway - I happened to
be playing with it when this quiz came out :)). I've bundled both this
entry and that one at:

   http://roscopeco.co.uk/code/ruby-quiz-entries/quiz61-dice-roller.tar.gz

Anyway, back to my real entry. I guess I took the short-cut route to
the dice-roller, and instead of parsing out the expressions I instead
decided to 'coerce' them to Ruby code, by just implementing the 'd'
operator with a 'rolls' method on Fixnum, and using gsub to convert
the input expression.

   d3*2 => 1.rolls(3)*2
   (5d5-4)d(16/d4)+3 => (5.rolls(5)-4).rolls(16/1.rolls(4))+3
   d%*7 => 1.rolls(100)*7

This is implemented in the DiceRoller.parse method, which returns the
string. You can just 'eval' this of course, or use the 'roll' method
(also provided as a more convenient class method that wraps the whole
thing up for you) to do it. Ruby runs the expression, and gives back
the result. I almost feel like I cheated...?

As well as the main 'roll.rb' I also included a separate utility that
uses loaded dice to find min/max achievable. All three files can be
executed, and if you enable --verbose mode on Ruby you'll see the
dice rolls and parsed expressions.

----------[MAIN (roll.rb)]-----------
#!/usr/local/bin/ruby

···

#
# Ruby Quiz 61, the quick way
# by Ross Bamford

# Just a debugging helper
module Kernel
   def dbg(*s)
     puts(*s) if $VERBOSE|| @dice_debug
   end
   attr_writer :dice_debug
   def dice_debug?; @dice_debug; end
end

# Need to implement the 'rolls' method. Wish it didn't have to
# be on Fixnum but for this it makes the parsing *lots* easier.
class Fixnum
   def self.roll_proc=(blk)
     @roll_proc = blk
   end

  def self.roll_proc
     @roll_proc ||= method(:rand).to_proc
   end

   def rolls(sides)
     (1..self).inject(0) { |s,v| s + Fixnum.roll_proc[sides] }
   end
end

# Here's the roller.
class DiceRoller
   class << self
     # Completely wrap up a roll
     def roll(expr, count = 1, debug = false)
       new(expr,debug).roll(count)
     end

     # The main 'parse' method. Just really coerces the code to Ruby
     # and then compiles to a block that returns the result.
     def parse(expr)
       # very general check here. Will pass lots of invalid syntax,
       # but hopefully that won't compile later. This removes the
       # possibility of using variables and the like, but that wasn't
       # required anyway. The regexps would be a bit more difficult
       # if we wanted to do that.
       raise SyntaxError, "'#{expr}' is not a valid dice expression", [] if expr =~ /[^d\d\(\)\+\-\*\/\%]|[^d]%|d-|\*\*/

       # Rubify!
       s = expr.gsub( /([^\d\)])d|^d/, '\11d') # fix e.g. 'd5' and '33+d3' to '1.d5' and '33+1d3'
       s.gsub!( /d%/, 'd(100)' ) # fix e.g. 'd%' to 'd(100)'
       s.gsub!( /d([\+\-]?\d+)/, '.rolls(\1)') # fix e.g. '3d8' to '3.rolls(8) (*)
       s.gsub!( /d\(/, '.rolls(') # fix e.g. '2d(5+5)' to '2.rolls(5+5)'

       # (*) This line treats + or - straight after 'd' as a unary sign,
       # so you can have '3d-8*7' => '3.rolls(+8)-7'
       # This would throw a runtime error from rolls, though.

       # Make a block. Doing it this way gets Ruby to compile it now
       # so we'll reliably get fail fast on bad syntax.
       dbg "PARS: #{expr} => #{s}"
       begin
         eval("lambda { #{s} }")
       rescue Exception => ex
         raise SyntaxError, "#{expr} is not a valid dice expression", []
       end
     end
   end

   # Create a new roller that rolls the specified dice expression
   def initialize(expr, debug = false)
     dbg "NEW : #{to_s}: #{expr} => #{expr_code}"
     @expr_code, @expr, @debug = expr, DiceRoller.parse(expr), debug
   end

   # Get hold of the original expression and compiled block, respectively
   attr_reader :expr_code, :expr

   # Roll this roller count times
   def roll(count = 1)
     dbg " ROLL: #{to_s}: #{count} times"
     r = (1..count).inject([]) do |totals,v|
       this_r = begin
         expr.call
       rescue Exception => ex
         raise RuntimeError, "'#{expr_code}' raised: #{ex}", []
       end

       dbg " r#{v}: rolled #{this_r}"
       totals << this_r
     end

     r.length < 2 ? r[0] : r
   end
end

# Library usage:
#
# require 'roll'
#
# # is the default:
# # Fixnum.roll_proc = lambda { |sides| rand(sides) + 1 }
#
# DiceRoller.roll('1+2*d6')
#
# d = DiceRoller.new('((3d%)+8*(d(5*5)))')
# d.roll(5)
#
# d = DiceRoller.new('45*10d3') # debug
#
# # ... or
# one_roll = d.expr.call
#

# command-line usage
if $0 == __FILE__
   unless expr = ARGV[0]
     puts "Usage: ruby [--verbose] roll.rb expr [count]"
   else
     (ARGV[1] || 1).to_i.times { print "#{DiceRoller.roll(expr)} " }
     print "\n"
   end
end

-----------[UTIL: minmax.rb]----------
#!/usr/local/bin/ruby

require 'roll'

LOW_DICE = lambda { |sides| 1 }
HIGH_DICE = lambda { |sides| sides }

# Adds a 'minmax' method that uses loaded dice to find
# min/max achievable for a given expression.
#
# Obviously not thread safe, but then neither is the
# whole thing ;D
class DiceRoller
   def self.minmax(expr)
     old_proc = Fixnum.roll_proc
     Fixnum.roll_proc = LOW_DICE
     low = DiceRoller.roll(expr)

     Fixnum.roll_proc = HIGH_DICE
     high = DiceRoller.roll(expr)
     Fixnum.roll_proc = old_proc

     [low,high]
   end
end

if $0 == __FILE__
   if expr = ARGV[0]
     min, max = DiceRoller.minmax(expr)
     puts "Expression: #{expr} ; min / max = #{min} / #{max}"
   else
     puts "Usage: minmax.rb <expr>"
   end
end

-----------[TEST: test.rb]----------
#!/usr/local/bin/ruby
#
# Ruby Quiz, number 61 - Dice roller
# This entry by Ross Bamford (rosco<at>roscopeco.co.uk)

require 'test/unit'
require 'roll'

ASSERTS = {
   '1' => 1,
   '1+2' => 3,
   '1+3*4' => 13,
   '1*2+4/8-1' => 1,
   'd1' => 1,
   '1d1' => 1,
   'd10' => 10,
   '1d10' => 10,
   '10d10' => 100,
   'd3*2' => 6,
   '5d6d7' => 210, # left assoc
   '2d3+8' => 14, # not 22
   '(2d(3+8))' => 22, # not 14
   'd3+d3' => 6,
   '33+d3+10' => 46,
   'd2*2d4' => 16,
   'd(2*2)+d4' => 8,
   'd%' => 100,
   '2d%' => 200,
   'd%*7' => 700,
   '14+3*10d2' => 74,
   '(5d5-4)d(16/d4)+3' => 87, #25d4 + 3
   '3d+8/8' => 3 #3d(+8)/8
}

ERRORS = {

   # Bad input, all should raise exception
   'd' => SyntaxError,
   '3d' => SyntaxError,
   '3d-8' => SyntaxError, # - # of sides
   '3ddd6' => SyntaxError,
   '3%2' => SyntaxError,
   '%d' => SyntaxError,
   '+' => SyntaxError,
   '4**3' => SyntaxError
}

# bit messy, but can't get class methods on Fixnum
Fixnum.roll_proc = lambda { |sides| sides }

class TestDiceRoller < Test::Unit::TestCase
   def initialize(*args)
     super
   end

   ASSERTS.each do |expr, expect|
     eval <<-EOC
       def test_good_#{expr.hash.abs}
         expr, expect = #{expr.inspect}, #{expect.inspect}
         puts "\n-----------------------\n\#{expr} => \#{expect}" if $VERBOSE
         res = DiceRoller.roll(expr)
         puts "Returned \#{res}\n-----------------------" if $VERBOSE
         assert_equal expect, res
       end
     EOC
   end

   ERRORS.each do |expr, expect|
     eval <<-EOC
       def test_error_#{expr.hash.abs}
         expr, expect = #{expr.inspect}, #{expect.inspect}
         assert_raise(#{expect}) do
           puts "\n-----------------------\n\#{expr} => \#{expect}" if $VERBOSE
           res = DiceRoller.roll(expr)
           puts "Returned \#{res}\n-----------------------" if $VERBOSE
         end
       end
     EOC
   end
end

--
Ross Bamford - rosco@roscopeco.remove.co.uk

Well, here is my first solution to a quizz ^^
I tried to use racc for that ... so you need to generate the ruby script
using :

$ racc roll.y -o roll.rb

Otherwise, it is pretty simple ...

A small explanation is included within the file. If needed, I will post
the generated file.

Pierre

roll.y (4.05 KB)

Here is my submission. Yes, this is my first completed Ruby Quiz :wink:
Thanks to Eric Mahurin's syntax.rb for making this work. I've attached
it as well, because it's not easily accessible otherwise :wink:

-austin

roll.rb (3.51 KB)

syntax.rb (11.1 KB)

Here is my solution. I convert the expression into RPN (using the algorithm
described in the Wikipedia article) and then calculate it (I have added a
'd' method to Fixnum so that I can use it like the standard arithmetic
operators). My solution is not very strict, so it allows '%' as an alias for
100 anywhere in the expression (not just after a 'd'), but I think that
should not be a big problem. It also ignores other characters, so whitespace
is allowed anywhere.

Pablo

···

---

#!/usr/bin/ruby

class Fixnum
  def d(b)
    (1..self).inject(0) {|s,x| s + rand(b) + 1}
  end
end

class Dice

  def initialize(exp)
    @expr = to_rpn(exp)
  end
  
  def roll
    stack = []
    @expr.each do |token|
      case token
        when /\d+/
          stack << token.to_i
        when /[-+*\/d]/
          b = stack.pop
          a = stack.pop
          stack << a.send(token.to_sym, b)
      end
    end
    stack.pop
  end
  
  private
  
  def to_rpn(infix)
    stack, rpn, last = [], [], nil
    infix.scan(/\d+|[-+*\/()d%]/) do |token|
      case token
        when /\d+/
          rpn << token
        when '%'
          rpn << "100"
        when /[-+*\/d]/
          while stack.any? && stronger(stack.last, token)
            rpn << stack.pop
          end
          rpn << "1" unless last =~ /\d+|\)|%/
          stack << token
        when '('
          stack << token
        when ')'
          while (op = stack.pop) && (op != '(')
            rpn << op
          end
      end
      last = token
    end
    while op = stack.pop
      rpn << op
    end
    rpn
  end
  
  def stronger(op1, op2)
    (op1 == 'd' && op2 != 'd') || (op1 =~ /[*\/]/ && op2 =~ /[-+]/)
  end
  
end

if $0 == __FILE__
  d = Dice.new(ARGV[0])
  (ARGV[1] || 1).to_i.times { print "#{d.roll} " }
end

Here is my solution.

It's a recursive descent parser, that parses the full BNF posted by Matthew Moss. It doesn't "compile" the expresion into nodes or something similar, instead it evaluates the expression while parsing (so it has to be reparsed for every dice rolling). It uses StringScanner, which was quite handy for this task. (And it also uses eval() :wink:

Dominik

require "strscan"

class Dice
     def initialize(expr)
         @expr = expr.gsub(/\s+/, "")
     end

     def roll
         s = StringScanner.new(@expr)
         res = expr(s)
         raise "garbage after end of expression" unless s.eos?
         res
     end

     private

     def split_expr(s, sub_expr, sep)
         expr = []
         loop do
             expr << send(sub_expr, s)
             break unless s.scan(sep)
             expr << s[1] if s[1]
         end
         expr
     end

     def expr(s)
         eval(split_expr(s, :fact, /([+\-])/).join)
     end

     def fact(s)
         eval(split_expr(s, :term, /([*\/])/).join)
     end

     def term(s)
         first_rolls = s.match?(/d/) ? 1 : unit(s)
         dices = s.scan(/d/) ? split_expr(s, :dice, /d/) : []
         dices.inject(first_rolls) do |rolls, dice|
             raise "invalid dice (#{dice})" unless dice > 0
             (1..rolls).inject(0) { |sum, _| sum + rand(dice) + 1 }
         end
     end

     def dice(s)
         s.scan(/%/) ? 100 : unit(s)
     end

     def unit(s)
         if s.scan(/(\d+)/)
             s[1].to_i
         else
             unless s.scan(/\(/) && (res = expr(s)) && s.scan(/\)/)
                 raise "error in expression"
             end
             res
         end
     end
end

if $0 == __FILE__
     begin
         d = Dice.new(ARGV[0])
         puts (1..(ARGV[1] || 1).to_i).map { d.roll }.join(" ")
     rescue => e
         puts e
     end
end

I didn't try anything fancy for this. I did try to get eval to do all
the work, but ran into too many problems. Here's my solution:

$searches = [
    [/\(\d*\)/, lambda{|m| m[1..-2]}],
    [/^d/, lambda{|m| "1d"}],
    [/d%/, lambda{|m| "d100"}],
    [/(\+|-|\*|\/|\()d\d+/, lambda{|m| m[0..0]+'1'+m[1..-1]}],
    [/\d+d\d+/, lambda{|m| dice(*m.split('d').map {|i|i.to_i}) }],
    [/\d+(\*|\/)\d+/, lambda{|m| eval m}],
    [/\d+(\+|-)\d+/, lambda{|m| eval m}]
]
def parse(to_parse)
    s = to_parse
    while(s =~ /d|\+|-|\*|\/|\(|\)/)
        $searches.each do |search|
            if(s =~ search[0]) then
                s = s.sub(search[0], &search[1])
                break
            end
        end
    end
    s
end

def dice(times, sides)
    Array.new(times){rand(sides)+1}.inject(0) {|s,i|s+i}
end

srand
string = ARGV[0]
(puts "usage: #{$0} <string> [<iterations>]"; exit) if !string
(ARGV[1] || 1).to_i.times { print parse(string), ' ' }

-----Horndude77

this is my first ruby quiz, and here comes my solution.

as a couple of other solutions, it uses ruby to do the dirty work, and implements the diceroll as a method on integer.

!g

q61-roll.rb (2.18 KB)