[QUIZ] Dice Roller (#61)

Hi,

here is my second solution. Quite a bit longer, but a lot nicer.
For this I implemented a simple recursive descent parser class that allows the tokens and the grammar to be defined in a very clean ruby syntax. I think I'd really like to see a production quality parser(generator) using something like this grammar format.

class RDParser
   attr_accessor :pos
   attr_reader :rules

   def initialize(&block)
     @lex_tokens = []
     @rules = {}
     @start = nil
     instance_eval(&block)
   end

   def parse(string)
     @tokens = []
     until string.empty?
       raise "unable to lex '#{string}" unless @lex_tokens.any? do |tok|
         match = tok.pattern.match(string)
         if match
           @tokens << tok.block.call(match.to_s) if tok.block
           string = match.post_match
           true
         else
           false
         end
       end
     end
     @pos = 0
     @max_pos = 0
     @expected = []
     result = @start.parse
     if @pos != @tokens.size
       raise "Parse error. expected: '#{@expected.join(', ')}', found '#{@tokens[@max_pos]}'"
     end
     return result
   end

   def next_token
     @pos += 1
     return @tokens[@pos - 1]
   end

   def expect(tok)
     t = next_token
     if @pos - 1 > @max_pos
       @max_pos = @pos - 1
       @expected = []
     end
     return t if tok === t
     @expected << tok if @max_pos == @pos - 1 && !@expected.include?(tok)
     return nil
   end

   private

   LexToken = Struct.new(:pattern, :block)

   def token(pattern, &block)
     @lex_tokens << LexToken.new(Regexp.new('\\A' + pattern.source), block)
   end

   def start(name, &block)
     rule(name, &block)
     @start = @rules[name]
   end

   def rule(name)
     @current_rule = Rule.new(name, self)
     @rules[name] = @current_rule
     yield
     @current_rule = nil
   end

   def match(*pattern, &block)
     @current_rule.add_match(pattern, block)
   end

   class Rule
     Match = Struct.new :pattern, :block

     def initialize(name, parser)
       @name = name
       @parser = parser
       @matches = []
       @lrmatches = []
     end

     def add_match(pattern, block)
       match = Match.new(pattern, block)
       if pattern[0] == @name
         pattern.shift
         @lrmatches << match
       else
         @matches << match
       end
     end

     def parse
       match_result = try_matches(@matches)
       return nil unless match_result
       loop do
         result = try_matches(@lrmatches, match_result)
         return match_result unless result
         match_result = result
       end
     end

     private

     def try_matches(matches, pre_result = nil)
       match_result = nil
       start = @parser.pos
       matches.each do |match|
         r = pre_result ? [pre_result] : []
         match.pattern.each do |token|
           if @parser.rules[token]
             r << @parser.rules[token].parse
             unless r.last
               r = nil
               break
             end
           else
             nt = @parser.expect(token)
             if nt
               r << nt
             else
               r = nil
               break
             end
           end
         end
         if r
           if match.block
             match_result = match.block.call(*r)
           else
             match_result = r[0]
           end
           break
         else
           @parser.pos = start
         end
       end
       return match_result
     end
   end
end

parser = RDParser.new do
   token(/\s+/)
   token(/\d+/) {|m| m.to_i }
   token(/./) {|m| m }

   start :expr do
     match(:expr, '+', :term) {|a, _, b| a + b }
     match(:expr, '-', :term) {|a, _, b| a - b }
     match(:term)
   end

   rule :term do
     match(:term, '*', :dice) {|a, _, b| a * b }
     match(:term, '/', :dice) {|a, _, b| a / b }
     match(:dice)
   end

   def roll(times, sides)
     (1..times).inject(0) {|a, b| a + rand(sides) + 1 }
   end

   rule :dice do
     match(:atom, 'd', :sides) {|a, _, b| roll(a, b) }
     match('d', :sides) {|_, b| roll(1, b) }
     match(:atom)
   end

   rule :sides do
     match('%') { 100 }
     match(:atom)
   end

   rule :atom do
     match(Integer)
     match('(', :expr, ')') {|_, a, _| a }
   end
end

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

Wow. That is very cool. Thanks for sharing!

James Edward Gray II

···

On Jan 8, 2006, at 3:42 PM, Pablo Hoch wrote:

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

I've decided to bite the bullet and post my overlong solution.

It's got a few extras in there:

$ ./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'm getting to grips with block-passing as a routine technique - the
evaluate() method "yield"s its result so that it can provide multiple
results - tens of thousands if you ask it to try every possible roll
involving lots of dice. The roll_dice method had to be written
recursively for that to work:

   def roll_dice( numdice, sides )
     if ( numdice == 0 )
       yield null_value
     else
       roll_one(sides) do |first|
  roll_dice( numdice-1, sides ) do |rest|
    yield( first + rest )
  end
       end
     end
   end

Depending on how roll_one has been overridden, "first" and "rest" can be integers, frequency distributions, or objects representing the history
of every roll to get to this state. In the last case, roll_one will
yield "sides" times, to give you every possible roll

I'm not quite comfortable with things like redefining Integer#+
If I had done that, I could have avoided a lot of kind_of? calls in
stuff like this:

   def eval_binop( force_same_type = true )
     subtree(:left).evaluate do |l|
       subtree(:right).evaluate do |r|
  if force_same_type
    if r.kind_of?( Roll_stat ) and ! l.kind_of?( Roll_stat )
      l = Roll_stat.new(l)
    end
  end
  yield(l,r)
       end
     end
   end

The whole thing can generate the probability distributions reasonably
quickly - I had ideas of approximating large numbers of dice (100d6
and so on) with the appropriate normal distribution ("clipped" by max
and min values).

The exhaustive output is very large, though. It would be worth
optimising that by taking out permutations.

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

dice.rb (12.9 KB)

} On Sat, Jan 07, 2006 at 03:56:47AM +0900, Ruby Quiz wrote:
} [...]
} } [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.

Okay, so I said I wanted the full BNF, and I thought it would be useful if
I could find a convenient Ruby lex/yacc. Well, I couldn't. I now have two
solutions, both using my own parsing. Both use eval. One adds a method to
Fixnum to let eval do even more work. The more complicated version with
syntax trees, which came first, took roughly two hours to work out. The
second, simpler version with the Fixnum method took about 20 minutes to
build from the first version. Note that I maintained left associativity
with the d operator in both methods without having the 3dddd6 problem
Matthew Moss mentioned.

test61.rb runs the code on commandline arguments
61.rb is the complicated syntax tree version
61alt.rb is the simpler Fixnum method version

##### test61.rb ################################################################

#require '61'
require '61alt'

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

##### 61.rb ####################################################################

module DiceRoller

  class ArithOperator
    def initialize(left, op, right)
      @left = left
      @op = op
      @right = right
    end

    def to_i
      return (eval "#{@left.to_i}#{@op}#{@right.to_i}")
    end
  end

  class DieOperator
    #op is a dummy
    def initialize(left, op, right)
      @left = left
      @right = right
    end

    def to_i
      count = @left.to_i
      fail "Die count must be nonnegative: '#{count}'" if count < 0
      die = @right.to_i
      fail "Die size must be positive: '#{die}'" if die < 1
      return (1..count).inject(0) { |sum, waste| sum + (rand(die)+1) }
    end
  end

  OpClass = { '+' => ArithOperator,
              '-' => ArithOperator,
              '*' => ArithOperator,
              '/' => ArithOperator,
              'd' => DieOperator }

  def lex(str)
    tokens = str.scan(/(00)|([-*\/()+d%0])|([1-9][0-9]*)|(.+)/)
    tokens.each_index { |i|
      tokens[i] = tokens[i].compact[0]
      if not /^(00)|([-*\/()+d%0])|([1-9][0-9]*)$/ =~ tokens[i]
        if /^\s+$/ =~ tokens[i]
          tokens[i] = nil
        else
          fail "Found garbage in expression: '#{tokens[i]}'"
        end
      end
    }
    return tokens.compact
  end

  def validate_and_cook(tokens)
    oper = /[-*\/+d]/
    num = /(\d+)|%/
    last_was_op = true
    paren_depth = 0
    prev = ''
    working = []
    tokens.each_index { |i|
      tok = tokens[i]
      if num =~ tok
        fail 'A number cannot follow an expression!' if not last_was_op
        fail 'Found spurious zero or number starting with zero!' if tok == '0'
        if ( tok == '00' || tok == '%' )
          fail 'Can only use % or 00 after d!' if prev != 'd'
          tokens[i] = 100
          working << 100
        else
          working << tok.to_i
        end
        last_was_op = false
      elsif oper =~ tok
        if last_was_op
          #handle case of dX meaning 1dX
          if tok == 'd'
            fail 'A d cannot follow a d!' if prev == RollMethod
            working << 1
          else
            fail 'An operator cannot follow a operator!'
          end
        end
        working << tok
        last_was_op = true
      elsif tok == "("
        fail 'An expression cannot follow an expression!' if not last_was_op
        paren_depth += 1
        working << :p_open
      elsif tok == ")"
        fail 'Incomplete expression at close paren!' if last_was_op
        fail 'Too many close parens!' if paren_depth < 1
        paren_depth -= 1
        last_was_op = false
        working << :p_close
      else #what did I miss?
        fail "What kind of token is this? '#{tok}'"
      end
      prev = tok
    }
    fail 'Missing close parens!' if paren_depth != 0
    return working
  end

  def parse_parens(tokens)
    working = []
    i = 0
    while i < tokens.length
      if tokens[i] == :p_open
        i += 1
        paren_depth = 0
        paren_tokens = []
        while (tokens[i] != :p_close) || (paren_depth > 0)
          if tokens[i] == :p_open
            paren_depth += 1
          elsif tokens[i] == :p_close
            paren_depth -= 1
          end
          paren_tokens << tokens[i]
          i += 1
        end
        working << parse(paren_tokens)
      else
        working << tokens[i]
      end
      i += 1
    end
    return working
  end

  def parse_ops(tokens, regex)
    fail "Something broke: len = #{tokens.length}" if tokens.length < 3 || (tokens.length % 2) == 0
    i = 1
    working = [ tokens[0] ]
    while i < tokens.length
      if regex =~ tokens[i].to_s
        op = OpClass[tokens[i]]
        lindex = working.length-1
        working[lindex] = op.new(working[lindex], tokens[i], tokens[i+1])
      else
        working << tokens[i]
        working << tokens[i+1]
      end
      i += 2
    end
    return working
  end

  #scan for parens, then d, then */, then +-
  def parse(tokens)
    working = parse_parens(tokens)
    fail "Something broke: len = #{working.length}" if (working.length % 2) == 0
    working = parse_ops(working, /^d$/) if working.length > 1
    fail "Something broke: len = #{working.length}" if (working.length % 2) == 0
    working = parse_ops(working, /^[*\/]$/) if working.length > 1
    fail "Something broke: len = #{working.length}" if (working.length % 2) == 0
    working = parse_ops(working, /^[+-]$/) if working.length > 1
    fail "Something broke: len = #{working.length}" if working.length != 1
    return working[0]
  end

  def parse_dice(str)
    tokens = lex(str)
    return parse(validate_and_cook(tokens))
  end

end

class Dice

  def initialize(expression)
    @expression = parse_dice(expression)
  end

  def roll
    return @expression.to_i
  end

  private

  include DiceRoller

end

##### 61alt.rb #################################################################

module DiceRoller

  RollMethod = '.roll'

  def lex(str)
    tokens = str.scan(/(00)|([-*\/()+d%0])|([1-9][0-9]*)|(.+)/)
    tokens.each_index { |i|
      tokens[i] = tokens[i].compact[0]
      if not /^(00)|([-*\/()+d%0])|([1-9][0-9]*)$/ =~ tokens[i]
        if /^\s+$/ =~ tokens[i]
          tokens[i] = nil
        else
          fail "Found garbage in expression: '#{tokens[i]}'"
        end
      end
    }
    return tokens.compact
  end

  def validate_and_cook(tokens)
    oper = /[-*\/+d]/
    num = /(\d+)|%/
    last_was_op = true
    paren_depth = 0
    prev = ''
    working = []
    tokens.each_index { |i|
      tok = tokens[i]
      if num =~ tok
        fail 'A number cannot follow an expression!' if not last_was_op
        fail 'Found spurious zero or number starting with zero!' if tok == '0'
        if ( tok == '00' || tok == '%' )
          fail 'Can only use % or 00 after d!' if prev != RollMethod
          tokens[i] = 100
          tok = 100
        else
          tok = tok.to_i
        end
        if prev == RollMethod
          working << "(#{tok})"
        else
          working << tok
        end
        last_was_op = false
      elsif oper =~ tok
        tok = RollMethod if tok == 'd'
        if last_was_op
          #handle case of dX meaning 1dX
          if tok == RollMethod
            fail 'A d cannot follow a d!' if prev == RollMethod
            working << 1
          else
            fail 'An operator cannot follow a operator!'
          end
        end
        working << tok
        last_was_op = true
      elsif tok == "("
        fail 'An expression cannot follow an expression!' if not last_was_op
        paren_depth += 1
        working << tok
      elsif tok == ")"
        fail 'Incomplete expression at close paren!' if last_was_op
        fail 'Too many close parens!' if paren_depth < 1
        paren_depth -= 1
        last_was_op = false
        working << tok
      else #what did I miss?
        fail "What kind of token is this? '#{tok}'"
      end
      prev = tok
    }
    fail 'Missing close parens!' if paren_depth != 0
    return working
  end

  def parse_dice(str)
    tokens = lex(str)
    return validate_and_cook(tokens).to_s
  end

end

class Fixnum
  def roll(die)
    fail "Die count must be nonnegative: '#{self}'" if self < 0
    fail "Die size must be positive: '#{die}'" if die < 1
    return (1..self).inject(0) { |sum, waste| sum + (rand(die)+1) }
  end
end

class Dice

  def initialize(expression)
    @expression = parse_dice(expression)
  end

  def roll
    return (eval @expression)
  end

  private

  include DiceRoller

end

···

On Sat, Jan 07, 2006 at 04:00:52AM +0900, Gregory Seidman wrote:

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

Actually 3d6 means roll a 6 sided die 3 times so you would have a result of 3-18

so this:

roll.rb "3d6" 6

Would actully be: (RND = Random)

RND(3-18) RND(3-18) RND(3-18) RND(3-18) RND(3-18) RND(3-18)

Below is 3d6 from the DnD Dice Roller on Wizards.com. The +0 would be
a modifier from depending if it was an attack roll or a defense roll.
For our purposes you would remove the +0

Roll(3d6)+0:
1,6,6,+0
Total:13

DnD Dice Roller:

Will

···

On 1/6/06, Matthew Moss <matthew.moss.coder@gmail.com> wrote:

--
Will Shattuck ( willshattuck.at.gmail.com )
Home Page: http://www.thewholeclan.com/will

When you get to your wit's end, you'll find God lives there.

Just don't tell me that the first one is 18/00.

-austin

···

On 06/01/06, Matthew Moss <matthew.moss.coder@gmail.com> wrote:

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

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

I guess that must be a D&D inside half-joke because I'm totally confused.

Don't worry about explaining it as I just needed to know what that command, roll.rb "3d6" 6, did.

~ ryan ~

···

On Jan 6, 2006, at 2:39 PM, Matthew Moss wrote:

roll.rb "3d6" 6

18 18 18 18 18 18

On Jan 6, 2006, at 2:49 PM, Austin Ziegler wrote:

Just don't tell me that the first one is 18/00.

On Jan 6, 2006, at 2:53 PM, James Edward Gray II wrote:

<dies laughing> They all were, of course.

On Jan 6, 2006, at 2:53 PM, Matthew Moss wrote:

Actually, you're right, but actually my post was a half-joke. The
munchkin players seem to roll 18's every time. :wink:

forgive my ignorance... BNF?

w

···

On 1/6/06, Matthew Moss <matthew.moss.coder@gmail.com> wrote:

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

--
Will Shattuck ( willshattuck.at.gmail.com )
Home Page: http://www.thewholeclan.com/will

When you get to your wit's end, you'll find God lives there.

Moin,

Austin Ziegler wrote:

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.

How exactly would those d10s differ from a d100?

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

Not only a different curve, but also some values would be impossible to
get (as 13 and 51)

*Sascha

···

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

so 1+2*3 == (1+2)*3 == 9?

Dave

···

On 1/6/06, Matthew Moss <matthew.moss.coder@gmail.com> wrote:

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

[great explanation snipped]

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.

If the 10's dice is 3 and the 1's dice is 1, you get 31.

What do you need to roll to get a 0 and 100?

I could see this working if the dice were 0..9 and you add one to the final result,
but you said that dice should be 1..x. So do you subtract one from each
digit, then add one to the final result?

Example:

10's 1's
1 1 => (1-1)(1-1) => (00)+1 => 1
4 1 => (4-1)(1-1) => (30)+1 => 31
10 10 => (10-1)(10-1) => (99)+1 => 100

Jim

···

On Jan 6, 2006, at 4:31 PM, Austin Ziegler wrote:

"Matthew D Moss" <matthew.moss.coder@gmail.com> writes:

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

So what are the maximum and minimum values of this?

···

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

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.

Ahhh.... this must be a game system you don't know. =)

Austin Ziegler wrote:

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,

Uh, of course you can make such a polyhedron. Consider the
Egyptian and Mayan pyramids as examples of 5-sided polyhedron
(four triangles on the sides and a square on the bottom).
Adjusting the steepness of the sides can make it as fair
or unfair as you'd want.

Sure, they're not regular polyhedra, but neither is the d30 you spoke of.

Pierre Barbier de Reuille schrieb:

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

Hmm so that expr has 18 as maximum result with fully loaded dice?

3d(d(d(d6)))
3d(d(d6))
3d(d6)
3d6

=> 18

Is that correct?

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

That is only true if the d operator is right-associative. According to the
original spec, it is left-associative and, therefore, 3dddd6 is a syntax
error. Mind you, the second option Matthew gave is to make it
right-associative, which is what you have done. I chose to treat it as a
syntax error.

} Pierre
--Greg

