Stage 4 - Character Types

Introduction

So far we have created a multiple-room dungeon that the user can move between. We have also populated the dungeon with characters that the user can interact with.

During this stage we will refine our characters by:

  • defining two character types:

    • friend

    • enemy

  • change our current characters to one of these types of characters

  • adjust our interactions to allow for different types of characters

Class Diagram

Our new class diagram has two new classes Enemy and Friend.

lesson 4 class diagram

Notice that they both have arrows pointing towards the Character class, that’s because they are both child classes of the parent class. A child class inherits all the attributes and methods from the parent class. In addition, they may have extra attributes and methods, or they can even overwrite an attribute or method they inherit. Let’s look at our class diagram to see this in action.

Inheritance

Inheritance is a concept in object-oriented programming (OOP) that allows you to create a new class based on an existing class. Think of it like a family tree, where a child class inherits characteristics from its parent class, just like how a child inherits traits from their parents.

Inheritance makes it easier to reuse code and add new classes without having to rewrite the same information over and over again. It also makes it easier to keep track of different types of animals and what they have in common and what makes them unique.

Enemy class

The Enemy class:

  • inherits from the Character class the name, description, and conversation attributes as well as the describe, talk, and hug methods.

  • adds the weakness attribute

  • overwrites the Character fight method with its own fight method

Friend class

  • inherits from the Character class the name, description, and conversation attributes as well as the describe, talk, and fight methods.

  • overwrites the Character hug method with its own hug method

Why use inheritance?

In the end our two child classes operate similarly to two classes with the following class diagrams (blue text indicates overwritten methods).

lesson 4 child classes

So why don’t we just two separate classes?

Remember the DRY principle? → Don’t Repeat Yourself?

If we have two describe methods that are exactly the same, we want to only write it once. This ensures code that is more accurate and easier to maintain.

For example, if I want to change the wording of the describe method, I will only need to change it in the Character class. The change will flow down to the Friend and Enemy classes. Similarly, if there is an error in the talk method, then I only need to fix it in the Character class.

OOP Terminology

OOP can have several names for the same concept. I will be consistent throughout this course, but if you use other resources, they may use different terminology.

  • parent class → superclass or base class

  • child class → subclass or derived class

Let’s make these changes to the code.

Define different character types

Open your character.py file and add the highlighted code below to create the Friend class:

Create Friend class

 1# character.py
 2
 3class Character():
 4    
 5    def __init__(self, name):
 6        # initialises the character object
 7        self.name = name
 8        self.description = None
 9        self.conversation = None
10
11    def describe(self):
12        # sends a description of the character to the terminal
13        print(f"{self.name} is here, {self.description}")
14
15    def talk(self):
16        # send converstation to the terminal
17        if self.conversation is not None:
18            print(f"{self.name}: {self.conversation}")
19        else:
20            print(f"{self.name} doesn't want to talk to you")
21
22    def hug(self):
23        # the character responds to a hug
24        print(f"{self.name} doesn't want to hug you")
25
26    def fight(self):
27        # the character response to a threat
28        print(f"{self.name} doesn't want to fight you")
29
30class Friend(Character):
31    
32    def __init__(self, name):
33        # initialise the Friend object by calling the character initialise
34        super().__init__(name)

Investigating that code:

  • class Friend(Character): → defines the Friend class

    • (Character) → tells Python that Character is the parent class of Friend

  • def __init__(self, name): → automatically runs when you create a Friend object

  • # initialise the Friend object by calling the character initialise → method descriptive comment

  • super().__init__(name) → this is very new

    • tells Python to run the __init__ method of the parent class (superclass)

      • running Character __init__ will inherits all the attributes and method from Character

    • the __init__ of Character requires a name so we pass the name argument

Create Enemy class

To create the Enemy class, add the highlighted code:

 1# character.py
 2
 3class Character():
 4    
 5    def __init__(self, name):
 6        # initialises the character object
 7        self.name = name
 8        self.description = None
 9        self.conversation = None
