Evaluate dice rolling notation strings

前端 未结 14 1427
甜味超标
甜味超标 2021-01-30 14:55

Rules

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

To

相关标签:
14条回答
  • 2021-01-30 15:21

    F# (no eval and no regex)

    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.

    0 讨论(0)
  • 2021-01-30 15:24

    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.

    0 讨论(0)
  • 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
    
    0 讨论(0)
  • 2021-01-30 15:27

    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
    
    0 讨论(0)
  • 2021-01-30 15:27

    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
    
    0 讨论(0)
  • 2021-01-30 15:33

    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()
    
    0 讨论(0)
提交回复
热议问题