Dice Roller (#61)

this is my first ruby quiz, and here comes my solution.

as a couple of other solutions, it uses ruby to do the dirty work, and
implements the diceroll as a method on integer.

!g

On 1/6/06, Jim F. [email protected] wrote:

On 1/6/06, James Edward G. II [email protected] wrote:
Actually, when rolled together, both dice are zero-based. The
double-nought is the only special combination of 00 → 100. When
rolled singly, a d10 has 0 → 10. Rolling a 0 is never possible.

No wonder I don’t play D&D. I don’t think I am smart enough.

Heh, I wouldn’t say that. But I will admit that most of my attraction
to the game is the mental challenge of managing (and taking advantage
of) the complex rule system. Call me munchkin. Shrug.

What does 0 → 10 mean. Does it mean a dice can have the
values 0,1,2,3…10?

No, that was a typo on my part. Should have been 0 → 9 (e.g. rand(10)).

If so, why is a 0 never possible?

Zero is possible, but is interpreted as a 10. So the effective range
is 1 → 19 (rand(10) + 1). In this respect, the outcome of a d10
follows the same rules of the outcome from a d6, d8 or d20 (rand(N) +
1). The presentation on the dice is the only difference. As noted by
Austin, this is primarily due to space limitations, but also for
convenience when using two d10 to simulate a d100.

Jacob F.

On 1/6/06, Jacob F. [email protected] wrote:

Actually, when rolled together, both dice are zero-based. The
double-nought is the only special combination of 00 → 100. When
rolled singly, a d10 has 0 → 10. Rolling a 0 is never possible.

On 1/6/06, Jim F. [email protected] wrote:

What does 0 → 10 mean. Does it mean a dice can have the
values 0,1,2,3…10?

On 1/9/06, Jacob F. [email protected] wrote:

No, that was a typo on my part. Should have been 0 → 9 (e.g. rand(10)).

Ok, I must fix this previous apology to Jim F… 0 → 10 was not
a typo. He just misinterpreted my notation. And seeing as I followed
his misinterpretation before second glance, it’s perfectly justified.
In the paragraph above the → represented “is interpreted as”. So when
rolling two ten sided dice for a d100, double-nought (‘00’) is
interpreted as 100. When rolling a single d10, 0 is interpreted as 10.

Jacob F.

On 1/7/06, Reinder V. [email protected] wrote:

([1,21])d([4,16])+3 = ([a,b]d[c,d] = [ac,bd] if a,b,c, and d > 0)

[4,336]+3 = ([x,y] + c = [x+c,y+c])

[7,339]

As pointed out by others, the result should actually by [4,339]. The
error above is in the second to last step. [a,b]d[c,d] != [ac,bd],
it should be [a,b]d[c,d] = [a,bd]. The minimum effective value on a
die roll is always one, regardless of the number of sides (ie. whether
using a c-sided die or d-sided die). The minimum number of rolls being
a, the minimum roll total would then be a
1 = a. So we have:

[1,21]d[4,16]+3 =
[1,21*16]+3 =
[1+3,336+3] =
[4,339]

Jacob F.

On 1/7/06, Ron M [email protected] wrote:

Uh, of course you can make such a polyhedron. Consider the
Egyptian and Mayan pyramids as examples of 5-sided polyhedron
(four triangles on the sides and a square on the bottom).
Adjusting the steepness of the sides can make it as fair
or unfair as you’d want.

Sure, they’re not regular polyhedra, but neither is the d30 you spoke of.

In fact, I’ve seen a variation on this for the infamous “d3” somtimes
used in D&D. The standard approach is to roll a d6 then divide the
result by two (rounding up). However, one dice seller instead offers a
non-regular tetrahedron where one vertex is stretched out away from
the fourth side. This makes the three stretched side much more likely
than the fourth side, and given the dynamics of the dice, it is very
unlikely for it to land standing on that fourth side. So you have, in
effect, a three sided die. If the fourth side does ever come up, you
just reroll.

Jacob F.

I really love this mailing list.
It’s simple and with threaded view it’s perfect.
But sometimes a forum with an edit function is handy too :>

Here’s my solution.

I spent a few hours writing a BNF parser which was supposed to let me do
this:

– begin buggy code –
CENT = BnfTerm.new(/(%)/ ) { ‘100’ }
INTEGER = /([1-9][0-9]*)/
DICE = BnfTerm.new(CENT,:|,INTEGER)
term = BnfTerm.new()
ROLL = BnfTerm.new(term, /d/, DICE) {|a,b|
(1…a.to_i).inject(0){|s,i|s+rand(b.to_i)+1} }
term.define(DICE, :|,ROLL) {|m| m}
#…
class Dice
@@rule = DIEROLL
def initialize expr
@expr = expr
end
def roll
@@rule.parse(@expr)
end
end
– end –
but it was too brittle, and it would go into endless recursion on a
lot of valid inputs.

So I switched to a quick,short simple solution: add a #d method to
integer and let eval do the work:

— dice.rb –
class Integer
def d n
(0…self).inject(0){|s,i| s+rand(n)+1}
end
end

class Dice
def initialize str
@rule= str.gsub(/%/,‘100’).gsub(/([^\d)]|^)d/,’\1 1d’) # %->100
and bare d ->1d
while @rule.gsub!(/([^.])d(\d+|(.*))/,’\1.d(\2)’) #
‘dX’ -> ‘.d(X)’
end
#repeat to deal with nesting
end
def roll
eval(@rule)
end
end

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


