[SOLUTION] Dice Roller (#61)

Discussion in 'Ruby' started by John Earles, Jan 8, 2006.

  1. John Earles

    John Earles Guest

    Hi!=A0 This is my first entry to a RUBY-QUIZ.=A0 Sure I could have opted =
    to use
    'eval', but I figured I might as well learn a little about expression
    parsing.=A0 Fun stuff!
    My entry is focused around The Shunting Yard Algorithm, allowing me to
    parse, transform and evaluate in the same step.=A0 I do not keep the =
    transform, so it is slightly inefficient to perform multiple 'rolls'.=A0 =
    is an optimization for another day.
    I also got to try out some new regexp stuff.=A0 Of particular note is =
    the use
    of (?=3Dd) in the expression used to search for d's that need an =
    implicit 1
    lvalue.=A0 I was having problems with 5dddd7 type commands until I =
    this allowed the target d NOT to be consumed by the regexp.
    As a final note I am a Ruby Newbie, and a Java developer by day, so any =
    or comments on my coding would be appreciated.=A0 Thanks!
    - John
    $DEBUG =3D false
    # Dice Roller entry point
    def roll_dice( dice_command, roll_count )
    =A0 begin
    =A0=A0=A0 puts "Executing #{roll_count} roll(s) of #{dice_command}"
    =A0=A0=A0 results, total =3D Dice.new( dice_command ).roll( roll_count )
    =A0=A0=A0 puts "Result: [#{results.join(', ')}] =3D> #{total}"=A0=20
    =A0 rescue Exception =3D> e
    =A0=A0=A0 puts "Roll error: #{e}"
    =A0 end
    class Dice=A0=20
    =A0 # operator =3D> [precendence, associativity]
    =A0 @@operators =3D { "d" =3D> [3, :right],
    =A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0 "*" =3D> [2, :left] =
    , "/" =3D> [2, :left],
    =A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0 "+" =3D> [1, :left] =
    , "-" =3D> [1, :left]
    =A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0 }
    =A0 # Initialize the Stacks and load the dice instructions
    =A0 def initialize( dice_command )=A0=A0=A0=20
    =A0=A0=A0 if $DEBUG
    =A0=A0=A0=A0=A0 alias :d :dnd_roll_loaded
    =A0=A0=A0 else
    =A0=A0=A0=A0=A0 alias :d :dnd_roll_random
    =A0=A0=A0 end
    =A0=A0=A0 @operator_stack, @value_stack =3D [], []
    =A0=A0=A0 prepare_instructions( dice_command )
    =A0 end
    =A0 def roll( roll_count )
    =A0=A0=A0 results =3D (1..roll_count).collect { execute }
    =A0=A0=A0 [results, results.inject {|sum, item| sum + item } || 0]
    =A0 end=A0=20
    =A0 private
    =A0 # The infix command is parsed into tokens and then executed using
    =A0=A0# The Shunting Yard Algorithm. Evaluation is done "on-the-fly" as
    =A0 # items are placed on the value stack (acting as the post-fix =
    =A0 def execute
    =A0=A0=A0 @operator_stack.clear
    =A0=A0=A0 @value_stack.clear
    =A0=A0=A0 # Process the tokens in L -> R order
    =A0=A0=A0 # Look for non-digit characters and numbers
    =A0=A0=A0 @instructions.scan(/\D|\d+/) do | token |
    =A0=A0=A0=A0=A0 case token
    =A0=A0=A0=A0=A0=A0=A0 when "("
    =A0=A0=A0=A0=A0=A0=A0=A0=A0 @operator_stack.push token
    =A0=A0=A0=A0=A0=A0=A0 when /\d+/ # any number
    =A0=A0=A0=A0=A0=A0=A0=A0=A0 @value_stack.push token.to_i
    =A0=A0=A0=A0=A0=A0=A0 when /[-\+*\/d]/ # the operators
    =A0=A0=A0=A0=A0=A0=A0=A0=A0 finished =3D false
    =A0=A0=A0=A0=A0=A0=A0=A0=A0 until finished or @operator_stack.empty?
    =A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0 if higher_operator(token)
    =A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0 finished =3D true
    =A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0 else
    =A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0 resolve_expression
    =A0=A0=A0=A0=A0=A0=A0=A0=A0=A0=A0 end
    =A0=A0=A0=A0=A0=A0=A0=A0=A0 end
    =A0=A0=A0=A0=A0 =A0=A0=A0=A0@operator_stack.push token
    =A0=A0=A0=A0=A0=A0=A0 when ")"
    =A0=A0=A0=A0=A0=A0=A0=A0=A0 resolve_expression while =
    @operator_stack.last !=3D "("
    =A0=A0=A0=A0=A0=A0=A0=A0=A0 @operator_stack.pop
    =A0=A0=A0=A0=A0=A0=A0 else
    =A0=A0=A0=A0=A0=A0=A0=A0=A0 raise "Invalid token found: #{token}"
    =A0=A0=A0=A0=A0 end
    =A0=A0=A0 end
    =A0=A0=A0 resolve_expression while !@operator_stack.empty?
    =A0=A0=A0 raise "Unexpected problem. #{@value_stack.size} values remain =
    execution." \
    =A0=A0=A0=A0=A0 unless @value_stack.size =3D=3D 1
    =A0=A0=A0 @value_stack.pop
    =A0 end=A0=A0=A0=20
    =A0 def resolve_expression
    =A0=A0=A0 opr, rhv, lhv =3D @operator_stack.pop, @value_stack.pop, =
    =A0=A0=A0 raise "No more values left for #{opr} to consume!" unless rhv =
    && lhv
    =A0=A0=A0 value =3D (opr =3D=3D "d") ? value =3D d( lhv, rhv ) : =
    lhv.send( opr, rhv )
    =A0=A0=A0 @value_stack.push value.to_i
    =A0 end
    =A0 def dnd_roll_random( roll_count, die_value )
    =A0=A0=A0 (1..roll_count).inject(0) { |value, item| value + ( =
    rand(die_value) + 1
    ) }
    =A0 end
    =A0 def dnd_roll_loaded( roll_count, die_value )
    =A0=A0=A0 roll_count * die_value
    =A0 end
    =A0 def higher_operator(opr)
    =A0=A0=A0 if associativity(opr) =3D=3D :left
    =A0=A0=A0=A0=A0 precedence(opr) > precedence(@operator_stack.last)
    =A0=A0=A0 else
    =A0=A0=A0=A0=A0 precedence(opr) >=3D =
    =A0=A0=A0 end
    =A0 end
    =A0 def precedence(opr)
    =A0=A0=A0 @@operators[opr] ? @@operators[opr][0] : 0
    =A0 end
    =A0 def associativity(opr)
    =A0=A0=A0 @@operators[opr] ? @@operators[opr][1] : :left
    =A0 end
    =A0 def prepare_instructions( dice_command )
    =A0=A0=A0 # 1) Eliminate all whitespace.
    =A0=A0=A0 # 2) Substitute d100 for d%=20
    =A0=A0=A0 # 3) Insert the implied 1 if a d is the first character
    =A0 =A0=A0#=A0=A0=A0 or is preceded by an operator other than ')'
    =A0=A0=A0 @instructions =3D dice_command.gsub(/\s+/, '')=A0=A0=A0=20
    =A0=A0=A0 @instructions.gsub!(/d%/, 'd100')
    =A0=A0=A0 @instructions.gsub!(/([-\+*\/(d]|\A)(?=3Dd)/, '\11')=A0=A0=20
    =A0=A0=A0 puts "Normalized instructions: #@instructions" if $DEBUG
    =A0=A0=A0 raise "Unmatched left / right parenthesis" unless \
    =A0=A0=A0=A0=A0 @instructions.scan(/\(/).size =3D=3D =
    =A0 end=A0=20
    # Argument parsing
    if $0 =3D=3D __FILE__
    =A0 raise "DiceRoller dice_command [roll_count=3D1]" unless =
    ARGV.length )
    =A0 roll_dice(ARGV[0], ARGV[1] ? ARGV[1].to_i : 1)

    No virus found in this outgoing message.
    Checked by AVG Free Edition.
    Version: 7.1.371 / Virus Database: 267.14.15/223 - Release Date: =
    John Earles, Jan 8, 2006
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. Ruby Quiz

    [QUIZ] Dice Roller (#61)

    Ruby Quiz, Jan 6, 2006, in forum: Ruby
    Morus Walter
    Jan 10, 2006
  2. Paul Novak
    Joby Bednar
    Jan 10, 2006
  3. Stefan Walk

    [SOLUTION] Dice Roller

    Stefan Walk, Jan 8, 2006, in forum: Ruby
    Stefan Walk
    Jan 8, 2006
  4. Matthew Moss

    [QUIZ.SUMMARY] Dice Roller (#61)

    Matthew Moss, Jan 12, 2006, in forum: Ruby
    J. Ryan Sobol
    Jan 13, 2006
  5. Hd Pwnz0r

    Dice Roller

    Hd Pwnz0r, Sep 7, 2010, in forum: Ruby
    Josh Cheek
    Sep 7, 2010

Share This Page