10
11    def describe(self):
12        # sends a description of the character to the terminal
13        print(f"{self.name} is here, {self.description}")
14
15    def talk(self):
16        # send converstation to the terminal
17        if self.conversation is not None:
18            print(f"{self.name}: {self.conversation}")
19        else:
20            print(f"{self.name} doesn't want to talk to you")
21
22    def hug(self):
23        # the character responds to a hug
24        print(f"{self.name} doesn't want to hug you")
25
26    def fight(self):
27        # the character response to a threat
28        print(f"{self.name} doesn't want to fight you")
29
30class Friend(Character):
31    
32    def __init__(self, name):
33        # initialise the Friend object by calling the character initialise
34        super().__init__(name)
35
36class Enemy(Character):
37    
38    def __init__(self,name):
39        # initialise the Enemy object by calling the character initialise
40        super().__init__(name)
41        self.weakness = None

Now unpack the code that create the Enemy class:

  • class Enemy(Character): → define the Enemy class as a child of the Character class

  • def __init__(self,name): → automatically runs when an Enemy object is created

  • # initialise the Enemy object by calling the character initialise → method descriptive comment

  • super().__init__(name) → runs the parent class’ __init__ method which causes inheritance

  • self.weakness = None → adds an additional weakness attribute to all Enemy objects

Now that we have two character types, we need to change the characters that we have created.

Change character types

Return to main.py, and change the highlighted code:

 1# main.py
 2
 3from room import Room
 4from character import Friend, Enemy
 5
 6# create rooms
 7cavern = Room("Cavern")
 8cavern.description = ("A room so big that the light of your torch doesn’t reach the walls.")
 9
10armoury = Room("Armoury")
11armoury.description = ("The walls are lined with racks that once held weapons and armour.")
12
13lab = Room("Laboratory")
14lab.description = ("A strange odour hangs in a room filled with unknownable contraptions.")
15
16# link rooms
17cavern.link_rooms(armoury,"south")
18armoury.link_rooms(cavern,"north")
19armoury.link_rooms(lab,"east")
20lab.link_rooms(armoury,"west")
21
22
23# create characters
24ugine = Enemy("Ugine")
25ugine.description = "a huge troll with rotting teeth."
26ugine.weakness = "cheese"
27
28nigel = Friend("Nigel")
29nigel.description = "a burly dwarf with golden bead in woven through his beard."
30nigel.conversation = "Well youngan, what are you doing here?"
31
32# add characters to rooms
33armoury.character = ugine
34lab.character = nigel
35
36'''
37# describe the rooms
38cavern.describe()
39armoury.describe()
40lab.describe()
41'''
42
43# initialise variables
44running = True
45current_room = cavern
46
47# ----- MAIN LOOP -----
48while running:
49    current_room.describe()
50    
51    command = input("> ").lower()
52    
53    if command in ["north", "south", "east", "west"]:
54        current_room = current_room.move(command)
55    elif command == "talk":
56        if current_room.character is not None:
57            current_room.character.talk()
58        else:
59            print("There is no one here to talk to")
60    elif command == "hug":
61        if current_room.character is not None:
62            current_room.character.hug()
63        else:
64            print("There is no one here to hug")
65    elif command== "fight":
66        if current_room.character is not None:
67            current_room.character.fight()
68        else:
69            print("There is no one here to fight")
70    elif command == "quit":
71        running = False
72    else:
73        print("I don't understand.")

Investigating that code:

  • from character import Friend, Enemy → we will no longer have Character objects, but rather Friend and Enemy objects

  • ugine = Enemy("Ugine") → changes Ugine to an Enemy object

  • ugine.weakness = "cheese"Enemy object have a weakness attribute, Ugine’s is cheese

  • nigel = Friend("Nigel") → changes Nigel to a Friend object

Refactoring testing

What we have just done is called refactoring our code. That is, we have made a change to our code, without changing what it does. Whenever you refactor your code the next step should always be testing, so let’s test.

What do we need to test. We need to make sure that we can still have all the same interactions with both Ugine and Nigel. Draw up the testing table below and then complete it.

Character

Interaction

Expected Result

Actual Result

Ugine

talk

Ugine

hug

Ugine

fight

Nigel

talk

Nigel

hug

Nigel

fight

If all your expected results match your actual results then there is no problems, otherwise, you need to troubleshoot where your mistakes.

Adjusting the interactions

We want to change our interactions according to the character’s type. We don’t want to hug our enemies, nor do we want to fight our friends. In OOP this is called polymorphism.

Polymorphism

