Skip to content

Commit 5b10718

Browse files
committed
Modifications to introduction and config json.
1 parent 42c1670 commit 5b10718

2 files changed

Lines changed: 213 additions & 58 deletions

File tree

exercises/practice/dnd-character/.approaches/config.json

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,35 @@
33
"authors": ["colinleach",
44
"BethanyG"],
55
"contributors": []
6-
}
6+
},
7+
"approaches": [
8+
{
9+
"uuid": "952835d1-e9d1-4dc3-b2c2-aad703e8db2e",
10+
"slug": "ability-method",
11+
"title": "Ability Method",
12+
"blurb": "Use one method to generate dice rolls and return ability values.",
13+
"authors": ["bethanyg"]
14+
},
15+
{
16+
"uuid": "5022c80c-8ca1-45e4-aa5f-d91bb3f53441",
17+
"slug": "dice-roll-static-method",
18+
"title": "Dice Roll Static Method",
19+
"blurb": "Use a separate static method to conduct dice rolls.",
20+
"authors": ["bethanyg"]
21+
},
22+
{
23+
"uuid": "98af6c1d-5ab4-476d-9041-30b8f55e13eb",
24+
"slug": "stand-alone-dice-roll-function",
25+
"title": "Stand-alone Dice Roll Function",
26+
"blurb": "Create a dice roll function outside the Character class.",
27+
"authors": ["bethanyg"]
28+
},
29+
{
30+
"uuid": "5dd9d9b0-bfa5-43b2-8e95-787425349fc4",
31+
"slug": "loop-and-setattr-in-init",
32+
"title": "Loop and setattr in init",
33+
"blurb": "Use a tuple of attributes, a loop, and setattr in init to assign ability values.",
34+
"authors": ["bethanyg"]
35+
}
36+
]
737
}
Lines changed: 182 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
11
# Introduction
22

3-
This exercise has two main purposes:
3+
The DnD Character exercise has two main purposes:
44

5-
- To practice class-based programming, especially initialization of instance variables.
6-
- To practice random numbers.
5+
1. Practice class-based programming, especially initialization of instance variables and the creation of methods.
6+
2. Practice the use of pseudo-random numbers and methods in the Python [`random`][random] module.
77

8-
There are no complicated decisions to make about which algorithm to use, as the tests constrain the implementation.
8+
There are no complicated decisions to make about which algorithm to use, as the tests for this exercise constrain the implementation.
9+
However, there is variation in how class and object variables are declared and initialized, how methods are declared, and which random functions are employed.
10+
11+
These approaches will mostly explore these variations in Python syntax.
912

10-
This document will mostly explore some variations in Python syntax.
1113

1214
## General considerations
1315

14-
Several items are specifically required by the tests:
16+
Several items are specifically required and tested for:
1517

16-
- A standalone function called `modifier()`.
18+
- A standalone (_outside the `Character` class_) function called `modifier()`.
1719
- A `Character` class.
18-
- Instance variables for 6 named abilities, plus one for hitpoints.
19-
- An instance method called `ability()` to handle dice throws.
20+
- Instance variables for 6 named abilities, plus one for hit points.
21+
- An instance method called `ability()` to return ability values.
2022

21-
Further methods are optional, but may be helpful with random dice rolls.
23+
Further methods are optional, but may be helpful to simulate random dice rolls and calculate ability scores.
24+
Some methods (_such as `ability()`, or the optional `dice_roll()`_) may be better as [static methods][static-methods], since they do not necessarily require the class or object as a parameter but are still tied to the class logic/purpose.
2225

23-
## The `modifier()` function
2426

25-
This function needs integer division:
27+
### The `modifier()` function
2628

27-
```python
28-
def modifier(constitution):
29-
return (constitution - 10) // 2
30-
```
29+
This stand-alone function modifies the characters constitution score by subtracting 10 from the total and dividing by 2.
3130

32-
In Python 3.x, the division operators are `/` for floating point and `//` for integer division (ignore any old Python 2.x documentation you may find).
31+
In Python 3.x, the division operators are `/` for floating point and `//` for integer or 'floor' division.
32+
`modifier()` needs to use integer division, and return a result that does not have a decimal.
33+
Function equivalents to `/` and `//` are [`operator.truediv(a, b)`][operator-trudiv] and [`operator.floordiv(a, b)`][operator-floordiv] (or_[`math.floor(x)`][math-floor]_).
3334

