[SOLUTION] Dice Roller (#61)

J

John Earles

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!
=A0
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 =
'postfix'
transform, so it is slightly inefficient to perform multiple 'rolls'.=A0 =
That
is an optimization for another day.
=A0
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 =
discovered
this allowed the target d NOT to be consumed by the regexp.
=A0
As a final note I am a Ruby Newbie, and a Java developer by day, so any =
tips
or comments on my coding would be appreciated.=A0 Thanks!
=A0
- John
=A0
-------------------
=A0
$DEBUG =3D false
=A0
# 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
end
=A0
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
=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=20
=A0=A0=A0 @operator_stack, @value_stack =3D [], []
=A0=A0=A0 prepare_instructions( dice_command )
=A0 end
=A0
=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=20
=A0 private
=A0=20
=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 =
"output").
=A0 def execute
=A0=A0=A0 @operator_stack.clear
=A0=A0=A0 @value_stack.clear
=A0
=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=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=A0=A0=20
=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=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=A0=A0=20
=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=A0 resolve_expression while !@operator_stack.empty?
=A0
=A0=A0=A0 raise "Unexpected problem. #{@value_stack.size} values remain =
after
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
=A0 def resolve_expression
=A0=A0=A0 opr, rhv, lhv =3D @operator_stack.pop, @value_stack.pop, =
@value_stack.pop
=A0=A0=A0 raise "No more values left for #{opr} to consume!" unless rhv =
&& lhv
=A0=A0=A0=20
=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=20
=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
=A0 def dnd_roll_loaded( roll_count, die_value )
=A0=A0=A0 roll_count * die_value
=A0 end
=A0=20
=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 =
precedence(@operator_stack.last)=A0=A0=A0=20
=A0=A0=A0 end
=A0 end
=A0=20
=A0 def precedence(opr)
=A0=A0=A0 @@operators[opr] ? @@operators[opr][0] : 0
=A0 end
=A0
=A0 def associativity(opr)
=A0=A0=A0 @@operators[opr] ? @@operators[opr][1] : :left
=A0 end
=A0
=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=20
=A0=A0=A0 raise "Unmatched left / right parenthesis" unless \
=A0=A0=A0=A0=A0 @instructions.scan(/\(/).size =3D=3D =
@instructions.scan(/\)/).size
=A0 end=A0=20
end
=A0
# Argument parsing
if $0 =3D=3D __FILE__
=A0 raise "DiceRoller dice_command [roll_count=3D1]" unless =
(1..2).include?(
ARGV.length )
=A0 roll_dice(ARGV[0], ARGV[1] ? ARGV[1].to_i : 1)
end
=A0

--=20
No virus found in this outgoing message.
Checked by AVG Free Edition.
Version: 7.1.371 / Virus Database: 267.14.15/223 - Release Date: =
06/01/2006
=20
 

Ask a Question

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

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,743
Messages
2,569,478
Members
44,899
Latest member
RodneyMcAu

Latest Threads

Top