Polymorphism is a concept in object-oriented programming (OOP) that allows objects of different classes to respond to the same method call in different ways. This is like having multiple people with different jobs, all able to perform the same action, but in their own unique way.

Polymorphism allows for objects of different classes to be treated as objects of their class or as objects of a parent class, without having to know the exact type of the object. This makes it easier to write generic code that can work with objects of multiple classes, making your code more flexible and adaptable to changes in the future.

Adjusting the hug method

Currently, the hug method is inherited from the Character class, which basically says the character doesn’t want to hug you. This is fine for enemies, so we don’t have to change the Enemy class, but this is not what we want our friends to do, so let’s change the Friend class.

Return to the character.py file and add the highlighted code:

 1# character.py
 2
 3class Character():
 4    
 5    def __init__(self, name):
 6        # initialises the character object
 7        self.name = name
 8        self.description = None
 9        self.conversation = None
10
11    def describe(self):
12        # sends a description of the character to the terminal
13        print(f"{self.name} is here, {self.description}")
14
15    def talk(self):
16        # send converstation to the terminal
17        if self.conversation is not None:
18            print(f"{self.name}: {self.conversation}")
19        else:
20            print(f"{self.name} doesn't want to talk to you")
21
22    def hug(self):
23        # the character responds to a hug
24        print(f"{self.name} doesn't want to hug you")
25
26    def fight(self):
27        # the character response to a threat
28        print(f"{self.name} doesn't want to fight you")
29
30class Friend(Character):
31    
32    def __init__(self, name):
33        # initialise the Friend object by calling the character initialise
34        super().__init__(name)
35
36    def hug(self):
37        # the friend responds to a hug
38        print(f"{self.name} hugs you back.")
39
40class Enemy(Character):
41    
42    def __init__(self,name):
43        # initialise the Enemy object by calling the character initialise
44        super().__init__(name)
45        self.weakness = None

Investigating the code:

  • def hug(self): → defines the hug method for the Friend class

    • same name as Character method → replaces hug method for all Friend objects

  • # the friend responds to a hug → method’s explanatory comment

  • print(f"{self.name} hugs you back.") → display message using object’s name

Adjusting the fight method

Now it’s time to adjust the fight method for our Enemy class. We have a simple fight mechanic. Each Enemy has a weakness. If you use their weakness to fight them, you win, otherwise you loose.

The highlighted code below enacts this mechanic.

 1# character.py
 2
 3class Character():
 4    
 5    def __init__(self, name):
 6        # initialises the character object
 7        self.name = name
 8        self.description = None
 9        self.conversation = None
10
11    def describe(self):
12        # sends a description of the character to the terminal
13        print(f"{self.name} is here, {self.description}")
14
15    def talk(self):
16        # send converstation to the terminal
17        if self.conversation is not None:
18            print(f"{self.name}: {self.conversation}")
19        else:
20            print(f"{self.name} doesn't want to talk to you")
21
22    def hug(self):
23        # the character responds to a hug
24        print(f"{self.name} doesn't want to hug you")
25
26    def fight(self):
27        # the character response to a threat
28        print(f"{self.name} doesn't want to fight you")
29
30class Friend(Character):
31    
32    def __init__(self, name):
33        # initialise the Friend object by calling the character initialise
34        super().__init__(name)
35
36    def hug(self):
37        # the friend responds to a hug
38        print(f"{self.name} hugs you back.")
39
40class Enemy(Character):
41    
42    def __init__(self,name):
43        # initialise the Enemy object by calling the character initialise
44        super().__init__(name)
45        self.weakness = None
46
47    def fight(self, item):
48        # fights enemy with provided item and returns if player survives
49        if item == self.weakness:
50            print(f"You strike {self.name} down with {item}.")
51            return True
52        else:
53            print(f"{self.name} crushes you. Puny adventurer")
54            return False

Investing that code:

  • def fight(self, item): → defines the fight method for the Enemy class

    • accepts the item argument which is the weapon the player uses

  • # fights enemy with provided item and returns if player survives → method’s explanatory comment

  • if item == self.weakness: → checks if the item is this enemy’s weakness

  • print(f"You strike {self.name} down with {item}.") → displays success message

  • return True → informs main.py of victory in the fight

  • else: → when the item is not this enemy’s weakness

  • print(f"{self.name} crushes you. Puny adventurer") → displays failure message

  • return False → informs main.py of loss in the fight