34-
Integer division will always round *down*: not to the nearest integer and not towards zero:
35+
Integer division will always round "down": not to the nearest integer and not towards zero:
3536

3637
```python
3738
>>> 11 // 3
@@ -40,59 +41,124 @@ Integer division will always round *down*: not to the nearest integer and not to
4041
-4
4142
```
4243

43-
Using `math.floor()` here will work, but slightly misses the point of what is being practised, as well as forcing an unnecessary import.
44+
Here are examples using both operators and functions imported from `math` and from `operator`:
45+
46+
```python
47+
def modifier(constitution):
48+
return (constitution - 10)//2
49+
50+
# You can import the math module and use `floor()`
51+
from math import floor
4452

45-
## Dice rolls
53+
def modifier(constitution):
54+
return floor((constitution - 10)/2)
4655

47-
The instructions are to roll four 6-sided dice and record the sum of the largest three dice.
56+
# Another strategy is to use `floordiv()` from the operator module.
57+
from operator import floordiv
4858

49-
To roll a die we need the [`random`][random] module.
59+
def modifier(constitution):
60+
return floordiv((constitution - 10), 2)
61+
62+
```
5063

51-
There are various suitable options available, including `random.randint()` and `random.choice()` for a single throw or `random.sample()` for multiple rolls.
52-
Note that `randint(lower, upper)` *includes* the upper bound, in contrast to `range(lower, uppper + 1)`.
64+
Using function equivalents in a solution will work, but they do create overhead due to module import and function calls.
5365

54-
To roll four dice may need a list comprehension:
5566

