1
2 """
3 This module provides the Dice class (formula based random numbers)
4 """
5 import sys
6 from random import randint
7
8
9
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
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
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
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
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
67 if delimiter is None or len(values) != 2:
68 raise ValueError("unrecognized dice expression")
69
70
71 if delimiter == 'D' or delimiter == 'd':
72 try:
73 self.num_dice = 1 if values[0] == '' else int(values[0])
74
75
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
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
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
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
166 """
167 test cases:
168 """
169
170
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
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