Now that our fight method is ready, we need to change our fight event handler in main.py. Use the highlighted code below:

 1# main.py
 2
 3from room import Room
 4from character import Enemy, Friend
 5
 6# create rooms
 7cavern = Room("Cavern")
 8cavern.description = ("A room so big that the light of your torch doesn’t reach the walls.")
 9
10armoury = Room("Armoury")
11armoury.description = ("The walls are lined with racks that once held weapons and armour.")
12
13lab = Room("Laboratory")
14lab.description = ("A strange odour hangs in a room filled with unknownable contraptions.")
15
16# link rooms
17cavern.link_rooms(armoury,"south")
18armoury.link_rooms(cavern,"north")
19armoury.link_rooms(lab,"east")
20lab.link_rooms(armoury,"west")
21
22
23# create characters
24ugine = Enemy("Ugine")
25ugine.description = "a huge troll with rotting teeth."
26ugine.weakness = "cheese"
27
28nigel = Friend("Nigel")
29nigel.description = "a burly dwarf with golden bead in woven through his beard."
30nigel.conversation = "Well youngan, what are you doing here?"
31
32# add characters to rooms
33armoury.character = ugine
34lab.character = nigel
35
36'''
37# describe the rooms
38cavern.describe()
39armoury.describe()
40lab.describe()
41'''
42
43# initialise variables
44running = True
45current_room = cavern
46
47# ----- MAIN LOOP -----
48while running:
49    current_room.describe()
50    
51    command = input("> ").lower()
52    
53    if command in ["north", "south", "east", "west"]:
54        current_room = current_room.move(command)
55    elif command == "talk":
56        if current_room.character is not None:
57            current_room.character.talk()
58        else:
59            print("There is no one here to talk to")
60    elif command == "hug":
61        if current_room.character is not None:
62            current_room.character.hug()
63        else:
64            print("There is no one here to hug")
65    elif command== "fight":
66        if current_room.character is not None:
67            weapon = input("What will you fight with? > ").lower()
68            if current_room.character.fight(weapon):
69                current_room.character = None
70            else:
71                running = False
72        else:
73            print("There is no one here to fight")
74    elif command == "quit":
75        running = False
76    else:
77        print("I don't understand.")

Investigate the code:

  • weapon = input("What will you fight with? > ").lower() → asks the user to input their weapon

  • if current_room.character.fight(weapon): → checks to see if user wins the fight

    • current_room.character.fight(weapon) → calls the fight method displaying a message

    • if → since the fight method returns a Boolean indicating the player’s success, we can use this to check the fight result.

  • current_room.character = None → if the player won the fight, the room now has no character

  • else: → if the player looses the fight

  • running = False → set the main loop flag to False so the game will finish

Testing

Now we changed both the hug and fight methods, time to do some testing. Again we will use our testing table, and focus on the code we have changed.

Character

Interaction

Weapon

Expected Result

Actual Result

Ugine

fight

cheese

Ugine

fight

not cheese

Ugine

hug

-

Nigel

fight

-

Nigel

hug

-

Friend fight error

Did you get the following error?

1Traceback (most recent call last):
2  File "h:\GIT\python-oop-with-deepest-dungeon\python_files\stage_4\main.py", line 66, in <module>
3    if current_room.character.fight(weapon):
4       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
5TypeError: Character.fight() takes 1 positional argument but 2 were given

Why did we get the error? Let’s read the error message:

  • line 2 → the error is at line 66 of main.py

  • line 3 → the error is contained in if current_room.character.fight(weapon):

  • line 4 → the error is specifically in the call to fight

  • line 5fight was only expecting one argument (self), but we gave two (self,weapon)

So let’s think about this. We have two fight methods, which one was causing the problem? Well, Ugine worked fine, but Nigel didn’t, so it must be the fight method for friends.

That method is in our character.py file, so let’s look at it.

 1# character.py
 2
 3class Character():
 4    
 5    def __init__(self, name):
 6        # initialises the character object
 7        self.name = name
 8        self.description = None
 9        self.conversation = None
