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

-- 
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