Skip to content

Instantly share code, notes, and snippets.

@jsocol
Last active June 22, 2020 01:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jsocol/74c3f3e38d37c7dcefc847389564854a to your computer and use it in GitHub Desktop.
Save jsocol/74c3f3e38d37c7dcefc847389564854a to your computer and use it in GitHub Desktop.
A Monte Carlo dice roller to investigate ability score generation methods and other probability questions
import argparse
import random
__all__ = ['d4', 'd6', 'd8', 'd10', 'd12', 'd20', 'd100']
class D():
"""A die.
Simulates an n-sided die, e.g. to create a d6, pass 6:
>>> d4, d6, d8, d10, d12, d20 = D(4), D(6), D(8), D(10), D(12), D(20)
To roll, you can call d6.roll() (or just d6()):
>>> d6.roll()
<<< 4
>>> d6()
<<< 2
Or even just:
>>> d6
<<< 3
(NB: I'm sure I owe someone an apology for that.)
Need to roll with (dis) advantage?
>>> d20, d20
<<< (20, 13)
But the best way to use these is to do math with dice. For example, if you
need to roll 3d8 + 4:
>>> 3 * d8 + 4
<<< 22
Or add dice:
>>> d8 + d4
<<< 9
Remember these are random rolls, so the examples above are not guaranteed.
"""
def __init__(self, sides):
self.sides = sides
def roll(self):
"""Roll the dice!"""
return random.randint(1, self.sides)
def __call__(self):
return self.roll()
def __mul__(self, n):
"""Roll n (an integer) number of dice."""
return sum([self.roll() for _ in range(n)])
__rmul__ = __mul__
def __add__(self, n):
"""Add an integer or another die."""
if isinstance(n, D):
n = n.roll()
return n + self.roll()
__radd__ = __add__
def __repr__(self):
return str(self.roll())
def __str__(self):
return 'd%d' % self.sides
d4 = D(4)
d6 = D(6)
d8 = D(8)
d10 = D(10)
d12 = D(12)
d20 = D(20)
d100 = D(100)
def _get_cli_options():
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--count', type=int, default=1)
parser.add_argument('-s', '--size', type=int, default=20)
parser.add_argument('-r', '--roll', default=None)
# parser.add_argument('--adv', action='store_true', default=False)
# parser.add_argument('--dis', action='store_true', default=False)
return parser.parse_args()
def _main():
options = _get_cli_options()
if options.roll is not None:
count, _, size = options.roll.partition('d')
count = int(count)
size = int(size)
else:
count = options.count
size = options.size
die = D(size)
rolls = [die.roll() for _ in range(count)]
total = sum(rolls)
print('rolling {}d{}'.format(count, size))
print('{}'.format(rolls))
print('total: {}'.format(total))
__main__ = _main
if __name__ == '__main__':
_main()
# coding: utf-8
"""A monte carlo dice simulator
Given a method for rolling a set of dice, generate a histogram of results by
actually trying it a bunch.
"""
import argparse
import importlib
import os
import sys
from collections import defaultdict
from dice import d4, d6, d8, d10, d12, d20, d100, D # noqa: F401
def mk_roller(*dice, drop=0):
"""Create a roller from a set of dice and a number to drop
If drop >= 0, drop the lowest rolls. If drop < 0, drop the highest.
"""
def roller():
rolls = [d.roll() for d in dice]
keep = len(dice) - abs(drop)
rolls = list(sorted(rolls, reverse=drop >= 0))[0:keep]
return sum(rolls)
roller.__name__ = [str(d) for d in dice].__str__() + ' drop %d' % drop
return roller
adv = mk_roller(d20, d20, drop=1)
adv.__doc__ = 'advantage'
disadv = mk_roller(d20, d20, drop=-1)
disadv.__doc__ = 'disadvantage'
def noam():
"""4d6 + 1d4 drop 2"""
rolls = [d6.roll(), d6.roll(), d6.roll(), d6.roll(), d4.roll()]
rolls = list(sorted(rolls, reverse=True))
rolls = rolls[0:3]
return sum(rolls)
def threed6():
"""3d6"""
return 3 * d6
def fourdrop1():
"""4d6 drop 1"""
rolls = sorted([d6.roll() for _ in range(4)])
top3 = list(reversed(rolls))[0:3]
return sum(top3)
def oned20():
"""1d20 to embrace chaos"""
return 1 * d20
def r4d6ro1dl1():
"""4d6, reroll 1s once, drop lowest"""
rolls = []
for _ in range(4): # 4d6
roll = d6.roll()
if roll == 1: # Reroll 1s once
roll = d6.roll()
rolls.append(roll)
rolls = list(sorted(rolls, reverse=True))
return sum(rolls[0:3])
TESTS = {
'3d6': threed6,
'4d6d1': fourdrop1,
'adv': adv,
'd20': oned20,
'disadv': disadv,
'noam': noam,
'4d6ro1d1': r4d6ro1dl1,
}
def calc_stats(totals, iters):
roll_total = sum(k * v for k, v in totals.items())
mean = roll_total / iters
variance = 0.0
for score, count in totals.items():
variance += ((score - mean) ** 2) * count
variance /= (iters - 1)
stdev = variance ** 0.5
return {
'mean': mean,
'stdev': stdev,
'variance': variance,
}
def rolltest(roller, iters=10000, precision=3, graph='normal', zoom=1,
max_cols=None):
print('Method: %s\nDesc: %s' % (roller.__name__, roller.__doc__))
print('Iterations: %d, Precision: %d' % (iters, precision))
totals = defaultdict(int)
for _ in range(iters):
score = roller()
totals[score] += 1
stats = calc_stats(totals, iters)
print('Average: {mean:0.2f}; StDev: {stdev:0.2f}'.format(**stats))
output_fmt = '{0:3d} [{1:5.1f}%]: {2}'
if max_cols is None:
prefix_length = len(output_fmt.format(18, 15.1, ''))
max_cols = os.get_terminal_size().columns - prefix_length
if graph == 'normal':
for k in sorted(totals.keys()):
frac = round(totals[k] / iters, precision)
cols = int(frac * max_cols * zoom)
print(output_fmt.format(k, frac * 100., '*' * cols))
elif graph == 'atmost':
cumulative = 0.0
for k in sorted(totals.keys()):
cumulative += round(totals[k] / iters, precision)
cols = int(cumulative * max_cols)
print(output_fmt.format(k, cumulative * 100., '*' * cols))
elif graph == 'atleast':
cumulative = 1.0
for k in sorted(totals.keys()):
cols = int(cumulative * max_cols)
print(output_fmt.format(k, cumulative * 100., '*' * cols))
cumulative -= round(totals[k] / iters, precision)
else:
print('Unsupported graph mode: %s' % graph)
def make_parser():
parser = argparse.ArgumentParser()
parser.add_argument('-p', '--precision', default=3, type=int)
parser.add_argument('-i', '--iters', default=10000, type=int)
parser.add_argument('-t', '--test', dest='tests', choices=TESTS.keys(),
action='append', default=[])
parser.add_argument('-l', '--lambda', dest='lamb')
parser.add_argument('-r', '--raw')
parser.add_argument('--imp', help=('A dotted path to a roller, e.g. '
'--imp my.module.roller, where '
'roller is a callable'))
parser.add_argument('-g', '--graph', default='normal',
choices=['normal', 'atleast', 'atmost'])
parser.add_argument('-z', '--zoom', default=1, type=int)
parser.add_argument('-m', '--max-cols', default=None, type=int)
return parser
def get_opts(argv=None):
parser = make_parser()
if argv is None:
argv = sys.argv[1:]
return parser.parse_args(argv)
def main():
options = get_opts()
to_run = []
for test in options.tests:
to_run.append(TESTS[test])
if options.lamb:
roller = eval('lambda: %s' % options.lamb)
roller.__doc__ = 'user lambda: %s' % options.lamb
to_run.append(roller)
if options.raw:
roller = eval(options.raw)
roller.__doc__ = 'user input: %s' % options.raw
to_run.append(roller)
if options.imp:
path, _, method = options.imp.rpartition('.')
module = importlib.import_module(path)
roller = getattr(module, method)
to_run.append(roller)
for test in to_run:
rolltest(test, iters=options.iters, precision=options.precision,
graph=options.graph, zoom=options.zoom,
max_cols=options.max_cols)
if __name__ == '__main__':
main()
$ python rolltest.py --help
usage: rolltest.py [-h] [-p PRECISION] [-i ITERS]
[-t {3d6,4d6d1,adv,d20,disadv,noam,4d6ro1d1}] [-l LAMB]
[-r RAW] [--imp IMP] [-g {normal,atleast,atmost}] [-z ZOOM]
optional arguments:
-h, --help show this help message and exit
-p PRECISION, --precision PRECISION
-i ITERS, --iters ITERS
-t {3d6,4d6d1,adv,d20,disadv,noam,4d6ro1d1}, --test {3d6,4d6d1,adv,d20,disadv,noam,4d6ro1d1}
-l LAMB, --lambda LAMB
-r RAW, --raw RAW
--imp IMP A dotted path to a roller, e.g. --imp
my.module.roller, where roller is a callable
-g {normal,atleast,atmost}, --graph {normal,atleast,atmost}
-z ZOOM, --zoom ZOOM
$ python rolltest.py --test 4d6ro1d1 --zoom 2 --precision 4
Method: r4d6ro1dl1
Desc: 4d6, reroll 1s once, drop lowest
Iterations: 10000, Precision: 4
Average: 13.29; StDev: 2.42
6 [ 0.2%]: *
7 [ 0.9%]: ***
8 [ 1.8%]: *******
9 [ 3.7%]: ***************
10 [ 6.8%]: ****************************
11 [ 10.2%]: ******************************************
12 [ 12.8%]: *****************************************************
13 [ 14.6%]: ************************************************************
14 [ 15.4%]: ***************************************************************
15 [ 14.2%]: **********************************************************
16 [ 10.3%]: ******************************************
17 [ 6.6%]: ***************************
18 [ 2.5%]: **********
@jsocol
Copy link
Author