56-
```python
57-
def dice_rolls_1(self):
58-
return [random.randint(1, 6) for _ in range(4)]
67+
### Dice rolls
5968

60-
def dice_rolls_2(self):
61-
return [random.choice(range(1,7)) for _ in range(4)]
69+
The instructions are to roll four 6-sided dice and record the sum of the largest three rolls.
6270

63-
def four_dice_rolls(self):
64-
return random.sample(range(1, 7), 4)
71+
To simulate a roll of the dice, we need the [`random`][random] module which produces pseudo-random numbers.
72+
The [`secrets`][secrets] module, which produces cryptographically strong random numbers is not needed here.
73+
74+
Within `random`, there are various suitable methods available.
75+
These include [`random.randint()`][randint] to produce an integer in a range, [`random.choice()`][choice] to select from a sequence of values, or even [`random.sample()`][random-sample] for multiple rolls 'sampled' from a distribution, group, or range of values.
76+
77+
````exercism/note
78+
79+
`randint(lower, upper)` _**includes**_ the upper bound, in contrast to the built-in [`range(lower, upper)`][range], or Python's [slice notation][slice] which both _**exclude**_ the upper bound.
80+
81+
[slice]: https://docs.python.org/3/library/stdtypes.html#common-sequence-operations
82+
[range]: https://docs.python.org/3/library/stdtypes.html#range
83+
````
84+
85+
86+
To roll four dice may need a loop or comprehension:
87+
88+
```python
89+
from random import randint, choice, sample
90+
91+
# Choosing a pseudo-random number between 1 and 6 (inclusive) four different times.
92+
def dice_rolls_randint(self):
93+
rolls = []
94+
95+
for dice in rage(4):
96+
rolls.append(randint(1, 6))
97+
98+
return rolls
99+
100+
# Choosing from a range sequence between 1 and 7 (exclusive) four different times.
101+
def dice_rolls_choice(self):
102+
return [choice(range(1,7)) for dice in range(4)]
103+
104+
# Choosing for different 'samples' (rolls) from a sample range
105+
# between 1 and 7 (exclusive). This avoids a comprehension,
106+
# since random.sample returns a list of unique values from the range.
107+
def four_dice_rolls_sample(self):
108+
return sample(range(1, 7), 4)
109+
65110
```
66111

67-
Some community solutions begin with a call to `random.seed()` but (at least in recent versions of Python) calling this without a parameter has exactly the same effect as omitting it.
112+
Some community solutions begin with a call to `random.seed()` but (_at least in recent versions of Python_) calling this without a parameter has exactly the same effect as omitting it.
68113
The value of this method is in debugging situations when it helps to have a reproducible sequence of results.
69114
Then we would call it with a known seed such as `random.seed(42)`.
70115

71-
After rolling four dice, next we need to sum the largest three scores, discarding the lowest.
116+
After rolling four dice, we next need to sum the largest three scores, discarding the lowest.
117+
Many programmers use `sorted()` and a slice for determining the three largest values:
72118

73-
Most Python programmers use a list sort and slicing for this:
74119

75120
```python
76-
# the dice variable was generated as a 4-element list, as above
77-
sum(sorted(dice)[1:4])
121+
# The dice variable was generated as a 4-element list, as above.
122+
sum(sorted(dice)[1:])
123+
124+
# The list can also be reverse sorted.
125+
sum(sorted(dice, reverse=True)[:-1])
126+
78127
```
79128

80-
In some ways simpler, we could use the built-in `sum()` and `min()` functions to subtract the smallest score from the total:
129+
If slicing is hard to read or confusing, the built-ins `sum()` and `min()` can be used to subtract the smallest score from the total.
130+
In this second case, `dice` can be any sequence, not just a list.
131+
Because we are calculating a `min()` value, a generator expression cannot be used.
81132

82133
```python
83-
# the dice variable was generated as a 4-element enumerable, as above
134+
# The dice variable was generated as a 4-element sequence, as above.
84135
sum(dice) - min(dice)
136+
85137
```
86138

87-
In this second case, `dice` can be any enumerable, not just a list.
139+
This strategy might be considered more readable to some, since `min()` is very clear as to what is being calculated.
140+
This is also more efficient when the input list is long and the order of values doesn't need to be preserved.
141+
142+
143+
### The ability() Method
144+
145+
The directions do not state how `ability()` should be implemented, but a look at [the tests][dnd-tests] indicate that any of the character's ability scores or a freshly generated score can be returned provided the score falls in the required range.
146+
This means that the method can return a random ability value chosen from `vars(self).values()` **or** it can calculate and return a random value for use in an ability score.
147+
Some solutions use this method as their "dice roll", and call it to populate abilities in `__init__`.
148+
Other solutions separate out "dice rolls" or "ability score" logic into a separate method or function, leaving `ability()` as a method that returns a random choice from the character's already assigned abilities.
149+
150+
Either strategy above will pass the tests, but separating out "dice roll" logic from a random character ability score can be both clearer and more reusable in scenarios where the same "dice roll" is used to calculate/populate some other metric.
151+
Moving the "dice roll" or "ability score" out of the class altogether or making it a static method also lets it be called/used without instantiating the class and can make it clearer what is being calculated.
152+
88153

89154
## Class initialization
90155

91-
The various abilities need to be set just once for each character, which is most conveniently done in the class initializer.
156+
The various abilities need to be set just once for each character, which is most conveniently done in the class `__init__` method.
92157

93-
The examples below assume that `modifier()` and `self.ability()` are implemented as dicussed above
158+
The examples below assume that `modifier()` and `self.ability()` are implemented as discussed in the sections above.
159+
The explicit approach is simple but rather verbose and repetitive.
160+
It does have the advantage of declaring the abilities very explicitly, so that a reader can quickly determine how many and of what type the abilities are:
94161

95-
The explicit approach is simple but rather verbose and repetitive:
96162

97163
```python
98164
class Character:
@@ -104,33 +170,92 @@ class Character:
104170
self.wisdom = self.ability()
105171
self.charisma = self.ability()
106172
self.hitpoints = 10 + modifier(self.constitution)
173+
174+
...
175+
176+
177+
# Using a dice_roll static method instead of self.ability()
178+
class Character:
179+
def __init__(self):
180+
self.strength = Character.dice_roll()
181+
self.dexterity = Character.dice_roll()
182+
self.constitution = Character.dice_roll()
183+
self.intelligence = Character.dice_roll()
184+
self.wisdom = Character.dice_roll()
185+
self.charisma = Character.dice_roll()
186+
self.hitpoints = 10 + modifier(self.constitution)
187+
188+
...
107189
```
108190

109-
Alternatively, we could start from a tuple of ability names then loop over these using [`setattr()`][setattr]:
191+
192+
Alternatively, we could start from an iterable of ability names and loop over these using [`setattr()`][setattr] to write the values into the objects attribute dictionary.
193+
This sacrifices a bit of readability/clarity for less verbosity and (somewhat) easier ability additions:
194+
110195

111196
```python
112-
ABILITIES = (
113-
'strength',
114-
'dexterity',
115-
'constitution',
116-
'intelligence',
117-
'wisdom',
118-
'charisma'
119-
)
197+
# Setting a global ABILITIES constant.
198+
# This enables other classes to "see" the abilities.
199+
# This could be useful for a larger program that modifies
200+
# or adds abilities outside the Character class.
201+
ABILITIES = ('strength', 'dexterity', 'constitution',
202+
'intelligence', 'wisdom','charisma')
203+
120204

