Using OOP – Attack of the Orcs v1.0.0
The attack feature that you added in the previous game has made it a lot more interesting. You can see some friends coming back again and again to play the game. The new feature requests have started pouring in.
Here is a partial list of the requested features:
- New mission to acquire all the huts and defeat all the enemies. This also means the hut occupants should be revealed right at the beginning of the game.
- Ability to get healed in a friendly or unoccupied hut.
- Ability to abandon combat (or run away from the enemy). This is a strategic move to run away, get healed in a friendly hut, and resume combat.
- Introduce one or more horse riders to assist Sir Foo. They can take turns to acquire huts. Ideally, a user-configurable option.
- Ability to configure the maximum hit points for each enemy unit and each of the horse riders.
- Configurable total number of huts; for example, increase it to 10.
- Each hut can have either some gold or a weapon inside that Sir Foo and his friends can pick up.
- Have an elf rider join Sir Foo. His abilities give him a very high chance of winning with fewer attacks.
This is quite a long list. You are preparing a plan. Here is a partial list of things you will need to add to the existing code to implement some of these features:
- Keeping track of the hit points of multiple enemy units occupying various huts
- Maintaining the health record of Sir Foo and all accompanying horse riders
- Monitoring how many huts are acquired by Sir Foo's army
- Another dictionary or list to keep track of the gold in each hut, and another one for weapons; additionally, what if someone wants to put armor in the hut?
- Not to forget, yet another list of dictionary for each unit that accepts any of these goodies
- Ah! So they want an elf rider with its own traits and abilities...nice...thanks for the additional trouble!
That is already a long list. While you could still continue to use the functional programming approach, in such scenarios it will get tougher as the game evolves and new features get added.
Thankfully, object-oriented programming comes to the rescue. How about making Sir Foo an instance of a Knight
class
? With this, it should be easy to manage parameters relevant to Sir Foo. For example, an attribute, hitpoints
, can be used to keep track of Sir Foo's health instead of using the health_meter
dictionary in the earlier example. Similarly, the other attributes in the class can keep track of the amount of gold or weapons collected while acquiring the huts (another requested feature).
There is a lot more beyond this bookkeeping. The various methods of the class would enable a specific implementation of behaviors, such as attack, run, heal, and so on. The horse riders accompanying Sir Foo can also be instances of the class Knight
. Alternatively, you can create a new class called HorseRider
for all these units that accept commands from Sir Foo.
Prioritize the feature requests
For this new version, let's hand pick a few requested features from the earlier list. In fact, Sir Foo should be the one who makes this call:
It is now time to clearly define the targets for this release. You are not just adding new features to your application, but also making some fundamental changes to the code to accommodate future requests.
In this version, the mission is to acquire all of the five huts. Here, you will implement a new heal
feature to regain all the hit points for Sir Foo. You will also implement some strategic controls, such as running away from combat, getting healed in a friendly hut, and then returning rejuvenated to defeat the enemy.
We already discussed how creating a Knight
class will help simplify the handling of data and all other things related to Sir Foo, be it the hit points or the way he attacks enemies.
What other classes can be carved out? How about having the enemy as an object? The enemy could occupy multiple huts. Remember that we need to defeat all the enemies. Imagine the following scenario: Sir Foo injures an enemy in hut number 2, thereby reducing its hit points. Then, he moves on to another hut occupied by another enemy. Now, we need to maintain two separate hit point counters for each of these enemy units.
In a future version, you can expect users to ask for different enemy types with the ability to attack or heal, just like how we have it for Sir Foo. So, at this point, it makes sense to have a separate class, instances of which represent the enemy units. We will name this class OrcRider
. It will have similar attributes to the Knight
class. However, for simplicity, we will not give the enemy capabilities such as healing, changing huts, and so on.
There is something else we should consider. So far, huts
was just a simple Python list
object holding information about the occupant types as strings.
Looking at the requested features list, we also need bookkeeping for the amount of gold and armor in the hut and to update its occupant, depending on the result of the fight. In a future version, you may also want to show some statistics, such as a historic record of the occupants, changes in the amount of gold, and so on. For all this and more, we will create a class, Hut
.
Take a pen and paper and write down the important attributes we need for each class discussed so far. At this point, do not worry about classifying whether it is an instance variable or a class method that encapsulates instructions to perform specific tasks. Just write down what you think belongs to each class.
The following schematic shows a list of potential attributes for the Knight
, Hut
, and OrcRider
classes. The attribute names in strikethrough text indicate the potential attributes that won't be implemented in this illustration. But, it is always good to think ahead and keep it at the back of your mind during the design phase of the application:
This is not a complete specification, but we have a good starting point now. When Sir Foo enters an enemy hut, we have a choice to call the attack
method of the Knight
class. As before, the attack
method will randomly pick who gets injured and deduct the hit points for that character. In the Knight
class, it is convenient to have a new attribute, enemy,
that will represent the active opponent. In this example, enemy
will be an instance of the OrcRider
class.
Let's develop this design further. Did you notice that the Knight
and OrcRider
classes have several things in common? We will use the inheritance principle to create a superclass for these classes, and call it GameUnit
. We will move the common code to the superclass, and let the subclasses override the things they want to implement differently. In the next section, we will represent these classes with a Unified Modeling Language (UML)-like diagram.
Pseudo UML representation
The following diagram will help develop a basic understanding of how the various components talk to each other:
The preceding diagram is similar to a UML representation. It helps create a visual representation of a software design. In this book, we will loosely follow the UML representations. Let's call the diagrams used here pseudo UML diagrams (or UML-like diagrams).
Understanding the pseudo UML diagram
An explanation is in order for the UML-like convention used here. We will represent each class in the schematics as a rounded rectangle. It shows the class name followed by its attributes. The plus sign (+) before the attribute indicates that it is public. A protected or private method is generally represented with a negative sign (-). All the attributes shown in this diagram are public attributes. So, optionally, you could add a plus sign next to each attribute. In later chapters, we will follow this convention. For ease of illustration, only a few relevant public attributes will be listed. Observe that we are using different types of connectors in this diagram:
- The arrowhead with an empty triangle symbol represents inheritance; for example, the
Knight
class inherits from the GameUnit
class - The arrowhead with a filled diamond symbol represents object composition, for example, a
Hut
instance has an object of the GameUnit
class (or its subclasses) - The arrowhead with an empty diamond symbol represents object aggregation
Now, let's talk about the individual components of the diagram presented earlier.
The Knight
and OrcRider
classes inherit from GameUnit
. The Knight
class, in this case, will override default methods, such as attack
, heal
, and run_away
. The OrcRider
class will not have such overridden methods, as we will not give these capabilities to the enemy.
The Hut
class will have an occupant. The occupant can either be an instance of the Knight
or the OrcRider
, or the None
type if the hut is unoccupied. The filled diamond connector in the diagram indicates composition.
Tip
Object composition
It is an important OOP principle. It implies a has-a relationship. In this case, Hut
contains, or is composed of, some other object that is to be used to perform specific tasks. Just say it out loud; a Hut
has-a Knight
, a Hut
has-an OrcRider
, and so on.
In addition to the four classes discussed, we will introduce another one to encapsulate the top-level code. Let's call it AttackOfTheOrcs
. As there are five huts, a class method in AttackOfTheOrcs
creates that number of Hut
instances. This is object aggregation, shown by the empty diamond shaped arrow in the preceding diagram.
Have you noticed another has-a relationship in AttackOfTheOrcs
? The player
attribute in this class is an instance of the Knight
class, but in the future, this could change. This relationship is indicated by the filled diamond-head connector joining the Knight
and AttackOfTheOrcs
boxes.
With this high-level understanding, let's begin developing the code. Download the Python source file, ch01_ex03.py
. We will review only a few important methods in the code. Refer to this source file for the complete code.
Tip
The code for this example, ch01_ex03.py
, is all squished inside a single file. Is it good practice? Certainly not! As we go along, you will learn about best practices. Later in the book, we will discuss some important building blocks of application development, namely refactoring, coding standards, and design patterns. As an exercise, try to split the code into smaller modules and add code documentation.
The main execution code is shown here, along with some details of the AttackOfTheOrcs
class. In the __init__
method, we will initialize some instance variables and later update the values they hold. For example, self.player
represents the instance of the Knight
class when the game begins:
Tip
Just as a refresher, the __init__
method is somewhat similar to a constructor in languages such as C++; however, keep in mind some differences. For example, you cannot overload __init__
as you might do in these languages. Instead, you can easily accomplish this using optional arguments or the classmethod
decorator. We will cover some aspects later in the book.
Let's quickly review the play
and _occupy_huts
methods:
The self.player
is an instance of the Knight
class. We will call the acquire_hut
method of this instance where most of the high-level action happens. After this, the program simply looks for the health parameters of the player and the enemy. It also queries the Hut
instance to see if it is acquired.
Moving ahead, in the _occupy_hut
method, the objects of Hut
are created and appended to the self.huts
list. This method is shown in the following figure:
Note
Public, protected, and private in Python
You will notice that some methods of the AttackOfTheOrcs
class start with an underscore, for example, _process_user_choice()
. That is a way to say that this method is not meant for public use. It is intended to be used from within the class. Languages such as C++ define class access specifiers, namely, private
, protected
, and public
. These are used to put restrictions on the access of class attributes.
There is no such thing in Python. It allows outside access to the attributes with a single underscore as game._process_user_choice()
. If the attribute name starts with double underscores, you can't call it directly. For example, you can't directly call game.__process_user_choice()
. That being said, there is another way to access such attributes from outside. But let's not talk about it. Although Python allows you to access such attributes, it is is not good practice to do so!
Observe the acquire_hut
method of the Knight class:
Let's talk through this method next:
- First, we need to check whether the hut's occupant is a friend or an enemy. This is determined by the variable
is_enemy
, as shown in the preceding figure. - The hut's occupant can be of the following types: an instance of the
Knight
class, an instance of the OrcRider
class, or set to None
. - The
GameUnit
class, and its subclasses Knight
and OrcRider
, define a unit_type
attribute. This is just a string that is set as either 'friend'
or 'enemy'
. - Thus, to determine whether there is an enemy hiding in the hut, we will first check whether the
hut.occupant
is an instance of the superclass GameUnit
. If true, we will know it has a unit_type
parameter. So, we will check whether hut.occupant.unit_type
is equal to 'enemy'
. For the OrcRider
class, unit_type
is set to 'enemy'
by default. - The rest of the logic is simple. If the occupant is an enemy, it asks the user what to do next: attack or run away.
- The
Knight.attack
method is similar to the one discussed earlier. One change here is that we can access the health_meter
attribute of the injured unit and update it. - If
hut.occupant
happens to be 'friend'
or None
, it calls hut.acquire()
.
What happens when the Hut.acquire()
method is called? Here is the code snippet for the Hut
class:
The acquire
method simply updates the occupant
attribute with the object passed as an argument to this method.
Running Attack of the Orcs v1.0.0
It's play time! We have reviewed the most important methods of the new classes. You can review the rest of the code from the ch01_ex03.py
file, or better try to write these methods on your own. Run the application from the command line, like we did earlier. The following screenshot shows the game in action: