# vanishing triangles of dice-roll probabilities

```#!/usr/bin/python
# -*- coding: utf-8 -*-
"""I was surprised early one morning in May 2009, lying awake in bed,
to realize that the graph of 2d6 probabilities was a sort of
piecewise-linear approximation to the bell curve,
and that the 3d6 probability graph sort of looked like
```
So I wrote this program to compute and display
vanishing triangles of dice probabilities.
The bottom row is the number on the dice;
the next row up is the number of combinations of the dice giving it,
and therefore would be a probability if you divided by the total;
the next row up is differences between adjacent numbers of combinations;
the next row up is differences between adjacent differences
(from the row just mentioned);
and so on.

In the process I wrote yet another table-layout algorithm,
as if the world doesn’t already have enough of those.
Oh well.
It only took half an hour.

The vanishing triangle here shows
(by default)
that the probability distribution of 4d6
is piecewise-cubic in four segments.
Similarly 3d6 is indeed piecewise-quadratic (in three segments),
as 2d6 is piecewise-linear in two segments.

Interestingly, a chunk of Pascal’s triangle
appears in the corners of the table.

"""

import sys, re

def diffs(seq):
"Yield differences of adjacent items in seq."
seq = iter(seq)
last = seq.next()
for item in seq:
yield item - last
last = item

def vanishing_triangle(alist):
"Compute a vanishing triangle for the elements of alist."
for depth in range(len(alist)):
yield alist
alist = list(diffs(alist))

def dice_combos(dice, sides):
"Yield all the possible combinations of N dice with M sides."
if dice == 0:
yield ()
return
for number in range(1, sides+1):
for combo in dice_combos(dice=dice-1, sides=sides):
yield (number,) + combo

def layout_table(rows):
"Takes a sequence of sequences of strings; yields a sequence of strings."
assert rows
for row in rows:
assert len(row) == len(rows[0])

column_widths = [max(len(row[ii]) for row in rows)
for ii in range(len(rows[0]))]
formatstr = ''.join('%%%ds' % width for width in column_widths) + '\n'
for row in rows:
yield formatstr % tuple(row)

def triangle_to_table(xlab, triangle):
"Yield a sequence of strings formatting a triangle in a table."
width = len(xlab) * 2 - 1
rows = []
xlab_row = [item for label in xlab for item in [str(label), '']][:-1]
rows.append(xlab_row)

for data_row in triangle:
number_blank_columns = len(xlab) - len(data_row)
assert number_blank_columns >= 0

row = (('',) * number_blank_columns +
tuple([item for datum in data_row
for item in [str(datum), '']][:-1]) +
('',) * number_blank_columns)
assert len(row) == len(xlab_row), (data_row, len(row), row, xlab_row)
rows.append(row)

rows.reverse()
return layout_table(rows)

def vanishing_triangle_table(xlab, sequence):
tri = list(vanishing_triangle(sequence))
return triangle_to_table(xlab, tri)

def dice_combo_frequencies(dice, sides):
freqs = {}
for combo in dice_combos(dice=dice, sides=sides):
total = sum(combo)
freqs.setdefault(total, 0)
freqs[total] += 1
keys = sorted(freqs.keys())

return keys, [freqs[key] for key in keys]

def main(argv):
if len(argv) == 1:
dice, sides = 4, 6
elif len(argv) == 2:
mo = re.match(r'(\d+)d(\d+)\$', argv[1])
if not mo:
return usage()
dice = int(mo.group(1))
sides = int(mo.group(2))
else:
return usage()

print __doc__

keys, freqs = dice_combo_frequencies(dice=dice, sides=sides)
sys.stdout.writelines(vanishing_triangle_table(keys, freqs))

if __name__ == '__main__':
main(sys.argv)
--
To unsubscribe: http://lists.canonical.org/mailman/listinfo/kragen-hacks
```