From: Ross Bamford Date: 2006-01-09T04:43:07+09:00 Subject: Re: [QUIZ] Dice Roller (#61) Hi, This is my quiz entry for Ruby Quiz 61 (Dice Roller). It's actually the second idea I had, after starting out with Antlr (I still finished that one, because I wanted to get to grips with Antlr anyway - I happened to be playing with it when this quiz came out :)). I've bundled both this entry and that one at: http://roscopeco.co.uk/code/ruby-quiz-entries/quiz61-dice-roller.tar.gz Anyway, back to my real entry. I guess I took the short-cut route to the dice-roller, and instead of parsing out the expressions I instead decided to 'coerce' them to Ruby code, by just implementing the 'd' operator with a 'rolls' method on Fixnum, and using gsub to convert the input expression. d3*2 => 1.rolls(3)*2 (5d5-4)d(16/d4)+3 => (5.rolls(5)-4).rolls(16/1.rolls(4))+3 d%*7 => 1.rolls(100)*7 This is implemented in the DiceRoller.parse method, which returns the string. You can just 'eval' this of course, or use the 'roll' method (also provided as a more convenient class method that wraps the whole thing up for you) to do it. Ruby runs the expression, and gives back the result. I almost feel like I cheated...? As well as the main 'roll.rb' I also included a separate utility that uses loaded dice to find min/max achievable. All three files can be executed, and if you enable --verbose mode on Ruby you'll see the dice rolls and parsed expressions. ----------[MAIN (roll.rb)]----------- #!/usr/local/bin/ruby # # Ruby Quiz 61, the quick way # by Ross Bamford # Just a debugging helper module Kernel def dbg(*s) puts(*s) if $VERBOSE|| @dice_debug end attr_writer :dice_debug def dice_debug?; @dice_debug; end end # Need to implement the 'rolls' method. Wish it didn't have to # be on Fixnum but for this it makes the parsing *lots* easier. class Fixnum def self.roll_proc=(blk) @roll_proc = blk end def self.roll_proc @roll_proc ||= method(:rand).to_proc end def rolls(sides) (1..self).inject(0) { |s,v| s + Fixnum.roll_proc[sides] } end end # Here's the roller. class DiceRoller class << self # Completely wrap up a roll def roll(expr, count = 1, debug = false) new(expr,debug).roll(count) end # The main 'parse' method. Just really coerces the code to Ruby # and then compiles to a block that returns the result. def parse(expr) # very general check here. Will pass lots of invalid syntax, # but hopefully that won't compile later. This removes the # possibility of using variables and the like, but that wasn't # required anyway. The regexps would be a bit more difficult # if we wanted to do that. raise SyntaxError, "'#{expr}' is not a valid dice expression", [] if expr =~ /[^d\d\(\)\+\-\*\/\%]|[^d]%|d-|\*\*/ # Rubify! s = expr.gsub( /([^\d\)])d|^d/, '\11d') # fix e.g. 'd5' and '33+d3' to '1.d5' and '33+1d3' s.gsub!( /d%/, 'd(100)' ) # fix e.g. 'd%' to 'd(100)' s.gsub!( /d([\+\-]?\d+)/, '.rolls(\1)') # fix e.g. '3d8' to '3.rolls(8) (*) s.gsub!( /d\(/, '.rolls(') # fix e.g. '2d(5+5)' to '2.rolls(5+5)' # (*) This line treats + or - straight after 'd' as a unary sign, # so you can have '3d-8*7' => '3.rolls(+8)-7' # This would throw a runtime error from rolls, though. # Make a block. Doing it this way gets Ruby to compile it now # so we'll reliably get fail fast on bad syntax. dbg "PARS: #{expr} => #{s}" begin eval("lambda { #{s} }") rescue Exception => ex raise SyntaxError, "#{expr} is not a valid dice expression", [] end end end # Create a new roller that rolls the specified dice expression def initialize(expr, debug = false) dbg "NEW : #{to_s}: #{expr} => #{expr_code}" @expr_code, @expr, @debug = expr, DiceRoller.parse(expr), debug end # Get hold of the original expression and compiled block, respectively attr_reader :expr_code, :expr # Roll this roller count times def roll(count = 1) dbg " ROLL: #{to_s}: #{count} times" r = (1..count).inject([]) do |totals,v| this_r = begin expr.call rescue Exception => ex raise RuntimeError, "'#{expr_code}' raised: #{ex}", [] end dbg " r#{v}: rolled #{this_r}" totals << this_r end r.length < 2 ? r[0] : r end end # Library usage: # # require 'roll' # # # is the default: # # Fixnum.roll_proc = lambda { |sides| rand(sides) + 1 } # # DiceRoller.roll('1+2*d6') # # d = DiceRoller.new('((3d%)+8*(d(5*5)))') # d.roll(5) # # d = DiceRoller.new('45*10d3') # debug # # # ... or # one_roll = d.expr.call # # command-line usage if $0 == __FILE__ unless expr = ARGV[0] puts "Usage: ruby [--verbose] roll.rb expr [count]" else (ARGV[1] || 1).to_i.times { print "#{DiceRoller.roll(expr)} " } print "\n" end end ===================================== -----------[UTIL: minmax.rb]---------- #!/usr/local/bin/ruby require 'roll' LOW_DICE = lambda { |sides| 1 } HIGH_DICE = lambda { |sides| sides } # Adds a 'minmax' method that uses loaded dice to find # min/max achievable for a given expression. # # Obviously not thread safe, but then neither is the # whole thing ;D class DiceRoller def self.minmax(expr) old_proc = Fixnum.roll_proc Fixnum.roll_proc = LOW_DICE low = DiceRoller.roll(expr) Fixnum.roll_proc = HIGH_DICE high = DiceRoller.roll(expr) Fixnum.roll_proc = old_proc [low,high] end end if $0 == __FILE__ if expr = ARGV[0] min, max = DiceRoller.minmax(expr) puts "Expression: #{expr} ; min / max = #{min} / #{max}" else puts "Usage: minmax.rb " end end ===================================== -----------[TEST: test.rb]---------- #!/usr/local/bin/ruby # # Ruby Quiz, number 61 - Dice roller # This entry by Ross Bamford (roscoroscopeco.co.uk) require 'test/unit' require 'roll' ASSERTS = { '1' => 1, '1+2' => 3, '1+3*4' => 13, '1*2+4/8-1' => 1, 'd1' => 1, '1d1' => 1, 'd10' => 10, '1d10' => 10, '10d10' => 100, 'd3*2' => 6, '5d6d7' => 210, # left assoc '2d3+8' => 14, # not 22 '(2d(3+8))' => 22, # not 14 'd3+d3' => 6, '33+d3+10' => 46, 'd2*2d4' => 16, 'd(2*2)+d4' => 8, 'd%' => 100, '2d%' => 200, 'd%*7' => 700, '14+3*10d2' => 74, '(5d5-4)d(16/d4)+3' => 87, #25d4 + 3 '3d+8/8' => 3 #3d(+8)/8 } ERRORS = { # Bad input, all should raise exception 'd' => SyntaxError, '3d' => SyntaxError, '3d-8' => SyntaxError, # - # of sides '3ddd6' => SyntaxError, '3%2' => SyntaxError, '%d' => SyntaxError, '+' => SyntaxError, '4**3' => SyntaxError } # bit messy, but can't get class methods on Fixnum Fixnum.roll_proc = lambda { |sides| sides } class TestDiceRoller < Test::Unit::TestCase def initialize(*args) super end ASSERTS.each do |expr, expect| eval <<-EOC def test_good_#{expr.hash.abs} expr, expect = #{expr.inspect}, #{expect.inspect} puts "\n-----------------------\n\#{expr} => \#{expect}" if $VERBOSE res = DiceRoller.roll(expr) puts "Returned \#{res}\n-----------------------" if $VERBOSE assert_equal expect, res end EOC end ERRORS.each do |expr, expect| eval <<-EOC def test_error_#{expr.hash.abs} expr, expect = #{expr.inspect}, #{expect.inspect} assert_raise(#{expect}) do puts "\n-----------------------\n\#{expr} => \#{expect}" if $VERBOSE res = DiceRoller.roll(expr) puts "Returned \#{res}\n-----------------------" if $VERBOSE end end EOC end end ===================================== -- Ross Bamford - rosco@roscopeco.remove.co.uk