10
11    def describe(self):
12        # sends a description of the character to the terminal
13        print(f"{self.name} is here, {self.description}")
14
15    def talk(self):
16        # send converstation to the terminal
17        if self.conversation is not None:
18            print(f"{self.name}: {self.conversation}")
19        else:
20            print(f"{self.name} doesn't want to talk to you")
21
22    def hug(self):
23        # the character responds to a hug
24        print(f"{self.name} doesn't want to hug you")
25
26    def fight(self):
27        # the character response to a threat
28        print(f"{self.name} doesn't want to fight you")
29
30class Friend(Character):
31    
32    def __init__(self, name):
33        # initialise the Friend object by calling the character initialise
34        super().__init__(name)
35
36    def hug(self):
37        # the friend responds to a hug
38        print(f"{self.name} hugs you back.")
39
40class Enemy(Character):
41    
42    def __init__(self,name):
43        # initialise the Enemy object by calling the character initialise
44        super().__init__(name)
45        self.weakness = None
46
47    def fight(self, item):
48        # fights enemy with provided item and returns if player survives
49        if item == self.weakness:
50            print(f"You strike {self.name} down with {item}.")
51            return True
52        else:
53            print(f"{self.name} crushes you. Puny adventurer")
54            return False

Looking closely at the code:

  • lines 30 - 38 → the Friend class does not have a fight method, so it is using the inherited fight method from the Character class

  • line 26 → the Character fight method only accepts one argument (self), but how does this compare to the Enemy fight method?

  • line 47 → the Enemy fight method accepts two arguments (self, item)

Ok so we’ve found a discrepancy, but which one do we want to change? Remember we changed our main.py code to deal with fighting with a weapon, so the easiest way to solve this error is to add another argument to the Character fight method.

So make the following changes to character.py:

 1# character.py
 2
 3class Character():
 4    
 5    def __init__(self, name):
 6        # initialises the character object
 7        self.name = name
 8        self.description = None
 9        self.conversation = None
10
11    def describe(self):
12        # sends a description of the character to the terminal
13        print(f"{self.name} is here, {self.description}")
14
15    def talk(self):
16        # send converstation to the terminal
17        if self.conversation is not None:
18            print(f"{self.name}: {self.conversation}")
19        else:
20            print(f"{self.name} doesn't want to talk to you")
21
22    def hug(self):
23        # the character responds to a hug
24        print(f"{self.name} doesn't want to hug you")
25
26    def fight(self, item):
27        # the character response to a threat
28        print(f"{self.name} doesn't want to fight you")
29
30class Friend(Character):
31    
32    def __init__(self, name):
33        # initialise the Friend object by calling the character initialise
34        super().__init__(name)
35
36    def hug(self):
37        # the friend responds to a hug
38        print(f"{self.name} hugs you back.")
39
40class Enemy(Character):
41    
42    def __init__(self,name):
43        # initialise the Enemy object by calling the character initialise
44        super().__init__(name)
45        self.weakness = None
46
47    def fight(self, item):
48        # fights enemy with provided item and returns if player survives
49        if item == self.weakness:
50            print(f"You strike {self.name} down with {item}.")
51            return True
52        else:
53            print(f"{self.name} crushes you. Puny adventurer")
54            return False

Test again

Now we changed both the hug and fight methods, time to do some testing. Again we will use our testing table, and focus on the code we have changed.

Character

Interaction

Weapon

Expected Result

Actual Result

Ugine

fight

cheese

Ugine

fight

not cheese

Ugine

hug

-

Nigel

fight

-

Nigel

hug

-

Wait, another problem fighting Nigel, but this one is different. There is no error message, the program just ends when you fight him. This is what we call a logic error.

Types of programming errors

There are three basic categories of programming errors:

  • syntax errors

    • caused by not following the programming language rules

    • Python will not even run the program, and immediately display an error message

  • runtime errors

    • caused when Python tries to execute a command, but something is wrong

    • Python will run the program, but display an error when it comes across a runtime error

    • our fight method error was a runtime error

  • logic errors

    • caused when the program does exactly what you tell it to do, but not what you want it to do

    • Python will never display an error, but the program doesn’t do what you want it to do

    • these are the hardest to troubleshoot

Here is the interaction I got from running the code before it ended:

1You are in the laboratory
2A strange odour hangs in a room filled with unknownable contraptions.
3Nigel is here, a burly dwarf with golden bead in woven through his beard.
4To the west is the armoury
5> fight
6What will you fight with? > dog
7Nigel doesn't want to fight you