121205
class Character:
122206
def __init__(self):
207+
123208
for ability_name in ABILITIES:
124209
setattr(self, ability_name, self.ability())
210+
125211
self.hitpoints = modifier(self.constitution) + 10
126-
```
127212

128-
The key to this is that these two expressions have identical results:
213+
214+
# Conversely, we can declare ABILITIES as a
215+
# class attribute. This ensures that all objects made from
216+
# the class share ABILITIES and they are only added or
217+
# modified through the Character class. ABILITIES are not
218+
# visible globally.
219+
class Character:
129220

130-
```python
131-
setattr(self, 'strength`, self.ability())
132-
self.strength = self.ability()
221+
abilities = ('strength', 'dexterity', 'constitution',
222+
'intelligence', 'wisdom','charisma')
223+
224+
def __init__(self):
225+
226+
for ability_name in Character.abilities:
227+
setattr(self, ability_name, self.ability())
228+
229+
self.hitpoints = modifier(self.constitution) + 10
133230
```
134231

232+
Listing out each ability vs looping through and using `setatter()` has identical results for the object.
233+
Both calculate a score for an ability and write that ability + score into the object attribute dictionary when an object is instantiated from the class.
234+
235+
236+
## Putting things together
237+
238+
The four approaches below combine various options from the previous sections to show how a solution would work with them.
239+
More variations are possible, but these cover most of the main decision differences.
240+
241+
- [One `ability` method][approach-ability-method]
242+
- [Dice roll static method][approach-dice-roll-static-method]
243+
- [Dice roll stand-alone method][approach-stand-alone-dice-roll-function]
244+
- [Loop and `setatter()` in `__init__`][approach-loop-and-setattr-in-init]
245+
246+
247+
[approach-ability-method]: https://exercism.org/tracks/python/exercises/dnd-character/approaches/ability-method
248+
[approach-dice-roll-static-method]: https://exercism.org/tracks/python/exercises/dnd-character/approaches/dice-roll-static-method
249+
[approach-loop-and-setattr-in-init]: https://exercism.org/tracks/python/exercises/dnd-character/approaches/loop-and-setattr-in-init
250+
[approach-stand-alone-dice-roll-function]: https://exercism.org/tracks/python/exercises/dnd-character/approaches/tand-alone-dice-roll-function
251+
[choice]: https://docs.python.org/3/library/random.html#random.choice
252+
[dnd-tests]: https://github.com/exercism/python/blob/main/exercises/practice/dnd-character/dnd_character_test.py
253+
[math-floor]: https://docs.python.org/3/library/math.html#math.floor
254+
[operator-floordiv]: https://docs.python.org/3/library/operator.html#operator.floordiv
255+
[operator-trudiv]: https://docs.python.org/3/library/operator.html#operator.truediv
256+
[randint]: https://docs.python.org/3/library/random.html#random.randint
257+
[random-sample]: https://docs.python.org/3/library/random.html#random.sample
135258
[random]: https://exercism.org/tracks/python/concepts/random
259+
[secrets]: https://docs.python.org/3/library/secrets.html
136260
[setattr]: https://docs.python.org/3/library/functions.html#setattr
261+
[static-methods]: https://www.digitalocean.com/community/tutorials/python-static-method

0 commit comments

Comments
 (0)