From: Austin Ziegler Date: 2006-01-09T05:29:15+09:00 Subject: Re: [QUIZ] Dice Roller (#61) ------=_Part_81463_5176244.1136752140088 Content-Type: text/plain; charset=ISO-8859-1 Content-Transfer-Encoding: quoted-printable Content-Disposition: inline Here is my submission. Yes, this is my first completed Ruby Quiz ;) Thanks to Eric Mahurin's syntax.rb for making this work. I've attached it as well, because it's not easily accessible otherwise ;) -austin ------=_Part_81463_5176244.1136752140088 Content-Type: application/octet-stream; name=roll.rb Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="roll.rb" #! /usr/bin/env ruby require 'syntax' # Ruby Quiz #61 by Matthew D Moss # Submission by Austin Ziegler # # > roll.rb "3d6" 6 # 12 7 13 16 11 17 # # Or, for something more complicated: # # > roll.rb "(5d5-4)d(16/d4)+3" # 31 # # The main code of roll.rb should look something like this: # # d = Dice.new(ARGV[0]) # (ARGV[1] || 1).to_i.times { print "#{d.roll} " } # # I've implemented it with a modified BNF and Eric Mahurin's syntax.rb. # # integer : "1" - "9" [ "0" - "9" ]* # white : [ " " | "\t" | "\n" ]* # unit : "(" expr ")" | integer # dice : "%" | unit # term : unit? [ "d" dice ]* # fact : term [ "*" | "/" term ]* # expr : fact [ "+" | "-" fact ]* # # I have also modified the core function as: # # e = ARGV[0] # c = (ARGV[1] || 1).to_i # d = Dice.new(e) # puts d.roll(c).join(" ") NULL = Syntax::NULL INF = +1.0 / 0.0 LOOP0 = (0 .. INF) LOOP1 = (1 .. INF) class Dice def initialize(dice) @dice = dice @dice_n = "#{@dice}\n" integer = ((("1" .. "9") * 1) + ("0" .. "9") * LOOP0).qualify do |m| m.to_s.to_i end white = ((" " | "\t" | "\n") * LOOP0).qualify { TRUE } expr = Syntax::Pass.new unit = ("(" + expr + ")").qualify { |m| m[1] } | integer.qualify { |m| m } dice = "%".qualify { |m| 100 } | unit.qualify { |m| m } term = ((unit | NULL) + (white + "d" + white + dice) * LOOP0).qualify do |m| sum = 0 if m[1].nil? rep = 1 xpr = m[0] elsif m[1].empty? sum = m[0] xpr = m[1] else rep = m[0] xpr = m[1] end xpr.each do |mm| case mm[0] when "d": sum = (1..rep).inject(sum) { |s, i| s + (rand(mm[1]) + 1) } else sum += rep end end sum end fact = (term + (white + ("*" | "/") + white + term) * LOOP0).qualify do |m| prod = m[0] m[1].each do |mm| case mm[0] when "*": prod *= mm[1] when "/": prod /= mm[1] end end prod end expr << (white + fact + (white + ("+" | "-") + white + fact) * LOOP0).qualify do |m| sum = m[0] m[1].each do |mm| case mm[0] when "+": sum += mm[1] when "-": sum -= mm[1] end end sum end @die_expr = expr end def roll(times = 1) (1 .. times).map { @die_expr === RandomAccessStream.new(@dice_n) } end def inspect @dice end end expr = ARGV[0] count = (ARGV[1] || 1).to_i if expr d = Dice.new(expr) puts d.roll(count).join(' ') else require 'test/unit' class TestDice < Test::Unit::TestCase def test_simple assert (1..4).include?(Dice.new("d4").roll) assert (1..6).include?(Dice.new("d6").roll) assert (1..8).include?(Dice.new("d8").roll) assert (1..10).include?(Dice.new("d10").roll) assert (1..12).include?(Dice.new("d12").roll) assert (1..20).include?(Dice.new("d20").roll) assert (1..30).include?(Dice.new("d30").roll) assert (1..100).include?(Dice.new("d100").roll) assert (1..100).include?(Dice.new("d%").roll) end def test_3d6 assert (3..18).include?(Dice.new("3d6").roll) end def test_complex assert (5..25).include?(Dice.new("5d5").roll) assert (1..21).include?(Dice.new("5d5-4").roll) assert [4, 5, 8, 16].include?(Dice.new("16/d4").roll) assert (1..336).include?(Dice.new("(5d5-4)d(16/d4)").roll) assert (4..339).include?(Dice.new("(5d5-4)d(16/d4)+3").roll) end end end ------=_Part_81463_5176244.1136752140088 Content-Type: application/octet-stream; name=syntax.rb Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="syntax.rb" # Classes in this module allow one to define BNF-like grammar directly in # Ruby # Author: Eric Mahurin # License: free, but you are at your own risk if you use it module Syntax # base class where common operators are defined class Base def |(other) Alteration.new(self,other) end def +(other) Sequence.new(self,other) end def *(multiplier) Repeat.new(self,multiplier) end def +@ Positive.new(self) end def -@ Negative.new(self) end def qualify(*args,&code) Qualify.new(self,*args,&code) end end # just passes the syntax through - needed for recursive syntax class Pass < Base def initialize(syntax=NULL) @syntax = if (syntax.kind_of?Base) syntax else Verbatim.new(syntax) end end def <<(syntax) initialize(syntax) end def ===(stream) @syntax===stream end end # generic code matches to the stream (first arg to the code) # [] operator allows additional arguments to be passed to the code class Code < Base def initialize(*args,&code) @args = args @code = code end def ===(stream,*args) # passing args here will bypass creating a new object (match = @code[stream,*(@args+args)]) || stream.buffered || raise(Error.new(stream,"a semantic error")) match end def [](*args) self.class.new(*(@args+args),&@code) end end # qualify the match with some code that takes the match # [] operator allows additional arguments to be passed to the code class Qualify < Base def initialize(syntax=NULL,*args,&code) @syntax = if (syntax.kind_of?Base) syntax else Verbatim.new(syntax) end @args = args @code = code end def ===(stream,*args) # passing args here will bypass creating a new object (match = (@syntax===stream)) || (return match) (match = @code[match,*(@args+args)]) || stream.buffered || raise(Error.new(stream,"a semantic qualification error")) match end def [](*args) self.class.new(@syntax,*(@args+args),&@code) end end # sequence of syntaxes class Sequence < Base def initialize(*syntaxes) @syntax = syntaxes.collect do |syntax| if (syntax.kind_of?Base) syntax else Verbatim.new(syntax) end end end def +(other) self.class.new(*(@syntax+[other])) # pull in multiple sequence items end def <<(other) @syntax << ((other.kind_of?Base)?other:Verbatim.new(other)) end def ===(stream) matches = [] @syntax.each do |syntax| match = (syntax===stream) if (!match) matches=NIL break end matches << match if match!=TRUE end matches end end # alternative syntaxes class Alteration < Base def initialize(*syntaxes) @syntax = syntaxes.collect do |syntax| if (syntax.kind_of?Base) syntax else Verbatim.new(syntax) end end end def |(other) self.class.new(*(@syntax+[other])) # pull in multiple alteration items end def <<(other) @syntax << ((other.kind_of?Base)?other:Verbatim.new(other)) end def ===(stream) match = nil @syntax.detect do |syntax| match = stream.buffer { |stream| syntax===stream } end match || stream.buffered || raise(Error.new(stream,nil,"an alteration")) match end alias =~ === end # repeating syntax class Repeat < Base def initialize(syntax,multiplier) @syntax = if (syntax.kind_of?Base) syntax else Verbatim.new(syntax) end @repeat = if (multiplier.kind_of?Proc) multiplier else lambda do |matches| compare = (multiplier<=>(matches.length+1)) if (compare==0) compare = (multiplier<=>matches.length) end compare end end end def ===(stream) matches = [] while ((compare=@repeat[matches])>=0) if (compare>0) unless (match = (@syntax===stream)) return NIL end else unless (match = stream.buffer { |stream| @syntax===stream }) break end end matches << match end # filter out simple TRUE elements matches = matches.find_all { |match| match!=TRUE } matches end end # positive syntactic predicate class Positive < Base def initialize(syntax) @syntax = if (syntax.kind_of?Base) syntax else Verbatim.new(syntax) end end def ===(stream) stream.buffer { |stream| match = (@syntax===stream); FALSE } if (match) TRUE else stream.buffered || raise(Error.new(stream,nil,"a positive syntatic predicate")) FALSE end end end # negative syntactic predicate class Negative < Positive def ===(stream) stream.buffer { |stream| match = (@syntax===stream); FALSE } if (!match) TRUE else stream.buffered || raise(Error.new(stream,nil,"a negative syntatic predicate")) FALSE end end end # all atoms can also use ~ to invert what's matches # element match (uses === to match) class Atom < Base def initialize(pattern,length=NIL,invert=FALSE) @pattern = pattern @length = length @invert = invert end def ~@ new(pattern,length,!invert) end def ===(stream) element = stream.get(@length) match = (@pattern===element) match = !match if (@invert) if (match==TRUE) element || TRUE else match || begin stream.buffered || raise(Error.new(stream,element.inspect,@pattern.inspect)) FALSE end end end end # element set (uses include? to match) class Set < Atom def ===(stream) element = stream.get(@length) match = @pattern.include?(element) match = !match if (@invert) if (match==TRUE) element || TRUE else match || begin stream.buffered || raise(Error.new(stream,element.inspect,"one of these: #{@pattern.to_s}")) FALSE end end end end # element lookup array or hash (uses [] to match) # translation will occur if the lookup returns anything but TRUE class Lookup < Atom def =~(stream) element = stream.get(@length) match = @pattern[element] match = !match if (@invert) if (match==TRUE) element || TRUE else match || begin stream.buffered || raise(Error.new(stream,element.inspect,"one of these: #{@pattern.keys.to_s}")) FALSE end end end end # element sequence that knows its length class Verbatim < Atom def initialize(pattern,invert=FALSE) @pattern = pattern @invert = invert end def ~@ new(pattern,!invert) end def ===(stream) element = stream.get(@pattern.length) if (element) match = (@pattern===element) match = !match if (@invert) else match = FALSE end if (match==TRUE) element || TRUE else match || begin stream.buffered || raise(Error.new(stream,element.inspect,@pattern.inspect)) FALSE end end end end # any element class Any < Atom def initialize(length=NIL,invert=FALSE) @length = length @invert = invert end def ~@ # create a never matching Atom new(length,!invert) end def ===(stream) element = stream.get(@length) !@invert && element end end ANY = Any.new # zero length constants FLUSH = Code.new { |stream| stream.flush; TRUE } FAIL = Code.new { FALSE } NULL = Code.new { TRUE } NULLS = Code.new { [] } EOF = Code.new { !(element = stream.get) } # exception class for handling syntax errors class Error < RuntimeError attr_accessor(:stream,:found,:expected) def initialize(stream=nil,found=nil,expected=nil) @stream = stream @found = found @expected = expected end def to_s err = [super] err << "found #{found.to_s}" if found err << "expected #{expected.to_s}" if expected err << stream.location.to_s if stream err * ", " end end end # class acts like an iterator over a string/array/etc except that using # buffer allows one go back to a certain point another class could be # designed to work on an IO/File class RandomAccessStream def initialize(s,pos=0) @s = s @pos = pos @buffered = NIL self end def get(termination=NIL) if (@pos>=@s.length) # end of file/string/array element = NIL elsif (!termination) # read one character/element element = @s[@pos] @pos += 1 else # read a sub-string/sub-array pos1 = (termination.kind_of?(Integer)) ? @pos+termination : (t = @s.index(termination,@pos)) ? t+termination.length : @s.length element = @s[@pos...pos1] @pos = pos1 end element end def buffer(&code) old_buffered = @buffered @buffered = @pos if (!@buffered || @pos<@buffered) pos = @pos match = NIL match = code[self] if (@buffered && @buffered<=pos) @buffered = old_buffered elsif (!match) raise(IndexError,"need to rewind buffer, but it was flushed") end @pos = pos if !match match end def flush @buffered = NIL end def buffered @buffered ? TRUE : FALSE end def location "index #{@pos} in #{@s.inspect}" end end # Put stuff in String to have Syntax objects magically appear. class String def |(other) Syntax::Verbatim.new(self)|other end def +@ +Syntax::Verbatim.new(self) end def -@ -Syntax::Verbatim.new(self) end alias _repeat * def *(other) if (other.kind_of?Numeric) _repeat(other) else Syntax::Verbatim.new(self)*other end end alias _concat + def +(other) if (other.kind_of?String) _concat(other) else Syntax::Verbatim.new(self)+other end end def ===(other) if (other.kind_of?String) self==other else Syntax::Verbatim.new(self)===other end end def qualify(&code) Syntax::Verbatim.new(self).qualify(&code) end end # Allow an Array to look more like a Hash with keys and values class Array def keys (0...length).find_all { |i| self[i] } end def values find_all { | element | element } end end # make things fully comparable to Ranges # also * makes a Syntax class Range include Comparable def <=>(other) if (other=self.end else other>self.end end) -1 else 0 end end alias _old_equal == def ==(other) if (other.kind_of?Range) # undocumented previous functionality _old_equal(other) else (self<=>other)==0 end end def *(other) Syntax::Atom.new(self,1)*other end end ------=_Part_81463_5176244.1136752140088--