Dice Roller (#61)

Hi,

I finally finished a Ruby Q.! Albeit by means of a goofy
method_missing hack. But it was fun.

Here 'tis:

#!/usr/bin/env ruby

expr = ARGV[0] || abort(‘Please specify expression, such as
“(5d5-4)d(16/d4)+3”’)
expr = expr.dup # unfreeze

class Object
def method_missing(name, args)
# Intercept dieroll-method calls, like :5d5, and compute
# their value:
if name.to_s =~ /^
(\d
)d(\d+)$/
rolls = [1, $1.to_i].max
nsides = $2.to_i
(1…rolls).inject(0) {|sum,n| sum + (rand(nsides) + 1)}
else
raise NameError, [name, *args].inspect
end
end
end

class String
def die_to_meth
# Prepend underscore to die specs, like (5d5-4) -> (5d5-4)
# making them grist for our method_missing mill:
self.gsub(/\b([0-9]d[0-9])\b/, '
\1’)
end
end

expr.gsub!(/d%/,“d100”) # d% support

inner->outer reduce

true while expr.gsub!(/(([^()]*))/) {eval($1.die_to_meth)}
p eval(expr.die_to_meth)

Gregory S. a écrit :

} >
} > Feel free to do what you like.
That is only true if the d operator is right-associative. According to the
original spec, it is left-associative and, therefore, 3dddd6 is a syntax
error. Mind you, the second option Matthew gave is to make it
right-associative, which is what you have done. I chose to treat it as a
syntax error.

Well, I disagree …
As I see the things, there are two “d” operators :

  • one left-associative infix operator (i.e. 3d6)
  • one prefix operator (i.e. d6)

The first “d” in your example is the infix one, thus left-associative,
while the other ones are prefix … and there is nothing to tell about
association as they get only one argument …

If I’m not clear enough, I hope it will be when we will be allowed to
disclose our solutions :wink:

Pierre

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.

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.

I used the same approach, but found that ** is right-associative (as
it’s generally defined outside of Ruby). To confirm the
associativity for yourself, try this: 234. If it’s left
associative, it should equal 84 (4096), right-associativity gives
2
81 (a lot). I ended up doing a lot more redefining and mucking
about:

Dice Ruby
d *

/ -

  • <<

Interestingly, the difference between a left-associating and a right-
associating ‘d’ operator isn’t particularly visible from the ‘loaded-
dice’ testing common on the list. For example, 2d2d6 gives a maximum
of 24 whichever associativity is used, but the distributions of the
two solutions are vastly different; the left-associative result has a
minimum value of 4, the right-associative result has a minimum of 2.

Here’s my solution, which maintains correct associativity for ‘d’
according to the initial quiz, but does a lot more mucking about with
Fixnum:

matthew smillie.

#!/usr/local/bin/ruby

class Fixnum
alias old_mult *
alias old_div /
alias old_plus +
alias old_minus -

def >>(arg) old_minus(arg) end
def <<(arg) old_plus(arg) end
def -(arg) old_div(arg) end
def +(arg) old_mult(arg) end

def *(arg)
sum = 0
self.times do
sum = sum.old_plus(rand(arg).old_plus(1))
end
sum
end
end

