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

Discussion in 'Ruby' started by Paul Novak, Jan 8, 2006.

  1. Paul Novak

    Paul Novak Guest

    ------=_Part_19248_25669739.1136746627816
    Content-Type: text/plain; charset=ISO-8859-1
    Content-Transfer-Encoding: quoted-printable
    Content-Disposition: inline

    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.=20
    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
    http://www.onlamp.com/pub/a/python/2002/07/11/recipes.html?page=3D3

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

    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 Integ=
    er"
    end
    # roll the dice
    (0..self).to_a.inject{|x,y| x + rand(sides)}+self
    end
    end

    dice_expression =3D ARGV[0]

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

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

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

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

    ------=_Part_19248_25669739.1136746627816
    Content-Type: application/octet-stream; name=roll.rb
    Content-Transfer-Encoding: 7bit
    Content-Disposition: attachment; filename="roll.rb"

    #!/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
    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)} " }

    ------=_Part_19248_25669739.1136746627816--
     
    Paul Novak, Jan 8, 2006
    #1
    1. Advertising

  2. Paul Novak

    Matthew Moss Guest

    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. =3D)

    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 =3D {
    :integer =3D> /[1-9][0-9]*/,
    :percent =3D> /%/,
    :lparen =3D> /\(/,
    :rparen =3D> /\)/,
    :plus =3D> /\+/,
    :minus =3D> /-/,
    :times =3D> /\*/,
    :divide =3D> /\//,
    :dice =3D> /d/
    }

    class Lexer
    def initialize(str)
    @str =3D str
    end

    include Enumerable
    def each
    s =3D @str
    until s.empty?
    (tok, pat) =3D TOKENS.find { |tok, pat| s =3D~ pat && $`.empty? =
    }
    raise "Bad input!" if tok.nil?
    yield(tok, $&)
    s =3D s[$&.length .. -1]
    end
    end
    end

    class Parser
    def initialize(tok)
    @tokens =3D tok.to_a
    @index =3D 0
    @marks =3D []
    end

    def action
    @marks.push(@index)
    end

    def commit
    @marks.pop
    end

    def rollback
    @index =3D @marks.last
    end

    def next
    tok =3D @tokens[@index]
    raise "Out of tokens!" if tok.nil?
    @index +=3D 1
    tok
    end
    end

    def initialize(str)
    @parser =3D Parser.new(Lexer.new(str))
    @dice =3D 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 =3D @parser.next
    rescue
    res =3D lhs
    else
    case tok[0]
    when :plus
    rhs =3D fact
    res =3D expr_(proc { lhs.call + rhs.call })
    when :minus
    rhs =3D fact
    res =3D expr_(proc { lhs.call - rhs.call })
    else
    @parser.rollback
    res =3D 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 =3D @parser.next
    rescue
    res =3D lhs
    else
    case tok[0]
    when :times
    rhs =3D term
    res =3D fact_(proc { lhs.call * rhs.call })
    when :divide
    rhs =3D term
    res =3D fact_(proc { lhs.call / rhs.call })
    else
    @parser.rollback
    res =3D lhs
    end
    end

    @parser.commit
    res
    end

    def term
    # dice
    # unit term_

    begin
    res =3D dice(proc { 1 })
    rescue
    res =3D term_(unit)
    end

    res
    end

    def term_(lhs)
    # dice term_
    # nil
    begin
    res =3D term_(dice(lhs))
    rescue
    res =3D lhs
    end

    res
    end

    def dice(lhs)
    # 'd' spec

    @parser.action

    tok =3D @parser.next
    case tok[0]
    when :dice
    rhs =3D spec
    res =3D proc { (1 .. lhs.call).inject(0) {|s,v| s +=3D rand(rhs.cal=
    l)+1 }}
    else
    @parser.rollback
    raise "Expected dice, found #{tok[0]} '#{tok[1]}'\n"
    end

    @parser.commit
    res
    end

    def spec
    # '%'
    # unit

    @parser.action

    tok =3D @parser.next
    case tok[0]
    when :percent
    res =3D proc { 100 }
    else
    @parser.rollback
    res =3D unit
    end

    @parser.commit
    res
    end

    def unit
    # '(' expr ')'
    # INT (non-zero, literal zero not allowed)

    @parser.action

    tok =3D @parser.next
    case tok[0]
    when :integer
    res =3D proc { tok[1].to_i }
    when :lparen
    begin
    res =3D expr
    tok =3D @parser.next
    raise unless tok[0] =3D=3D :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 =3D Dice.new(ARGV[0] || "d6")
    (ARGV[1] || 1).to_i.times { print "#{d.roll} " }
     
    Matthew Moss, Jan 9, 2006
    #2
    1. Advertising

  3. Paul Novak

    Joby Bednar Guest

    Re: Dice Roller (#61) We don't need no steenking leexer/parsers

    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
     
    Joby Bednar, Jan 10, 2006
    #3
    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
    Replies:
    106
    Views:
    894
    Morus Walter
    Jan 10, 2006
  2. John Earles

    [SOLUTION] Dice Roller (#61)

    John Earles, Jan 8, 2006, in forum: Ruby
    Replies:
    0
    Views:
    151
    John Earles
    Jan 8, 2006
  3. Stefan Walk

    [SOLUTION] Dice Roller

    Stefan Walk, Jan 8, 2006, in forum: Ruby
    Replies:
    0
    Views:
    91
    Stefan Walk
    Jan 8, 2006
  4. Matthew Moss

    [QUIZ.SUMMARY] Dice Roller (#61)

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

    Dice Roller

    Hd Pwnz0r, Sep 7, 2010, in forum: Ruby
    Replies:
    7
    Views:
    251
    Josh Cheek
    Sep 7, 2010
Loading...

Share This Page