|
| 1 | +# Separate Dice Rolls into a Stand-Alone Dice Roll Function |
| 2 | + |
| 3 | + |
| 4 | +```python |
| 5 | +from random import choice, randint |
| 6 | +from operator import floordiv |
| 7 | + |
| 8 | + |
| 9 | +class Character: |
| 10 | + def __init__(self): |
| 11 | + self.strength = dice_rolls() |
| 12 | + self.dexterity = dice_rolls() |
| 13 | + self.constitution = dice_rolls() |
| 14 | + self.intelligence = dice_rolls() |
| 15 | + self.wisdom = dice_rolls() |
| 16 | + self.charisma = dice_rolls() |
| 17 | + |
| 18 | + self.hitpoints = 10 + modifier(self.constitution) |
| 19 | + |
| 20 | + def ability(self): |
| 21 | + return choice([*vars(self).values()]) |
| 22 | + |
| 23 | + |
| 24 | +def dice_rolls(): |
| 25 | + values = sorted(randint(1, 6) for item in range(4)) |
| 26 | + return sum(values[1:]) |
| 27 | + |
| 28 | +def modifier(constitution): |
| 29 | + return floordiv((constitution - 10), 2) |
| 30 | +``` |
| 31 | + |
| 32 | + |
| 33 | +This approach separates the `ability()` method from a stand-alone function that calculates dice rolls. |
| 34 | +`ability()` returns the value of a randomly chosen character ability using [`random.choice`][randon-choice], but does not roll dice or calculate values. |
| 35 | +Instead, `dice_rolls()` handles the rolls/values, using [`random.randint`][random-randint] to generate them. |
| 36 | +The argument for this is that the logic/functionality of rolling dice 4 times and summing the top three values is not really related to a DnD character or their abilities - it is independent and likely useful across a wider scope than just the character class. |
| 37 | +It also makes it cleaner to maintain, should the method or number of the dice rolls change. |
| 38 | + |
| 39 | +`dice_rolls()` is then called in `__init__()` to populate the listed-out character attributes. |
| 40 | +`self.hitpoints` calls the second stand-alone `modifier()` function, adding it to 10 for the character's `hitpoints` attribute. |
| 41 | +Note that `modifier()` uses the [`operator.floordiv`][operator-floordiv] method to trunkate the value. |
| 42 | + |
| 43 | +This approach is valid and passes all the tests. |
| 44 | +However, it will trigger an analyzer comment about there being "too few public methods", since there are no methods for this class beyond `attribute()`. |
| 45 | + |
| 46 | +The "too few" rule encourages you to think about the design of the class: is it worth the effort to create the class if it only holds attribute values for a character? |
| 47 | +What other functionality should this class hold? |
| 48 | +Should the `dice_roll()` function be outside or inside (_as a regular method or a static method_) the class? |
| 49 | + |
| 50 | +None of these (_including the analyzer complaint about too few methods_) is a hard and fast rule or requirement - all are considerations for the class as you build out a larger program. |
| 51 | + |
| 52 | +An alternative is to write a [dataclass][dataclass], although the design discussion and questions above remain the same: |
| 53 | + |
| 54 | + |
| 55 | +```python |
| 56 | +from random import choice, sample |
| 57 | +from dataclasses import dataclass |
| 58 | + |
| 59 | +@dataclass |
| 60 | +class Character: |
| 61 | + |
| 62 | + strength: int = 0 |
| 63 | + dexterity: int = 0 |
| 64 | + constitution: int = 0 |
| 65 | + intelligence: int = 0 |
| 66 | + wisdom: int = 0 |
| 67 | + charisma: int = 0 |
| 68 | + hitpoints: int = 0 |
| 69 | + |
| 70 | + def __post_init__(self): |
| 71 | + for ability in vars(self): |
| 72 | + setattr(self, ability, dice_rolls()) |
| 73 | + |
| 74 | + self.hitpoints = 10 + modifier(self.constitution) |
| 75 | + |
| 76 | + def ability(self): |
| 77 | + return choice([*vars(self).values()]) |
| 78 | + |
| 79 | + |
| 80 | +def dice_rolls(): |
| 81 | + values = sample(range(1, 7), 4) |
| 82 | + return sum(values) - min(values) |
| 83 | + |
| 84 | +def modifier(constitution): |
| 85 | + return (constitution - 10)//2 |
| 86 | +``` |
| 87 | + |
| 88 | + |
| 89 | +Note that here there is a [`__post_init__`][post-init] method to assign ability values to the attributes, and that the attributes must start with a default value (_otherwise, they can't be assigned to in post-init_). |
| 90 | +`hitpoints` has the same treatment as the other attributes, and requires assignment in post-init. |
| 91 | + |
| 92 | +`dice_rolls()` uses [`random.sample`][random-sample] for roll values here and `modifier()` uses the floor-division operator `//`. |
| 93 | + |
| 94 | +[dataclass]: https://docs.python.org/3/library/dataclasses.html |
| 95 | +[operator-floordiv]: https://docs.python.org/3/library/operator.html#operator.floordiv |
| 96 | +[post-init]: https://docs.python.org/3/library/dataclasses.html#post-init-processing |
| 97 | +[random-randint]: https://docs.python.org/3/library/random.html#random.randint |
| 98 | +[random-sample]: https://docs.python.org/3/library/random.html#random.randint |
| 99 | +[randon-choice]: https://docs.python.org/3/library/random.html#random.choice |
0 commit comments