[QUIZ] Dice Roller (#61)

Another solution:

#! /usr/bin/ruby

# change this to some fixed value for reproducable results
def random(i)
# i
  # FIXME: check rand's usabilty for throwing dices...
  rand(i)+1
end

class DiceExpr

  def initialize(rolls, sides)
    @rolls, @sides = rolls, sides
  end

  def to_i
    sides = @sides.to_i
    (1..@rolls.to_i).inject(0) { | sum, i | sum += random(sides) }
  end

  def to_s
    "(#{@rolls}d#{@sides})"
  end

end

class Expr

  def initialize(lhs, rhs, op)
    @lhs, @rhs, @op = lhs, rhs, op
  end

  def to_i
    @lhs.to_i.send(@op, @rhs.to_i)
  end

  def to_s
    "(#{@lhs}#{@op}#{@rhs})"
  end

end

class Dice

  def initialize(expr)
    @expr_org = @expr_str = expr
    next_token
    @expr = addend()
    if @token
      raise "parser error: tokens left: >#{@fulltoken}#{@expr_str}<"
    end
  end

  # "lexer"
  @@regex = Regexp.compile(/^\s*([()+\-*\/]|[1-9][0-9]*|d%|d)\s*/)
  def next_token
    @prev_token = @token
    return @token = nil if @expr_str.empty?
    match = @@regex.match(@expr_str)
    if !match
      raise "parser error: cannot tokenize input #{@expr_str}"
    end
    @expr_str = @expr_str[match.to_s.length, @expr_str.length]
    @fulltoken = match.to_s # for "tokens left" error message only...
    @token = match[1]
  end

  # "parser"
  # bit lengthy but basically straightforward
  def number() # number or parenthesized expression
    raise "unexpeced >)<" if ( @token == ')' )
    if ( @token == '(' )
      next_token
      val = addend
      raise "parser error: parenthesis error, expected ) got #{@token}" if @token != ')'
      next_token
      return val
    end
    raise "parse error: number expected, got #{@token}" if @token !~ /^[0-9]*$/
    next_token
    @prev_token
  end

  def dice()
    if ( @token == 'd' )
      rolls = 1
    else
      rolls = number()
    end
    while ( @token == 'd' || @token == 'd%' )
      if @token == 'd%'
        rolls = DiceExpr.new(rolls, 100)
        next_token
      else
        next_token
        sides = number()
        raise "parser error: missing sides expression" if !sides
        rolls = DiceExpr.new(rolls, sides)
      end
    end
    rolls
  end

  def factor()
    lhs = dice()
    while ( @token == '*' || @token == '/' )
      op = @token
      next_token
      rhs = dice()
      raise "parser error: missing factor" if !rhs
      lhs = Expr.new(lhs, rhs, op)
    end
    lhs
  end

  def addend()
    lhs = factor()
    while ( @token == '+' || @token == '-' )
      op = @token
      next_token
      rhs = factor()
      raise "parser error: missing addend" if !rhs
      lhs = Expr.new(lhs, rhs, op)
    end
    lhs
  end

  def to_s
    "#{@expr_org} -> #{@expr.to_s}"
  end

  def roll
    @expr.to_i
  end

end

d = Dice.new(ARGV[0])

#puts d.to_s

(ARGV[1] || 1).to_i.times { print "#{d.roll} " }
puts

I suspect user error.

The correct answer will always be between 3 and 18 for 3d6.

-austin

···

On 06/01/06, J. Ryan Sobol <ryansobol@gmail.com> wrote:

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?

--
Austin Ziegler * halostatue@gmail.com
               * Alternate: austin@halostatue.ca

Hmm, that example looks wrong now that you mention it. It should be 6 numbers between 3 and 18 (the roll of 3 six-sided dice).

James Edward Gray II

···

On Jan 6, 2006, at 1:29 PM, J. Ryan Sobol wrote:

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

Sticking with typical integer division (ie, round-down) is fine.

If you wanted to extend the syntax to support round-up division (using '\'
perhaps) or other options, feel free. Extra credit.

A lot of extra credit if you add syntax to support some RPGs/home rules
where you might want 3d6, but you'll actually roll 4d6 and toss the lowest.

···

On 1/6/06, Jacob Fugal <lukfugl@gmail.com> wrote:

On 1/6/06, Ruby Quiz <james@grayproductions.net> wrote:
> Or, for something more complicated:
>
> > roll.rb "(5d5-4)d(16/d4)+3"
> 31

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

Jacob Fugal

Ha ha... Must have copied the wrong line when writing up the quiz
description.

That should look like this:

roll.rb "3d6" 6

18 18 18 18 18 18

=)

