A Simple GM Tool: A Die Roller

Posted on January 27, 2010

I’ve been playing RPGs on Google Wave for a few months now and I really like the die-rolling bots like randomleetwenty. I decided it was necessary to have a simple command-line script which can read simple die-rolling expressions common to most RPG players and print the result. It’s a simple script I wrote in Python and perhaps if you’re interested in learning Python, it might be a good starter script to read and tinker with.

One thing to note is that this is a very naive rolling script. I didn’t bother getting into writing a die rolling algorithm that simulates a more “realistic” randomness. That might be a topic for another post. Just know that if you use this script, you’re mostly going to get middle-of-the-road numbers and will RARELY ever see a maximum or minimum roll. In other words, I would be surprised if you ever got a crit or fumble from this script.

(Sorry for the column spill!)

#!/usr/bin/env python

##################################################################
# roll - A naive die roller using a common die expression syntax #
#                                                                #
# (c) 2009, 2010, James King - http://agentultra.com             #
#                                                                #
# Licensed under the GPLv2                                       #
##################################################################

import operator
import random
import re
import sys

USAGE = "Usage: `roll `nn"
USAGE += "Expressions:nd()"

def print_help_and_exit(err):
    print >> sys.stderr, err
    print >> sys.stderr, USAGE
    sys.exit()

def parse_die_expression(expr):
    """
    >>> parse_die_expression("3d6+1")
    (3, 6, , 1)
    >>> parse_die_expression("3d6")
    (3, 6, None, None)
    >>> parse_die_expression("Hello, world!")
    Traceback (most recent value last):
        ...
    ValueError, I don't understand: Hello, world!
    """

    DIE_EXPR = re.compile(r'(d+)d(d+)(?=([+,-,*,/])(d+))?')
    OPERS = {'+': operator.add,
             '-': operator.sub,
             '*': operator.mul,
             '/': operator.div}

    try:
        (num, sides, oper, mod) = DIE_EXPR.findall(expr)[0]
    except IndexError, e:
        raise ValueError, "I don't understand: %s" % expr

    if oper != '' and mod != '':
            mod = int(mod)
            oper = OPERS[oper]
    else:
        oper = None
        mod = None

    return (int(num), int(sides), oper, mod)

def roll_dice(num, sides, operator=None, modifier=None):
    """
    >>> import operator
    >>> result = roll_dice(1, 20, operator=operator.add, modifier=1)
    >>> isinstance(result, int)
    True
    >>> roll_dice(1, 20, sum, 1)
    Traceback (most recent call last):
        ...
    TypeError: operator must be callable and accept 2 args and return an int
    """
    die_total = sum([random.randint(1, sides) for d in range(1, num + 1)])

    if operator and modifier:

        try:
            result = operator(die_total, modifier)
        except ZeroDivisionError:
            print_help_and_exit('Cannot divide by zero!')
        except TypeError:
            err = ("operater must be callable and accept 2 args"
                      " and return an int")
            raise TypeError, err
        else:
            return result

    else:
        return die_total

if __name__ == '__main__':

    try:
        expr = sys.argv[1]
    except IndexError, e:
        print_help_and_exit('Missing argument!')
    else:

        try:
            (num, sides, oper, mod) = parse_die_expression(expr)
        except ValueError, e:
            print_help_and_exit(e)

        result = roll_dice(num, sides, operator=oper, modifier=mod)
        print >> sys.stdout, result

Just save it to a file, make it executable, and either create an alias to it or add it to your PATH. You will need Python installed of course. Windows users might need to do something extra, I don’t know (I haven’t used Windows in so long that I’m no help there, sorry).

Anyways, I’ll try and get around to posting other little snippets I find useful for enhancing my RPG experience as I go.