Last active
June 22, 2020 01:58
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ 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%]: ********** |
Author
jsocol
commented
Apr 25, 2020
Nifty! I need to learn python. But this seems like a good application!
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:
$ 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