···

On 1/6/06, J. Ryan Sobol <ryansobol@gmail.com> wrote:

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

I would appreciate the full BNF, please.

Okay, this is what I've done in my current version that takes care of basic
precedence and associativity.

INTEGER = /[1-9][0-9]*/

expr: fact
    > expr '+' fact
    > expr '-' fact

fact: term
    > fact '*' term
    > fact '/' term

term: unit
    > [term] 'd' dice

dice: '%'
    > unit

unit: '(' expr ')'
    > INTEGER

Actually, this is slightly different than my current version, which after
reexamining to extract this BNF, I found a minor error (in handling of the
term rules and handling of the optional arg). My own code has a morphed
version of this BNF in order to code up a recursive descent parser, but this
BNF shows one way to handle the precedence/association rules.

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

Okay, that output is bogus. However, it is not rand(16) at all. It's:

  (1..3).inject(0) { |sum, ii| sum + (rand(6) + 1) }

The fact that it is three 6-sided dice rolled is important (and is
perhaps more important in a PRNG) because the weighting is a little
different. With rand(16) + 3 you're just as likely to get 3 as you are
18. With three rand(6) + 1 values, you're going to get much closer to a
bell curve than a straight probability line. This is a good thing,
because in D&D, 10 is described as absolutely average and 12 is the
high-end for most people. Adventurers, of course, can go to 18, but even
16 is good. Gandalf would be an 18 INT; Sam might be an 11 INT (INT ==
"intelligence").

Or, for something more complicated:

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

31

What is the -4 and the /d4 do?

  (5d5-4) => Roll a 5-sided dice 5 times and take the sum, subtract 4.
      => Result will be between 1 and 21.
  (16 / d4) => Roll a 4-sided dice and divide 16 by the result.
      => Result will be 4, 5, 8, or 16.
  d => Roll a [4, 5, 8, or 16]-sided dice 1-21 times and total.
      => The total result will be between 1 and 336.
  +3 => Add three to the result.
      => The final result will be between 4 and 339.

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.

  d binds tighter than addition.

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

Yes.

and

100 == 00

No. d00/d%/d100 all refer to values from 1 to 100. It should be
considered impossible to get a value of 0 from dice. Strictly speaking,
d100 should be a special case simulated where you are rolling two d10
values and treating one of them as the 10s and one of them as the 1s.
Again, it results in a slightly different curve than a pure d100 result
would be. One gaming system developed by Gary Gygax after he was ousted
from TSR in the mid-80s used what he termed d10x, which was d10*d10,
resulting in values from 1 - 100 with a radically different probability
curve than a normal d100.

The "natural" dice created are:

  d4, d6, d8, d10, d12, d20

Novelty dice created in the past include:

  d30, d100

The latter is quite unwieldy.

Strictly speaking, it is not possible to make a die (polyhedron) with an
odd number of faces, but d5 can be simulated by doing a rounded d10/2 or
d20/4.

-austin

···

On 06/01/06, Jim Freeze <jim@freeze.org> wrote:

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

--
Austin Ziegler * halostatue@gmail.com
               * Alternate: austin@halostatue.ca

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

All binary operators are left associative, so 5d6d7 is (5d6)d7.

Hopefully this ASCII art comes through clean:

Parse tree for: (5d5-4)d(16/d4)+3

                _______________ + _______________
                > >
        _______ d _______ 3
        > >
    ___ - ___ ___ / ___
    > > > >
