2012-03-20 22:23:53 +00:00
#!/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
2012-03-21 04:57:25 +00:00
import argparse
2012-03-20 22:23:53 +00:00
# Implements a "dice" - that is, a series of dice plus modifiers
class Dice ( ) :
2012-03-21 16:08:58 +00:00
# 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_desc ( 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 ' [dD+-] \ d+ ' , desc ) :
if m [ 0 ] == ' - ' :
mod + = int ( m )
elif m [ 0 ] == ' + ' :
mod + = int ( m [ 1 : ] )
elif m [ 0 ] == ' D ' :
drop_high = int ( m [ 1 : ] )
elif m [ 0 ] == ' d ' :
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 )
2012-03-20 22:23:53 +00:00
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
2012-03-21 16:08:58 +00:00
def num_rolls ( self ) :
return self . times_rolled
2012-03-20 22:23:53 +00:00
2012-03-21 16:08:58 +00:00
# Rolls the dice, returns a structure
# containing the result plus metadata
2012-03-21 04:57:25 +00:00
def roll ( self , verbose = False ) :
2012-03-20 22:23:53 +00:00
results = [ ]
rerolled = [ ]
# 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 ( )
dropped = [ ]
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
if verbose :
2012-03-21 04:57:25 +00:00
drop_info = ' '
reroll_info = ' '
2012-03-20 22:23:53 +00:00
if dropped :
2012-03-21 14:57:59 +00:00
drop_info = ' [dropped {} ] ' . format ( ' , ' . join ( [ ' {} ' . format ( x ) for x in dropped ] ) )
2012-03-20 22:23:53 +00:00
if rerolled :
2012-03-21 14:57:59 +00:00
reroll_info = ' [rerolled {} ] ' . format ( ' , ' . join ( [ ' {} ' . format ( x ) for x in rerolled ] ) )
2012-03-20 22:23:53 +00:00
2012-03-21 14:57:59 +00:00
print ( ' {dice} : {results} {drop} {reroll} {total} ' . format ( dice = self , results = results , total = total , drop = drop_info , reroll = reroll_info ) )
2012-03-20 22:23:53 +00:00
else :
print ( ' {dice} : {total} ' . format ( dice = self , total = total ) )
self . times_rolled + = 1
def __str__ ( self ) :
reroll_info = ' '
drop_info = ' '
mod_info = ' '
if self . reroll :
2012-03-21 14:57:59 +00:00
reroll_info = ' [reroll {} ' . format ( self . reroll )
2012-03-20 22:23:53 +00:00
if self . reroll_times :
2012-03-21 14:57:59 +00:00
reroll_info = reroll_info + ' x {} ' . format ( self . reroll_times )
reroll_info = reroll_info + ' ] '
2012-03-20 22:23:53 +00:00
if self . drop_low :
2012-03-21 14:57:59 +00:00
drop_info = drop_info + ' [drop low {} ] ' . format ( self . drop_low )
2012-03-20 22:23:53 +00:00
if self . drop_high :
2012-03-21 14:57:59 +00:00
drop_info = drop_info + ' [drop high {} ] ' . format ( self . drop_high )
2012-03-20 22:23:53 +00:00
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 )
2012-03-21 16:08:58 +00:00
# This takes command-line input as dice description strings
# and builds a list of Dice objects
2012-03-20 22:23:53 +00:00
def parse_input ( args ) :
dice_list = [ ]
for arg in args :
2012-03-21 16:08:58 +00:00
dice_list . append ( Dice . from_desc ( arg ) )
2012-03-20 22:23:53 +00:00
return dice_list
2012-03-21 04:57:25 +00:00
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 ) :
2012-03-20 22:23:53 +00:00
2012-03-21 04:57:25 +00:00
( + | - ) N Add or subtract N from the total
2012-03-21 14:57:59 +00:00
dN Drop the lowest - rolling N dice from the total
DN Drop the highest - rolling N dice from the total
2012-03-21 04:57:25 +00:00
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 :
1 d20 + 5 roll 1 twenty - sided die , and add 5 to the result
6 d6l1h1 roll 6 six - sided dice , and drop both the highest and the lowest roll
4 d6l1r2x1 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 )
2012-03-20 22:23:53 +00:00
2012-03-21 04:57:25 +00:00
for i in range ( settings . repeat ) :
for dice in dice_list :
dice . roll ( verbose = settings . verbose )
2012-03-20 22:23:53 +00:00
if __name__ == ' __main__ ' :
main ( )