In [1]:
from __future__ import absolute_import, division, print_function
from builtins import *
import scipy.stats as stats
import scipy.misc as scm
import matplotlib.pyplot as plt
import random
from itertools import product
%matplotlib inline
In [2]:
def successes(tries, prob):
return {x: stats.binom.pmf(x, tries, prob) for x in range(tries+1)}
In [3]:
def compound_successes(profile, prob):
return {x: sum(stats.binom.pmf(x, y, prob) * profile[y] for y in profile if y >= x) for x in profile}
In [4]:
def chances_from_dice(target_val, n, faces):
return (1 / faces ** n) * sum(((-1) ** i) * scm.comb(n, i) * scm.comb(target_val - faces * i - 1, n - 1)
for i in range((target_val - n) // faces + 1))
In [40]:
def assign_damage(profile, damage_list, target, specials=None):
random.seed()
n = len(set(damage_list))
output = {0: profile[0] * 100}
if n == 1:
for h in profile:
if h == 0 or profile[h] < 0.001: continue
if h * damage_list[0] <= target['total']:
output[h * damage_list[0]] = profile[h] * 100
else:
output[target['total']] += profile[h] * 100
return output
for h in profile:
if h == 0 or profile[h] < 0.001: continue
for i in range(4000):
throw = []
for j in range(h):
if specials and 'multi-melta' in specials:
throw.append(max(random.choice(damage_list), random.choice(damage_list)))
else:
throw.append(random.choice(damage_list))
spare_wounds = target['wounds']
for dice in throw:
if dice >= spare_wounds:
dice = spare_wounds
spare_wounds = target['wounds']
else:
spare_wounds = spare_wounds - dice
result = min(sum(throw), target['total'])
if result in output:
output[result] += 0.025 * profile[h]
else:
output[result] = 0.025 * profile[h]
return output
In [24]:
def damage_dealt(profile, target):
damage_list = [min(x, target['wounds']) for x in damage_types[profile['damage']]]
if isinstance(profile['shots'], int):
hits = successes(profile['shots'], profile['p_hit'])
else:
if ('specials' in profile) and ('demolisher' in profile['specials']) and (target['total'] / target['wounds'] > 5):
profile['shots'] = 'd6'
hits = compound_successes(hits_profiles(profile['shots']) , profile['p_hit'])
wounds = compound_successes(hits, wound_chance(profile['S'], target['T']))
unsaved = compound_successes(wounds, min(min(1 - target['save'] - profile.get('AP', 0) / 6, 1), 1 - target.get('invul', 0)))
if 'FNP' in target:
unsaved = compound_successes(unsaved, 1 - target['FNP'])
return assign_damage(unsaved, damage_list, target, profile.get('specials', None))
In [7]:
def wound_chance(S, T):
if S == T:
return 1 / 2
elif S >= 2 * T:
return 5 / 6
elif S > T:
return 2 / 3
elif 2 * S <= T:
return 1 / 6
else:
return 1 / 3
In [8]:
def wound_chart(profiles, targets, titles=None):
titles = product(*titles) if titles else 0
if len(profiles) * len(targets) == 1:
xs = damage_dealt(profiles[0], targets[0]).keys()
ys = damage_dealt(profiles[0], targets[0]).values()
plt.bar(xs, ys, color = 'forestgreen', edgecolor= 'darkgreen', width = 1, align = 'center')
plt.axis('tight')
plt.set_xlabel('Wounds')
plt.set_ylabel('Probability, %')
if titles:
plt.title('{1} vs {0}'.format(*titles.next()), fontweight = 'bold')
else:
plt.title('{shots} shots, {damage} damage, p_hit: {p_hit:.0%}, S: {S}'.format(**profiles[0]))
plt.figure(1).text(0.95, 0.81, 'Expected: {:.1f} wounds'.format(sum(x * y / 100 for x, y in zip(xs, ys))),
fontsize = 12, fontweight = 'bold')
plt.figure(1).text(0.95, 0.68, 'Target profile', fontsize = 12, fontweight = 'bold')
plt.figure(1).text(0.95, 0.64, '\n'.join(['{}: {:.2g}'.format(key, value) for key, value in targets[0].items()]),
verticalalignment='top', fontsize = 12)
plt.show()
return None
num_rows = (len(profiles) * len(targets) + 1) // 2
f, axarr = plt.subplots(num_rows, 2, figsize=(13.5, 4 * num_rows), sharey=True, sharex=True)
plt.subplots_adjust(wspace=0.76)
counter = 0
xbound = [0, 0]
for target in targets:
for profile in profiles:
frame = axarr[counter] if num_rows == 1 else axarr[counter // 2, counter % 2]
(xs, ys) = zip(*damage_dealt(profile, target).iteritems())
xbound = [min(xbound[0], min(xs)), max(xbound[1], max(xs))]
frame.set_xlim(xbound[0] - 0.5, xbound[1] + 0.5)
frame.bar(xs, ys, color = 'forestgreen', edgecolor= 'darkgreen', width = 1, align = 'center')
frame.set_xlabel('Wounds')
frame.set_ylabel('Probability, %')
if titles:
frame.set_title('{1} vs {0}'.format(*titles.next()), fontweight = 'bold')
else:
frame.set_title('{shots} shots, {damage} damage, p_hit: {p_hit:.0%}, S: {S}'.format(**profile), fontweight = 'bold')
frame.text(1.05, 0.81, 'Expected: {:.1f} wounds'.format(sum(x * y / 100 for x, y in zip(xs, ys))),
fontsize = 12, fontweight = 'bold', transform = frame.transAxes)
frame.text(1.05, 0.64, '\n'.join(['{}: {:.2g}'.format(key, value) for key, value in target.items()]),
verticalalignment='top', fontsize = 12, transform = frame.transAxes)
frame.text(1.05, 0.68, 'Target profile', fontsize = 12, fontweight = 'bold', transform = frame.transAxes)
counter += 1
plt.show()
return None
In [45]:
damage_types = {'1': [1], '2': [2], '3': [3], '4': [4], 'd3': [1, 2, 3], 'd6': [1, 2, 3, 4, 5, 6]}
hits_profiles = lambda code: {x: chances_from_dice(x, int(code[0]), int(code[-1])) for x in range(0, int(code[0])*int(code[-1]))}
profiles = [{'shots': '3d6', 'p_hit': 1, 'S': 6, 'damage': '2', 'AP': -1},
{'shots': 6, 'p_hit': 1 / 2 + 1 / 6 * 1 / 2, 'S': 7, 'damage': '2', 'AP': -1}]
targets = [{'T': 6, 'wounds': 10, 'total': 10, 'save': 1 / 2, 'invul': 1 / 3},
{'T': 5, 'wounds': 10, 'total': 10, 'save': 1 / 2, 'invul': 1 / 3},
{'T': 3, 'wounds': 1, 'total': 10, 'save': 1 / 6, 'invul': 1 / 2, 'FNP': 1 / 6}]
In [46]:
wound_chart(profiles, targets, [['Ravager', 'Venom', '10 Wyches'], ['3 inferno cannons', '3 Autocannons, Take Aim!']])