matthew.moss.coder
1/9/2006 10:43:00 PM
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} " }