Troubleshooting a logic error

Troubleshooting logic errors is a bit like detective work. You need to trace the program flow to work out where the error is.

So we’ll start our investigation in the main.py. Looking at the main loop, we can be confident that the problem involves the fight event handler, so let’s zoom into that.

65    elif command== "fight":
66        if current_room.character is not None:
67            weapon = input("What will you fight with? > ").lower()
68            if current_room.character.fight(weapon):
69                current_room.character = None
70            else:
71                running = False
72        else:
73            print("There is no one here to fight")

In the test when the user fought Nigel:

  • the user got the message Nigel doesn't want to fight you

    • this comes from the call to the fight method

    • line 68 must of been executed

  • the game ended

    • therefore running needed to be changed to False

    • line 71 must of been executed

  • the only way that line 71 could have been executed would be if the user lost their fight with Nigel

  • line 68 determines if the user won the fight, so lets look closely at this.

    • if current_room.character.fight(weapon): → makes a call to the fight method and gets a Boolean response indicating success

    • since Nigel is a friend we need to look at the Friend fight method

So zooming into the fight method in the Character class in character.py:

26    def fight(self, item):
27        # the character response to a threat
28        print(f"{self.name} doesn't want to fight you")

Now I can see the problem. If main.py is expecting a Boolean value, it won’t get one because the Character fight method doesn’t return anything.

Well, that’s not entirely correct. All Python functions (including methods) return a value. If the return statement is not used, then the default values of None is returned, but why does that stop our game?

Let’s zoom back in to the fight handler in main.py to understand.

65    elif command== "fight":
66        if current_room.character is not None:
67            weapon = input("What will you fight with? > ").lower()
68            if current_room.character.fight(weapon):
69                current_room.character = None
70            else:
71                running = False
72        else:
73            print("There is no one here to fight")

Looking at line 4:

  • for Nigel current_room.character.fight(weapon) will return None

  • line 4 becomes if None: which equates to if False:

  • jumps to the else statement on line 6

  • which means line 7 is executed changing running to False

Ok, that all makes sense. Now we have to fix the problem. What we need is for line 4 to receive a True when it calls the Character fight method.

Jump back to character.py and add the highlighted code below to solve our logic error.

 1# character.py
 2
 3class Character():
 4    
 5    def __init__(self, name):
 6        # initialises the character object
 7        self.name = name
 8        self.description = None
 9        self.conversation = None
10
11    def describe(self):
12        # sends a description of the character to the terminal
13        print(f"{self.name} is here, {self.description}")
14
15    def talk(self):
16        # send converstation to the terminal
17        if self.conversation is not None:
18            print(f"{self.name}: {self.conversation}")
19        else:
20            print(f"{self.name} doesn't want to talk to you")
21
22    def hug(self):
23        # the character responds to a hug
24        print(f"{self.name} doesn't want to hug you")
25
26    def fight(self, item):
27        # the character response to a threat
28        print(f"{self.name} doesn't want to fight you")
29        return True
30
31class Friend(Character):
32    
33    def __init__(self, name):
34        # initialise the Friend object by calling the character initialise
35        super().__init__(name)
36
37    def hug(self):
38        # the friend responds to a hug
39        print(f"{self.name} hugs you back.")
40
41class Enemy(Character):
42    
43    def __init__(self,name):
44        # initialise the Enemy object by calling the character initialise
45        super().__init__(name)
46        self.weakness = None
47
48    def fight(self, item):
49        # fights enemy with provided item and returns if player survives
50        if item == self.weakness:
51            print(f"You strike {self.name} down with {item}.")
52            return True
53        else:
54            print(f"{self.name} crushes you. Puny adventurer")
55            return False

Third test lucky

Let’s test and make sure that our logic error has been solved. Again, complete the test table below.

Character

Interaction

Weapon

Expected Result

Actual Result

Ugine

fight

cheese

Ugine

fight

not cheese

Ugine

hug

-

Nigel

fight

-

Nigel

hug

-

Hopefully all your test have passed.

Stage 4 task

Now it is time for your to implement the Make phase.

Consider the additional character or characters that you have added, and change them into either a Friend, or an Enemy. Don’t forget their weakness if they are an enemy.