jsocol commented Apr 25, 2020

$ python rolltest.py -t 3d6 -t 4d6d1 -r 'mk_roller(d6, d6, d6, d6, d4, drop=2)' -i100000 -p3
Method: threed6
Desc:  3d6
Iterations: 100000, Precision: 3
 3: *****
 4: **************
 5: ****************************
 6: **********************************************
 7: ***********************************************************************
 8: ************************************************************************************************
 9: *****************************************************************************************************************
10: ******************************************************************************************************************************
11: ***************************************************************************************************************************
12: *********************************************************************************************************************
13: **************************************************************************************************
14: ***********************************************************************
15: ***********************************************
16: ***************************
17: **************
18: *****
Method: fourdrop1
Desc:  4d6 drop 1
Iterations: 100000, Precision: 3
 3: *
 4: ***
 5: ********
 6: ****************
 7: *****************************
 8: ***********************************************
 9: *********************************************************************
10: ************************************************************************************************
11: *******************************************************************************************************************
12: **********************************************************************************************************************************
13: **********************************************************************************************************************************
14: ***************************************************************************************************************************
15: *****************************************************************************************************
16: **************************************************************************
17: ******************************************
18: *****************
Method: ['d6', 'd6', 'd6', 'd6', 'd4'] drop 2
Desc:  user input: mk_roller(d6, d6, d6, d6, d4, drop=2)
Iterations: 100000, Precision: 3
 3: 
 4: *
 5: ***
 6: ********
 7: *****************
 8: *******************************
 9: ******************************************************
