You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- 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.
7
7
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.
9
12
10
-
This document will mostly explore some variations in Python syntax.
11
13
12
14
## General considerations
13
15
14
-
Several items are specifically required by the tests:
16
+
Several items are specifically required and tested for:
15
17
16
-
- A standalone function called `modifier()`.
18
+
- A standalone (_outside the `Character` class_) function called `modifier()`.
17
19
- 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.
20
22
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.
22
25
23
-
## The `modifier()` function
24
26
25
-
This function needs integer division:
27
+
### The `modifier()` function
26
28
27
-
```python
28
-
defmodifier(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.
31
30
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]_).
33
34
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:
35
36
36
37
```python
37
38
>>>11//3
@@ -40,59 +41,124 @@ Integer division will always round *down*: not to the nearest integer and not to
40
41
-4
41
42
```
42
43
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
+
defmodifier(constitution):
48
+
return (constitution -10)//2
49
+
50
+
# You can import the math module and use `floor()`
51
+
from math import floor
44
52
45
-
## Dice rolls
53
+
defmodifier(constitution):
54
+
return floor((constitution -10)/2)
46
55
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
48
58
49
-
To roll a die we need the [`random`][random] module.
59
+
defmodifier(constitution):
60
+
return floordiv((constitution -10), 2)
61
+
62
+
```
50
63
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.
53
65
54
-
To roll four dice may need a list comprehension:
55
66
56
-
```python
57
-
defdice_rolls_1(self):
58
-
return [random.randint(1, 6) for _ inrange(4)]
67
+
### Dice rolls
59
68
60
-
defdice_rolls_2(self):
61
-
return [random.choice(range(1,7)) for _ inrange(4)]
69
+
The instructions are to roll four 6-sided dice and record the sum of the largest three rolls.
62
70
63
-
deffour_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.
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
+
defdice_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
+
defdice_rolls_choice(self):
102
+
return [choice(range(1,7)) for dice inrange(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
+
deffour_dice_rolls_sample(self):
108
+
return sample(range(1, 7), 4)
109
+
65
110
```
66
111
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.
68
113
The value of this method is in debugging situations when it helps to have a reproducible sequence of results.
69
114
Then we would call it with a known seed such as `random.seed(42)`.
70
115
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:
72
118
73
-
Most Python programmers use a list sort and slicing for this:
74
119
75
120
```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
+
78
127
```
79
128
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.
81
132
82
133
```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.
84
135
sum(dice) -min(dice)
136
+
85
137
```
86
138
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
+
88
153
89
154
## Class initialization
90
155
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.
92
157
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:
94
161
95
-
The explicit approach is simple but rather verbose and repetitive:
96
162
97
163
```python
98
164
classCharacter:
@@ -104,33 +170,92 @@ class Character:
104
170
self.wisdom =self.ability()
105
171
self.charisma =self.ability()
106
172
self.hitpoints =10+ modifier(self.constitution)
173
+
174
+
...
175
+
176
+
177
+
# Using a dice_roll static method instead of self.ability()
178
+
classCharacter:
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
+
...
107
189
```
108
190
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
+
110
195
111
196
```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
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]
0 commit comments