Warning - This site is moving to https://getthecodingbug.anvil.app
Topics covered
Classes
creating your own classes
making a game using your classes
Getters and Setters
Inheritance
Polymorphism
Aggregation - objects within objects
Classes recap
Even though we haven't talked much about classes and object orientation in previous lessons, we have worked with classes all the time. In fact, everything is a class in Python.
In the Tanks program, we invoked the pygame class's "init" method at the start and the display class's "set_caption" method to display the name of our game at the top of the window.
import pygame
pygame.init()
pygame.display.set_caption("Tanks")
Creating your own class
You are now going to make your own classes to build a text-based adventure game.
This is an interactive invented world described in text. It can be filled with different rooms, items, obstacles, or anything your imagination allows. The player interacts with the world by typing commands, and the game describes the result of the player’s commands.
To keep things simple you should make each new class in a separate file. Because there will be several files, I suggest you create a new folder, called "Adventure" and save all the following files in that folder.
The first class we will be named "room" so make an empty New File and save it as "room.py" in the new folder "Adventure". Next put these statements in "room.py":
class Room():
def __init__(self, room_name):
self.name = room_name
self.description = None
A few things to note here. The class name starts with a capital letter, this is a convention in Python, to distinguish it from a variable or function. Also it has an open and close bracket following it, which is empty in our case. We will cover putting things in here later when we discuss "Inhertiance".
Finally, the class has to have at least one function, with the name: "__init__". This is called the "constructor".
Here we add "self" and "room_name" as parameters that the constructor and class will use.
We always refer to attributes within the object in the format "self.name_of_attribute" to tell Python that we are referring to a piece of data within the object
"self" means "this object". Setting the attribute values to "None" means that they will start off with no value.
Instantiating an object
Now create another empty New File and save it as "main.py", and place these statements in it:
from room import Room
kitchen = Room("Kitchen")
The main program first imports the "room" class you have just made, and then "instantiates" an object called "kitchen", and passes the room name to the class as "Kitchen". Instantiating, calls the "__init__" function and in our class, sets the "room_name" attribute.
You should be able to run "main.py" but it will not display anything at this stage, unless you have made a mistake, because we have not yet created any methods that will output or return anything.
Getters and Setters
The room class has an attribute "self.description" but no way to populate it. You can rectify this with a function, just after the "def __init__ function", add the following lines, in the "room.py" file:
def set_description(self, room_description):
self.description = room_description
This is called a "setter" function. You can also create a "getter" function, like this:
def get_description(self):
Now add these lines to your main program:
kitchen.set_description("A dank and dirty room buzzing with flies.")
print(kitchen.get_description())
Here we are using "setter" function to set the description, then the "getter" function to get and print the description.
Run the program to see if you get the description of the room displayed.
Lets add another "getter" for the object's name, like this:
def get_name(self):
This will be used later on in this lesson.
Linking rooms
In our game we would like to have lots of rooms, and so we need to add some attributes and methods to handle linking multiple room objects together. We will add a dictionary of all of the rooms which are linked to a Room object.
You need to make a sketch or plan view of the rooms in your adventure, so that you can plot possible routes through the adventure. Here is an example:
return self.name
return self.description
We can ask the dictionary for a specific element by name. This will be useful in our adventure game, because we can ask to go to the room in a particular direction. For example, here is how we would refer to the room to the east:
self.linked_rooms["east"]
So, in the "room.py" add this attribute, just under the "self.description = None" statement:
self.linked_rooms = {}
Then insert this function, just after the "def __init__(self, room_name):" function
def link_room(self, room_to_link, direction):
self.linked_rooms[direction] = room_to_link
Link the rooms together
The dining hall is to the south of the kitchen, so we am going to use the "link_room" method on the kitchen object in your "main.py" file, like this:
kitchen.link_room(dining_hall, "south")
But first we need to create the dining hall, like this:
dining_hall = Room("Dining Hall")
dining_hall.set_description("A large room with ornate golden decorations on each wall")
Next, lets add the ballroom, like this:
ballroom = Room("Ballroom")
Finally, link all the rooms together like this:
kitchen.link_room(dining_hall, "south")
dining_hall.link_room(kitchen, "north")
dining_hall.link_room(ballroom, "west")
ballroom.link_room(dining_hall, "east")
ballroom.set_description("A vast room with a shiny wooden floor")
print(self.name)
print("-------------------")
print(self.description)
for direction in self.linked_rooms:
room = self.linked_rooms[direction]
print("The " + room.get_name() + " is " + direction)
You could, of course, make the links one-way, or only open when the player has achieved some goal.
Displaying the rooms
You may want to display some information about the room you have just entered, like this:
The Dining Hall
-------------------
A large room with ornate golden decorations on every wall
The Kitchen is north
The Ballroom is west
To do this, add a new method to the Room class to report the room name, description, and the directions of all the rooms connected to it. Go back to the "room.py" file and, below the link_room method, add a new method which will display all of the rooms linked to the current room object.
def get_details(self):
This method displays the name of the room, its description, then loops through the dictionary "self.linked_rooms" and, for every defined "direction", displays that direction and the name of the room in that direction.
Go back to the "main.py" file and remove (or comment-out) the line which reads:
"print(kitchen.get_description())"
Then, at the bottom of your script, call this method on the dining hall object, then run the code to see the two rooms linked to the dining hall.
dining_hall.get_details()
if direction in self.linked_rooms:
return self.linked_rooms[direction]
else:
print("You can't go that way")
return self
Finally, add lines to check the situation in the other two rooms. These just to test the "get_details" function works OK - you can remove them, or comment them out later.
Moving between rooms
Now, let’s add a method to allow the player to move between rooms.
Go to the "room.py" file and the "move" method below the "get_details" method. This "move" method has a parameter for the direction in which the player would like to move. If this direction is one of the directions linked to, the method returns the room object that is in that direction. If there is no room in the dictionary in that direction, the method returns "self" – i.e. the player is linked back to the room they were already in, and so stay where they are.
def move(self, direction):
Now, go back to "main.py" and remove the "dining_hall.get_details()" and any other ".get_details()" statements we put in earlier to test that function.
Then add some code at the bottom of the script to create a loop, letting the player move between rooms.
current_room = kitchen
while True:
print("\n")
current_room.get_details()
command = input("> ")
current_room = current_room.move(command)
The new variable "current_room" will always hold the name the room we are currently in.
Then the program will loop forever, prompting for a new command.
Before you run your 'main.py' script, you should be aware of the following: If you are using the SublimeText editor, it will not work by pressing 'F5' because SublimeText cannot handle the 'input' statement. So switch the to IDLE editor, load up your 'main.py' and run it and continue editing it from there instead.
Run the program and enter "east", "south" etc. and see if you can move from room to room.
Don’t forget to also try directions that won’t work to see whether your game handles these correctly.
Item class
You have successfully created a Room class, complete with a constructor, attributes, getters, and setters.
Now test your skills and have a go at creating a class from scratch. The challenge is to create a class to represent an item you might find in the game, such as a sword.
Create a new Python file, save it as "item.py", and create a class called Item inside it.
The Item class should have the following:
Attributes for the name and the description of the item
A constructor method
Getters and setters for the name and the description of the item
Any additional attributes and methods you would like to add
Don’t forget to test your Item class by creating an Item object and then calling the methods.
Character class and Inheritance
In previous lessons we used classes written by other people, and at the start of this lesson we wrote our own class from scratch. Now we will extend a class by adding our own functionality to existing code.
Extending a class takes advantage of a concept called "inheritance" – the new class can be said to inherit properties from its parent class. To extend a class, we can define new attributes or methods, and also change the behaviour of existing methods.
In this section we will be creating characters to inhabit our game world.
Start by creating a New File and saving it as "character.py", then cut-n-paste the code below and save it again.
class Character():
# Create a character
def __init__(self, char_name, char_description):
self.name = char_name
self.description = char_description
self.conversation = None
# Describe this character
def describe(self):
print( self.name + " is here!" )
print( self.description )
# Set what this character will say when talked to
def set_conversation(self, conversation):
self.conversation = conversation
# Talk to this character
def talk(self):
if self.conversation is not None:
print("[" + self.name + " says]: " + self.conversation)
else:
print(self.name + " doesn't want to talk to you")
# Fight with this character
def fight(self, combat_item):
print(self.name + " doesn't want to fight with you")
return True
You’ll see that to create a Character object, the constructor needs two parameters – the character’s name, and a description of the character:
def __init__(self, char_name, char_description):
self.name = char_name
self.description = char_description
self.conversation = None
Create and test a character
Create a new Python file and save it as "character_test.py". Inside this file, create a character object.
from character import Character
dave = Character("Dave", "A smelly zombie")
Call the method "describe" on the object you created to show the character’s description on the screen.
dave.describe()
Run the "character_test.py" program and you should get something like this:
Dave is here!
A smelly zombie
Conversations with characters
Examine the Character class inside "character.py" to find the name of the method which sets the "conversation" attribute
Add code to "main.py" to call this method and give "Dave" a line of dialogue.
Add code to "main.py" to call a different method which talks to "Dave".
Extending the Character class
So far, so good – we can use this code as the basis for our characters. However, not every character in the game will have the same characteristics. Some will be friends and some will be enemies, and they may behave differently.
For example, I might create two characters: "Dave", the "smelly zombie", and "Catrina", the "friendly skeleton".
I might want to fight with enemies such as "Dave", but I wouldn’t want to fight with friendly characters such as "Catrina".
We will make a "subclass" of "Character" called "Enemy". It will use the "Character" class as a basis, but add some more functionality specific to enemies.
So, in the existing "character.py" file, start a new class below the "Character" class.
class Enemy(Character):
The name of this new class is "Enemy", but we have put "Character" inside the brackets to tell Python that the "Enemy" class will "inherit" all of the "attributes" and "methods" from "Character".
"Character" is called the "superclass" of "Enemy", and "Enemy" is a "subclass" of "Character".
Now let’s write some code to create a "constructor" for an enemy object – this looks identical to the "constructor" of "Character":
def __init__(self, char_name, char_description):
As we said, "Character" is the "superclass" of "Enemy", so we need to ensure that "Enemy" "inherits" from "Character". To do this, we have to call the "superclass constructor" method inside the "constructor" of the "Enemy" class.:
super().__init__(char_name, char_description)
This line of code means “To make an Enemy, first make a Character object and then we’ll customise it”.
To recap, your "Enemy" class should look like this:
class Enemy(Character):
def __init__(self, char_name, char_description):
super().__init__(char_name, char_description)
Save this code. Now let’s prove that, with the help of these two lines of code, the "Enemy" class has "inherited" all of the properties of the "Character" class.
Go back to your "character_test.py" file and change it so that all references to the "Character" class become references to the "Enemy" class:
from character import Enemy
dave = Enemy("Dave", "A smelly zombie")
dave.describe()
Run the code and test it out. You should be able to see the description of "Dave" and talk to him just as you did before.
Polymorphism
An object of a "subclass" is also considered to be an object of its "superclass". This concept is called "polymorphism". In simpler terms, a <subclass> is a <superclass>.
So in our example, an "enemy" is a "character". If a function requires a "character" object as a parameter, we can give the function an "enemy" object instead and the code will still work. However, this does not work the other way around – if the function requires an "Enemy" object, we cannot give it a "character" object, because "Enemy" is a more specialised version of "Character".
Adding new attributes and methods to our sub-class
At the moment our "Enemy" class is functionally identical to the "Character" class, which is a bit pointless. That’s why we’ll start to add new functionality to customise it.
Let’s add a new "attribute" to specify the weakness of the "enemy" character, so that we can defeat the character in-game. An object of the class "Character" doesn’t have an "attribute" called "weakness" – this is a customisation we are adding to the "Enemy" class.
Each "enemy" will be vulnerable to an item which can be found in the game. For example, Superman would be vulnerable to Kryptonite. Eventually we will use the "Item" class you wrote earlier to add items to the game, but for now we’ll just represent an item as a string.
Inside your "Enemy" class, move to the line below the one calling the "superclass constructor".
Set the value of the weakness attribute to be initialised as None.
self.weakness = None
Add "getter" and "setter" methods to "Enemy" so that you can add a "weakness" for an "enemy".
Fighting Dave
The "Character" class has a method called "fight()". Since we don’t want to fight with non-hostile characters, it simply returns a message that the character “doesn’t want to fight with you”.
print(self.name + " doesn't want to fight with you")
Inside the "Enemy" class, we will add a new implementation of the "fight" method to allow us to fight an enemy. This will override the implementation of "fight()" provided inside "Character".
Start your method like this:
def fight(self, combat_item):
"combat_item" is a string containing the name of an item, for example “sword” or “banana”.
If the "combat_item" the player uses to fight with the enemy is the enemy’s "weakness", print out a message saying that the player won the fight, and return "True". Otherwise, print out a message saying that the player lost the fight, and return "False".
def fight(self, combat_item):
if combat_item == self.weakness:
print("You fend " + self.name + " off with the " + combat_item )
return True
else:
print(self.name + " crushes you, puny adventurer")
return False
Go back to your "character_test.py" file and add some new code at the bottom of the page. First, add a "weakness" for "Dave" by calling the "setter" method you just wrote. In my game, "Dave" is vulnerable to "cheese" because he just loves to eat it!
dave.set_weakness("cheese")
Then call the "fight()" method on "Dave", choosing an object to fight with:
print("What will you fight with?")
fight_with = input()
dave.fight(fight_with)
Save and run your code. If you choose to fight "Dave" with a "melon", you will see this outcome of the fight:
Dave is here!
A smelly zombie
[Dave says]: What's up, dude!
What will you fight with?
melon
Dave crushes you, puny adventurer
Run the code again, but this time enter “cheese” as the item you will fight with:
Dave is here!
A smelly zombie
[Dave says]: What's up, dude!
What will you fight with?
cheese
You fend Dave off with the cheese
Aggregation - objects within objects
We have written a class for creating an enemy, so let’s add "Dave" the zombie to our game.
Add the following line of code at the top of "main.py" to import your "Enemy" class.
from character import Enemy
Remember that "character" is the name of the Python file from which you are importing ("character.py"), and "Enemy" is the name of the "class" you are importing from it.
Below the code which links the rooms together, we need to recreate "Dave".
Make sure you include some code to set Dave’s "conversation" and "weakness" "attributes".
dave = Enemy("Dave", "A smelly zombie")
dave.set_conversation("Brrlgrh... rgrhl... brains...")
dave.set_weakness("cheese")
We would like to situate "Dave" inside a room. To do this, we have to add a new parameter inside the "Room" class in "room.py" so that a "room object" knows when it has a "character" inside it. Edit your "Room" class "constructor" to add a new field.
self.character = None
A "room" object now has a "character" object inside it – this is called "aggregation". In practice a room may be empty, in which case this "attribute" will stay as None. The important thing is that a room now has the capability to contain a "character".
Now add "getter" and "setter" methods to enable putting a "character" inside a "room".
Putting Dave in a room
Let’s put "Dave" in the "dining hall" using the new "setter" method you just wrote. Add this code to "main.py", immediately below the code where you created "Dave":
dining_hall.set_character(dave)
You may be thinking “Hang on a second, the room can contain a "character", but Dave is an enemy!”
We are allowed to add an "enemy" instead of a "character" because of "polymorphism" – an "enemy" is a "character".
Now, in the main game loop, before you ask the user for a command, check whether there is an enemy in the room using your "getter" method.
If the answer is yes, describe the enemy using the describe method which was inherited by Enemy from the Character class.
Note, just insert the new code (in green) below, to the existing code (in blue) in "main.py".
while True:
print("\n")
current_room.get_details()
inhabitant = current_room.get_character()
if inhabitant is not None:
inhabitant.describe()
command = input("> ")
current_room = current_room.move(command)
Save and run your program. Move from the kitchen to the dining hall, and you should see the description of Dave the zombie appear.
More interactions
At the moment our game only allows us to move between rooms using the basic loop:
current_room = kitchen
while True:
print("\n")
current_room.get_details()
inhabitant = current_room.get_character()
if inhabitant is not None:
inhabitant.describe()
command = input("> ")
current_room = current_room.move(command)
Now that we have "characters", let’s expand this loop so that it performs different actions depending on the "command" that is given. Here is some code to start you off:
command = input("> ")
# Check whether a direction was typed
if command in ["north", "south", "east", "west"]:
current_room = current_room.move(command)
elif command == "talk":
# Add code here
Here are some challenges for you to try.
Challenge 1
Add some functionality to the game loop so that different commands can be recognised. For example, your program should react to the following user inputs:
"talk" - talk to the inhabitant of this room (if there is one)
"fight" - ask what the player would like to fight with, and then fight the inhabitant of this room (if there is one)
Challenge 2
If you lose a "fight" with an "enemy", the game should end. Hint: the "fight" method we wrote in the "Enemy" class returns "True" if you survive, and "False" if you do not.
Challenge 3
Add an additional "enemy" character to inhabit a different room in your game. Perhaps you might want to add more methods in the "Enemy" class in order to be able to interact with enemies in other ways. For example, can you steal from an enemy, bribe an enemy, or send it to sleep?
Challenge 4
For a bigger challenge, why not extend the "Character" class in a different way? Perhaps you could create a "Friend" "subclass" with some custom "methods" and "attributes". For example, you could "hug" the friendly character, or offer them a gift. You can use the built in Python function "isinstance()" to check whether an "object" is an "instance" of a particular class.
Once you have tried these challenges or if you cannot get them to work go to this page, to see a complete game.
See how it works and perhaps you can improve or change it to your own requirements:
Complete Game: Lesson 8 a