___ d ___ 4 16 ___ d ___
5 5 1 4

Morus Walter a écrit :

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

IMO, all the parenthesis must be resolved before going further.
Thus in the example you rool :
5d5
1d4
and the last one with the result of the two computations.

Otherwise, it would be impraticle to do that with real dices (mmmmhh ...)

Pierre

I'm not sure whether this made it to the list first time I sent it, or
is just delayed. Here it is again in case folks missed it...

I would appreciate the full BNF, please.

Okay, this is what I've done in my current version that takes care of
basic precedence and associativity.

INTEGER = /[1-9][0-9]*/

expr: fact
    > expr '+' fact
    > expr '-' fact

fact: term
    > fact '*' term
    > fact '/' term

term: unit
    > [term] 'd' dice

dice: '%'
    > unit

unit: '(' expr ')'
    > INTEGER

Actually, this is slightly different than my current version, which
after reexamining to extract this BNF, I found a minor error (in
handling of the term rules and handling of the optional arg). My own
code has a morphed version of this BNF in order to code up a recursive
descent parser, but this BNF shows one way to handle the
precedence/association rules.

The quiz shows incorrect output for 3d6. The line reading "72 64 113
33 78 82" was mistakenly copied from a different set of dice.

"3d6" means: (rand(6)+1) + (rand(6)+1) + (rand(6)+1)

(The +1 are because rand is zero-based, but dice are one-based.)

- is subtraction, so -4 means subtract 4.

/ is division, so /d4 means roll a d4 and divide that into the
expression left of the /

d% == d100

d00 is not valid; there is no such thing as a zero-sided die (although
if you want to make 00 an extension to imply d100 in your own
implementation, that's fine).

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.

This dice roller is, for the most part, a simple integer calculator
with addition, subtraction, multiplication, division, and grouping via
parentheses. In order to turn it into a dice calculator, we add the
'd' (dice) binary operator. The required right argument to 'd' is the
number of sides on the die, while the option left argument (defaulting
to 1) is how many to roll and sum.

So 16 / d4 means "roll one 4-sided die and divide the result into 16".

With the game systems I know, and I admit I haven't played for a couple
of years, 5d6d7 would not be a legal expression, and would raise an
exception.

/Henrik

···

On Fri, 2006-01-06 at 23:45, Robert Retzbach wrote:

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

--

http://www.henrikmartensson.org/ - Reflections on software development

Matthew Moss a écrit :

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.

Well, why do you say it's invalid ? Given the simplified BNF it must be
read as :

3d(d(d(d6)))

and it is perfectly valid as there is no other way to understand that ...

Pierre

Leexer/parsers? We ain't got no Leexer/parsers. We don't need no
Leexer/parsers. I don't have to show you any steenking Leexer/parser.
Just call eval and use Ruby's fine lexer/parser (apologies to Mel
Brooks, John Huston and Banditos Mexicanos everywhere).

This approach uses regex substitutions to first munge the input
expression to deal with the default cases (like d6 to 1d6 and 1% to
1d100), then it substitutes ** for d and hands it over to the
evaluator and prints the result.

Conveniently, ** has the desired
precedence relative to the other operators, plus it is binary and
left-associative. This feels so evil. Seduced by the Dark Side I am.

