I was playing a fun little board game called U.S. Patent No. 1 about time traveling to register your time machine as the first U.S. patent. I started to wonder what the dice probabilities were for basic attacks. In the game, you rolls 2 dice to attack, and your opponent rolls 1 die for defense. If the attack result is greater than the defense, then a time machine part is "disabled", and if it is greater by 5 or more, than it is "destroyed"; otherwise it "misses". However, this is not the end: players can also have attack and defense bonuses ranging from 0-12, which are added to the roll result before making the comparison.
To begin my program, I first did a straightforward brute force calculation, where each number in the array was a probability out of 216, and attack and defense bonus from 0 through 12 were cycled through.
But this was a bit slower than I wanted (~500ms, not terrible), so I decided to utilize a bit more math and hardcode the probability distribution of two dice to use multiplication instead of addition. I also replaced the dict with enum keys with a list where 0 is "MISSED", 1 is "DISABLED", and 2 is "DESTROYED", which seemed to significantly reduce overhead by my timings (from ~200ms to ~50ms).
But then I looked more carefully at my results and realized something that is in retrospect obvious about probability distributions of this nature: adding one to both the attack and defense bonus means the bonuses have no affect on the distribution. Represented symbolically, the distribution for A, D is always going to be the same as the distribution for A + N, D + N for integers N. So instead of calculating separate values for attack and defense bonuses, I can just calculate a raw value for attack shift (which also reformatted my array to be smaller and have different dimensions):
from itertools import product import numpy as np def calculate(): two_dice_prob = [0, *range(6), *range(6,0,-1)] result = np.zeros((25,3)) for attack_shift in range(-12, 13): result_counts =  * 3 for defense, attack in product(range(1,7), range(2,13)): base_attack = attack attack += attack_shift if attack - 5 >= defense: result_counts += two_dice_prob[base_attack] elif attack > defense: result_counts += two_dice_prob[base_attack] else: result_counts += two_dice_prob[base_attack] result[attack_shift+12] = result_counts return result if __name__ == '__main__': print("Result:") print(calculate())
The result is a numpy array where the top is an attack with a penalty of -12, and the bottom is the attack with a bonus of 12, and every other row has an attack bonus of 1 more than the previous row. By merit of processing less cases, it now only takes ~6ms.
I was wondering how I could optimize the calculate function even more. I suspect I am not utilizing
numpy's efficiency fully, since I am still not completely familiar with it.