A month ago I wrote a python script to accurately calculate probabilities for OL dice.
from scipy.special import binom
debug = True
def cashed(function):
if not hasattr(cashed, 'level'):
cashed.level = 0
def cashed_function(*args):
if debug:
dbgprint()
dbgprint(function.__name__ + '{}'.format(args))
cashed.level += 1
if not hasattr(cashed_function, 'mem'):
cashed_function.mem = {}
if args not in cashed_function.mem:
dbgprint('first time calculating')
cashed_function.mem[args] = function(*args)
if debug:
cashed.level -= 1
dbgprint('result: {}'.format(cashed_function.mem[args]))
return cashed_function.mem[args]
return cashed_function
def dbgprint(*args):
if debug:
print('| '*cashed.level + ' '.join(str(el) for el in args))
@cashed
def simple_dice_comb(result, die_type, number_of_dice):
k, d, n = result, die_type, number_of_dice
if any( x < 0 for x in (k, d, n)) or k < n or k > n*d:
return 0
elif n == 0:
return int(k == 0)
elif n == 1:
return int(k > 0 and k <= d)
else:
b = 1
while 2*b < n:
b <<= 1
return sum( simple_dice_comb(s, d, b) * simple_dice_comb(k-s, d, n-b)
for s in range(max(b, k - (n-b)*d), min(b*d, k+b-n) + 1) )
def simple_dice_min_comb(result, die_type, number_of_dice, minimum_die):
k, d, n, e = result, die_type, number_of_dice, minimum_die
return simple_dice_comb(k - n*(e-1), d-e+1, n)
@cashed
def dice_comb(result, die_type, number_of_dice=1, advantage=0):
k, d, n, m = result, die_type, number_of_dice, advantage
if any( x < 0 for x in (k, d, n) ) or d < 2 or k < n or k > d*n:
return 0
elif n == 0:
return int(k == 0) * d**m
elif m == 0:
return simple_dice_comb(k, d, n)
elif m > 0:
return sum( sum( simple_dice_min_comb(k-r*e, d, n-r, e+1) * int(binom(n+m, n-r))
* sum( (e-1)**(m-s) * int(binom(r+m, r+s))
for s in range(m+1) )
for r in range(max(1, n*(e+1) - k), (min(n, (d*n-k) / (d-e)) if d>e else n) + 1) )
for e in range(max(1, k - (n-1)*d), min(d, k/n) + 1) )
else:
return sum( sum( simple_dice_comb(k-r*a, a-1, n-r) * int(binom(n-m, n-r))
* sum( (d-a)**(-m-s) * int(binom(r-m, r+s))
for s in range(1-m) )
for r in range(max(1, k-n*(a-1)), (min(n, (k-n)/(a-1)) if a>1 else n) + 1) )
for a in range(max(1, int(0.5+float(k)/n)), min(d, k - (n-1)) + 1) )
binom = cashed(binom)
@cashed
def dice_prob(result, die_type=6, number_of_dice=1, advantage=0):
k, d, n, m = result, die_type, number_of_dice, advantage
if any( x < 0 for x in (k, d, n) ) or d < 2 or k < n:
return 0.
if n == 0:
return float(result == 0)
normal = dice_comb(k, d-1, n, m)
if m >= 0:
normal += sum( sum( dice_comb(s, d-1, n-mu, m) * dice_prob(k-mu*d-s, d, mu) * binom(n+m, mu)
for s in range(n-mu, min(k-mu*(d+1), (d-1)*(n-mu)) + 1) )
for mu in range(1, min(n, k/d+1)) )
if k >= (d+1)*n:
normal += dice_prob(k-d*n, d, n) * sum( (d-1)**(m-mu) * binom(n+m, n+mu)
for mu in range(m+1) )
else:
normal += sum( dice_comb(k, d-1, n, m+mu) * binom(n-m, mu)
for mu in range(1, -m+1) )
normal += sum( sum( dice_comb(s, d-1, n-mu) * dice_prob(k-mu*d-s, d, mu) * binom(n-m, n-mu)
for s in range(n-mu, k-mu*(d+1) + 1) )
for mu in range(1, min(n, k/d + 1)) )
if k >= (d+1)*n:
normal += dice_prob(k-d*n, d, n)
return float(normal) / d**(abs(m)+n)
def att(result, attribute_level, advantage):
l = int(attribute_level)
if l == 0:
return 100. * dice_prob(result, 20, 1, advantage)
else:
d = 2 + 2*l - 6*(l/5) - 4*(l/8) + 2*(l/10)
n = 1 + l/5 + l/8
return 100. * sum( dice_prob(s, 20) * dice_prob(result - s, d, n, advantage)
for s in range(1, result) )
How it works: simple_dice_comb calculates the number of possibilities to roll a certain result with a number of identical dice, without any advantage shenanigans. Simple enough.
simple_dice_min_comb does the same, but it counts only the possibilities, where all dice are higher than a specific value. The trick is that this is equal to rolling a lower result with smaller dice.
dice_comb now includes dis/advantage. If you take advantage, for example, the function looks at all the possible values for the highest disregarded die, and sees how many possibilities there are to roll the desired result with the highest disregarded die value as lower limit. However, explosions are not considered yet.
Next, dice_prob turns the number of combinations into a probability. Here we finally check explosions.
The final step is to combine a d20 with the attribute dice, which is done in the function att.
I think that the results of this functions are exact up to numerical inaccuracies.