OOP with D&D: Inheritance

Using D&D to Understand the Four Pillars of OOP, Part I

BenMauss
Level Up Coding

--

Update 3/24/2021: I wanted to take a moment to thank Florian Salihovic for helping me identify areas that needed clarification.

Today, we’ll be discussing Inheritance, one of the “Four Pillars of Object-Oriented Programming”. The programming language we’ll be using in our discussion is Python. I should preface that I have a bias for Python. Why? Because the syntax is simpler. That’s it.

So let’s get started!

What are the Four Pillars of OOP?

The Four Pillars are as follows:

Inheritance: The ability of an object in a subclass to inherit the behaviors and attributes from its parent class.

Abstraction: A process where you show only “relevant” data and “hide” unnecessary details of an object from the user. It’s like turning on a car. The car manufacturer knows that “turning the key” (or pushing the button) is just a part of the process of that gets the car’s engine to start. On the other hand, a driver doesn’t need to to know every detail about that process, they only need to know that if you turn the key/push the button, the engine will start. That is abstraction. In OOP, it’s giving the developer the ability to create classes and packages, and the user only needs to how/when to access and use the methods, not how it works.

Polymorphism: The ability of a function or method to operate on many different types. For example, print(len("hello") and print(len([1, 2, 3, 4, "a", "b", "c"])) are both valid uses of the function len(), despite that it is operating on a string and a list. In OOP, this also allows us to define methods in a child class with the same name as one in the parent class (called Method Overriding).

Encapsulation: The ability to wrap data (variables) and methods in a single unit (such as a class) to restrict direct access to them and prevent accidental modification.

These four principles build a strong foundation for all object-oriented programming languages. Missing a single component can cause the language to become bug/error prone, take up too much memory, and require substantially more code than is necessary (the kind that would make my gripes about Java look like I’m just being petty…which…I am, but you know what I mean).

Today, we’ll discuss how Inheritance works. Time for some Dungeons & Dragons!

D&D

There are a couple of reasons D&D is great for practicing OOP. First, it is a game with hundreds of diverse creatures and other assets that are unique but still share similar traits (e.g. an Elf and a Human are both “humanoids” despite being different races and having different abilities).

Another reason why D&D works well with OOP is because most video games have some mechanic that is analogous to D&D. From the obvious RPGs to First-Person Shooters, all games implement a “dice roll” mechanic of some sort. Don’t believe me? Think of how in an FPS your weapon is more accurate if you’re character model is in a state of rest. This is the equivalent to the “Advantage” mechanic in D&D, where you increase the odds of hitting your target by rolling your ability check twice and using the larger number. In the video game, you gain “advantage” by decreasing movement speed and crouching. On the other hand, by running and gunning, you are disadvantaged (roll your ability check twice and use the lesser number) and are less likely to hit your target.

With that out of the way, let’s get started!

The Primordial Class

Thinking of our first parent class can be difficult. Parent classes should be very general. If we want to make a parent class from which ALL sentient objects in the game are derived, then it should only contain attributes and behaviors that all sentient characters, NPCs, and monsters have in common. You might be thinking that a great first class will have attributes like name, race, hit-points, alignment, armor class, etc., like in the stat sheet here for an Aatxe,:

Source: Open5e.com

Most of the stats above are universal, with a few exceptions like Legendary Actions. This would make a great starting place! But there is something else that is shared by everything in the game: The Dice!

Dice roll dictates the success and failure of nearly every action of every single sentient being in the game. Thus, we want all of our creatures and characters to inherit a dice roll method.

So we’ll start there!

Here, we have started a class that defines what dice come in the set and the possible results one could get from a single roll of a die (Note: A d2, or a die with 2 sides, isn’t a die. It’s essentially a coin flip which isn’t in the normal D&D mechanics, but it’s included for a Dungeon Master to use at their discretion, such as when an NPC has a 50% chance of a successful action). The __init__method is the Initializer (or “Constructor”, if you’re a Java kinda person) in Python. Attributes inside this method are considered default attributes for all child classes. The argument self is a Python argument. When a class is instantiated as an object, the object itself is implicitly passed as the first argument in all methods. To account for this, the standard practice is to add the argument self as a parameter. You’ll see an example of this when we add the next method: The Ability Check!

The Ability Check is the most important mechanic in D&D. In the game, if you wanted to perform some action that would require some skill (like convince someone to sell all of their possessions for a bean, or run on a wall), you must perform an ability check. To do this, you roll a d20 and add in any modifiers that your character might have. The Dungeon Master (DM) will assign a number to represent difficulty for the action, called a Difficulty Class (DC). Your ability check (the sum of your dice roll and any modifiers you might have) must be greater than, or equal to the DC.

For example, let’s say you want your character to run on the wall. Running on a wall is difficult, but you don’t need to be a professional acrobat to do it, so your DM says that you need to roll at least a 14. This means that for your ability check, your roll and your modifiers need to add up to a minimum of 14.

Modifiers are related to your character’s stats. If your character is really good at acrobatics, they would naturally have a higher chance of success, so they would have a larger positive modifier, like “add 5 to all acrobatics ability checks”. So if your dice roll was 9, you would still succeed at the wall run because your +5 modifier turned your 9 into a 14.

However, let’s say your character wasn’t the athletic type and you had a -1 modifier. Well, even if you rolled a 14, the -1 modifier turns it into a 13 and you failed because performing stunts is difficult for you.

Rolling a “Natural 20”, meaning the result of the dice roll itself was 20, without modifiers, guarantees your success (the DM can’t force you to fail just because it ruins their plans).

Rolling a 1 (a “Natural 1”), however, guarantees you fail and you fail in a big way (whatever bad can happen, will happen). It doesn’t matter what your modifiers are, an over-powered character can’t escape the DM’s wrath.

I know that was a bit of a tangent, but we need to know how the mechanic works if we’re going to recreate it in code. So, with all of this knowledge, we’ll create the ability_check method (Note: a method is like a function, except that is bound to a class and its instance variables).

As you can see, we’ve got arguments for advantage and disadvantage, a character’s modifiers, and the number of dice to roll (remember that with advantage and disadvantage, you must roll a d20 twice and pick the larger or lesser number, respectively). Let’s test this out!

Going back to the wall example. Jennifer is the name of our character, and we made some dice for her by creating an instance of the DiceSet class. Jennifer is extremely athletic and excels in acrobatic feats, so she has a +5 modifier for her acrobatic ability checks. Sadly, she is having an off day and she rolled a “modified” 10, and she failed, slipping on the wall. Even Olympians have bad days.

Let’s say she had a little pep in her step from drinking a great cup of coffee which gives her advantage:

Awesome, she passed! Now, what if someone spiked her morning coffee and now her judgement is off, giving her disadvantage:

Lucky for her, her muscle memory was able to kick in and save her from falling! (Note: Just because you’re disadvantaged, doesn’t mean that you’re guaranteed to fail. It just increases the probability of failing by forcing you to roll twice and pick the lower number, even if one of your rolls was a Nat20 like the one above. That’s why Jennifer passed the Ability Check. The lower number, a modified 16, was still greater than the Difficulty Class of a running on a wall, 14.)

Inheritance

OK! Our method works, and our first parent class is working properly. Now let’s create a child class that will be the blueprint for sentient objects in the game. We’ll call it Sentient because all of its attributes will be ones that are shared by both monsters and characters.

Above, you can see that we passed DiceSet inside the parentheses when we declared the sentient class. This sets up the parent-child relationship; Sentient is a child of DiceSet. This raises a very good question. Should a class that is the parent of all sentient classes, be the child of a class that represents dice? Typically, you’d want the names of parent and child classes to have some relevance to each other to reflect their relationship. For instance, the hierarchy of a Sparrow class would look like this:

Here, we see a clear relationship; a form of family tree. There is the ancestor class, Animal, which is a parent to Bird (since a bird IS AN animal), which has the child class Sparrow(a sparrow IS A bird). Thus, classes should follow an IS-A(N) relationship. But we all know that sentient things aren’t examples of dice sets. A sentient being HAS A dice set. This get’s into a more nuanced characteristic of Inheritance called “Composition”, which we won’t discuss right now, since I’m already teaching you to play D&D. So is my example appropriate? Absolutely not. Are we going to go with it anyway? Yes, but just understand that a more appropriate use would be to either make DiceSet an interface that other classes can implement, or keep rules and gameplay mechanics completely separate from Sentient class objects and their children.

We’ll fix part of this glaring problem when we talk about Abstraction next week. For now, let’s check to see if a Sentient class, inherited everything from DiceSet class.

As we can see, our Aatxe (named Barry) inherited everything from its parent class! And Jennifer? Has she inherited anything from this new relationship?

Jennifer has no roll modifiers because she’s a DiceSet object. A parent class object cannot inherit the traits of a child class.

Now, let’s make a Monster class that is a child of the Sentient class. We mentioned earlier that one big difference between characters and monsters is that only monsters have Legendary Actions. Therefore, the Monster class will have this attribute for themselves and a method to use them.

Next we’ll create a new Aatxe monster, this time it will be an object of the Monster class. Since its parent is the Sentient class, we should be able to define the monster’s attributes at instantiation, just like we can with it’s parent class. We’ll also define the Aatxe implementation of the legendary_actions method.

Alright, let’s check out what Leslie can do! Can we access her race (a parent attribute)?

What are her options for Legendary Actions?

If selling a time-share in Orlando, Florida had a Difficulty Class of 15, could she make the sale?

We passed Leslie’s Charisma modifier (she’ll need to be a smooth talker, afterall) as our modifier argument for the ability_check method, and she rolled a 16. That time-share is sold, baby!

Levity aside, this demonstrates that descendant classes and objects also inherit attributes and methods from their ancestors, as well.

What about Sibling classes (classes that share the same parent)? Can sibling classes inherit each other’s methods? Let’s make a character class and see if it inherits its sibling class’ legendary_actions method.

So objects of the Character class can override the name attribute they inherit from their parent class, but cannot inherit the legendary_actions method of from its sibling class.

Why Classes?

You might have noticed that I have primarily been using classes as data storage. Why not just use an array or dictionary then? Good question!

A class creates a blueprint or template to create objects that you can store data (like character information) or behaviors (methods that allow your objects to perform a function, like rolling an ability check). Thanks to inheritance, we can substantially reduce the amount of code that we need to write, since the subclass will have access to all of the methods and attributes they were capable of inheriting. Therefore, I don’t need to rewrite the ability_check method for EVERY single monster or character.

Next time, we’ll be discussing another Pillar of OOP: Abstraction. See you then!

--

--