-Adam

On Sun, 08 Jan 2006 19:33:18 -0000, a complete idiot (i.e. me) wrote:

As well as the main ‘roll.rb’ I also included a separate utility that
uses loaded dice to find min/max achievable.

Yeah, well, the penny dropped. That’ll teach me to try and understand
maths - I did so well at avoiding it in the main thing, then decided to
try that…

Now I’ve succeeded in making a complete fool of myself I’ll just get
me
coat…

On Sat, Jan 07, 2006 at 04:00:52AM +0900, Gregory S. wrote:
} On Sat, Jan 07, 2006 at 03:56:47AM +0900, Ruby Q. wrote:
} […]
} } [NOTE: The BNF above is simplified here for clarity and space. If
} } requested, I will make available the full BNF description I’ve used
in my
} } own solution, which incorporates the association and precedence
rules.]
}
} I would appreciate the full BNF, please.

Okay, so I said I wanted the full BNF, and I thought it would be useful
if
I could find a convenient Ruby lex/yacc. Well, I couldn’t. I now have
two
solutions, both using my own parsing. Both use eval. One adds a method
to
Fixnum to let eval do even more work. The more complicated version with
syntax trees, which came first, took roughly two hours to work out. The
second, simpler version with the Fixnum method took about 20 minutes to
build from the first version. Note that I maintained left associativity
with the d operator in both methods without having the 3dddd6 problem
Matthew M. mentioned.

test61.rb runs the code on commandline arguments
61.rb is the complicated syntax tree version
61alt.rb is the simpler Fixnum method version

test61.rb

################################################################

#require ‘61’
require ‘61alt’

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

61.rb

####################################################################

module DiceRoller

class ArithOperator
def initialize(left, op, right)
@left = left
@op = op
@right = right
end

def to_i
  return (eval "#{@left.to_i}#{@op}#{@right.to_i}")
end

end

class DieOperator
#op is a dummy
def initialize(left, op, right)
@left = left
@right = right
end

def to_i
  count = @left.to_i
  fail "Die count must be nonnegative: '#{count}'" if count < 0
  die = @right.to_i
  fail "Die size must be positive: '#{die}'" if die < 1
  return (1..count).inject(0) { |sum, waste| sum + (rand(die)+1) }
end

end

OpClass = { ‘+’ => ArithOperator,
‘-’ => ArithOperator,
‘*’ => ArithOperator,
‘/’ => ArithOperator,
‘d’ => DieOperator }

def lex(str)
tokens = str.scan(/(00)|([-/()+d%0])|([1-9][0-9])|(.+)/)
tokens.each_index { |i|
tokens[i] = tokens[i].compact[0]
if not /^(00)|([-/()+d%0])|([1-9][0-9])$/ =~ tokens[i]
if /^\s+$/ =~ tokens[i]
tokens[i] = nil
else
fail “Found garbage in expression: ‘#{tokens[i]}’”
end
end
}
return tokens.compact
end

