def make_ccdf_plot(die, offset=0, sd=None): if sd is None: gaussian = Die.gaussian(die) else: gaussian = Die.gaussian(die.mean(), sd) print('KS: %0.2f%%' % (die.ks_stat(gaussian) * 100.0)) print('Above end: %0.2f%%' % ((gaussian > die.max_outcome()) * 100.0)) gaussian_clipped = gaussian.clip(die) for outcome, die_chance, gaussian_chance in zip(die.outcomes(), die.ccdf(), gaussian_clipped.ccdf()): print( '%d: %0.2f%%, %0.2f%%, %0.3f, %0.3f, %+0.2f%%' % (outcome, 100.0 * die_chance, 100.0 * gaussian_chance, die_chance / gaussian_chance, gaussian_chance / die_chance, 100.0 * (gaussian_chance - die_chance))) fig = plt.figure(figsize=figsize) ax = plt.subplot(111) ax.grid() ax.plot(die.outcomes() + offset, die.ccdf() * 100.0, marker='.') ax.plot(gaussian.outcomes() + offset, gaussian.ccdf() * 100.0, marker='.') ax.set_ylim(bottom=0, top=100) ax.set_ylabel('Chance (%) of rolling at least') return ax
def iter_explode(die_size): for i in range(1, die_size - 1): for j in range(i + 1, die_size - 1): name = 'success on %d+, explode on %d+' % (i + 1, j + 1) explode_chance = (die_size - j) / die_size yield Die.from_faces([0] * i + [1] * (die_size - i)).explode( 10, chance=explode_chance).rename(name)
def plot_opposed_fixed_total(ax, single_die, die_counts, die_name): coef = 4.0 * single_die.mean() * single_die.mean() / single_die.variance() legend = [] for die_count in die_counts: x = [] cdf = [] for die_count_a in range(die_count + 1): die_count_b = die_count - die_count_a die_bonus_a = numpy.sqrt( numpy.maximum(die_count_a * coef - variance_b, 0.0)) die_bonus_b = numpy.sqrt( numpy.maximum(die_count_b * coef - variance_b, 0.0)) opposed = single_die.repeat_and_sum( die_count_a) - single_die.repeat_and_sum(die_count_b) p = opposed >= Die.coin() x.append(die_bonus_b - die_bonus_a) cdf.append(p) pmf = (numpy.diff(cdf, prepend=0.0) + numpy.diff(cdf, append=1.0)) * 50.0 marker = marker_map[die_count] if die_count in marker_map else '.' ax.plot(x, pmf, marker=marker) legend.append('%dd%s' % (die_count, die_name)) ax.legend(legend, loc='upper right') ax.set_xlabel('Bonus disparity after conversion to roll-over') ax.set_ylabel('"Chance" (%)') ax.grid(which="both")
def plot_opposed_fixed_side(ax, single_die, die_counts, die_name): coef = 4.0 * single_die.mean() * single_die.mean() / single_die.variance() legend = [] for die_count_a in die_counts: x = [] ccdf = [] for die_count_b in range(0, 100): die_bonus_a = numpy.sqrt( numpy.maximum(die_count_a * coef - variance_b, 0.0)) die_bonus_b = numpy.sqrt( numpy.maximum(die_count_b * coef - variance_b, 0.0)) opposed = single_die.repeat_and_sum( die_count_a) - single_die.repeat_and_sum(die_count_b) p = opposed >= Die.coin() # p = (opposed > 0) / ((opposed > 0) + (opposed < 0)) x.append(die_bonus_b - die_bonus_a) ccdf.append(p) #pmf = (numpy.diff(cdf, prepend=0.0) + numpy.diff(cdf, append=1.0)) * 50.0 pmf = -numpy.diff(ccdf, prepend=1.0) * 100.0 marker = marker_map[die_count_a] if die_count_a in marker_map else '.' ax.plot(x + 0.5 * numpy.diff(x, append=x[-1]), pmf, marker=marker) legend.append('%dd%s' % (die_count_a, die_name)) ax.legend(legend, loc='upper right') ax.set_xlabel('Bonus disparity after conversion to roll-over') ax.set_ylabel('"Chance" (%)') ax.grid(which="both") ax.set_ylim(bottom=0)
def iter_negative_success(die_size): for i in range(1, die_size - 1): for j in range(i + 1, die_size - 1): name = 'success on %d+, negative success on %d or lower' % (j + 1, i) yield Die.from_faces([-1] * i + [0] * (j - i) + [1] * (die_size - i - j)).rename(name)
def bf_keep_highest(die, num_dice, num_keep): if num_keep == 0: return Die(0) counter = brute_force.BruteForceCounter() for rolls in numpy.ndindex((len(die), ) * num_dice): total = sum(sorted(rolls)[-num_keep:]) + num_keep * die.min_outcome() mass = numpy.product(die.pmf()[numpy.array(rolls)]) counter.insert(total, mass) return counter.die()
def die(self): min_outcome = min(self.data.keys()) max_outcome = max(self.data.keys()) pmf = numpy.zeros((max_outcome - min_outcome + 1, )) for outcome, mass in self.data.items(): pmf[outcome - min_outcome] += mass die = Die(pmf, min_outcome) return die
def iter_standard_dice(): for die_size in [3, 4, 5, 6, 7, 8, 9, 10, 12]: metadata = { 'size' : die_size, 'threshold' : 0, 'feature' : 'standard die', 'faces' : [str(x) for x in range(1, die_size+1)], 'notes' : '', } yield Die.d(die_size), metadata
def bf_keep_lowest(num_keep, *dice): if num_keep == 0: return Die(0) counter = brute_force.BruteForceCounter() shape = tuple(len(die) for die in dice) min_outcome = sum(sorted(die.min_outcome() for die in dice)[:num_keep]) for rolls in numpy.ndindex(shape): total = sum(sorted(rolls)[:num_keep]) + min_outcome mass = numpy.product( [die.pmf()[roll] for die, roll in zip(dice, rolls)]) counter.insert(total, mass) return counter.die()
def make_pmf_plot(die, offset=0, sd=None): if sd is None: gaussian = Die.gaussian(die) else: gaussian = Die.gaussian(die.mean(), sd) print('Var:', die.variance()) print('MAD median:', die.mad_median()) fig = plt.figure(figsize=figsize) ax = plt.subplot(111) ax.grid() print('Gaussian mode:', gaussian.mode()) ax.plot(die.outcomes() + offset, die.pmf() * 100.0, marker='.') ax.plot(gaussian.outcomes() + offset, gaussian.pmf() * 100.0, marker='.') ax.set_ylim(bottom=0) ax.set_ylabel('Chance (%) of rolling exactly') return ax
def make_mos_plot(die, offset=0, sd=None): if sd is None: gaussian = Die.gaussian(die) else: gaussian = Die.gaussian(die.mean(), sd) fig = plt.figure(figsize=figsize) ax = plt.subplot(111) ax.grid() x = numpy.arange(min(0, die.min_outcome()), die.max_outcome() + 1) y_die = [die.margin_of_success(t).mean() for t in x] y_gaussian = [gaussian.margin_of_success(t).mean() for t in x] ax.plot(x + offset, y_die, marker='.') ax.plot(x + offset, y_gaussian, marker='.') ax.set_xlim(left=x[0], right=x[-1]) ax.set_ylim(bottom=0) ax.set_ylabel('Mean margin of success') return ax
def iter_dice(die_size, explode): for faces in iter_faces(die_size): base_die = Die.from_faces(faces) if not explode: yield base_die.rename('d' + str(die_size) + str(faces)) else: if faces[0] == -1: return max_explode = min( faces.count(1) // 2, faces.count(0), die_size // 4) for explode_faces in range(1, max_explode + 1): if faces[-explode_faces] != 1: break explode_chance = explode_faces / die_size yield base_die.explode(10, chance=explode_chance).rename( 'd%d%s!%s' % (die_size, faces, explode_faces))
def iter_exploding_dice(): for die_size in [4, 6, 8, 10, 12]: for i in range(1, die_size): faces = [0]*i + [1]*(die_size-i) for num_explode in range(0, die_size-i+1): metadata = { 'size' : die_size, 'threshold' : i+1, 'feature' : 'none', 'faces' : [str(x) for x in faces], 'notes' : '', } if num_explode == 1: metadata['feature'] = 'explode on %d' % die_size elif num_explode == (die_size-i): metadata['feature'] = 'explode on any success' elif num_explode > 0: metadata['feature'] = 'explode on %d+' % (die_size - num_explode + 1) for j in range(num_explode): metadata['faces'][-1-j] += '!' if die_size == 10 and i == 7 and num_explode == 1: metadata['notes'] = 'New World of Darkness' yield Die.from_faces(faces).explode(200, chance=num_explode/die_size), metadata
dpi = 120 def plot_extremeness(ax, die, **kwargs): extremeness = numpy.minimum(die.cdf(), die.ccdf()) ax.plot(die.outcomes(), extremeness, **kwargs) def semilogy_extremeness(ax, die, **kwargs): print('%0.3f | %0.3f' % (die.mean(), die.standard_deviation())) extremeness = numpy.minimum(die.cdf(), die.ccdf()) extremeness[extremeness > 0.5] = 1.0 ax.semilogy(die.outcomes(), 1.0 / extremeness, **kwargs) die_3d6 = Die.d(3, 6) points_3d6 = die_3d6.relabel(point_buy_to_use).repeat_and_sum(6) die_3d6r1 = Die.d(3, 5) + 3 points_3d6r1 = die_3d6r1.relabel(point_buy_to_use).repeat_and_sum(6) die_3d6r2 = Die.d(3, 4) + 6 points_3d6r2 = die_3d6r2.relabel(point_buy_to_use).repeat_and_sum(6) die_4d6kh3 = Die.d(6).repeat_and_keep_and_sum(4, keep_highest=3) points_4d6kh3 = die_4d6kh3.relabel(point_buy_to_use).repeat_and_sum(6) die_4d6r1kh3 = Die.d(5).repeat_and_keep_and_sum(4, keep_highest=3) + 3 points_4d6r1kh3 = die_4d6r1kh3.relabel(point_buy_to_use).repeat_and_sum(6) die_5d6kh3 = Die.d(6).repeat_and_keep_and_sum(5, keep_highest=3)
def hitch_chance(self): return Die.min(*self.dice) <= 0
fig = plt.figure(figsize=figsize) ax = plt.subplot(111) ax.plot(g_x, g_ccdf * 100.0, linestyle=':') ax.plot(x_ccdf, ccdf * 100.0, marker='.') ax.grid() ax.set_xlim(left, right) ax.set_xlabel('Deviation from mean (SDs)') ax.set_ylabel('Chance to hit (%)') ax.set_ylim(0, 100.0) ax.set_title('%d pool size (KS = %0.2f%%)' % (pool_size, ks * 100.0)) ccdf_frame_path = 'output/frames/ccdf_%03d.png' plt.savefig(ccdf_frame_path % frame_index, dpi = dpi, bbox_inches = "tight") plt.close() make_webm(pmf_frame_path, 'output/success_pool_roe_%s_pmf.webm' % name) make_webm(ccdf_frame_path, 'output/success_pool_roe_%s_ccdf.webm' % name) #make_anim(Die.coin(1/6), 'd6_2plus') #make_anim(Die.coin(2/6), 'd6_3plus') make_anim(Die.coin(3/6), 'd6_4plus') #make_anim(Die.coin(4/6), 'd6_5plus') #make_anim(Die.coin(5/6), 'd6_6plus') exalted2e = Die.from_faces([0]*6 + [1]*3 + [2]) owod = Die.from_faces([-1] + [0]*5 + [1]*4) nwod = Die.from_faces([0]*7 + [1]*3).explode(10, chance=0.1) #make_anim(exalted2e, 'exalted2e') #make_anim(owod, 'owod') #make_anim(nwod, 'nwod')
legend.append('%d pips' % (pip_count, )) ax.legend(legend, loc='upper right') ax.set_xlabel('Roll-over number needed to hit') ax.set_ylabel('Chance (%)') ax.grid(which="both") die_counts_simple = [1, 2, 3, 4, 5, 6, 8, 10, 15, 20] die_counts_standard = die_counts_simple # coin fig = plt.figure(figsize=figsize) ax = plt.subplot(111) plot_pmf(ax, Die.coin(), die_counts_simple, '(d6>=4)') ax.set_xlim(left=left, right=right) ax.set_ylim(bottom=0) plt.savefig('output/sum_pool_4plus.png', dpi=dpi, bbox_inches="tight") # 2 plus on d6 fig = plt.figure(figsize=figsize) ax = plt.subplot(111) plot_pmf(ax, Die.coin(5 / 6), die_counts_simple, '(d6>=2)') ax.set_xlim(left=left, right=right) ax.set_ylim(bottom=0) plt.savefig('output/sum_pool_2plus.png', dpi=dpi, bbox_inches="tight")
def test_d_multiple(): assert Die.d(1, 2, 2).weights() == pytest.approx([2, 3, 2, 1])
def objective(sd): gaussian = Die.gaussian(10.5, sd) return Die.d20.ks_stat(gaussian)
ax.grid(which="both") ax.set_ylim(bottom=0) die_counts_simple = [1, 2, 3, 4, 5, 6, 8, 10, 15, 20] die_counts_standard = [1, 2, 3, 4, 5, 6] left = -4 right = 4 # 3+ on d6 fig = plt.figure(figsize=figsize) ax = plt.subplot(111) plot_opposed_fixed_side(ax, Die.coin(2 / 3), die_counts_simple, '(d6>=3)') ax.set_xlim(left=left, right=right) plt.savefig('output/add_pool_opposed_3plus.png', dpi=dpi, bbox_inches="tight") # coin fig = plt.figure(figsize=figsize) ax = plt.subplot(111) plot_opposed_fixed_side(ax, Die.coin(), die_counts_simple, '(d6>=4)') ax.set_xlim(left=left, right=right) plt.savefig('output/add_pool_opposed_4plus.png', dpi=dpi, bbox_inches="tight") # 5+ on d6
import matplotlib as mpl import matplotlib.pyplot as plt figsize = (8, 4.5) dpi = 120 right = 4 max_success = 4 x = numpy.arange(0.0, right + 1e-6, 0.1) y = [numpy.zeros_like(x) for i in range(max_success + 1)] for i, half_life in enumerate(x): mean = half_life * numpy.log(2) die = Die.poisson(mean, max_outcome=max_success + 1) ccdf = die.ccdf() for num_successes in range(max_success + 1): y[num_successes][i] = ccdf[num_successes] fig = plt.figure(figsize=figsize) ax = plt.subplot(111) ax.grid() legend = [] for num_successes in range(1, max_success + 1): ax.plot(x, 100.0 * y[num_successes]) if num_successes == 1: legend.append('%d success' % num_successes) else:
def make_anim(die, name): coef = 2.0 * die.mean() / die.standard_deviation() offset = offset_sd * offset_sd / (coef * coef) for frame_index, pool_size_a in enumerate(pool_sizes): die_bonus_a = coef * numpy.sqrt(pool_size_a + offset) pool_a = die.repeat_and_sum(pool_size_a) x = [] ccdf = [] for pool_size_b in range(0, 201): die_bonus_b = coef * numpy.sqrt(pool_size_b + offset) opposed = pool_a - die.repeat_and_sum(pool_size_b) p = opposed >= Die.coin() # Coin flip on ties. # p = (opposed > 0) / ((opposed > 0) + (opposed < 0)) # Reroll ties. x.append(die_bonus_b - die_bonus_a) ccdf.append(p) x = numpy.array(x) ccdf = numpy.array(ccdf) pmf = -numpy.diff(ccdf, prepend=1.0) # compute ks ks_ccdf = 0.5 * scipy.special.erfc(x / 2.0) ks = numpy.max(numpy.abs(ccdf - ks_ccdf)) # pmf plot fig = plt.figure(figsize=figsize) ax = plt.subplot(111) g_x = numpy.arange(left, right + 0.001, 0.001) g_pmf = numpy.exp(-0.5 * numpy.square(g_x / numpy.sqrt(2))) / ( 2.0 * numpy.sqrt(numpy.pi)) ax.plot(g_x, g_pmf, linestyle=':') ax.plot(x + 0.5 * numpy.diff(x, append=x[-1]), pmf * pool_a.standard_deviation() * 2.0, marker='.') ax.grid() ax.set_xlim(left, right) ax.set_xlabel('Difference in roll-over result') ax.set_ylabel('Normalized probability') ax.set_ylim(0, 0.3) ax.set_title('%d pool size for side A (KS = %0.2f%%)' % (pool_size_a, ks * 100.0)) pmf_frame_path = 'output/frames/pmf_%03d.png' plt.savefig(pmf_frame_path % frame_index, dpi=dpi, bbox_inches="tight") plt.close() # ccdf plot fig = plt.figure(figsize=figsize) ax = plt.subplot(111) g_x = numpy.arange(left, right + 0.001, 0.001) g_ccdf = 50.0 * scipy.special.erfc(g_x / 2.0) ax.plot(g_x, g_ccdf, linestyle=':') ax.plot(x, ccdf * 100.0, marker='.') ax.grid() ax.set_xlim(left, right) ax.set_xlabel('Side A roll-over disadvantage') ax.set_ylabel('Chance for side A to win (%)') ax.set_ylim(0, 100.0) ax.set_title('%d pool size for side A (KS = %0.2f%%)' % (pool_size_a, ks * 100.0)) ccdf_frame_path = 'output/frames/ccdf_%03d.png' plt.savefig(ccdf_frame_path % frame_index, dpi=dpi, bbox_inches="tight") plt.close() make_webm(pmf_frame_path, 'output/success_pool_roe_opposed_%s_pmf.webm' % name) make_webm(ccdf_frame_path, 'output/success_pool_roe_opposed_%s_ccdf.webm' % name)
def test_coin(): b = Die.bernoulli() c = Die.coin() assert b.pmf() == pytest.approx([0.5, 0.5]) assert c.pmf() == pytest.approx([0.5, 0.5])
from hdroller import Die result = '' base_bw_dice = 24 bw_scale = 2 for pbta_bonus in [-1, 0, 1, 2, 3]: num_bw_dice = int(bw_scale * pbta_bonus + base_bw_dice) bw_die = Die.coin(0.5).repeat_and_sum(num_bw_dice) pbta_high_chance = Die.d(2, 6) + pbta_bonus >= 11 pbta_mid_chance = Die.d(2, 6) + pbta_bonus >= 7 bw_target = base_bw_dice - (base_bw_dice // 2) bw_high_chance = bw_die >= bw_target + int(2 * bw_scale) bw_mid_chance = bw_die >= bw_target result += '\t'.join( str(x) for x in [ pbta_bonus, num_bw_dice, pbta_mid_chance, bw_mid_chance, pbta_high_chance, bw_high_chance ]) result += '\n' with open('output/pbta_bw.csv', mode='w') as outfile: outfile.write(result)
# Minimize distance on d20 curve. def objective(sd): gaussian = Die.gaussian(10.5, sd) return Die.d20.ks_stat(gaussian) print( scipy.optimize.minimize_scalar(objective, bounds=(5.0, 8.0), method='bounded')) d20 = Die.d20 opposed_d20 = d20 - d20 - Die.coin() figsize = (8, 4.5) dpi = 150 def make_pmf_plot(die, offset=0, sd=None): if sd is None: gaussian = Die.gaussian(die) else: gaussian = Die.gaussian(die.mean(), sd) print('Var:', die.variance()) print('MAD median:', die.mad_median()) fig = plt.figure(figsize=figsize)
plt.savefig(pmf_frame_path % frame_index, dpi=dpi, bbox_inches="tight") plt.close() # ccdf plot fig = plt.figure(figsize=figsize) ax = plt.subplot(111) g_x = numpy.arange(left, right + 0.001, 0.001) g_ccdf = 50.0 * scipy.special.erfc(g_x / 2.0) ax.plot(g_x, g_ccdf, linestyle=':') ax.plot(x, ccdf * 100.0, marker='.') ax.grid() ax.set_xlim(left, right) ax.set_xlabel('Side A roll-over disadvantage') ax.set_ylabel('Chance for side A to win (%)') ax.set_ylim(0, 100.0) ax.set_title('%d pool size for side A (KS = %0.2f%%)' % (pool_size_a, ks * 100.0)) ccdf_frame_path = 'output/frames/ccdf_%03d.png' plt.savefig(ccdf_frame_path % frame_index, dpi=dpi, bbox_inches="tight") plt.close() make_webm(pmf_frame_path, 'output/success_pool_roe_opposed_%s_pmf.webm' % name) make_webm(ccdf_frame_path, 'output/success_pool_roe_opposed_%s_ccdf.webm' % name) make_anim(Die.coin(3 / 6), 'd6_4plus')
# pf2e fig = plt.figure(figsize=figsize) ax = plt.subplot(111) ax.set_xlabel('Number needed to hit') ax.set_ylabel('Mean damage') ax.grid(which='both') legend = [] x = numpy.arange(-35, 20) semilogy_mean_damage(ax, Die.d(12) + 4, x=x, damage=6.5, linestyle='--', zorder=2.1) legend.append('d12+4, damage = 6.5') semilogy_mean_damage_mos(ax, Die.d(24) - 1, damage_mult=1.0, x=x) legend.append('d24-2, damage = MoS + 1') semilogy_mean_damage_pf2e_mos(ax, Die.d(20) + 1, damage_mult=6.5, x=x) legend.append('d20, damage = 6.5 if hit, double if MoS >= 10') ax.set_xlim(left=-20, right=20) ax.set_ylim(bottom=1, top=50) ax.legend(legend, loc='upper right')
def test_ks_stat_flat_number(): assert Die(10).ks_stat(Die(10)) == 0.0 assert Die(10).ks_stat(Die(9)) == 1.0
def iter_simple_dice(die_size): # standard die metadata = { 'size' : die_size, 'threshold' : 0, 'feature' : 'standard die', 'faces' : [str(x) for x in range(1, die_size+1)], 'notes' : '', } yield Die.d(die_size), metadata # exploding standard die (problem: VMR too high to be useful) """ metadata = { 'size' : die_size, 'threshold' : 0, 'feature' : 'exploding standard die', 'faces' : [str(x) for x in range(1, die_size+1)], 'notes' : '', } metadata['faces'][-1] += '!' yield Die.d(die_size).explode(10), metadata """ # odd standard dice if die_size % 2 == 0 and die_size >= 6 and die_size <= 10: metadata = { 'size' : die_size-1, 'threshold' : 0, 'feature' : 'standard die', 'faces' : [str(x) for x in range(1, die_size-1+1)], 'notes' : '', } yield Die.d(die_size-1), metadata # no feature for i in range(1, die_size): faces = [0]*i + [1]*(die_size-i) metadata = { 'size' : die_size, 'threshold' : i+1, 'feature' : 'none', 'faces' : [str(x) for x in faces], 'notes' : '', } if die_size == 6 and i == 3: metadata['notes'] = 'Burning Wheel' if die_size == 6 and i == 4: metadata['notes'] = 'Shadowrun 4e' yield Die.from_faces(faces), metadata # negative on 1 for i in range(2, die_size): faces = [-1] + [0]*(i-1) + [1]*(die_size-i) metadata = { 'size' : die_size, 'threshold' : i+1, 'feature' : '-1 success on bottom face', 'faces' : [str(x) for x in faces], 'notes' : '', } if die_size == 10 and i == 6: metadata['notes'] = 'Old World of Darkness' yield Die.from_faces(faces), metadata # double on max for i in range(1, die_size-1): faces = [0]*i + [1]*(die_size-i-1) + [2] metadata = { 'size' : die_size, 'threshold' : i+1, 'feature' : '2 successes on top face', 'faces' : [str(x) for x in faces], 'notes' : '', } if die_size == 10 and i == 6: metadata['notes'] = 'Exalted 2e' yield Die.from_faces(faces), metadata # explode for i in range(1, die_size): faces = [0]*i + [1]*(die_size-i) metadata = { 'size' : die_size, 'threshold' : i+1, 'feature' : 'explode on top face', 'faces' : [str(x) for x in faces], 'notes' : '', } metadata['faces'][-1] += '!' if die_size == 10 and i == 7: metadata['notes'] = 'New World of Darkness' yield Die.from_faces(faces).explode(100, chance=1/die_size), metadata """
def iter_double_success(die_size): for i in range(1, die_size - 1): for j in range(i + 1, die_size - 1): name = 'success on %d+, double success on %d+' % (i + 1, j + 1) yield Die.from_faces([0] * i + [1] * (j - i) + [2] * (die_size - i - j)).rename(name)