class Dice
def initialize(str)
# make assumed '1’s explicit - do it twice to cover cases
# like ‘3ddd6’ which would otherwise miss one match.
@dice = str.gsub(/([+-/d])(d)/) { |s| “#{$1}1#{$2}” }
@dice = @dice.gsub(/([+-
/d])(d)/) { |s| “#{$1}1#{$2}” }
# sub all the operators.
@dice = @dice.gsub(/+/, “<<”)
@dice = @dice.gsub(/-/, “>>”)
@dice = @dice.gsub(/*/, “+”)
@dice = @dice.gsub(///, “-”)
@dice = @dice.gsub(/d/, “*”)
end

def roll
eval(@dice)
end
end

d = Dice.new(ARGV[0])
(ARGV[1] || 1).to_i.times { print "#{d.roll} " }


Matthew S. [email protected]
Institute for Communicating and Collaborative Systems
University of Edinburgh

Ruby Q. wrote:

roll.rb “3d6” 6
72 64 113 33 78 82

Or, for something more complicated:

roll.rb “(5d5-4)d(16/d4)+3”
31

My submission isn’t going to win points for brevity - at 600+ lines it’s
maybe a bit long to post here.

It’s got a few extras in there, though:

$ ./dice.rb “(5d5-4)d(16/d4)+3”
45

$ ./dice.rb “3d6” 6
11 7 10 13 9 14

$ ./dice.rb -dist “2d5 + 1dd12”
Distribution:
3 0.0103440355940356
4 0.0276987734487735
5 0.0503975468975469
6 0.0773292448292448
7 0.107660533910534
8 0.120036676286676
9 0.120568783068783
10 0.112113997113997
11 0.096477873977874
12 0.07495670995671
13 0.0588945406445407
14 0.0457661135161135
15 0.0345793650793651
16 0.0250565175565176
17 0.0171049783549784
18 0.0107247474747475
19 0.00596632996632997
20 0.00290909090909091
21 0.00113636363636364
22 0.000277777777777778
Check total: 1.0
Mean 9.75 std. dev 3.37782803325187

$ ./dice.rb -cheat “2d5 + 1dd12” 19
19 : D5=2 D5=5 D12=12 D12=12 p=0.000277777777777778

$ ./dice.rb -cheat “2d5 + 1dd12” 25
Cannot get 25

I’ve shoved it on
http://homepage.ntlworld.com/a.mcguinness/files/dice.rb

Good catch. I felt uncomfortable building this without unit testing.
It should be possible to write good repeatable tests using srand in
place of rand…

On Sun, 08 Jan 2006 19:33:18 -0000, Ross B.
[email protected] wrote:

This is implemented in the DiceRoller.parse method, which returns the
string.

Sorry, I changed that. It gives a block now.

Hi,

This is my quiz entry for Ruby Q. 61 (Dice Roller). It’s actually the
second idea I had, after starting out with Antlr (I still finished that
one, because I wanted to get to grips with Antlr anyway - I happened to
be playing with it when this quiz came out :)). I’ve bundled both this
entry and that one at:

http://roscopeco.co.uk/code/ruby-quiz-entries/quiz61-dice-roller.tar.gz

Anyway, back to my real entry. I guess I took the short-cut route to
the dice-roller, and instead of parsing out the expressions I instead
decided to ‘coerce’ them to Ruby code, by just implementing the ‘d’
operator with a ‘rolls’ method on Fixnum, and using gsub to convert
the input expression.

d3*2 => 1.rolls(3)*2
(5d5-4)d(16/d4)+3 => (5.rolls(5)-4).rolls(16/1.rolls(4))+3
d%*7 => 1.rolls(100)*7

This is implemented in the DiceRoller.parse method, which returns the
string. You can just ‘eval’ this of course, or use the ‘roll’ method
(also provided as a more convenient class method that wraps the whole
thing up for you) to do it. Ruby runs the expression, and gives back
the result. I almost feel like I cheated…?

As well as the main ‘roll.rb’ I also included a separate utility that
uses loaded dice to find min/max achievable. All three files can be
executed, and if you enable --verbose mode on Ruby you’ll see the
dice rolls and parsed expressions.

----------[MAIN (roll.rb)]-----------
#!/usr/local/bin/ruby

Ruby Q. 61, the quick way

by Ross B.

Just a debugging helper

module Kernel
def dbg(*s)
puts(*s) if $VERBOSE|| @dice_debug
end
attr_writer :dice_debug
def dice_debug?; @dice_debug; end
end

Need to implement the ‘rolls’ method. Wish it didn’t have to

be on Fixnum but for this it makes the parsing lots easier.

class Fixnum
def self.roll_proc=(blk)
@roll_proc = blk
end

def self.roll_proc
@roll_proc ||= method(:rand).to_proc
end

def rolls(sides)
(1…self).inject(0) { |s,v| s + Fixnum.roll_proc[sides] }
end
end

Here’s the roller.

class DiceRoller
class << self
# Completely wrap up a roll
def roll(expr, count = 1, debug = false)
new(expr,debug).roll(count)
end

 # The main 'parse' method. Just really coerces the code to Ruby
 # and then compiles to a block that returns the result.
 def parse(expr)
   # very general check here. Will pass lots of invalid syntax,
   # but hopefully that won't compile later. This removes the
   # possibility of using variables and the like, but that wasn't
   # required anyway. The regexps would be a bit more difficult
   # if we wanted to do that.
   raise SyntaxError, "'#{expr}' is not a valid dice expression", [] 

if
expr =~ /[^d\d()+-*/%]|[^d]%|d-|**/

   # Rubify!
   s = expr.gsub( /([^\d\)])d|^d/,   '\11d')          # fix e.g. 

‘d5’
and ‘33+d3’ to ‘1.d5’ and ‘33+1d3’
s.gsub!( /d%/, ‘d(100)’ ) # fix e.g.
‘d%’
to ‘d(100)’
s.gsub!( /d([+-]?\d+)/, ‘.rolls(\1)’) # fix e.g.
‘3d8’
to '3.rolls(8) (*)
s.gsub!( /d(/, ‘.rolls(’) # fix e.g.
‘2d(5+5)’ to ‘2.rolls(5+5)’

   # (*) This line treats + or - straight after 'd' as a unary sign,
   # so you can have '3d-8*7' => '3.rolls(+8)-7'
   # This would throw a runtime error from rolls, though.

   # Make a block. Doing it this way gets Ruby to compile it now
   # so we'll reliably get fail fast on bad syntax.
   dbg "PARS: #{expr} => #{s}"
   begin
     eval("lambda { #{s} }")
   rescue Exception => ex
     raise SyntaxError, "#{expr} is not a valid dice expression", []
   end
 end

end

Create a new roller that rolls the specified dice expression

def initialize(expr, debug = false)
dbg “NEW : #{to_s}: #{expr} => #{expr_code}”
@expr_code, @expr, @debug = expr, DiceRoller.parse(expr), debug
end

Get hold of the original expression and compiled block,

respectively
attr_reader :expr_code, :expr

Roll this roller count times

def roll(count = 1)
dbg " ROLL: #{to_s}: #{count} times"
r = (1…count).inject([]) do |totals,v|
this_r = begin
expr.call
rescue Exception => ex
raise RuntimeError, “‘#{expr_code}’ raised: #{ex}”, []
end

   dbg "    r#{v}: rolled #{this_r}"
   totals << this_r
 end

 r.length < 2 ? r[0] : r

end
end

Library usage:

require ‘roll’

# is the default:

# Fixnum.roll_proc = lambda { |sides| rand(sides) + 1 }

DiceRoller.roll(‘1+2*d6’)

d = DiceRoller.new(‘((3d%)+8*(d(5*5)))’)

d.roll(5)

d = DiceRoller.new(‘45*10d3’) # debug

# … or

one_roll = d.expr.call

command-line usage

if $0 == FILE
unless expr = ARGV[0]
puts “Usage: ruby [–verbose] roll.rb expr [count]”
else
(ARGV[1] || 1).to_i.times { print "#{DiceRoller.roll(expr)} " }
print “\n”
end
end

-----------[UTIL: minmax.rb]----------
#!/usr/local/bin/ruby

require ‘roll’

LOW_DICE = lambda { |sides| 1 }
HIGH_DICE = lambda { |sides| sides }

Adds a ‘minmax’ method that uses loaded dice to find

min/max achievable for a given expression.

Obviously not thread safe, but then neither is the

whole thing ;D

class DiceRoller
def self.minmax(expr)
old_proc = Fixnum.roll_proc
Fixnum.roll_proc = LOW_DICE
low = DiceRoller.roll(expr)

 Fixnum.roll_proc = HIGH_DICE
 high = DiceRoller.roll(expr)
 Fixnum.roll_proc = old_proc

 [low,high]

end
end

if $0 == FILE
if expr = ARGV[0]
min, max = DiceRoller.minmax(expr)
puts “Expression: #{expr} ; min / max = #{min} / #{max}”
else
puts “Usage: minmax.rb ”
end
end

-----------[TEST: test.rb]----------
#!/usr/local/bin/ruby

Ruby Q., number 61 - Dice roller

This entry by Ross B. (roscoroscopeco.co.uk)

require ‘test/unit’
require ‘roll’

ASSERTS = {
‘1’ => 1,
‘1+2’ => 3,
‘1+34’ => 13,
'1
2+4/8-1’ => 1,
‘d1’ => 1,
‘1d1’ => 1,
‘d10’ => 10,
‘1d10’ => 10,
‘10d10’ => 100,
‘d32’ => 6,
‘5d6d7’ => 210, # left assoc
‘2d3+8’ => 14, # not 22
‘(2d(3+8))’ => 22, # not 14
‘d3+d3’ => 6,
‘33+d3+10’ => 46,
'd2
2d4’ => 16,
‘d(2*2)+d4’ => 8,
‘d%’ => 100,
‘2d%’ => 200,
‘d%7’ => 700,
'14+3
10d2’ => 74,
‘(5d5-4)d(16/d4)+3’ => 87, #25d4 + 3
‘3d+8/8’ => 3 #3d(+8)/8
}

ERRORS = {

Bad input, all should raise exception

‘d’ => SyntaxError,
‘3d’ => SyntaxError,
‘3d-8’ => SyntaxError, # - # of sides
‘3ddd6’ => SyntaxError,
‘3%2’ => SyntaxError,
‘%d’ => SyntaxError,
‘+’ => SyntaxError,
‘4**3’ => SyntaxError
}

bit messy, but can’t get class methods on Fixnum

Fixnum.roll_proc = lambda { |sides| sides }

class TestDiceRoller < Test::Unit::TestCase
def initialize(*args)
super
end

ASSERTS.each do |expr, expect|
eval <<-EOC
def test_good_#{expr.hash.abs}
expr, expect = #{expr.inspect}, #{expect.inspect}
puts “\n-----------------------\n#{expr} => #{expect}” if
$VERBOSE
res = DiceRoller.roll(expr)
puts “Returned #{res}\n-----------------------” if $VERBOSE
assert_equal expect, res
end
EOC
end

ERRORS.each do |expr, expect|
eval <<-EOC
def test_error_#{expr.hash.abs}
expr, expect = #{expr.inspect}, #{expect.inspect}
assert_raise(#{expect}) do
puts “\n-----------------------\n#{expr} => #{expect}” if
$VERBOSE
res = DiceRoller.roll(expr)
puts “Returned #{res}\n-----------------------” if $VERBOSE
end
end
EOC
end
end

Very interesting, and different solutions, this time!

Here’s my recursive descent solution with histogram:

=begin
Ruby Q. #61
by Matthew D Moss

Solution by Christer N.

“3d6” gives 3…18 randomly

“(5d5-4)d(16/d4)+3”

Backus Naur Form:

expr: term [’+’ expr | ‘-’ expr]
term: fact [’*’ term | ‘/’ term]
fact: [unit] ‘d’ dice
unit: ‘(’ expr ‘)’ | integer
dice: ‘%’ | term
integer: digit [integer]
digit: /[0-9]/

  • Integers are positive
  • The “d” (dice) expression XdY rolls a Y-sided die (numbered
    from 1 to Y) X times, accumulating the results. X is optional
    and defaults to 1.
  • All binary operators are left-associative.
  • Operator precedence:
    ( ) highest
    d
  • /
    •  lowest
      

Some game systems use d100 quite often, and may abbreviate it as “d%”
(but note that ‘%’ is only allowed immediately after a ‘d’).
=end
class String
def behead
return [’’,’’] if self == ‘’
[self[0…0], self[1…self.size]]
end
end

class Array
def sum
inject(0) {|sum,e| sum += e}
end

def histogram(header="")
width = 100
each_index {|i| self[i]=0 if self[i].nil?}
sum = self.sum
max = self.max if max.nil?
s = " " + header + “\n”
each_with_index do |x,i|
label = " " + format("%2.1f",100.0x/sum)+"%"
s += format("%2d",i) + " " + "
" * ((x-min) * width / (max-min)) +
label + “\n”
end
s += “\n”
end
end

class Dice

def statistics(expr, n=1000)
prob = []
n.times do
value = evaluate(expr)
prob[value]=0 if prob[value].nil?
prob[value] += 1
end
prob
end

def evaluate s
@sym, @s = s.behead
@stack = []
expr
pop
end

def drop (pattern)
raise 'syntax error: expected ’ + pattern unless pattern === @sym
@sym, @s = @s.behead
end

def push(x) @stack.push x end
def top2() @stack[-2] end
def top() @stack[-1] end
def pop() @stack.pop end

def calc value
pop
push value
end

def try symbol
return nil unless @sym == symbol
drop symbol
case symbol
when ‘+’ then expr; calc top2 + pop
when ‘-’ then expr; calc top2 - pop
when ‘*’ then term; calc top2 * pop
when ‘/’ then term; calc top2 / pop
when ‘%’ then push 100
when ‘(’ then expr; drop ‘)’
#when ‘d’ then dice; calc top2 * pop # debug mode
when ‘d’ # release mode
dice
sum = 0
sides = pop
count = pop
count.times {sum += rand(sides) + 1}
push sum
end
end

def expr
term
try(’+’) or try(’-’)
end

def term
fact
try(’*’) or try(’/’)
end

def fact
@sym == ‘d’ ? push(1) : unit # implicit 1
try(‘d’)
end

def dice
#unit unless try(’%’)# if 5d6d7 is not accepted
term unless try(’%’) # if 5d6d7 is accepted
end

def unit
integer @sym.to_i unless try(’(’)
end

def integer(i)
return if @sym == ‘’
digit = /[0-9]/
drop(digit)
digit === @sym ? integer( 10 * i + @sym.to_i ) : push(i)
end
end

require ‘test/unit’
class TestDice < Test::Unit::TestCase
def t (actual, expect)
assert_equal expect, actual
end
def test_all

t(/[0-9]/==="0", true)
t(/[0-9]/==="a", false)
t "abc".behead, ["a","bc"]
t "a".behead, ["a",""]
t "".behead, ["",""]

dice = Dice.new()
print dice.statistics("d6").histogram("d6")
print dice.statistics("2d6").histogram("2d6")
print dice.statistics("(d6)d6",10000).histogram("(d6)d6")

#t dice.evaluate("(6)"), 6
#t dice.evaluate("12+34"), 46
#t dice.evaluate("3*4+2"), 14
#t dice.evaluate("5+6+7"), 18
#t dice.evaluate("5+6-7"), 4
#t dice.evaluate("(5+6)+7"), 18
#t dice.evaluate("5"), 5
#t dice.evaluate("5+(6+7)"), 18
#t dice.evaluate("(5+6+7)"), 18
#t dice.evaluate("5*6*7"), 210
#t dice.evaluate("2+3*4"), 14
#t dice.evaluate("12+13*14"), 194
#t dice.evaluate("(2+3)*4"), 20
#t dice.evaluate("(5d5-4)d(16/1d4)+3"), 45
#t dice.evaluate("(5d5-4)d(400/1d%)+3"), 87
#t dice.evaluate("1"), 1
#t dice.evaluate("1+2"),3
#t dice.evaluate("1+3*4"),13
#t dice.evaluate("1*2+4/8-1"), 1
#t dice.evaluate("d1"),1
#t dice.evaluate("1d1"),1
#t dice.evaluate("1d10"), 10
#t dice.evaluate("10d10"),100
#t dice.evaluate("d3*2"), 6
#t dice.evaluate("2d3+8"), 14
#t dice.evaluate("(2*(3+8))"),22
#t dice.evaluate("d3+d3"),6
#t dice.evaluate("d2*2d4"),16
#t dice.evaluate("2d%"),200
#t dice.evaluate("14+3*10d2"), 74
#t dice.evaluate("(5d5-4)d(16/d4)+3"),87
#t dice.evaluate("d10"), 10
#t dice.evaluate("d%"),100
#t dice.evaluate("d(2*2)+d4"),8
#t dice.evaluate("(5d6)d7"), 210
#t dice.evaluate("5d(6d7)"), 210
#t dice.evaluate("5d6d7)"), 210
#t dice.evaluate("12d13d14)"), 2184
#t dice.evaluate("12*d13)"), 156
#t dice.evaluate("12+d13)"), 25

end
end

There it goes, using eval for simplicity, but at least compiling the

dice into a Proc:

class Integer
def d(n) # evil }:slight_smile:
(1…self).inject(0) { |a,e| a + rand(n) + 1 }
end
end

class Dice
def initialize(dice)
@src = dice.gsub(/d(%|00)(\D|$)/, ‘d100\2’).
gsub(/d(\d+)/, ‘d(\1)’).
gsub(/(\d+|))d/, ‘\1.d’).
gsub(/\d+/) { $&.gsub(/^0+/, ‘’) }

raise ArgumentError, "invalid dice: `#{dice}'"  if @src =~ 

/[^-+/*()d0-9. ]/

begin
  @dice = eval "lambda{ #@src }"
  roll                      # try the dice
rescue
  raise ArgumentError, "invalid dice: `#{dice}'"
end

end

def d(n)
1.d(n)
end

def roll
@dice.call
end
end

unless $DEBUG
d = Dice.new(ARGV[0] || “d6”)
puts Array.new((ARGV[1] || 1).to_i) { d.roll }.join(" ")
else
$DEBUG = false # only makes test/unit verbose now

warn “This is a heuristic test-suite. Please re-run (or increase N)
on failure.”

require ‘test/unit’

N = 100000

class TestDice < Test::Unit::TestCase
def test_00_invalid_dice
assert_raises(ArgumentError) { Dice.new(“234%21”) }
assert_raises(ArgumentError) { Dice.new("%d5") }
assert_raises(ArgumentError) { Dice.new(“d5%”) }
assert_raises(ArgumentError) { Dice.new(“d%5”) }
end

def test_10_fixed_expr
  dice_min_max({
    '1'                   => [1, 1],
    '1+2'                 => [3, 3],
    '1+3*4'               => [13, 13],
    '1*2+4/8-1'           => [1, 1],
    'd1'                  => [1, 1],
    '1d1'                 => [1, 1],
    '066d1'               => [66, 66]
  }, 10)
end

def test_20_small_dice
  dice_min_max({
    'd10'                 => [1, 10],
    '1d10'                => [1, 10],
    'd3*2'                => [2, 6],
    '2d3+8'               => [10, 14],    # not 22
    '(2d(3+8))'           => [2, 22],    # not 14
    'd3+d3'               => [2, 6],
    'd2*2d4'              => [2, 16],
    'd(2*2)+d4'           => [2, 8]
  })
end

def test_30_percent_dice
  dice_min_max({
    'd%'                  => [1, 100],
    '2d%'                 => [2, 200]
  }, 100_000)
end

def test_40_complicated_dice
  dice_min_max({
    '10d10'               => [10, 100],
    '5d6d7'               => [5, 210],   # left assoc
    '14+3*10d2'           => [44, 74],
    '(5d5-4)d(16/d4)+3'   => [4, 339],
  }, 1_000_000)
end

def dice_min_max(asserts, n=10_000)
  asserts.each { |k, v|
    dice = Dice.new k

    v2 = (1..n).inject([1.0/0.0, 0]) { |(min, max), e|
      r = dice.roll
      [[min, r].min, [max, r].max]
    }

    assert_equal v, v2, k
  }
end

end
end

END

Well, here is my first solution to a quizz ^^
I tried to use racc for that … so you need to generate the ruby script
using :

$ racc roll.y -o roll.rb

Otherwise, it is pretty simple …

A small explanation is included within the file. If needed, I will post
the generated file.

Pierre

Here is my submission. Yes, this is my first completed Ruby Q. :wink:
Thanks to Eric M.'s syntax.rb for making this work. I’ve attached
it as well, because it’s not easily accessible otherwise :wink:

-austin

Here is my solution. I convert the expression into RPN (using the
algorithm
described in the Wikipedia article) and then calculate it (I have added
a
‘d’ method to Fixnum so that I can use it like the standard arithmetic
operators). My solution is not very strict, so it allows ‘%’ as an alias
for
100 anywhere in the expression (not just after a ‘d’), but I think that
should not be a big problem. It also ignores other characters, so
whitespace
is allowed anywhere.

Pablo


#!/usr/bin/ruby

class Fixnum
def d(b)
(1…self).inject(0) {|s,x| s + rand(b) + 1}
end
end

class Dice

def initialize(exp)
@expr = to_rpn(exp)
end

def roll
stack = []
@expr.each do |token|
case token
when /\d+/
stack << token.to_i
when /[-+*/d]/
b = stack.pop
a = stack.pop
stack << a.send(token.to_sym, b)
end
end
stack.pop
end

private

def to_rpn(infix)
stack, rpn, last = [], [], nil
infix.scan(/\d+|[-+/()d%]/) do |token|
case token
when /\d+/
rpn << token
when ‘%’
rpn << “100”
when /[-+
/d]/
while stack.any? && stronger(stack.last, token)
rpn << stack.pop
end
rpn << “1” unless last =~ /\d+|)|%/
stack << token
when ‘(’
stack << token
when ‘)’
while (op = stack.pop) && (op != ‘(’)
rpn << op
end
end
last = token
end
while op = stack.pop
rpn << op
end
rpn
end

def stronger(op1, op2)
(op1 == ‘d’ && op2 != ‘d’) || (op1 =~ /[*/]/ && op2 =~ /[-+]/)
end

end

if $0 == FILE
d = Dice.new(ARGV[0])
(ARGV[1] || 1).to_i.times { print "#{d.roll} " }
end

Hi,

Here is my solution #1 for this nice quiz. Hacky and short, without (!)
using eval… :wink:

module Dice
def self.roll(expr)
expr = expr.gsub(/\s/, ‘’)
while
expr.sub!(/(([^()]+))/) { roll($1) } ||
expr.sub!(/(\A|[^\d])–(\d+)/, ‘\1\2’) ||
expr.sub!(/d%/, ‘d100’) ||
expr.sub!(/(\d+)d(\d+)/) { (1…$1.to_i).inject(0) {|a, b| a +
rand($2.to_i) + 1} } ||
expr.sub!(/d(\d+)/, ‘1d\1’) ||
expr.sub!(/(\d+)/(-?\d+)/) { $1.to_i / $2.to_i } ||
expr.sub!(/(\d+)*(-?\d+)/) { $1.to_i * $2.to_i } ||
expr.sub!(/(-?\d+)-(-?\d+)/) { $1.to_i - $2.to_i } ||
expr.sub!(/(-?\d+)+(-?\d+)/) { $1.to_i + $2.to_i }
end
return $1.to_i if /\A(-?\d+)\Z/ =~ expr
raise “Error evaluating dice expression, stuck at ‘#{expr}’”
end
end

(ARGV[1] || 1).to_i.times { print "#{Dice.roll(ARGV[0])} " }
puts

On Jan 8, 2006, at 3:42 PM, Pablo Hoch wrote:

Here is my solution. I convert the expression into RPN (using the
algorithm
described in the Wikipedia article) and then calculate it (I have
added a
‘d’ method to Fixnum so that I can use it like the standard arithmetic
operators).

Wow. That is very cool. Thanks for sharing!

James Edward G. II

Hi,

here is my second solution. Quite a bit longer, but a lot nicer.
For this I implemented a simple recursive descent parser class that
allows the tokens and the grammar to be defined in a very clean ruby
syntax. I think I’d really like to see a production quality
parser(generator) using something like this grammar format.

class RDParser
attr_accessor :pos
attr_reader :rules

def initialize(&block)
@lex_tokens = []
@rules = {}
@start = nil
instance_eval(&block)
end

def parse(string)
@tokens = []
until string.empty?
raise “unable to lex '#{string}” unless @lex_tokens.any? do |tok|
match = tok.pattern.match(string)
if match
@tokens << tok.block.call(match.to_s) if tok.block
string = match.post_match
true
else
false
end
end
end
@pos = 0
@max_pos = 0
@expected = []
result = @start.parse
if @pos != @tokens.size
raise “Parse error. expected: ‘#{@expected.join(’, ‘)}’, found
‘#{@tokens[@max_pos]}’”
end
return result
end

def next_token
@pos += 1
return @tokens[@pos - 1]
end

def expect(tok)
t = next_token
if @pos - 1 > @max_pos
@max_pos = @pos - 1
@expected = []
end
return t if tok === t
@expected << tok if @max_pos == @pos - 1 &&
!@expected.include?(tok)
return nil
end

private

LexToken = Struct.new(:pattern, :block)

def token(pattern, &block)
@lex_tokens << LexToken.new(Regexp.new(’\A’ + pattern.source),
block)
end

def start(name, &block)
rule(name, &block)
@start = @rules[name]
end

def rule(name)
@current_rule = Rule.new(name, self)
@rules[name] = @current_rule
yield
@current_rule = nil
end

def match(*pattern, &block)
@current_rule.add_match(pattern, block)
end

class Rule
Match = Struct.new :pattern, :block

 def initialize(name, parser)
   @name = name
   @parser = parser
   @matches = []
   @lrmatches = []
 end

 def add_match(pattern, block)
   match = Match.new(pattern, block)
   if pattern[0] == @name
     pattern.shift
     @lrmatches << match
   else
     @matches << match
   end
 end

 def parse
   match_result = try_matches(@matches)
   return nil unless match_result
   loop do
     result = try_matches(@lrmatches, match_result)
     return match_result unless result
     match_result = result
   end
 end

 private

 def try_matches(matches, pre_result = nil)
   match_result = nil
   start = @parser.pos
   matches.each do |match|
     r = pre_result ? [pre_result] : []
     match.pattern.each do |token|
       if @parser.rules[token]
         r << @parser.rules[token].parse
         unless r.last
           r = nil
           break
         end
       else
         nt = @parser.expect(token)
         if nt
           r << nt
         else
           r = nil
           break
         end
       end
     end
     if r
       if match.block
         match_result = match.block.call(*r)
       else
         match_result = r[0]
       end
       break
     else
       @parser.pos = start
     end
   end
   return match_result
 end

end
end

parser = RDParser.new do
token(/\s+/)
token(/\d+/) {|m| m.to_i }
token(/./) {|m| m }

start :expr do
match(:expr, ‘+’, :term) {|a, _, b| a + b }
match(:expr, ‘-’, :term) {|a, _, b| a - b }
match(:term)
end

rule :term do
match(:term, ‘*’, :dice) {|a, _, b| a * b }
match(:term, ‘/’, :dice) {|a, _, b| a / b }
match(:dice)
end

def roll(times, sides)
(1…times).inject(0) {|a, b| a + rand(sides) + 1 }
end

rule :dice do
match(:atom, ‘d’, :sides) {|a, , b| roll(a, b) }
match(‘d’, :sides) {|
, b| roll(1, b) }
match(:atom)
end

rule :sides do
match(’%’) { 100 }
match(:atom)
end

rule :atom do
match(Integer)
match(’(’, :expr, ‘)’) {|_, a, _| a }
end
end

(ARGV[1] || 1).to_i.times { print "#{parser.parse(ARGV[0])} " }
puts

On Jan 8, 2006, at 3:53 PM, Dennis Ranke wrote:

For this I implemented a simple recursive descent parser class that
allows the tokens and the grammar to be defined in a very clean
ruby syntax.

Awesome!

I think I’d really like to see a production quality parser
(generator) using something like this grammar format.

I agree. This is fantastic.

So what do we have to do to get you to add the polish and make it
available? :slight_smile:

James Edward G. II

Here is my solution.

It’s a recursive descent parser, that parses the full BNF posted by
Matthew M… It doesn’t “compile” the expresion into nodes or something
similar, instead it evaluates the expression while parsing (so it has to
be reparsed for every dice rolling). It uses StringScanner, which was
quite handy for this task. (And it also uses eval() :wink:

Dominik

require “strscan”

class Dice
def initialize(expr)
@expr = expr.gsub(/\s+/, “”)
end

 def roll
     s = StringScanner.new(@expr)
     res = expr(s)
     raise "garbage after end of expression" unless s.eos?
     res
 end

 private

 def split_expr(s, sub_expr, sep)
     expr = []
     loop do
         expr << send(sub_expr, s)
         break unless s.scan(sep)
         expr << s[1] if s[1]
     end
     expr
 end

 def expr(s)
     eval(split_expr(s, :fact, /([+\-])/).join)
 end

 def fact(s)
     eval(split_expr(s, :term, /([*\/])/).join)
 end

 def term(s)
     first_rolls = s.match?(/d/) ? 1 : unit(s)
     dices = s.scan(/d/) ? split_expr(s, :dice, /d/) : []
     dices.inject(first_rolls) do |rolls, dice|
         raise "invalid dice (#{dice})" unless dice > 0
         (1..rolls).inject(0) { |sum, _| sum + rand(dice) + 1 }
     end
 end

 def dice(s)
     s.scan(/%/) ? 100 : unit(s)
 end

 def unit(s)
     if s.scan(/(\d+)/)
         s[1].to_i
     else
         unless s.scan(/\(/) && (res = expr(s)) && s.scan(/\)/)
             raise "error in expression"
         end
         res
     end
 end

end

if $0 == FILE
begin
d = Dice.new(ARGV[0])
puts (1…(ARGV[1] || 1).to_i).map { d.roll }.join(" ")
rescue => e
puts e
end
end

I didn’t try anything fancy for this. I did try to get eval to do all
the work, but ran into too many problems. Here’s my solution:

$searches = [
[/(\d*)/, lambda{|m| m[1…-2]}],
[/^d/, lambda{|m| “1d”}],
[/d%/, lambda{|m| “d100”}],
[/(+|-|*|/|()d\d+/, lambda{|m| m[0…0]+‘1’+m[1…-1]}],
[/\d+d\d+/, lambda{|m| dice(*m.split(‘d’).map {|i|i.to_i}) }],
[/\d+(*|/)\d+/, lambda{|m| eval m}],
[/\d+(+|-)\d+/, lambda{|m| eval m}]
]
def parse(to_parse)
s = to_parse
while(s =~ /d|+|-|*|/|(|)/)
$searches.each do |search|
if(s =~ search[0]) then
s = s.sub(search[0], &search[1])
break
end
end
end
s
end

def dice(times, sides)
Array.new(times){rand(sides)+1}.inject(0) {|s,i|s+i}
end

srand
string = ARGV[0]
(puts “usage: #{$0} []”; exit) if !string
(ARGV[1] || 1).to_i.times { print parse(string), ’ ’ }

-----Horndude77

Sorry for the noise.

On Sun, 08 Jan 2006 19:38:03 -0000, I wrote:

On Sun, 08 Jan 2006 19:33:18 -0000, I also wrote:

This is implemented in the DiceRoller.parse method, which returns the
string.

Sorry, I changed that. It gives a block now.

There were a few other inaccuracies in the comments, where I’d obviously
not been merciless enough when refactoring. Specifically, a (*) comment
about one of the parse regexps (no longer applies) and a comment in the
tests about Fixnum and class methods (from before I realised my error).
Oh, and a debug message used still referenced an attr that was no longer
set up at that point.

I updated the archive at the link I posted with those removed, and also
took the chance to slip in a tiny fix for whitespace (just remove it all
at the start of the parse) but I guess it doesn’t ‘count’ for the quiz
:slight_smile:

Anyway, thanks all concerned for the fun quiz - this is the first one
I’ve
done so take it easy on my solution :slight_smile:

(Oh, and I missed [SOLUTION] before and since a lot of people seem to be
doing that I felt just [QUIZ] might get missed).

The original solution post was this one:

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/174815

I’ve decided to bite the bullet and post my overlong solution.

It’s got a few extras in there:

$ ./dice.rb “(5d5-4)d(16/d4)+3”
45

$ ./dice.rb “3d6” 6
11 7 10 13 9 14

$ ./dice.rb -dist “2d5 + 1dd12”
Distribution:
3 0.0103440355940356
4 0.0276987734487735
5 0.0503975468975469
6 0.0773292448292448
7 0.107660533910534
8 0.120036676286676
9 0.120568783068783
10 0.112113997113997
11 0.096477873977874
12 0.07495670995671
13 0.0588945406445407
14 0.0457661135161135
15 0.0345793650793651
16 0.0250565175565176
17 0.0171049783549784
18 0.0107247474747475
19 0.00596632996632997
20 0.00290909090909091
21 0.00113636363636364
22 0.000277777777777778
Check total: 1.0
Mean 9.75 std. dev 3.37782803325187

$ ./dice.rb -cheat “2d5 + 1dd12” 19
19 : D5=2 D5=5 D12=12 D12=12 p=0.000277777777777778

$ ./dice.rb -cheat “2d5 + 1dd12” 25
Cannot get 25

I’m getting to grips with block-passing as a routine technique - the
evaluate() method "yield"s its result so that it can provide multiple
results - tens of thousands if you ask it to try every possible roll
involving lots of dice. The roll_dice method had to be written
recursively for that to work:

def roll_dice( numdice, sides )
if ( numdice == 0 )
yield null_value
else
roll_one(sides) do |first|
roll_dice( numdice-1, sides ) do |rest|
yield( first + rest )
end
end
end
end

Depending on how roll_one has been overridden, “first” and “rest” can be
integers, frequency distributions, or objects representing the history
of every roll to get to this state. In the last case, roll_one will
yield “sides” times, to give you every possible roll

I’m not quite comfortable with things like redefining Integer#+
If I had done that, I could have avoided a lot of kind_of? calls in
stuff like this:

def eval_binop( force_same_type = true )
subtree(:left).evaluate do |l|
subtree(:right).evaluate do |r|
if force_same_type
if r.kind_of?( Roll_stat ) and ! l.kind_of?( Roll_stat )
l = Roll_stat.new(l)
end
end
yield(l,r)
end
end
end

The whole thing can generate the probability distributions reasonably
quickly - I had ideas of approximating large numbers of dice (100d6
and so on) with the appropriate normal distribution (“clipped” by max
and min values).

The exhaustive output is very large, though. It would be worth
optimising that by taking out permutations.

I’ve shoved it on
http://homepage.ntlworld.com/a.mcguinness/files/dice.rb and attached it.