[QUIZ] Dice Roller (#61)

J

Jacob Fugal

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 Fugal
 
R

Robert Retzbach

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 :>
 
A

Adam Shelly

Here's my solution.

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

-- begin buggy code --
CENT =3D BnfTerm.new(/(%)/ ) { '100' }
INTEGER =3D /([1-9][0-9]*)/
DICE =3D BnfTerm.new(CENT,:|,INTEGER)
term =3D BnfTerm.new()
ROLL =3D 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 =3D DIEROLL
def initialize expr
@expr =3D 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=3D 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 =20
#repeat to deal with nesting
end
def roll
eval(@rule)
end
end

d =3D Dice.new(ARGV[0]||'d6')
(ARGV[1] || 1).to_i.times { print "#{d.roll} " }
puts
 
G

Gregory Seidman

56:47AM +0900, Ruby Quiz 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 Moss 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 = tokens.compact[0]
if not /^(00)|([-*\/()+d%0])|([1-9][0-9]*)$/ =~ tokens
if /^\s+$/ =~ tokens
tokens = nil
else
fail "Found garbage in expression: '#{tokens}'"
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
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 = 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 == :p_open
i += 1
paren_depth = 0
paren_tokens = []
while (tokens != :p_close) || (paren_depth > 0)
if tokens == :p_open
paren_depth += 1
elsif tokens == :p_close
paren_depth -= 1
end
paren_tokens << tokens
i += 1
end
working << parse(paren_tokens)
else
working << tokens
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.to_s
op = OpClass[tokens]
lindex = working.length-1
working[lindex] = op.new(working[lindex], tokens, tokens[i+1])
else
working << tokens
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 = tokens.compact[0]
if not /^(00)|([-*\/()+d%0])|([1-9][0-9]*)$/ =~ tokens
if /^\s+$/ =~ tokens
tokens = nil
else
fail "Found garbage in expression: '#{tokens}'"
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
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 = 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
 
R

Ross Bamford

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...
 
D

Dennis Ranke

James said:
I agree. This is fantastic.

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

I have put it on my mental to-do list, but that doesn't necessarily mean
that I actually get around to doing it. ;)
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
 
M

Morus Walter

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
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,995
Messages
2,570,230
Members
46,817
Latest member
DicWeils

Latest Threads

Top