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. Eval does what we want after an override of Fixnum.** to give us our dice-rolling d-operator behavior. 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. Note that we get lot's of additional behavior we don't need, but the original spec said the BNF was incomplete, so I was lazy and left the undef[ining] of >>, << et al is 'as an exercise...' I don't know enough D&D to know if they might be useful. BTW. In the spirit of 'why can't we all just get along?': the dice rolling algorithm is ported from this Python code at http://www.onlamp.com/pub/a/python/2002/07/11/reci... from random import randrange def dice(num,sides): return reduce(lambda x,y,s=sides:x + randrange(s), range(num+1))+num here is the Ruby: #!/usr/bin/env ruby # # roll.rb # # fix up Fixnum to override ** with our desired d behavior class Fixnum def ** (sides) # validation if sides<1 raise "Invalid sides value: '#{sides}', must be a positive Integer" end if self<1 raise "Invalid number of rolls: '#{self}', must be a postitive Integer" end # roll the dice (0..self).to_a.inject{|x,y| x + rand(sides)}+self end end dice_expression = ARGV[0] # default number of rolls is 1, substitute d6 => 1d6 dice_expression = dice_expression.gsub(/(^|[^0-9)\s])(\s*d)/, '\11d') # d% => d100 dice_expression = dice_expression.gsub(/d%/,'d100 ') # this feels so dirty...substitute d => ** dice_expression = dice_expression.gsub(/d/, "**") (ARGV[1] || 1).to_i.times { print "#{eval(dice_expression)} " }

on 2006-01-08 19:58

on 2006-01-09 13:50

On Mon, Jan 09, 2006 at 03:57:20AM +0900, Paul Novak wrote: [...] } here is the Ruby: } } #!/usr/bin/env ruby } # } # roll.rb } # } } # fix up Fixnum to override ** with our desired d behavior } class Fixnum } def ** (sides) } # validation } if sides<1 } raise "Invalid sides value: '#{sides}', must be a positive Integer" } end } if self<1 } raise "Invalid number of rolls: '#{self}', must be a postitive Integer" } end } # roll the dice } (0..self).to_a.inject{|x,y| x + rand(sides)}+self I think I may be misunderstanding something here. You use 0..self, when I would think it would have to be either 0...self or 1..self to get the right number of rolls. Am I off? In my solution I used 1..self in much the same way. } end } end [...] --Greg

on 2006-01-09 15:05

No, the mis-understanding is all mine. You are right, otherwise it will include an extra roll. It should be: (1..self).to_a.inject(0){|x,y| x + rand(sides)}+self Further proof of the value of good unit tests. (Since (1..1).to_a returns a single-member array, you need to provide inject with an initial value to make it work.) Regards, Paul.

on 2006-01-09 23:43

Here's my own solution, which isn't nearly as pretty or clever as some of the stuff I've seen. I had started revising it, but it started looking more and more like Dennis Ranke's solution, which wasn't good since I was looking at his code to see how to do certain things in Ruby. =) In any case, I've decided to post my original, not-so-clever version. But I'm glad to see all the entries; it'll be interesting to write up a summary. I've certainly learned a lot. ---- class Dice TOKENS = { :integer => /[1-9][0-9]*/, :percent => /%/, :lparen => /\(/, :rparen => /\)/, :plus => /\+/, :minus => /-/, :times => /\*/, :divide => /\//, :dice => /d/ } class Lexer def initialize(str) @str = str end include Enumerable def each s = @str until s.empty? (tok, pat) = TOKENS.find { |tok, pat| s =~ pat && $`.empty? } raise "Bad input!" if tok.nil? yield(tok, $&) s = s[$&.length .. -1] end end end class Parser def initialize(tok) @tokens = tok.to_a @index = 0 @marks = [] end def action @marks.push(@index) end def commit @marks.pop end def rollback @index = @marks.last end def next tok = @tokens[@index] raise "Out of tokens!" if tok.nil? @index += 1 tok end end def initialize(str) @parser = Parser.new(Lexer.new(str)) @dice = expr end def roll @dice.call end def expr # fact expr_ expr_(fact) end def expr_(lhs) # '+' fact expr_ # '-' fact expr_ # nil @parser.action begin tok = @parser.next rescue res = lhs else case tok[0] when :plus rhs = fact res = expr_(proc { lhs.call + rhs.call }) when :minus rhs = fact res = expr_(proc { lhs.call - rhs.call }) else @parser.rollback res = lhs end end @parser.commit res end def fact # term fact_ fact_(term) end def fact_(lhs) # '*' term fact_ # '/' term fact_ # nil @parser.action begin tok = @parser.next rescue res = lhs else case tok[0] when :times rhs = term res = fact_(proc { lhs.call * rhs.call }) when :divide rhs = term res = fact_(proc { lhs.call / rhs.call }) else @parser.rollback res = lhs end end @parser.commit res end def term # dice # unit term_ begin res = dice(proc { 1 }) rescue res = term_(unit) end res end def term_(lhs) # dice term_ # nil begin res = term_(dice(lhs)) rescue res = lhs end res end def dice(lhs) # 'd' spec @parser.action tok = @parser.next case tok[0] when :dice rhs = spec res = proc { (1 .. lhs.call).inject(0) {|s,v| s += rand(rhs.call)+1 }} else @parser.rollback raise "Expected dice, found #{tok[0]} '#{tok[1]}'\n" end @parser.commit res end def spec # '%' # unit @parser.action tok = @parser.next case tok[0] when :percent res = proc { 100 } else @parser.rollback res = unit end @parser.commit res end def unit # '(' expr ')' # INT (non-zero, literal zero not allowed) @parser.action tok = @parser.next case tok[0] when :integer res = proc { tok[1].to_i } when :lparen begin res = expr tok = @parser.next raise unless tok[0] == :rparen rescue @parser.rollback raise "Expected (expr), found #{tok[0]} '#{tok[1]}'\n" end else @parser.rollback raise "Expected integer, found #{tok[0]} '#{tok[1]}'\n" end @parser.commit res end end # main d = Dice.new(ARGV[0] || "d6") (ARGV[1] || 1).to_i.times { print "#{d.roll} " }

on 2006-01-10 03:36

This isn't so much a solution to the quiz, but more of an addition to the whole dice rolling thing. Assume you have an array of numbers between 1-6... outputs the face of the dice in ascii art, one for each element in the array: class Array def to_dice logic = [ lambda{|n| '+-----+ '}, lambda{|n| (n>3 ? '|O ' : '| ')+(n>1 ? ' O| ' : ' | ')}, lambda{|n| (n==6 ? '|O ' : '| ')+ (n%2==1 ? 'O' : ' ')+(n==6 ? ' O| ' : ' | ')}, lambda{|n| (n>1 ? '|O ' : '| ')+(n>3 ? ' O| ' : ' | ')} ] str='' 5.times {|row| self.each {|n| str += logic[row%4].call(n) } str+="\n" } str end end #Example: puts [1,2,3,4,5,6].to_dice -Joby