Skip to content

Commit ed6e2fd

Browse files
committed
Added 4 approaches as examples of possible solutions based on the intro.
1 parent 768b3c5 commit ed6e2fd

4 files changed

Lines changed: 273 additions & 0 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Use the `ability()` Method to Generate and Return the Dice Rolls
2+
3+
4+
```python
5+
from random import sample
6+
7+
class Character:
8+
def __init__(self):
9+
self.strength = self.ability()
10+
self.dexterity = self.ability()
11+
self.constitution = self.ability()
12+
self.intelligence = self.ability()
13+
self.wisdom = self.ability()
14+
self.charisma = self.ability()
15+
16+
self.hitpoints = 10 + modifier(self.constitution)
17+
18+
def ability(self):
19+
values = sample(range(1, 7), 4)
20+
return sum(values) - min(values)
21+
22+
def modifier(constitution):
23+
return (constitution - 10)//2
24+
```
25+
26+
27+
This approach uses a single `ability()` method to calculate the dice rolls and return an ability value.
28+
`ability()` is then called in `__init__()` to populate the listed-out character attributes.
29+
`self.hitpoints` calls the stand-alone `modifier()` function, adding it to 10 for the character's hitpoints attribute.
30+
31+
This approach is valid and passes all the tests.
32+
However, it will trigger an analyzer comment about there being "too few public methods", since there are no methods for this class beyond the one that calculates attribute values.
33+
34+
35+
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?
36+
What other functionality should this class hold?
37+
Should you separate dice rolls from ability values?
38+
Is the class better as a [dataclass][dataclass], with dice roll as a utility function?
39+
40+
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.
41+
42+
43+
[dataclass]: https://docs.python.org/3/library/dataclasses.html
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Move Dice Rolls Into a Static Method Separate from the `ability` Method
2+
3+
4+
```python
5+
from math import floor
6+
from random import choice
7+
8+
class Character:
9+
def __init__(self):
10+
self.strength = Character.dice_rolls()
11+
self.dexterity = Character.dice_rolls()
12+
self.constitution = Character.dice_rolls()
13+
self.intelligence = Character.dice_rolls()
14+
self.wisdom = Character.dice_rolls()
15+
self.charisma = Character.dice_rolls()
16+
17+
self.hitpoints = 10 + modifier(self.constitution)
18+
19+
def ability(self):
20+
return choice([*vars(self).values()])
21+
22+
@staticmethod
23+
def dice_rolls():
24+
values = sorted(choice(range(1,7)) for dice in range(4))[::-1]
25+
return sum(values[:-1])
26+
27+
def modifier(constitution):
28+
return floor((constitution - 10)/2)
29+
```
30+
31+
This approach separates the `ability()` method from a [`static method`][staticmethod] that calculates dice rolls.
32+
`ability()` returns the value of a randomly chosen character ability using [`random.choice`][random-choice] but does not roll dice or calculate values.
33+
Instead, `dice_rolls()` handles the rolls/values using [`random.choice`][random-choice] for selection.
34+
35+
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.
36+
However, it might be tidier to include it in the character class, rather than "clutter" the program or module with an additional stand-alone function.
37+
Declaring `dice_rolls()` as a static method allows other callers to use the function with or without instantiating a new `Character` object.
38+
It also makes it cleaner to maintain, should the method or number of the dice rolls change.
39+
40+
`dice_rolls()` is then called in `__init__()` to populate the listed-out character attributes.
41+
Note that it needs to be called with the class name: `Character.dice_rolls()`.
42+
`self.hitpoints` then calls the second stand-alone `modifier()` function, adding it to 10 for the character's `hitpoints` attribute.
43+
`modifieer()` in this example uses [`math.floor`][math-floor] for calculating the `hitpoints` value.
44+
45+
[math-floor]: https://docs.python.org/3/library/math.html#math.floor
46+
[random-choice]: https://docs.python.org/3/library/random.html#random.choice
47+
[staticmethod]: https://docs.python.org/3/library/functions.html#staticmethod
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Loop Through an Abilities Tuple to Set Attribute Values in `__init__`
2+
3+
4+
```python
5+
from random import choice, sample
6+
7+
class Character:
8+
9+
abilities = ('strength', 'dexterity', 'constitution', 'intelligence', 'wisdom', 'charisma')
10+
11+
def __init__(self):
12+
for ability_type in Character.abilities:
13+
setattr(self, ability_type, Character.dice_rolls())
14+
15+
self.hitpoints = 10 + modifier(self.constitution)
16+
17+
def ability(self):
18+
return choice([*vars(self).values()])
19+
20+
@staticmethod
21+
def dice_rolls():
22+
values = sample(range(1, 7), 4)
23+
return sum(values) - min(values)
24+
25+
def modifier(constitution):
26+
return (constitution - 10)//2
27+
```
28+
29+
30+
This approach uses a `tuple` to hold character attributes in a [`class variable`][class-variable] or `class attribute`.
31+
Since this variable is common to all instances of the class, it can be looped through during object initialization to create instance variables and assign them values using [`setattr] [setattr].
32+
33+
This strategy has several benefits:
34+
1. The `__init__` is less verbose and the abilities are easier to maintain.
35+
2. Organizationally, attributes remain with the class, making it clearer where they apply.
36+
3. Attributes are inherited in any subclass and can be added to or overridden by them.
37+
4. Changes to attributes are reflected in all new objets and subclasses automatically.
38+
39+
Because `hitpoints` is calculated differently, it is assigned a value outside the `tuple` and `loop`.
40+
41+
The remainder of the class body is the same as in the [dice roll static method][approach-dice-roll-static-method] approach (_as is the variant below_).
42+
43+
44+
```python
45+
from random import choice, sample
46+
47+
CHARACTER_ABILITIES = ('strength', 'dexterity', 'constitution',
48+
'intelligence', 'wisdom', 'charisma')
49+
50+
class Character:
51+
52+
def __init__(self):
53+
for ability_type in CHARACTER_ABILITIES:
54+
setattr(self, ability_type, Character.dice_rolls())
55+
56+
self.hitpoints = 10 + modifier(self.constitution)
57+
58+
def ability(self):
59+
return choice([*vars(self).values()])
60+
61+
@staticmethod
62+
def dice_rolls():
63+
values = sample(range(1, 7), 4)
64+
return sum(values) - min(values)
65+
66+
def modifier(constitution):
67+
return (constitution - 10)//2
68+
```
69+
70+
Above, the character attributes are moved out of the class into a [`constant`][constant] at the module level.
71+
The Character `__init__` method loops through them using `setattr` to create instance variables and assign them values, similar to the first example.
72+
Again, because `hitpoints` is calculated differently, it is assigned a value outside the `tuple` and `loop`.
73+
Making the character attributes a constant has the advantage of being visible to all other classes and functions defined in the module.
74+
This could be easier if the attributes are being used by multiple classes beyond the Character class.
75+
This also avoids having to reference the Character class when using or modifying the abilities and could help with clarity and maintenance in the larger program.
76+
However, modifying the abilities in this context would also be visible at the module level, and could have wide or unintended consequences, so should be commented/documented carefully.
77+
78+
The remainder of the class body is the same as in the [dice roll static method][approach-dice-roll-static-method] approach (_as is the variant below_).
79+
80+
81+
[approach-dice-roll-static-method]: https://exercism.org/tracks/python/exercises/dnd-character/approaches/dice-roll-static-method
82+
[class-variable]: https://docs.python.org/3/tutorial/classes.html#class-and-instance-variables
83+
[constant]: https://peps.python.org/pep-0008/#constants
84+
[setattr]: https://docs.python.org/3/library/functions.html#setattr
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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

Comments
 (0)