[QUIZ.SOLUTION] Dice Roller (#61) We don't need no steenking leexer/parsers

Leexer/parsers? We ain't got no Leexer/parsers. We don't need no
Leexer/parsers. I don't have to show you any steenking Leexer/parser.
Just call eval and use Ruby's fine lexer/parser (apologies to Mel
Brooks, John Huston and Banditos Mexicanos everywhere).

This approach uses regex substitutions to first munge the input
expression to deal with the default cases (like d6 to 1d6 and 1% to
1d100), then it substitutes ** for d and hands it over to the
evaluator and prints the result.

Eval does what we want after an override of Fixnum.** to give us our
dice-rolling d-operator behavior. Conveniently, ** has the desired
precedence relative to the other operators, plus it is binary and
left-associative. This feels so evil. Seduced by the Dark Side I am.

Note that we get lot's of additional behavior we don't need, but the
original spec said the BNF was incomplete, so I was lazy and left the
undef[ining] of >>, << et al is 'as an exercise...' I don't know
enough D&D to know if they might be useful.

BTW. In the spirit of 'why can't we all just get along?': the dice
rolling algorithm is ported from this Python code at

from random import randrange
def dice(num,sides):
  return reduce(lambda x,y,s=sides:x + randrange(s), range(num+1))+num

here is the Ruby:

#!/usr/bin/env ruby

roll.rb (806 Bytes)

···

#
# roll.rb
#

# fix up Fixnum to override ** with our desired d behavior
class Fixnum
  def ** (sides)
    # validation
    if sides<1
      raise "Invalid sides value: '#{sides}', must be a positive Integer"
    end
    if self<1
      raise "Invalid number of rolls: '#{self}', must be a postitive Integer"
    end
    # roll the dice
     (0..self).to_a.inject{|x,y| x + rand(sides)}+self
  end
end

dice_expression = ARGV[0]

# default number of rolls is 1, substitute d6 => 1d6
dice_expression = dice_expression.gsub(/(^|[^0-9)\s])(\s*d)/, '\11d')

# d% => d100
dice_expression = dice_expression.gsub(/d%/,'d100 ')

# this feels so dirty...substitute d => **
dice_expression = dice_expression.gsub(/d/, "**")

(ARGV[1] || 1).to_i.times { print "#{eval(dice_expression)} " }

[...]
} here is the Ruby:
}
} #!/usr/bin/env ruby
} #
} # roll.rb
} #
}
} # fix up Fixnum to override ** with our desired d behavior
} class Fixnum
} def ** (sides)
} # validation
} if sides<1
} raise "Invalid sides value: '#{sides}', must be a positive Integer"
} end
} if self<1
} raise "Invalid number of rolls: '#{self}', must be a postitive Integer"
} end
} # roll the dice
} (0..self).to_a.inject{|x,y| x + rand(sides)}+self

I think I may be misunderstanding something here. You use 0..self, when I
would think it would have to be either 0...self or 1..self to get the right
number of rolls. Am I off? In my solution I used 1..self in much the same
way.

} end
} end
[...]

--Greg

···

On Mon, Jan 09, 2006 at 03:57:20AM +0900, Paul Novak wrote:

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

This isn't so much a solution to the quiz, but more of an addition to
the whole dice rolling thing. Assume you have an array of numbers
between 1-6... outputs the face of the dice in ascii art, one for each
element in the array:

class Array
  def to_dice
    logic = [
    lambda{|n| '+-----+ '},
    lambda{|n| (n>3 ? '|O ' : '| ')+(n>1 ? ' O| ' : ' | ')},
    lambda{|n| (n==6 ? '|O ' : '| ')+
       (n%2==1 ? 'O' : ' ')+(n==6 ? ' O| ' : ' | ')},
    lambda{|n| (n>1 ? '|O ' : '| ')+(n>3 ? ' O| ' : ' | ')}
    ]

    str=''
    5.times {|row|
      self.each {|n| str += logic[row%4].call(n) }
      str+="\n"
    }
    str
    end
end

#Example:
puts [1,2,3,4,5,6].to_dice

-Joby

No, the mis-understanding is all mine. You are right, otherwise it
will include an extra roll. It should be:
(1..self).to_a.inject(0){|x,y| x + rand(sides)}+self

Further proof of the value of good unit tests. (Since (1..1).to_a
returns a single-member array, you need to provide inject with an
initial value to make it work.)

Regards,

Paul.

···

On 1/9/06, Gregory Seidman <gsslist+ruby@anthropohedron.net> wrote:

} # roll the dice
} (0..self).to_a.inject{|x,y| x + rand(sides)}+self

I think I may be misunderstanding something here. You use 0..self, when I
would think it would have to be either 0...self or 1..self to get the right
number of rolls. Am I off? In my solution I used 1..self in much the same
way.