Big Mathematics And OL

Anyone else like analyzing the math both used to build OL anf that OL has the potential to invoke?

I manually calculated that there’s 1325 possible ways to partition a level one’s attribute points into perfect levels, not accounting for actually matching numbers to the stats, and I used a website to calculate the odds a super buffed up Learning roll could pass a bunch of different CRs. Like, advantage 6 on a level 10.

There were multiple attempts at properly capturing the stochastics behind an Action Roll, but as far as I know all of them use either test data (which is pretty good but not precise hurr my mathematics) or are a tiny bit off.
There was an anydice script, but that too isn’t perfect.

1 Like

Even with minor flaws, the math is pretty fun! Plus, the patterns help mae the game be rather comprehensible.

Way too much math for me to be trying to figure all that out. I just like that the way damage is done evens out fighters and mages.

I wonder if there are any mathematical ways to optimise the game/come up with new or better rules? Such as finding ways to improve combat or anything, or maybe fixing some balancing issues. I’m a big math enthusiast, so I’d love a challenge like that.

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.