def validate_and_cook(tokens)
oper = /[-*/+d]/
num = /(\d+)|%/
last_was_op = true
paren_depth = 0
prev = ‘’
working = []
tokens.each_index { |i|
tok = tokens[i]
if num =~ tok
fail ‘A number cannot follow an expression!’ if not last_was_op
fail ‘Found spurious zero or number starting with zero!’ if tok
== ‘0’
if ( tok == ‘00’ || tok == ‘%’ )
fail ‘Can only use % or 00 after d!’ if prev != ‘d’
tokens[i] = 100
working << 100
else
working << tok.to_i
end
last_was_op = false
elsif oper =~ tok
if last_was_op
#handle case of dX meaning 1dX
if tok == ‘d’
fail ‘A d cannot follow a d!’ if prev == RollMethod
working << 1
else
fail ‘An operator cannot follow a operator!’
end
end
working << tok
last_was_op = true
elsif tok == “(”
fail ‘An expression cannot follow an expression!’ if not
last_was_op
paren_depth += 1
working << :p_open
elsif tok == “)”
fail ‘Incomplete expression at close paren!’ if last_was_op
fail ‘Too many close parens!’ if paren_depth < 1
paren_depth -= 1
last_was_op = false
working << :p_close
else #what did I miss?
fail “What kind of token is this? ‘#{tok}’”
end
prev = tok
}
fail ‘Missing close parens!’ if paren_depth != 0
return working
end

def parse_parens(tokens)
working = []
i = 0
while i < tokens.length
if tokens[i] == :p_open
i += 1
paren_depth = 0
paren_tokens = []
while (tokens[i] != :p_close) || (paren_depth > 0)
if tokens[i] == :p_open
paren_depth += 1
elsif tokens[i] == :p_close
paren_depth -= 1
end
paren_tokens << tokens[i]
i += 1
end
working << parse(paren_tokens)
else
working << tokens[i]
end
i += 1
end
return working
end

def parse_ops(tokens, regex)
fail “Something broke: len = #{tokens.length}” if tokens.length < 3
|| (tokens.length % 2) == 0
i = 1
working = [ tokens[0] ]
while i < tokens.length
if regex =~ tokens[i].to_s
op = OpClass[tokens[i]]
lindex = working.length-1
working[lindex] = op.new(working[lindex], tokens[i],
tokens[i+1])
else
working << tokens[i]
working << tokens[i+1]
end
i += 2
end
return working
end

#scan for parens, then d, then /, then ±
def parse(tokens)
working = parse_parens(tokens)
fail “Something broke: len = #{working.length}” if (working.length %
2) == 0
working = parse_ops(working, /^d$/) if working.length > 1
fail “Something broke: len = #{working.length}” if (working.length %
2) == 0
working = parse_ops(working, /^[
/]$/) if working.length > 1
fail “Something broke: len = #{working.length}” if (working.length %
2) == 0
working = parse_ops(working, /^[±]$/) if working.length > 1
fail “Something broke: len = #{working.length}” if working.length !=
1
return working[0]
end

def parse_dice(str)
tokens = lex(str)
return parse(validate_and_cook(tokens))
end

end

class Dice

def initialize(expression)
@expression = parse_dice(expression)
end

def roll
return @expression.to_i
end

private

include DiceRoller

end

61alt.rb

#################################################################

module DiceRoller

RollMethod = ‘.roll’

def lex(str)
tokens = str.scan(/(00)|([-/()+d%0])|([1-9][0-9])|(.+)/)
tokens.each_index { |i|
tokens[i] = tokens[i].compact[0]
if not /^(00)|([-/()+d%0])|([1-9][0-9])$/ =~ tokens[i]
if /^\s+$/ =~ tokens[i]
tokens[i] = nil
else
fail “Found garbage in expression: ‘#{tokens[i]}’”
end
end
}
return tokens.compact
end

