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