Skip to content

Instantly share code, notes, and snippets.

@jsocol
Last active June 22, 2020 01:58
Show Gist options
  • 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 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)
# 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 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 because you live dangerously"""
return 1 * d20
TESTS = {
'3d6': threed6,
'4d6d1': fourdrop1,
'adv': adv,
'd20': oned20,
'disadv': disadv,
'noam': noam,
}
def rolltest(roller, iters=10000, precision=3, graph='normal'):
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
if graph == 'normal':
for k in sorted(totals.keys()):
cols = int(round(totals[k] / iters, precision) * 10 ** precision)
print('%2d: %s' % (k, '*' * cols))
elif graph == 'atleast' or graph == 'cdf':
max_cols = 100
cumulative = 0.0
for k in sorted(totals.keys()):
cumulative += round(totals[k] / iters, precision)
cols = int(cumulative * max_cols)
print('%2d: %s' % (k, '*' * cols))
elif graph == 'atmost':
max_cols = 100
cumulative = 1.0
for k in sorted(totals.keys()):
cols = int(cumulative * max_cols)
print('%2d: %s' % (k, '*' * cols))
cumulative -= round(totals[k] / iters, precision)
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'])
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)
if __name__ == '__main__':
main()
$ python rolltest.py -t 3d6 -t 4d6d1 -r 'mk_roller(d6, d6, d6, d6, d4, drop=2)' -i100000 -p2
Method: threed6
Desc: 3d6
Iterations: 100000, Precision: 2
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: 2
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: 2
3:
4:
5:
6: *
7: **
8: ***
9: *****
10: ********
11: ***********
12: **************
13: ***************
14: ***************
15: ************
16: ********
17: ****
18: **
@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