def validate_and_cook(tokens)
oper = /[-*/+d]/
num = /(\d+)|%/
last_was_op = true
paren_depth = 0
prev = ‘’
working = []
tokens.each_index { |i|
tok = tokens[i]
if num =~ tok
fail ‘A number cannot follow an expression!’ if not last_was_op
fail ‘Found spurious zero or number starting with zero!’ if tok
== ‘0’
if ( tok == ‘00’ || tok == ‘%’ )
fail ‘Can only use % or 00 after d!’ if prev != RollMethod
tokens[i] = 100
tok = 100
else
tok = tok.to_i
end
if prev == RollMethod
working << “(#{tok})”
else
working << tok
end
last_was_op = false
elsif oper =~ tok
tok = RollMethod if tok == ‘d’
if last_was_op
#handle case of dX meaning 1dX
if tok == RollMethod
fail ‘A d cannot follow a d!’ if prev == RollMethod
working << 1
else
fail ‘An operator cannot follow a operator!’
end
end
working << tok
last_was_op = true
elsif tok == “(”
fail ‘An expression cannot follow an expression!’ if not
last_was_op
paren_depth += 1
working << tok
elsif tok == “)”
fail ‘Incomplete expression at close paren!’ if last_was_op
fail ‘Too many close parens!’ if paren_depth < 1
paren_depth -= 1
last_was_op = false
working << tok
else #what did I miss?
fail “What kind of token is this? ‘#{tok}’”
end
prev = tok
}
fail ‘Missing close parens!’ if paren_depth != 0
return working
end

def parse_dice(str)
tokens = lex(str)
return validate_and_cook(tokens).to_s
end

end

class Fixnum
def roll(die)
fail “Die count must be nonnegative: ‘#{self}’” if self < 0
fail “Die size must be positive: ‘#{die}’” if die < 1
return (1…self).inject(0) { |sum, waste| sum + (rand(die)+1) }
end
end

class Dice

def initialize(expression)
@expression = parse_dice(expression)
end

def roll
return (eval @expression)
end

private

include DiceRoller

end

James Edward G. II wrote:

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

I agree. This is fantastic.

Thanks :slight_smile:

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

I have put it on my mental to-do list, but that doesn’t necessarily mean
that I actually get around to doing it. :wink:
Right now the grammar is subject to quite some restrictions that I would
like to remove, but I’ll need to learn more about parser generators to
do this.

Dennis

Another solution:

#! /usr/bin/ruby

change this to some fixed value for reproducable results

def random(i)

i

FIXME: check rand’s usabilty for throwing dices…

rand(i)+1
end

class DiceExpr

def initialize(rolls, sides)
@rolls, @sides = rolls, sides
end

def to_i
sides = @sides.to_i
([email protected]_i).inject(0) { | sum, i | sum += random(sides) }
end

def to_s
“(#{@rolls}d#{@sides})”
end

end

class Expr

def initialize(lhs, rhs, op)
@lhs, @rhs, @op = lhs, rhs, op
end

def to_i
@lhs.to_i.send(@op, @rhs.to_i)
end

def to_s
“(#{@lhs}#{@op}#{@rhs})”
end

end

class Dice

def initialize(expr)
@expr_org = @expr_str = expr
next_token
@expr = addend()
if @token
raise “parser error: tokens left: >#{@fulltoken}#{@expr_str}<”
end
end

“lexer”

@@regex = Regexp.compile(/^\s*([()±/]|[1-9][0-9]|d%|d)\s*/)
def next_token
@prev_token = @token
return @token = nil if @expr_str.empty?
match = @@regex.match(@expr_str)
if !match
raise “parser error: cannot tokenize input #{@expr_str}”
end
@expr_str = @expr_str[match.to_s.length, @expr_str.length]
@fulltoken = match.to_s # for “tokens left” error message only…
@token = match[1]
end

“parser”

bit lengthy but basically straightforward

def number() # number or parenthesized expression
raise “unexpeced >)<” if ( @token == ‘)’ )
if ( @token == ‘(’ )
next_token
val = addend
raise “parser error: parenthesis error, expected ) got #{@token}”
if @token != ‘)’
next_token
return val
end
raise “parse error: number expected, got #{@token}” if @token !~
/^[0-9]*$/
next_token
@prev_token
end

def dice()
if ( @token == ‘d’ )
rolls = 1
else
rolls = number()
end
while ( @token == ‘d’ || @token == ‘d%’ )
if @token == ‘d%’
rolls = DiceExpr.new(rolls, 100)
next_token
else
next_token
sides = number()
raise “parser error: missing sides expression” if !sides
rolls = DiceExpr.new(rolls, sides)
end
end
rolls
end

def factor()
lhs = dice()
while ( @token == ‘*’ || @token == ‘/’ )
op = @token
next_token
rhs = dice()
raise “parser error: missing factor” if !rhs
lhs = Expr.new(lhs, rhs, op)
end
lhs
end

def addend()
lhs = factor()
while ( @token == ‘+’ || @token == ‘-’ )
op = @token
next_token
rhs = factor()
raise “parser error: missing addend” if !rhs
lhs = Expr.new(lhs, rhs, op)
end
lhs
end

def to_s
“#{@expr_org} -> #{@expr.to_s}”
end

def roll
@expr.to_i
end

end

d = Dice.new(ARGV[0])

#puts d.to_s

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