From: Dominik Bathon Date: 2006-01-09T09:46:01+09:00 Subject: Re: [SOLUTION] Dice Roller (#61) 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