Evaluate dice rolling notation strings

2019-03-08 10:54发布

Rules

Write a function that accepts string as a parameter, returning evaluated value of expression in dice notation, including addition and multiplication.

To clear the things up, here comes EBNF definition of legal expressions:

roll ::= [positive integer], "d", positive integer
entity ::= roll | positive number
expression ::= entity { [, whitespace], "+"|"*"[, whitespace], entity }

Example inputs:

  • "3d6 + 12"
  • "4*d12 + 3"
  • "d100"

Using eval functions, or similar, is not forbidden, but I encourage to solving without using these. Re-entrancy is welcome.

I cannot provide test-cases, as output should be random ;).

Format your answers' titles: language, n characters (important notes — no eval, etc.)


My ruby solution, 92 81 characters, using eval:

def f s
eval s.gsub(/(\d+)?d(\d+)/i){eval"a+=rand $2.to_i;"*a=($1||1).to_i}
end

Another ruby solution, not shorter (92 characters), but I find it interesting — it still uses eval but this time in quite creative way.

class Fixnum
def**b
eval"a+=rand b;"*a=self
end
end
def f s
eval s.gsub(/d/,'**')
end

14条回答
我只想做你的唯一
2楼-- · 2019-03-08 10:58

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
查看更多
兄弟一词,经得起流年.
3楼-- · 2019-03-08 10:59

J

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
查看更多
乱世女痞
4楼-- · 2019-03-08 11:04

Python 124 chars with eval, 154 without.

Just to show python doesn't have to be readable, here's a 124 character solution, with a similar eval-based approach to the original:

import random,re
f=lambda s:eval(re.sub(r'(\d*)d(\d+)',lambda m:int(m.group(1)or 1)*('+random.randint(1,%s)'%m.group(2)),s))

[Edit] And here's a 154 character one without eval:

import random,re
f=lambda s:sum(int(c or 0)+sum(random.randint(1,int(b))for i in[0]*int(a or 1))for a,b,c in re.findall(r'(\d*)d(\d+)(\s*[+-]\s*\d+)?',s))

Note: both will work for inputs like "2d6 + 1d3 + 5" but don't support more advanced variants like "2d3d6" or negative dice ("1d6-4" is OK, but "1d6-2d4" isn't) (You can shave off 2 characters to not support negative numbers at all in the second one instead)

查看更多
放我归山
5楼-- · 2019-03-08 11:06

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.

查看更多
Lonely孤独者°
6楼-- · 2019-03-08 11:07

JavaScript solution, 340 chars when compressed (no eval, supports prefixed multiplicator and suffixed addition):

function comp (s, m, n, f, a) {
    m = parseInt( m );
    if( isNaN( m ) ) m = 1;
    n = parseInt( n );
    if( isNaN( n ) ) n = 1;
    f = parseInt( f );
    a = typeof(a) == 'string' ? parseInt( a.replace(/\s/g, '') ) : 0;
    if( isNaN( a ) ) a = 0;
    var r = 0;
    for( var i=0; i<n; i++ )
        r += Math.floor( Math.random() * f );
    return r * m + a;
};
function parse( de ) {
    return comp.apply( this, de.match(/(?:(\d+)\s*\*\s*)?(\d*)d(\d+)(?:\s*([\+\-]\s*\d+))?/i) );
}

Test code:

var test = ["3d6 + 12", "4*d12 + 3", "d100"];
for(var i in test)
    alert( test[i] + ": " + parse(test[i]) );

Compressed version (pretty sure you can do shorter):

function c(s,m,n,f,a){m=parseInt(m);if(isNaN(m))m=1;n=parseInt(n);if(isNaN(n))n=1;f=parseInt(f);a=typeof(a)=='string'?parseInt(a.replace(/\s/g,'')):0;if(isNaN(a))a=0;var r=0;for(var i=0;i<n;i++)r+=Math.floor(Math.random()*f);return r*m+a;};function p(d){return c.apply(this,d.match(/(?:(\d+)\s*\*\s*)?(\d*)d(\d+)(?:\s*([\+\-]\s*\d+))?/i));}
查看更多
We Are One
7楼-- · 2019-03-08 11:08

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 result
  • add adds the top two numbers on the stack and pushes the result
  • roll pops the dice size from the stack, then the count, rolls a size-sided dice count times and pushes the result
  • a number just pushes itself onto the stack

This 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()
查看更多
登录 后发表回答