10: ********************************************************************************
11: **************************************************************************************************************
12: ******************************************************************************************************************************************
13: ******************************************************************************************************************************************************
14: ***************************************************************************************************************************************************
15: ************************************************************************************************************************
16: ***********************************************************************************
17: ******************************************
18: *****************

@Scott-PG
Copy link

Nifty! I need to learn python. But this seems like a good application!

@jsocol
Copy link
Author

jsocol commented Apr 25, 2020

Advantage and disadvantage (whoops the original version had the graphs backwards):

$ python rolltest.py -t adv -t disadv -i100000 -p3 -g atleast
Method: ['d20', 'd20'] drop 1
Desc:  advantage
Iterations: 100000, Precision: 3
 1: ****************************************************************************************************
 2: ***************************************************************************************************
 3: ***************************************************************************************************
 4: *************************************************************************************************
 5: ************************************************************************************************
 6: *********************************************************************************************
 7: *******************************************************************************************
 8: ***************************************************************************************
 9: ***********************************************************************************
10: *******************************************************************************
11: **************************************************************************
12: *********************************************************************
13: ***************************************************************
14: *********************************************************
15: **************************************************
16: *******************************************
17: ***********************************
18: ***************************
19: ******************
20: *********
Method: ['d20', 'd20'] drop -1
Desc:  disadvantage
Iterations: 100000, Precision: 3
 1: ****************************************************************************************************
 2: ******************************************************************************************
 3: *********************************************************************************
 4: ************************************************************************
 5: ****************************************************************
 6: ********************************************************
 7: *************************************************
 8: ******************************************
 9: ***********************************
10: ******************************
11: ************************
12: ********************
13: ***************
14: ************
15: *********
16: ******
17: ****
18: **
19: *
20: 

@jsocol
Copy link
Author

jsocol commented May 19, 2020

$ python rolltest.py --test 4d6ro1d1 --precision 4 --graph atleast --max-cols 100
Method: r4d6ro1dl1
Desc:  4d6, reroll 1s once, drop lowest
Iterations: 10000, Precision: 4
Average: 13.27; StDev: 2.45
  4 [100.0%]: ****************************************************************************************************
  5 [100.0%]: ***************************************************************************************************
  6 [100.0%]: ***************************************************************************************************
  7 [ 99.6%]: ***************************************************************************************************
  8 [ 98.8%]: **************************************************************************************************
  9 [ 96.8%]: ************************************************************************************************
 10 [ 93.1%]: *********************************************************************************************
 11 [ 86.4%]: **************************************************************************************
 12 [ 76.0%]: ***************************************************************************
 13 [ 63.1%]: ***************************************************************
 14 [ 48.6%]: ************************************************
 15 [ 33.2%]: *********************************
 16 [ 20.0%]: *******************
 17 [  9.0%]: ********
 18 [  2.7%]: **

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment