fizbin

I'm just this guy, you know?

  • he/him

40-ish white guy in tech


Back when I was in (math) grad school, I had a friend at the same institution who was there getting her CS Ph.D. (and unlike me, she actually came out the other end with the intended degree)

While in grad. school she had a side business/hobby of designing RPGs (which she's still doing), and one time was designing an RPG that for reasons I don't remember had a design constraint that it had to use only six-sided dice, but could use a lot of them. The basic mechanic was that a player would roll a handful of dice, add them up, add or subtract bonuses and penalties that applied to the situation, and then see if the total was high enough to do whatever thing the player had been trying to do. Sometimes, the thing a player was trying to do was against another player or an NPC and in that situation the other player (or game master) would roll a handful of dice and (after bonuses and penalties on both sides) compare totals.

She had run some simulations to see how bonuses or penalties affected the success probability of certain actions and had noticed something very strange: a bonus/penalty (hereafter just a "bonus", with the understanding that those can be negative) against a fixed target had a significantly larger effect on the probability than when the same bonus was used against an opponent who was also rolling a bunch of dice.

Specifically, she found that a +X bonus against a fixed target was often similar in effect to a +(1.5*X) bonus against a fixed target. Her question to me was: why? What's going on?

Below the cut: I recreate some simulations to demonstrate the effect. (No solution today. Solution... IDK, tomorrow? Tuesday? Sometime. People in the comments might get it too)


So this post is also an excuse for me to see whether I can use the markdown conversion on jupyter notebooks to make decent cohost posts. So here goes, simulation via a Jupyter notebook running python:


So first we have some preliminaries that just set up some things that let us simulate rolling a bunch of dice:

import random
import tabulate

def make_roller(n_dice):
    def roll():
        return sum(random.choice([1,2,3,4,5,6]) for _ in range(n_dice))
    return roll

def run_simulation(n, sim):
    return sum(int(bool(sim())) for _ in range(n))

Now let's simulate rolling eight dice against a fixed target of 28 (the average roll with 8 dice) and let it play out with a bonus varying from -5 to 5:

roll_eight = make_roller(8)
bonuses = range(-5,6)
data1 = [
    run_simulation(
        100_000,
        lambda:roll_eight() + bonus > 28
    )/100_000 for bonus in bonuses]
tabulate.tabulate(
    zip(bonuses, data1),
    headers=("bonus", "success vs fixed"),
    tablefmt='html')
bonus success vs fixed
-5 0.12992
-4 0.17955
-3 0.23817
-2 0.30526
-1 0.37977
0 0.45956
1 0.54225
2 0.61701
3 0.694
4 0.76391
5 0.81995

Now let's do the same but instead of trying to beat a fixed 28, we'll try to beat an opponent also rolling eight dice, again with the same range of bonuses.
We'll also put them all in a table together.

data2 = [
    run_simulation(
        100_000,
        lambda:roll_eight() + bonus > roll_eight()
    )/100_000 for bonus in bonuses]
tabulate.tabulate(
    zip(bonuses, data1, data2),
    headers=("bonus", "vs fixed", "vs opponent"),
    tablefmt='html')
bonus vs fixed vs opponent
-5 0.12992 0.21131
-4 0.17955 0.25973
-3 0.23817 0.30608
-2 0.30526 0.35652
-1 0.37977 0.41421
0 0.45956 0.4727
1 0.54225 0.526
2 0.61701 0.58642
3 0.694 0.64108
4 0.76391 0.69444
5 0.81995 0.74497

The effect may be more obvious if we consider the difference for each column compared to the probability at bonus 0:

data1_adj = [x - data1[5] for x in data1]
data2_adj = [x - data2[5] for x in data2]
tabulate.tabulate(
    zip(bonuses, data1_adj, data2_adj),
    headers=("bonus", "change vs fixed", "change vs opponent"),
    tablefmt='html')
bonus change vs fixed change vs opponent
-5 -0.32964 -0.26139
-4 -0.28001 -0.21297
-3 -0.22139 -0.16662
-2 -0.1543 -0.11618
-1 -0.07979 -0.05849
0 0 0
1 0.08269 0.0533
2 0.15745 0.11372
3 0.23444 0.16838
4 0.30435 0.22174
5 0.36039 0.27227

Note that a +2 bonus against a fixed target has about the effect of a +3 bonus against an opponent. Likewise, compared the effect of a -3 penalty on a fixed target
and a -4 penalty against an opponent.

Now, to show the effect more clearly, we're going to get a bit ridiculous because the effect is more obvious with more dice. So let's repeat the calculations above but
with twenty dice and exploring bonuses from -10 to +10:

roller = make_roller(20)
bonuses = range(-10,11)
data1 = [
    run_simulation(
        100_000,
        lambda:roller() + bonus > 70
    )/100_000 for bonus in bonuses]
data2 = [
    run_simulation(
        100_000,
        lambda:roller() + bonus > roller()
    )/100_000 for bonus in bonuses]
data1_adj = [x - data1[10] for x in data1]
data2_adj = [x - data2[10] for x in data2]
tabulate.tabulate(
    zip(bonuses, data1_adj, data2_adj),
    headers=("bonus", "change vs fixed", "change vs opponent"),
    tablefmt='html')
bonus change vs fixed change vs opponent
-10 -0.38659 -0.31381
-9 -0.3623 -0.29133
-8 -0.33612 -0.2643
-7 -0.30841 -0.23859
-6 -0.27278 -0.20767
-5 -0.23568 -0.17729
-4 -0.19169 -0.14083
-3 -0.14617 -0.10967
-2 -0.09681 -0.06953
-1 -0.0472 -0.03594
0 0 0
1 0.05355 0.03667
2 0.10447 0.07155
3 0.15486 0.11092
4 0.20224 0.14626
5 0.25325 0.1793
6 0.29298 0.21333
7 0.32978 0.24444
8 0.36507 0.277
9 0.39607 0.30341
10 0.42102 0.32976

Note how close the change from a +7 or -7 against a fixed goal is to the change from a +10 or -10 against an opponent.


Hrm, not bad for a default conversion (that was the result of just exporting the notebook as markdown). I'll have to see tomorrow how/whether it's viable to use a notebook with images for a cohost post.


You must log in to comment.

in reply to @fizbin's post:

Indeed it is, which helps explain why +5 against a fixed target is similar to +7 against an opponent, and also +7 against a fixed target as opposed to +10 against an opponent. (Since 7/5 and 10/7 are so close to sqrt(2))