Write a function that accepts string as a parameter, returning evaluated value of expression in dice notation, including addition and multiplication.
To
233 characters
This solution should be fully generic, in that it can handle just about any string you throw at it, even something crazy such as:
43d29d16d21*9 + d7d9*91 + 2*d24*7
Need to define this globally:
let r = new System.Random()
Fully obfuscated version:
let f(s:string)=let g d o i p (t:string)=t.Split([|d|])|>Array.fold(fun s x->o s (p x))i in g '+'(+)0(g '*' (*) 1 (fun s->let b=ref true in g 'd'(+)1(fun t->if !b then b:=false;(if t.Trim()=""then 1 else int t)else r.Next(int t))s))s
Readable version:
let f (s:string) =
let g d o i p (t:string) =
t.Split([|d|]) |> Array.fold (fun s x -> o s (p x)) i
g '+' (+) 0 (g '*' (*) 1 (fun s ->
let b = ref true
g 'd' (+) 1 (fun t ->
if !b then b := false; (if t.Trim() = "" then 1 else int t)
else r.Next(int t)) s)) s
I'm challenging someone to beat this solution (in any language) without using eval
or regular expressions. I think it's likely to be possible, but I am interested in seeing the approach still.
python, 197 chars in obscured version.
Readable version: 369 chars. No eval, straight forward parsing.
import random
def dice(s):
return sum(term(x) for x in s.split('+'))
def term(t):
p = t.split('*')
return factor(p[0]) if len(p)==1 else factor(p[0])*factor(p[1])
def factor(f):
p = f.split('d')
if len(p)==1:
return int(f)
return sum(random.randint(1, int(g[1]) if g[1] else 6) for \
i in range(int(g[0]) if g[0] else 1))
compressed version: 258 chars, single character names, abused conditional expressions, shortcut in logical expression:
import random
def d(s):
return sum(t(x.split('*')) for x in s.split('+'))
def t(p):
return f(p[0])*f(p[1]) if p[1:] else f(p[0])
def f(s):
g = s.split('d')
return sum(random.randint(1, int(g[1] or 6)) for i in range(int(g[0] or 1))) if g[1:] else int(s)
obscured version: 216 chars, using reduce, map heavily to avoid "def", "return".
import random
def d(s):
return sum(map(lambda t:reduce(lambda x,y:x*y,map(lambda f:reduce(lambda x,y:sum(random.randint(1,int(y or 6)) for i in range(int(x or 1))), f.split('d')+[1]),t.split('*')),1),s.split('+')))
Last version: 197 chars, folded in @Brain's comments, added testing runs.
import random
R=reduce;D=lambda s:sum(map(lambda t:R(int.__mul__,map(lambda f:R(lambda x,y:sum(random.randint(1,int(y or 6))for i in[0]*int(x or 1)),f.split('d')+[1]),t.split('*'))),s.split('+')))
Tests:
>>> for dice_expr in ["3d6 + 12", "4*d12 + 3","3d+12", "43d29d16d21*9+d7d9*91+2*d24*7"]: print dice_expr, ": ", list(D(dice_expr) for i in range(10))
...
3d6 + 12 : [22, 21, 22, 27, 21, 22, 25, 19, 22, 25]
4*d12 + 3 : [7, 39, 23, 35, 23, 23, 35, 27, 23, 7]
3d+12 : [16, 25, 21, 25, 20, 18, 27, 18, 27, 25]
43d29d16d21*9+d7d9*91+2*d24*7 : [571338, 550124, 539370, 578099, 496948, 525259, 527563, 546459, 615556, 588495]
This solution can't handle whitespaces without adjacent digits. so "43d29d16d21*9+d7d9*91+2*d24*7" will work, but "43d29d16d21*9 + d7d9*91 + 2*d24*7" will not, due to the second space (between "+" and "d"). It can be corrected by first removing whitespaces from s, but this will make the code longer than 200 chars, so I'll keep the bug.
With cobbal's help, squeeze everything into 93 characters.
$ jconsole e=:".@([`('%'"_)@.(=&'/')"0@,)@:(3 :'":(1".r{.y)([:+/>:@?@$) ::(y&[)0".}.y}.~r=.y i.''d'''@>)@;: e '3d6 + 12' 20 e 10$,:'3d6 + 12' 19 23 20 26 24 20 20 20 24 27 e 10$,:'4*d12 + 3' 28 52 56 16 52 52 52 36 44 56 e 10$,:'d100' 51 51 79 58 22 47 95 6 5 64
Ruby, 166 characters, no eval
In my opinion quite elegant ;).
def g s,o=%w{\+ \* d}
o[a=0]?s[/#{p=o.pop}/]?g(s.sub(/(\d+)?\s*(#{p})\s*(\d+)/i){c=$3.to_i
o[1]?($1||1).to_i.times{a+=rand c}+a:$1.to_i.send($2,c)},o<<p):g(s,o):s
end
Deobfuscated version + comments:
def evaluate(string, opers = ["\\+","\\*","d"])
if opers.empty?
string
else
if string.scan(opers.last[/.$/]).empty? # check if string contains last element of opers array
# Proceed to next operator from opers array.
opers.pop
evaluate(string, opers)
else # string contains that character...
# This is hard to deobfuscate. It substitutes subexpression with highest priority with
# its value (e.g. chooses random value for XdY, or counts value of N+M or N*M), and
# calls recursively evaluate with substituted string.
evaluate(string.sub(/(\d+)?\s*(#{opers.last})\s*(\d+)/i) { a,c=0,$3.to_i; ($2 == 'd') ? ($1||1).to_i.times{a+=rand c}+a : $1.to_i.send($2,c) }, opers)
end
end
end
Ruby, 87 characters, uses eval
Here's my Ruby solution, partially based on the OP's. It's five characters shorter and only uses eval
once.
def f s
eval s.gsub(/(\d+)?[dD](\d+)/){n=$1?$1.to_i: 1;n.times{n+=rand $2.to_i};n}
end
A readable version of the code:
def f s
eval (s.gsub /(\d+)?[dD](\d+)/ do
n = $1 ? $1.to_i : 1
n.times { n += rand $2.to_i }
n
end)
end
Python, 452 bytes in the compressed version
I'm not sure if this is cool, ugly, or plain stupid, but it was fun writing it.
What we do is as follows: We use regexes (which is usually not the right tool for this kind of thing) to convert the dice notation string into a list of commands in a small, stack-based language. This language has four commands:
mul
multiplies the top two numbers on the stack and pushes the resultadd
adds the top two numbers on the stack and pushes the resultroll
pops the dice size from the stack, then the count, rolls a size-sided dice count times and pushes the resultThis command list is then evaluated.
import re, random
def dice_eval(s):
s = s.replace(" ","")
s = re.sub(r"(\d+|[d+*])",r"\1 ",s) #seperate tokens by spaces
s = re.sub(r"(^|[+*] )d",r"\g<1>1 d",s) #e.g. change d 6 to 1 d 6
while "*" in s:
s = re.sub(r"([^+]+) \* ([^+]+)",r"\1 \2mul ",s,1)
while "+" in s:
s = re.sub(r"(.+) \+ (.+)",r"\1 \2add ",s,1)
s = re.sub(r"d (\d+) ",r"\1 roll ",s)
stack = []
for token in s.split():
if token == "mul":
stack.append(stack.pop() * stack.pop())
elif token == "add":
stack.append(stack.pop() + stack.pop())
elif token == "roll":
v = 0
dice = stack.pop()
for i in xrange(stack.pop()):
v += random.randint(1,dice)
stack.append(v)
elif token.isdigit():
stack.append(int(token))
else:
raise ValueError
assert len(stack) == 1
return stack.pop()
print dice_eval("2*d12+3d20*3+d6")
By the way (this was discussed in the question comments), this implementation will allow strings like "2d3d6"
, understanding this as "roll a d3 twice, then roll a d6 as many times as the result of the two rolls."
Also, although there is some error checking, it still expects a valid input. Passing "*4" for example will result in an infinite loop.
Here is the compressed version (not pretty):
import re, random
r=re.sub
def e(s):
s=r(" ","",s)
s=r(r"(\d+|[d+*])",r"\1 ",s)
s=r(r"(^|[+*] )d",r"\g<1>1 d",s)
while"*"in s:s=r(r"([^+]+) \* ([^+]+)",r"\1 \2M ",s)
while"+"in s:s=r(r"(.+) \+ (.+)",r"\1 \2A ",s)
s=r(r"d (\d+)",r"\1 R",s)
t=[]
a=t.append
p=t.pop
for k in s.split():
if k=="M":a(p()*p())
elif k=="A":a(p()+p())
elif k=="R":
v=0
d=p()
for i in [[]]*p():v+=random.randint(1,d)
a(v)
else:a(int(k))
return p()