Part 3: Role with the Changes
This series of posts is dedicated to building tools that support a simulation of simplified D&D gameplay. To implement the simulation I’ll be using Python, PostgreSQL, Elastic, and Docker. The idea behind these blog posts is to document my progress through this project and interesting bits I find along the way. If you’d like to start from the beginning, have a look at part 1, where I talked about where this idea came from, and what I’d like to accomplish. In this part, I’ll build a class that will be used to randomize results.
Back in the mid 1960s, when D&D and other strategy games were being developed, there was challenge that needed to be met if the games were going to be marketable. How to fairly simulate the outcome of actions taken by the players in terms of success, failure, and the shades in between. According to Empire of Imagination: Gary Gygax and the Birth of Dungeons and Dragons, basing his idea on Leon Tucker’s work on World War II combat statistics, Gary Gygax wanted to break probabilities into 5% increments. He had been working with a group of gamers led by Tucker and Michael Reese, to create a game called Tractics. The combat system for that game was based on the same idea, originally implemented by way of a coffee can loaded with 20 labelled poker chips. At that time icosahedral (twenty-sided) dice existed but were too hard to acquire. But very soon the use of these dice would become the norm, as would the system that Gary designed.
What a great idea to design a game around these dice. It’s as if the Fates have materialized and are playing along with you. You can exclaim and point at their results as a focus. Successfully somersault, leap up, surprising a troll and knock them out with a superman punch; or comically fall out of a tree while chasing a squirrel (It’s a familiar, I know it!) The result rests on the outcome of the dice. They are a part of your own choosing, yet outside your will.
Oh, my Precious, what a fascination I had with those dice back in the day! A fascination that rekindled with the rise of internet dice companies competing against each other to make cooler products. Made from metal, wood, bone, and polymer, in every color and combination imaginable. These dice become the target of our love or disdain, depending on how we’re rolling at the time.
This is where the journey starts. The game designers, like Gary Gygax, were trying to interject a random component into their gameplay. I will do the same for the simulation. Instead of shiny beautiful dice, I will be using the Python programming language to implement a class that’ll generate random results.
Die is singular for dice, so I’m going to use that as the name for the class. When creating an instance of the Die class, a number can be passed to define the range (or, how many sides this one has.) If not, we’ll assume a 20 sided dice.
d6 = Die(6) # Create a six sided die d20 = Die() # Create a twenty sided die (default)
Before making a method to roll the die, some requirements need to be defined. There are some situations that should get handled within the Die class:
- Roll the die a number of times and report back the sum. The majority of the calls to this class will be using this method.
- Roll the die a number of times, drop the lowest roll, and report back the sum of the rest of the rolls. This will be used in the “standard” type of ability rolls. In that type, a six sided die is rolled four times and the lowest roll is removed from the total.
- Roll the die, with advantage. Roll the die twice, report back the highest roll.
- Roll the die, with disadvantage. Roll the die twice, report back the lowest roll.
- Roll the die, with resistance. In some cases, it’s possible to be resistant to certain types of damage. Resistant in D&D terms means that the damage would be halved. So, roll the die a number of times and report back the half of the sum.
Version 1 of Die.py
import random class Die(object): def __init__(self, sides=20 # the number of sides this die will have. ): self.sides = sides # Set passed or defaulted sides value for class # performRoll will be used by the other methods. # This simplifies the calls being done from the other methods # def performRoll(self, rolls, # How many rolls to do dropvalue=False, # Drop a value? dropfrom="Low", # If Dropping, from which end? halved=False # Cut sum in half? ): tmpHold = [] # list to hold all rolled values tot = 0 # variable to hold the total sum value for x in range(0, rolls): # place the roll results in the tmpHold list tmpHold.append(random.randint(1, self.sides)) # If dropping from either side sort the array accordingly if dropvalue: if dropfrom == "High": tmpHold.sort(reverse=True) else: tmpHold.sort() del tmpHold[0] # then remove the first value for y in tmpHold: # compute the sum of all values left tot = tot + y if halved: # halve value rounding down (floor) tot = tot // 2 return tot def roll(self, rolls=1, droplowest=False): if droplowest: result = self.performRoll(rolls, dropvalue=True, dropfrom="Low") else: result = self.performRoll(rolls, dropvalue=False) return result def rollWithAdvantage(self): return self.performRoll(2, dropvalue=True, dropfrom="Low") def rollWithDisadvantage(self): return self.performRoll(2, dropvalue=True, dropfrom="High") def rollWithResistance(self, rolls): return self.performRoll(rolls, dropvalue=False, halved=True) # The line below is a fancy way of saying that if this # file is run by itself, do the steps that follow. if __name__ == '__main__': d6 = Die(6) print(f'Roll d6 3 times: {d6.roll(3, droplowest=False)}') print(f'Roll d6 4 times, drop lowest: {d6.roll(4, droplowest=True)}') print(f'Roll with advantage: {d6.rollWithAdvantage()}') print(f'Roll with disadvantage: {d6.rollWithDisadvantage()}') print(f'Roll for damage with resistance: {d6.rollWithResistance(6)}')
I’ll implement a better testing process in the next post, but this will do to get things rolling.
$ python Die.py Roll d6 3 times: 9 Roll d6 4 times, drop lowest: 11 Roll with advantage: 6 Roll with disadvantage: 2 Roll for damage with resistance: 10
This first basic class should provide a test that a workable python environment exists. In the next post, the testing and debugging foundation will get built. Both of those will serve as sanity checks as the complexity of the classes crank up. Until then, go find a group and play!