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.

Revisions

  1. jsocol revised this gist Jun 22, 2020. 1 changed file with 39 additions and 1 deletion.
    40 changes: 39 additions & 1 deletion dice.py
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,4 @@
    import argparse
    import random


    @@ -82,4 +83,41 @@ def __str__(self):
    d10 = D(10)
    d12 = D(12)
    d20 = D(20)
    d100 = D(100)
    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()
  2. jsocol revised this gist May 19, 2020. 1 changed file with 8 additions and 4 deletions.
    12 changes: 8 additions & 4 deletions rolltest.py
    Original file line number Diff line number Diff line change
    @@ -98,7 +98,8 @@ def calc_stats(totals, iters):
    }


    def rolltest(roller, iters=10000, precision=3, graph='normal', zoom=1):
    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))

    @@ -111,8 +112,9 @@ def rolltest(roller, iters=10000, precision=3, graph='normal', zoom=1):
    print('Average: {mean:0.2f}; StDev: {stdev:0.2f}'.format(**stats))

    output_fmt = '{0:3d} [{1:5.1f}%]: {2}'
    prefix_length = len(output_fmt.format(18, 15.1, ''))
    max_cols = os.get_terminal_size().columns - prefix_length
    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()):
    @@ -149,6 +151,7 @@ def make_parser():
    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


    @@ -184,7 +187,8 @@ def main():

    for test in to_run:
    rolltest(test, iters=options.iters, precision=options.precision,
    graph=options.graph, zoom=options.zoom)
    graph=options.graph, zoom=options.zoom,
    max_cols=options.max_cols)


    if __name__ == '__main__':
  3. jsocol revised this gist May 19, 2020. 1 changed file with 35 additions and 58 deletions.
    93 changes: 35 additions & 58 deletions x-results.txt
    Original file line number Diff line number Diff line change
    @@ -1,58 +1,35 @@
    $ 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: **
    $ 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%]: **********
  4. jsocol revised this gist May 19, 2020. 1 changed file with 46 additions and 9 deletions.
    55 changes: 46 additions & 9 deletions rolltest.py
    Original file line number Diff line number Diff line change
    @@ -6,6 +6,7 @@
    """
    import argparse
    import importlib
    import os
    import sys
    from collections import defaultdict

    @@ -57,17 +58,47 @@ def oned20():
    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 rolltest(roller, iters=10000, precision=3, graph='normal'):
    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):
    print('Method: %s\nDesc: %s' % (roller.__name__, roller.__doc__))
    print('Iterations: %d, Precision: %d' % (iters, precision))

    @@ -76,24 +107,29 @@ def rolltest(roller, iters=10000, precision=3, graph='normal'):
    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}'
    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 * 10 ** precision)
    print('%3d [%5.1f%%]: %s' % (k, frac * 100., '*' * cols))
    cols = int(frac * max_cols * zoom)
    print(output_fmt.format(k, frac * 100., '*' * cols))
    elif graph == 'atmost':
    max_cols = 100
    cumulative = 0.0
    for k in sorted(totals.keys()):
    cumulative += round(totals[k] / iters, precision)
    cols = int(cumulative * max_cols)
    print('%3d [%5.1f%%]: %s' % (k, cumulative * 100., '*' * cols))
    print(output_fmt.format(k, cumulative * 100., '*' * cols))
    elif graph == 'atleast':
    max_cols = 100
    cumulative = 1.0
    for k in sorted(totals.keys()):
    cols = int(cumulative * max_cols)
    print('%3d [%5.1f%%]: %s' % (k, cumulative * 100., '*' * cols))
    print(output_fmt.format(k, cumulative * 100., '*' * cols))
    cumulative -= round(totals[k] / iters, precision)
    else:
    print('Unsupported graph mode: %s' % graph)
    @@ -112,6 +148,7 @@ def make_parser():
    'roller is a callable'))
    parser.add_argument('-g', '--graph', default='normal',
    choices=['normal', 'atleast', 'atmost'])
    parser.add_argument('-z', '--zoom', default=1, type=int)
    return parser


    @@ -147,8 +184,8 @@ def main():

    for test in to_run:
    rolltest(test, iters=options.iters, precision=options.precision,
    graph=options.graph)
    graph=options.graph, zoom=options.zoom)


    if __name__ == '__main__':
    main()
    main()
  5. jsocol revised this gist Apr 25, 2020. 1 changed file with 11 additions and 8 deletions.
    19 changes: 11 additions & 8 deletions rolltest.py
    Original file line number Diff line number Diff line change
    @@ -53,7 +53,7 @@ def fourdrop1():


    def oned20():
    """1d20 because you live dangerously"""
    """1d20 to embrace chaos"""
    return 1 * d20


    @@ -78,22 +78,25 @@ def rolltest(roller, iters=10000, precision=3, graph='normal'):

    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':
    frac = round(totals[k] / iters, precision)
    cols = int(frac * 10 ** precision)
    print('%3d [%5.1f%%]: %s' % (k, frac * 100., '*' * cols))
    elif graph == 'atmost':
    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':
    print('%3d [%5.1f%%]: %s' % (k, cumulative * 100., '*' * cols))
    elif graph == 'atleast':
    max_cols = 100
    cumulative = 1.0
    for k in sorted(totals.keys()):
    cols = int(cumulative * max_cols)
    print('%2d: %s' % (k, '*' * cols))
    print('%3d [%5.1f%%]: %s' % (k, cumulative * 100., '*' * cols))
    cumulative -= round(totals[k] / iters, precision)
    else:
    print('Unsupported graph mode: %s' % graph)


    def make_parser():
    @@ -148,4 +151,4 @@ def main():


    if __name__ == '__main__':
    main()
    main()
  6. jsocol revised this gist Apr 25, 2020. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion rolltest.py
    Original file line number Diff line number Diff line change
    @@ -53,7 +53,7 @@ def fourdrop1():


    def oned20():
    """1d20 like a psychopath"""
    """1d20 because you live dangerously"""
    return 1 * d20


  7. jsocol revised this gist Apr 25, 2020. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions rolltest.py
    Original file line number Diff line number Diff line change
    @@ -81,14 +81,14 @@ def rolltest(roller, iters=10000, precision=3, graph='normal'):
    cols = int(round(totals[k] / iters, precision) * 10 ** precision)
    print('%2d: %s' % (k, '*' * cols))
    elif graph == 'atleast' or graph == 'cdf':
    max_cols = 20 * precision
    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 = 20 * precision
    max_cols = 100
    cumulative = 1.0
    for k in sorted(totals.keys()):
    cols = int(cumulative * max_cols)
  8. jsocol revised this gist Apr 25, 2020. 1 changed file with 23 additions and 5 deletions.
    28 changes: 23 additions & 5 deletions rolltest.py
    Original file line number Diff line number Diff line change
    @@ -67,7 +67,7 @@ def oned20():
    }


    def rolltest(roller, iters=10000, precision=3):
    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))

    @@ -76,9 +76,24 @@ def rolltest(roller, iters=10000, precision=3):
    score = roller()
    totals[score] += 1

    for k in sorted(totals.keys()):
    cols = int(round(totals[k] / iters, precision) * 10 ** precision)
    print('%2d: %s' % (k, '*' * cols))
    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 = 20 * precision
    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 = 20 * precision
    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():
    @@ -92,6 +107,8 @@ def make_parser():
    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


    @@ -126,7 +143,8 @@ def main():
    to_run.append(roller)

    for test in to_run:
    rolltest(test, iters=options.iters, precision=options.precision)
    rolltest(test, iters=options.iters, precision=options.precision,
    graph=options.graph)


    if __name__ == '__main__':
  9. jsocol revised this gist Apr 25, 2020. 1 changed file with 58 additions and 0 deletions.
    58 changes: 58 additions & 0 deletions x-results.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,58 @@
    $ 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: **
  10. jsocol created this gist Apr 25, 2020.
    85 changes: 85 additions & 0 deletions dice.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,85 @@
    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)
    133 changes: 133 additions & 0 deletions rolltest.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,133 @@
    # 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 like a psychopath"""
    return 1 * d20


    TESTS = {
    '3d6': threed6,
    '4d6d1': fourdrop1,
    'adv': adv,
    'd20': oned20,
    'disadv': disadv,
    'noam': noam,
    }


    def rolltest(roller, iters=10000, precision=3):
    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

    for k in sorted(totals.keys()):
    cols = int(round(totals[k] / iters, precision) * 10 ** precision)
    print('%2d: %s' % (k, '*' * cols))


    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'))
    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)


    if __name__ == '__main__':
    main()