I used the same approach, but found that ** is right-associative (as it's generally defined outside of Ruby). To confirm the associativity for yourself, try this: 2**3**4. If it's left associative, it should equal 8**4 (4096), right-associativity gives 2**81 (a lot). I ended up doing a lot more redefining and mucking about:

Dice Ruby
  d *
  * +
  / -
  + <<
  - >>

Interestingly, the difference between a left-associating and a right-associating 'd' operator isn't particularly visible from the 'loaded-dice' testing common on the list. For example, 2d2d6 gives a maximum of 24 whichever associativity is used, but the distributions of the two solutions are vastly different; the left-associative result has a minimum value of 4, the right-associative result has a minimum of 2.

Here's my solution, which maintains correct associativity for 'd' according to the initial quiz, but does a lot more mucking about with Fixnum:

matthew smillie.

#!/usr/local/bin/ruby

class Fixnum
   alias old_mult *
   alias old_div /
   alias old_plus +
   alias old_minus -

   def >>(arg) old_minus(arg) end
   def <<(arg) old_plus(arg) end
   def -(arg) old_div(arg) end
   def +(arg) old_mult(arg) end

   def *(arg)
     sum = 0
     self.times do
       sum = sum.old_plus(rand(arg).old_plus(1))
     end
     sum
   end
end

class Dice
   def initialize(str)
     # make assumed '1's explicit - do it twice to cover cases
     # like '3ddd6' which would otherwise miss one match.
     @dice = str.gsub(/([+\-*\/d])(d)/) { |s| "#{$1}1#{$2}" }
     @dice = @dice.gsub(/([+\-*\/d])(d)/) { |s| "#{$1}1#{$2}" }
     # sub all the operators.
     @dice = @dice.gsub(/\+/, "<<")
     @dice = @dice.gsub(/-/, ">>")
     @dice = @dice.gsub(/\*/, "+")
     @dice = @dice.gsub(/\//, "-")
     @dice = @dice.gsub(/d/, "*")
   end

   def roll
     eval(@dice)
   end
end

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

···

----
Matthew Smillie <M.B.Smillie@sms.ed.ac.uk>
Institute for Communicating and Collaborative Systems
University of Edinburgh

Sorry, I changed that. It gives a block now.

···

On Sun, 08 Jan 2006 19:33:18 -0000, Ross Bamford <rosco@roscopeco.remove.co.uk> wrote:

This is implemented in the DiceRoller.parse method, which returns the
string.

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

Very interesting, and different solutions, this time!

Here's my recursive descent solution with histogram:

=begin
Ruby Quiz #61
by Matthew D Moss

Solution by Christer Nilsson

"3d6" gives 3..18 randomly

"(5d5-4)d(16/d4)+3"

Backus Naur Form:

expr: term ['+' expr | '-' expr]
term: fact ['*' term | '/' term]
fact: [unit] 'd' dice
unit: '(' expr ')' | integer
dice: '%' | term
integer: digit [integer]
digit: /[0-9]/

* Integers are positive
* 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

Some game systems use d100 quite often, and may abbreviate it as "d%"
(but note that '%' is only allowed immediately after a 'd').
=end
class String
  def behead
    return ['',''] if self == ''
    [self[0..0], self[1...self.size]]
  end
end

class Array
  def sum
    inject(0) {|sum,e| sum += e}
  end

  def histogram(header="")
    width = 100
    each_index {|i| self[i]=0 if self[i].nil?}
    sum = self.sum
    max = self.max if max.nil?
    s = " " + header + "\n"
    each_with_index do |x,i|
      label = " " + format("%2.1f",100.0*x/sum)+"%"
      s += format("%2d",i) + " " + "*" * ((x-min) * width / (max-min)) +
label + "\n"
    end
    s += "\n"
  end
end

class Dice

  def statistics(expr, n=1000)
    prob = []
    n.times do
      value = evaluate(expr)
      prob[value]=0 if prob[value].nil?
      prob[value] += 1
    end
    prob
  end

  def evaluate s
    @sym, @s = s.behead
    @stack = []
    expr
    pop
  end

  def drop (pattern)
    raise 'syntax error: expected ' + pattern unless pattern === @sym
    @sym, @s = @s.behead
  end

  def push(x) @stack.push x end
  def top2() @stack[-2] end
  def top() @stack[-1] end
  def pop() @stack.pop end

  def calc value
    pop
    push value
  end

  def try symbol
    return nil unless @sym == symbol
    drop symbol
    case symbol
    when '+' then expr; calc top2 + pop
    when '-' then expr; calc top2 - pop
    when '*' then term; calc top2 * pop
    when '/' then term; calc top2 / pop
    when '%' then push 100
    when '(' then expr; drop ')'
    #when 'd' then dice; calc top2 * pop # debug mode
    when 'd' # release mode
      dice
      sum = 0
      sides = pop
      count = pop
      count.times {sum += rand(sides) + 1}
      push sum
    end
  end

  def expr
    term
    try('+') or try('-')
  end

  def term
    fact
    try('*') or try('/')
  end

  def fact
    @sym == 'd' ? push(1) : unit # implicit 1
    try('d')
  end

  def dice
    #unit unless try('%')# if 5d6d7 is not accepted
    term unless try('%') # if 5d6d7 is accepted
  end

  def unit
    integer @sym.to_i unless try('(')
  end

  def integer(i)
    return if @sym == ''
    digit = /[0-9]/
    drop(digit)
    digit === @sym ? integer( 10 * i + @sym.to_i ) : push(i)
  end
end

require 'test/unit'
class TestDice < Test::Unit::TestCase
  def t (actual, expect)
    assert_equal expect, actual
  end
  def test_all

    t(/[0-9]/==="0", true)
    t(/[0-9]/==="a", false)
    t "abc".behead, ["a","bc"]
    t "a".behead, ["a",""]
    t "".behead, ["",""]

    dice = Dice.new()
    print dice.statistics("d6").histogram("d6")
    print dice.statistics("2d6").histogram("2d6")
    print dice.statistics("(d6)d6",10000).histogram("(d6)d6")

    #t dice.evaluate("(6)"), 6
    #t dice.evaluate("12+34"), 46
    #t dice.evaluate("3*4+2"), 14
    #t dice.evaluate("5+6+7"), 18
    #t dice.evaluate("5+6-7"), 4
    #t dice.evaluate("(5+6)+7"), 18
    #t dice.evaluate("5"), 5
    #t dice.evaluate("5+(6+7)"), 18
    #t dice.evaluate("(5+6+7)"), 18
    #t dice.evaluate("5*6*7"), 210
    #t dice.evaluate("2+3*4"), 14
    #t dice.evaluate("12+13*14"), 194
    #t dice.evaluate("(2+3)*4"), 20
    #t dice.evaluate("(5d5-4)d(16/1d4)+3"), 45
    #t dice.evaluate("(5d5-4)d(400/1d%)+3"), 87
    #t dice.evaluate("1"), 1
    #t dice.evaluate("1+2"),3
    #t dice.evaluate("1+3*4"),13
    #t dice.evaluate("1*2+4/8-1"), 1
    #t dice.evaluate("d1"),1
    #t dice.evaluate("1d1"),1
    #t dice.evaluate("1d10"), 10
    #t dice.evaluate("10d10"),100
    #t dice.evaluate("d3*2"), 6
    #t dice.evaluate("2d3+8"), 14
    #t dice.evaluate("(2*(3+8))"),22
    #t dice.evaluate("d3+d3"),6
    #t dice.evaluate("d2*2d4"),16
    #t dice.evaluate("2d%"),200
    #t dice.evaluate("14+3*10d2"), 74
    #t dice.evaluate("(5d5-4)d(16/d4)+3"),87
    #t dice.evaluate("d10"), 10
    #t dice.evaluate("d%"),100
    #t dice.evaluate("d(2*2)+d4"),8
    #t dice.evaluate("(5d6)d7"), 210
    #t dice.evaluate("5d(6d7)"), 210
    #t dice.evaluate("5d6d7)"), 210
    #t dice.evaluate("12d13d14)"), 2184
    #t dice.evaluate("12*d13)"), 156
    #t dice.evaluate("12+d13)"), 25
  end
end

···

--
Posted via http://www.ruby-forum.com/.

# There it goes, using eval for simplicity, but at least compiling the
# dice into a Proc:

class Integer
  def d(n) # evil }:slight_smile:
    (1..self).inject(0) { |a,e| a + rand(n) + 1 }
  end
end

class Dice
  def initialize(dice)
    @src = dice.gsub(/d(%|00)(\D|$)/, 'd100\2').
                gsub(/d(\d+)/, 'd(\1)').
                gsub(/(\d+|\))d/, '\1.d').
                gsub(/\d+/) { $&.gsub(/^0+/, '') }

    raise ArgumentError, "invalid dice: `#{dice}'" if @src =~ /[^-+\/*()d0-9. ]/

    begin
      @dice = eval "lambda{ #@src }"
      roll # try the dice
    rescue
      raise ArgumentError, "invalid dice: `#{dice}'"
    end
  end

  def d(n)
    1.d(n)
  end

  def roll
    @dice.call
  end
end

unless $DEBUG
  d = Dice.new(ARGV[0] || "d6")
  puts Array.new((ARGV[1] || 1).to_i) { d.roll }.join(" ")
else
  $DEBUG = false # only makes test/unit verbose now
  
  warn "This is a heuristic test-suite. Please re-run (or increase N) on failure."
  
  require 'test/unit'

  N = 100000

  class TestDice < Test::Unit::TestCase
    def test_00_invalid_dice
      assert_raises(ArgumentError) { Dice.new("234%21") }
      assert_raises(ArgumentError) { Dice.new("%d5") }
      assert_raises(ArgumentError) { Dice.new("d5%") }
      assert_raises(ArgumentError) { Dice.new("d%5") }
    end

    def test_10_fixed_expr
      dice_min_max({
        '1' => [1, 1],
        '1+2' => [3, 3],
        '1+3*4' => [13, 13],
        '1*2+4/8-1' => [1, 1],
        'd1' => [1, 1],
        '1d1' => [1, 1],
        '066d1' => [66, 66]
      }, 10)
    end

    def test_20_small_dice
      dice_min_max({
        'd10' => [1, 10],
        '1d10' => [1, 10],
        'd3*2' => [2, 6],
        '2d3+8' => [10, 14], # not 22
        '(2d(3+8))' => [2, 22], # not 14
        'd3+d3' => [2, 6],
        'd2*2d4' => [2, 16],
        'd(2*2)+d4' => [2, 8]
      })
    end

    def test_30_percent_dice
      dice_min_max({
        'd%' => [1, 100],
        '2d%' => [2, 200]
      }, 100_000)
    end

    def test_40_complicated_dice
      dice_min_max({
        '10d10' => [10, 100],
        '5d6d7' => [5, 210], # left assoc
        '14+3*10d2' => [44, 74],
        '(5d5-4)d(16/d4)+3' => [4, 339],
      }, 1_000_000)
    end

    def dice_min_max(asserts, n=10_000)
      asserts.each { |k, v|
        dice = Dice.new k

        v2 = (1..n).inject([1.0/0.0, 0]) { |(min, max), e|
          r = dice.roll
          [[min, r].min, [max, r].max]
        }
        
        assert_equal v, v2, k
      }
    end
  end
end

__END__

···

--
Christian Neukirchen <chneukirchen@gmail.com> http://chneukirchen.org

Hi,

Here is my solution #1 for this nice quiz. Hacky and short, without (!) using eval... :wink:

module Dice
   def self.roll(expr)
     expr = expr.gsub(/\s/, '')
     while
       expr.sub!(/\(([^()]+)\)/) { roll($1) } ||
       expr.sub!(/(\A|[^\d])\-\-(\d+)/, '\\1\\2') ||
       expr.sub!(/d%/, 'd100') ||
       expr.sub!(/(\d+)d(\d+)/) { (1..$1.to_i).inject(0) {|a, b| a + rand($2.to_i) + 1} } ||
       expr.sub!(/d(\d+)/, '1d\\1') ||
       expr.sub!(/(\d+)\/(\-?\d+)/) { $1.to_i / $2.to_i } ||
       expr.sub!(/(\d+)\*(\-?\d+)/) { $1.to_i * $2.to_i } ||
       expr.sub!(/(\-?\d+)\-(\-?\d+)/) { $1.to_i - $2.to_i } ||
       expr.sub!(/(\-?\d+)\+(\-?\d+)/) { $1.to_i + $2.to_i }
     end
     return $1.to_i if /\A(\-?\d+)\Z/ =~ expr
     raise "Error evaluating dice expression, stuck at '#{expr}'"
   end
end

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