···

On Sun, Jan 08, 2006 at 10:48:01PM +0900, Pierre Barbier de Reuille wrote:

Good catch. I felt uncomfortable building this without unit testing.
It should be possible to write good repeatable tests using srand in
place of rand...

···

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

For this I implemented a simple recursive descent parser class that allows the tokens and the grammar to be defined in a very clean ruby syntax.

Awesome!

I think I'd really like to see a production quality parser(generator) using something like this grammar format.

I agree. This is fantastic.

So what do we have to do to get you to add the polish and make it available? :slight_smile:

James Edward Gray II

···

On Jan 8, 2006, at 3:53 PM, Dennis Ranke wrote:

Sorry for the noise.

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

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

There were a few other inaccuracies in the comments, where I'd obviously not been merciless enough when refactoring. Specifically, a (*) comment about one of the parse regexps (no longer applies) and a comment in the tests about Fixnum and class methods (from before I realised my error). Oh, and a debug message used still referenced an attr that was no longer set up at that point.

I updated the archive at the link I posted with those removed, and also took the chance to slip in a tiny fix for whitespace (just remove it all at the start of the parse) but I guess it doesn't 'count' for the quiz :slight_smile:

Anyway, thanks all concerned for the fun quiz - this is the first one I've done so take it easy on my solution :slight_smile:

(Oh, and I missed [SOLUTION] before and since a lot of people seem to be doing that I felt just [QUIZ] might get missed).

The original solution post was this one:

  http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/174815

···

On Sun, 08 Jan 2006 19:38:03 -0000, I wrote:

On Sun, 08 Jan 2006 19:33:18 -0000, I also wrote:

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

<dies laughing> They all were, of course.

James Edward Gray II

···

On Jan 6, 2006, at 1:49 PM, Austin Ziegler wrote:

On 06/01/06, Matthew Moss <matthew.moss.coder@gmail.com> wrote:

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

Just don't tell me that the first one is 18/00.