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

**URL:** <https://www.ruby-forum.com/t/dice-roller-61-we-dont-need-no-steenking-leexer-parsers/51888>\
**Category:** Ruby\
**Created:** [January 8, 2006, 7:58pm UTC](https://www.ruby-forum.com/t/dice-roller-61-we-dont-need-no-steenking-leexer-parsers/51888 "2006-01-08T19:58:54Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![nanouk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/n/da6949/32.png) [@nanouk](https://www.ruby-forum.com/u/nanouk)\
**Post date:** [January 8, 2006, 7:58pm UTC](https://www.ruby-forum.com/t/dice-roller-61-we-dont-need-no-steenking-leexer-parsers/51888/1 "2006-01-08T19:58:54Z")

</div>

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

> **[Radar - O’Reilly](https://www.oreilly.com/radar/?page=3)**
>
> Now, next, and beyond: Tracking need-to-know trends at the intersection of business and technology

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

---

<div class="post-metadata">

**Author:** ![nanouk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/n/da6949/32.png) [@nanouk](https://www.ruby-forum.com/u/nanouk)\
**Post date:** [January 9, 2006, 1:50pm UTC](https://www.ruby-forum.com/t/dice-roller-61-we-dont-need-no-steenking-leexer-parsers/51888/2 "2006-01-09T13:50:44Z")

</div>

On Mon, Jan 09, 2006 at 03:57:20AM +0900, Paul N. 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

---

<div class="post-metadata">

**Author:** ![nanouk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/n/da6949/32.png) [@nanouk](https://www.ruby-forum.com/u/nanouk)\
**Post date:** [January 9, 2006, 3:05pm UTC](https://www.ruby-forum.com/t/dice-roller-61-we-dont-need-no-steenking-leexer-parsers/51888/3 "2006-01-09T15:05:37Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![nanouk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/n/da6949/32.png) [@nanouk](https://www.ruby-forum.com/u/nanouk)\
**Post date:** [January 10, 2006, 3:36am UTC](https://www.ruby-forum.com/t/dice-roller-61-we-dont-need-no-steenking-leexer-parsers/51888/4 "2006-01-10T03:36:09Z")

</div>

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

---

<div class="post-metadata">

**Author:** ![nanouk](https://www.ruby-forum.com/letter_avatar_proxy/v4/letter/n/da6949/32.png) [@nanouk](https://www.ruby-forum.com/u/nanouk)\
**Post date:** [January 9, 2006, 11:43pm UTC](https://www.ruby-forum.com/t/dice-roller-61-we-dont-need-no-steenking-leexer-parsers/51888/5 "2006-01-09T23:43:51Z")

</div>

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