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

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
4d7bf65b4675b3e9575617929ab123f1?d=identicon&s=25 Paul Novak (Guest)
on 2006-01-08 19:58
(Received via mailing list)
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
http://www.onlamp.com/pub/a/python/2002/07/11/reci...

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
#

# 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)} " }
Bf6862e2a409078e13a3979c00bba1d6?d=identicon&s=25 Gregory Seidman (Guest)
on 2006-01-09 13:50
(Received via mailing list)
On Mon, Jan 09, 2006 at 03:57:20AM +0900, Paul Novak wrote:
[...]
} 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
4d7bf65b4675b3e9575617929ab123f1?d=identicon&s=25 Paul Novak (Guest)
on 2006-01-09 15:05
(Received via mailing list)
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.
81d609425e306219d54d793a0ad98bce?d=identicon&s=25 Matthew Moss (Guest)
on 2006-01-09 23:43
(Received via mailing list)
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}  " }
D05623be23ac664b917ddd66bb3ed4ae?d=identicon&s=25 Joby Bednar (Guest)
on 2006-01-10 03:36
(Received via mailing list)
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
This topic is locked and can not be replied to.