Introduction
After weeks of anticipation, you and your friends get together for your very first game of Dungeons & Dragons (D&D).
Since this is the first session of the game, each player has to generate a character to play with.
The character’s abilities are determined by rolling 6-sided dice, but where are the dice?
With a shock, you realize that your friends are waiting for you to produce the dice; after all it was your idea to play D&D!
Panicking, you realize you forgot to bring the dice, which would mean no D&D game.
As you have some basic coding skills, you quickly come up with a solution: you’ll write a program to simulate dice rolls.
Instructions
Instructions
For a game of Dungeons & Dragons, each player starts by generating a character they can play with.
This character has, among other things, six abilities; strength, dexterity, constitution, intelligence, wisdom and charisma.
These six abilities have scores that are determined randomly.
You do this by rolling four 6-sided dice and recording the sum of the largest three dice.
You do this six times, once for each ability.
Your character’s initial hitpoints are 10 + your character’s constitution modifier.
You find your character’s constitution modifier by subtracting 10 from your character’s constitution, divide by 2 and round down.
Write a random character generator that follows the above rules.
For example, the six throws of four dice may look like:
- 5, 3, 1, 6: You discard the 1 and sum 5 + 3 + 6 = 14, which you assign to strength.
- 3, 2, 5, 3: You discard the 2 and sum 3 + 5 + 3 = 11, which you assign to dexterity.
- 1, 1, 1, 1: You discard the 1 and sum 1 + 1 + 1 = 3, which you assign to constitution.
- 2, 1, 6, 6: You discard the 1 and sum 2 + 6 + 6 = 14, which you assign to intelligence.
- 3, 5, 3, 4: You discard the 3 and sum 5 + 3 + 4 = 12, which you assign to wisdom.
- 6, 6, 6, 6: You discard the 6 and sum 6 + 6 + 6 = 18, which you assign to charisma.
Because constitution is 3, the constitution modifier is -4 and the hitpoints are 6.
Most programming languages feature (pseudo-)random generators, but few programming languages are designed to roll dice.
One such language is [Troll][troll].
[troll]: https://di.ku.dk/Ansatte/?pure=da%2Fpublications%2Ftroll-a-language-for-specifying-dicerolls(84a45ff0-068b-11df-825d-000ea68e967b)%2Fexport.html
Dig Deeper
Ability Method
Use the ability() Method to Generate and Return the Dice Rolls
from random import sample
class Character:
def __init__(self):
self.strength = self.ability()
self.dexterity = self.ability()
self.constitution = self.ability()
self.intelligence = self.ability()
self.wisdom = self.ability()
self.charisma = self.ability()
self.hitpoints = 10 + modifier(self.constitution)
def ability(self):
values = sample(range(1, 7), 4)
return sum(values) - min(values)
def modifier(constitution):
return (constitution - 10)//2
This approach uses a single ability() method to calculate the dice rolls and return an ability value.
ability() is then called in __init__() to populate the listed-out character attributes.
self.hitpoints calls the stand-alone modifier() function, adding it to 10 for the character’s hitpoints attribute.
This approach is valid and passes all the tests.
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.
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?
What other functionality should this class hold?
Should you separate dice rolls from ability values?
Is the class better as a dataclass, with dice roll as a utility function?
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.
Dice Roll Static Method
Move Dice Rolls Into a Static Method Separate from the ability Method
from math import floor
from random import choice
class Character:
def __init__(self):
self.strength = Character.dice_rolls()
self.dexterity = Character.dice_rolls()
self.constitution = Character.dice_rolls()
self.intelligence = Character.dice_rolls()
self.wisdom = Character.dice_rolls()
self.charisma = Character.dice_rolls()
self.hitpoints = 10 + modifier(self.constitution)
def ability(self):
return choice([*vars(self).values()])
@staticmethod
def dice_rolls():
values = sorted(choice(range(1,7)) for dice in range(4))[::-1]
return sum(values[:-1])
def modifier(constitution):
return floor((constitution - 10)/2)
This approach separates the ability() method from a static method that calculates dice rolls.
ability() returns the value of a randomly chosen character ability using random.choice but does not roll dice or calculate values.
Instead, dice_rolls() handles the rolls/values using random.choice for selection.
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.
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.
Declaring dice_rolls() as a static method allows other callers to use the function with or without instantiating a new Character object.
It also makes it cleaner to maintain, should the method or number of the dice rolls change.
dice_rolls() is then called in __init__() to populate the listed-out character attributes.
Note that it needs to be called with the class name: Character.dice_rolls().
self.hitpoints then calls the second stand-alone modifier() function, adding it to 10 for the character’s hitpoints attribute.
modifieer() in this example uses math.floor for calculating the hitpoints value.
Stand-alone Dice Roll Function
Separate Dice Rolls into a Stand-Alone Dice Roll Function
from random import choice, randint
from operator import floordiv
class Character:
def __init__(self):
self.strength = dice_rolls()
self.dexterity = dice_rolls()
self.constitution = dice_rolls()
self.intelligence = dice_rolls()
self.wisdom = dice_rolls()
self.charisma = dice_rolls()
self.hitpoints = 10 + modifier(self.constitution)
def ability(self):
return choice([*vars(self).values()])
def dice_rolls():
values = sorted(randint(1, 6) for item in range(4))
return sum(values[1:])
def modifier(constitution):
return floordiv((constitution - 10), 2)
This approach separates the ability() method from a stand-alone function that calculates dice rolls.
ability() returns the value of a randomly chosen character ability using random.choice, but does not roll dice or calculate values.
Instead, dice_rolls() handles the rolls/values, using random.randint to generate them.
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.
It also makes it cleaner to maintain, should the method or number of the dice rolls change.
dice_rolls() is then called in __init__() to populate the listed-out character attributes.
self.hitpoints calls the second stand-alone modifier() function, adding it to 10 for the character’s hitpoints attribute.
Note that modifier() uses the operator.floordiv method to trunkate the value.
This approach is valid and passes all the tests.
However, it will trigger an analyzer comment about there being “too few public methods”, since there are no methods for this class beyond ability().
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?
What other functionality should this class hold?
Should the dice_roll() function be outside or inside (as a regular method or a static method) the class?
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.
An alternative is to write a dataclass, although the design discussion and questions above remain the same:
from random import choice, sample
from dataclasses import dataclass
@dataclass
class Character:
strength: int = 0
dexterity: int = 0
constitution: int = 0
intelligence: int = 0
wisdom: int = 0
charisma: int = 0
hitpoints: int = 0
def __post_init__(self):
for ability in vars(self):
setattr(self, ability, dice_rolls())
self.hitpoints = 10 + modifier(self.constitution)
def ability(self):
return choice([*vars(self).values()])
def dice_rolls():
values = sample(range(1, 7), 4)
return sum(values) - min(values)
def modifier(constitution):
return (constitution - 10)//2
Note that here there is a __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).
hitpoints has the same treatment as the other attributes, and requires assignment in post-init.
dice_rolls() uses random.sample for roll values here and modifier() uses the floor-division operator //.
Source: Exercism python/dnd-character