Module dice
[hide private]
[frames] | no frames]

Source Code for Module dice

  1  #!/usr/bin/python3 
  2  """ 
  3  This module provides the Dice class (formula based random numbers) 
  4  """ 
  5  import sys 
  6  from random import randint 
  7   
  8   
  9  # pylint: disable=too-few-public-methods 
10 -class Dice(object):
11 """ 12 This class supports formula based dice rolls. 13 The formulae can be described in a few (fairly standard) formats: 14 - DnD style ... D100, 3D6, 3D6+4 15 - ranges ... 4-12 16 - simple numbers ... 50 17 18 @ivar num_dice: (int) number of dice to be rolled, None if a range 19 @ivar dice_type: (int) number of faces on each die, None if a range 20 @ivar plus: (int) number to be added to the roll 21 @ivar min_value: (int) lowest legal value in range, None if a formula 22 @ivar max_value: (int) highest legal value range, None if a formula 23 """ 24 25 # pylint: disable=too-many-branches
26 - def __init__(self, formula):
27 """ 28 instantiate a roller for a specified formula 29 30 @param formula: (string) description of roll 31 @raise ValueError: illegal formula expression 32 """ 33 self.num_dice = None 34 self.dice_type = None 35 self.min_value = None 36 self.max_value = None 37 self.plus = 0 38 39 # formula must be a string (or integer) 40 if isinstance(formula, int): 41 self.plus = formula 42 return 43 elif not isinstance(formula, str): 44 raise ValueError("non-string dice expression") 45 46 # if it is just a number, this is simple 47 if formula.isnumeric(): 48 self.plus = int(formula) 49 return 50 elif formula[0] == '-' and formula[1:].isnumeric(): 51 self.plus = int(formula) 52 return 53 54 # figure out what kind of expression this is 55 delimiter = None 56 if 'D' in formula: 57 delimiter = 'D' 58 values = formula.split(delimiter) 59 elif 'd' in formula: 60 delimiter = 'd' 61 values = formula.split(delimiter) 62 elif '-' in formula: 63 delimiter = '-' 64 values = formula.split(delimiter) 65 66 # see if it has known form and 2 values 67 if delimiter is None or len(values) != 2: 68 raise ValueError("unrecognized dice expression") 69 70 # process the values 71 if delimiter == 'D' or delimiter == 'd': 72 try: 73 self.num_dice = 1 if values[0] == '' else int(values[0]) 74 75 # there might be a plus after the dice type 76 if '+' in values[1]: 77 parts = values[1].split('+') 78 values[1] = parts[0] 79 values.append(parts[1]) 80 else: 81 values.append('0') 82 83 self.dice_type = 100 if values[1] == '%' else int(values[1]) 84 self.plus = int(values[2]) 85 except ValueError: 86 raise ValueError("non-numeric value in dice expression") 87 else: 88 try: 89 self.min_value = int(values[0]) 90 self.max_value = int(values[1]) 91 if self.min_value >= self.max_value: 92 self.min_value = None 93 self.max_value = None 94 raise ValueError("illegal range in dice expression") 95 except ValueError: 96 raise ValueError("non-numeric value in dice expression")
97
98 - def str(self):
99 """ 100 return string representation of these dice" 101 """ 102 if self.num_dice is not None and self.dice_type is not None: 103 descr = "{}D{}".format(self.num_dice, self.dice_type) 104 if self.plus > 0: 105 descr += "+{}".format(self.plus) 106 elif self.min_value is not None and self.max_value is not None: 107 descr = "{}-{}".format(self.min_value, self.max_value) 108 elif self.plus != 0: 109 descr = str(self.plus) 110 else: 111 descr = "" 112 113 return descr
114
115 - def roll(self):
116 """ 117 roll this set of dice and return result 118 @return: (int) resulting value 119 """ 120 total = 0 121 122 if self.num_dice is not None and self.dice_type is not None: 123 for _ in range(self.num_dice): 124 total += randint(1, self.dice_type) 125 elif self.min_value is not None and self.max_value is not None: 126 total = randint(self.min_value, self.max_value) 127 128 return total + self.plus
129 130 131 # UNIT TESTING
132 -def test(formula, min_expected, max_expected, rolls=20):
133 """ 134 test that a formula generates rolls w/expected values 135 @param formula: (string) for the DIce 136 @param min_expected: minimum expected value 137 @param max_expected: maximum expecetd value 138 @param rolls: number of test rolls 139 """ 140 dice = Dice(formula) 141 min_rolled = 666666 142 max_rolled = -666666 143 for _ in range(rolls): 144 rolled = dice.roll() 145 if rolled < min_rolled: 146 min_rolled = rolled 147 if rolled > max_rolled: 148 max_rolled = rolled 149 150 result = " legal formula " 151 if isinstance(formula, str): 152 result += '"' + formula + '"' 153 else: 154 result += str(formula) 155 result += " ({}): returns {} values between {} and {}".\ 156 format(dice.str(), rolls, min_rolled, max_rolled) 157 print(result) 158 159 assert min_rolled >= min_expected, "roll returns below-minimum values" 160 assert max_rolled <= max_expected, "roll returns above-maximum values" 161 162 return min_rolled >= min_expected and max_rolled <= max_expected
163 164
165 -def main():
166 """ 167 test cases: 168 """ 169 170 # test valid dice expressions 171 tests_run = 0 172 tests_passed = 0 173 174 tests_run += 1 175 if test("3D4", 3, 12, 40): 176 tests_passed += 1 177 178 tests_run += 1 179 if test("d20", 1, 20, 80): 180 tests_passed += 1 181 182 tests_run += 1 183 if test("D%", 1, 100, 300): 184 tests_passed += 1 185 186 tests_run += 1 187 if test("2D2+3", 5, 7): 188 tests_passed += 1 189 190 tests_run += 1 191 if test("3-9", 3, 9): 192 tests_passed += 1 193 194 tests_run += 1 195 if test("47", 47, 47, 10): 196 tests_passed += 1 197 198 tests_run += 1 199 if test(47, 47, 47, 10): 200 tests_passed += 1 201 202 tests_run += 1 203 if test("-3", -3, -3, 10): 204 tests_passed += 1 205 206 # test detection of invalid expressions 207 for formula in ["2D", "D", "xDy", 208 "4-2", "-", "3-", "x-y", 209 "7to9"]: 210 tests_run += 1 211 try: 212 dice = Dice(formula) 213 sys.stderr.write(" ERROR: illegal formula {} accepted as {}\n". 214 format(formula, dice.str())) 215 except ValueError: 216 print(" illegal formula {}: {}". 217 format(formula, sys.exc_info()[1])) 218 tests_passed += 1 219 220 print() 221 if tests_run == tests_passed: 222 print("Passed all {} Dice tests".format(tests_passed)) 223 else: 224 print("FAILED {}/{} Dice tests".format(tests_run-tests_passed, 225 tests_run))
226 227 228 if __name__ == "__main__": 229 main() 230