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() ;-)

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