Dice sums comparison probability
1. Review


The name calculate is vague. Something like attack_freq would be more specific.
There's no docstring. What does calculate do? What does it return?
The values in two_dice_prob are not probabilities, they are counts or frequencies. (To get probabilities, you'd have to divide by 36.) I would use a name like two_dice_freq.
By default numpy.zeros gives you an array of floats, but the result array only contains integers, so you could specify dtype=int when creating it. (Alternatively, by constructing the result using NumPy throughout, it can be arranged that it has the right data type.)
When working with NumPy it's nearly always fastest if you structure the code to consist of a sequence of whole-array operations, rather than looping over the elements in native Python.

In this case we can use numpy.mgrid to construct arrays containing all possibilities for attack shift, defence roll and attack roll:

shift, defence, attack = np.mgrid[-12:13, 1:7, 2:13]


Then we can find the difference between attack and defence simultaneously for all possibilities:

diff = attack + shift - defence


The three outcome classes can now be computed by comparing the difference against 0 and 5 to get arrays of Booleans, and then assembling the outcomes into a single array using numpy.stack:

missed = diff &lt;= 0
destroyed = diff &gt;= 5
disabled = ~(missed | destroyed)
outcome = np.stack((missed, disabled, destroyed), axis=1)


The reason for choosing to stack along axis=1 is so that the outcome array has the right shape, that is, (25, 3, 6, 11). We multiply the last axis by the two-dice frequencies, and then sum over the last two axes. This leaves us with an array of frequencies with shape (25, 3) as required:

return (outcome * two_dice_freq).sum(axis=(2, 3))


If we had chosen to stack along axis=0, then at this point we would have an array with shape (3, 25) and we'd have to transpose it before returning. By choosing the right axis to stack along we avoided this transposition.


2. Revised code

import numpy as np

def attack_freq():
    """Return array with shape (25, 3), whose (i + 12)'th row contains the
    frequencies of the three outcome classes (missed, disabled,
    destroyed) when attack bonus minus defense bonus is i.

    """
    two_dice_freq = [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1]
    shift, defence, attack = np.mgrid[-12:13, 1:7, 2:13]
    diff = attack + shift - defence
    missed = diff &lt;= 0
    destroyed = diff &gt;= 5
    disabled = ~(missed | destroyed)
    outcome = np.stack((missed, disabled, destroyed), axis=1)
    return (outcome * two_dice_freq).sum(axis=(2, 3))


This computes the same results as the code in the post:

&gt;&gt;&gt; np.array_equal(calculate(), attack_freq())
True


but it's roughly four times as fast:

&gt;&gt;&gt; from timeit import timeit
&gt;&gt;&gt; timeit(calculate, number=1000)
0.38954256599993187
&gt;&gt;&gt; timeit(attack_freq, number=1000)
0.09676001900004394


The actual runtimes are so small, less than a millisecond, that this speedup doesn't really matter in practice, since you'd only need to build this table once. However, the general technique, of applying a series of whole-array NumPy operations instead of looping in native Python, can make practical differences in other kinds of program, so it's worth practicing the technique even in small cases like this.