#!/usr/bin/python # # Command-line dice roller # # It assumes that the Mersenne Twister is acceptable. # Support for real entropy is an RFE import random import sys import re import argparse # Implements a "dice" - that is, a series of dice plus modifiers class Dice(): # This builds dice out of a simple input format # All of the dark regex magic is contained to this function, # lest it corrupt innocent code @classmethod def from_str(cls, desc): if not re.match(r'^\d+d\d+', desc): raise Exception('Dice format invalid. See --help') (n, s) = re.match(r'^(\d+)d(\d+)', desc).groups() num = int(n) sides = int(s) mod = 0 drop_low = 0 drop_high = 0 reroll = 0 reroll_times = 0 for m in re.findall(r'[lh+-]\d+', desc): if m[0] == '-': mod += int(m) elif m[0] == '+': mod += int(m[1:]) elif m[0] == 'h': drop_high = int(m[1:]) elif m[0] == 'l': drop_low = int(m[1:]) m = re.search(r'r(\d+)(x\d+)?', desc) if m: (r, rt) = m.groups() reroll = int(r) if rt: reroll_times = int(rt[1:]) return cls(num, sides, mod, drop_low, drop_high, reroll, reroll_times) def __init__(self, num, sides, mod, drop_low=0, drop_high=0, reroll=0, reroll_times=0): self.num = num self.sides = sides self.mod = mod self.drop_low = drop_low self.drop_high = drop_high self.reroll = reroll self.reroll_times = reroll_times self.times_rolled = 0 def num_rolls(self): return self.times_rolled # Rolls the dice, returns a structure # containing the result plus metadata def roll(self, verbose=False): results = [] rerolled = [] dropped = [] # Roll the actual dice, handling rerolls for i in range(0, self.num): count = 0 result = 0 while result <= self.reroll or result == 0: if self.reroll_times and count >= self.reroll_times: break result = random.randint(1, self.sides) count += 1 if result <= self.reroll: rerolled.append(result) results.append(result) # Drop any high or low, if specified if self.drop_low or self.drop_high: results.sort() if self.drop_low: dropped.extend(results[:self.drop_low]) results = results[self.drop_low:] if self.drop_high: dropped.extend(results[-1 * self.drop_high:]) results = results[:-1 * self.drop_high] random.shuffle(results) # Get the results total = sum(results) + self.mod self.times_rolled += 1 return {'total': total, 'rolls': results, 'dropped': dropped, 'rerolled': rerolled} def __str__(self): reroll_info = '' drop_info = '' mod_info = '' if self.reroll: reroll_info = ' [reroll {}'.format(self.reroll) if self.reroll_times: reroll_info = reroll_info + 'x{}'.format(self.reroll_times) reroll_info = reroll_info + ']' if self.drop_low: drop_info = drop_info + ' [drop low {}]'.format(self.drop_low) if self.drop_high: drop_info = drop_info + ' [drop high {}]'.format(self.drop_high) if self.mod > 0: mod_info = '+{}'.format(self.mod) elif self.mod < 0: mod_info = '-{}'.format(self.mod) return "{num}d{sides}{mod}{drop}{reroll}".format(num=self.num, sides=self.sides, mod=mod_info, drop=drop_info, reroll=reroll_info) # This takes command-line input as dice description strings # and builds a list of Dice objects def parse_input(args): dice_list = [] for arg in args: dice_list.append(Dice.from_str(arg)) return dice_list def parse_args(): parser = argparse.ArgumentParser(description='Roll dice based on descriptions passed in on the command line', formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('--repeat', '-r', metavar='N', type=int, default=1, help='repeat the given rolls N times') parser.add_argument('--verbose', '-v', action='store_true', help='print detailed information about each roll') parser.add_argument('dice', nargs=argparse.REMAINDER, help=""" Dice are input in the following form: XdY[modifiers] This will roll X Y-sided dice and apply the specified modifiers. Modifiers can be any of the following (where N and M are integers): (+|-)N Add or subtract N from the total lN Drop the lowest-rolling N dice from the total hN Drop the highest-rolling N dice from the total rN[xM] Any dice that roll <= N will be rerolled. If the optional 'xM' option is specified, dice will be rerolled a maximum of M times. Otherwise each die will be rerolled until the result is > N Examples: 1d20+5 roll 1 twenty-sided die, and add 5 to the result 6d6l1h1 roll 6 six-sided dice, and drop both the highest and the lowest roll 4d6l1r2x1 roll 4 six-sided dice. Any dice rolling a 1 or 2 will be rerolled once. If the result is still 1 or 2 it is kept. The lowest die is dropped from the result """) return parser.parse_args() def main(): settings = parse_args() dice_list = parse_input(settings.dice) for i in range(settings.repeat): for dice in dice_list: ret = dice.roll() if settings.verbose: drop_info = '' reroll_info = '' if ret['dropped']: drop_info = ' [dropped {}]'.format(','.join(['{}'.format(x) for x in ret['dropped']])) if ret['rerolled']: reroll_info = ' [rerolled {}]'.format(','.join(['{}'.format(x) for x in ret['rerolled']])) print('{dice}: {rolls}{drop}{reroll} {total}'.format(dice=dice, rolls=ret['rolls'], total=ret['total'], drop=drop_info, reroll=reroll_info)) else: print('{dice}: {total}'.format(dice=dice, total=ret['total'])) if __name__ == '__main__': main()