Using functions – Attack of the Orcs v0.0.5
In the last section, you wrote a quick set of instructions to create a nice little command-line game. You asked your friends to try it out and they kind of liked it (perhaps they were just trying to be nice!). You received the first feature request for the game.
"I think this game has good potential to grow. How about including combat in the next version of the game? When Sir Foo encounters an enemy, he should not just give up that easily. Fight with the enemy! Let the combat decide the winner. "-your friend |
You liked the idea and decided to add this capability to the code in the next version. Additionally, you also want to make it more interactive.
The script you wrote for the first program was small. However, as we go on adding new features, it will soon become a maintenance headache. As a step further, we will wrap the existing code into small functions so that the code is easier to manage. In functional programming, the focus is typically on function arrangement and their composition. For example, you can build complicated logic using a simple set of reusable functions.
Revisiting the previous version
Before adding any new features, let's revisit the script that you wrote in the previous version (version 0.0.1). We will identify the blocks of code that can be wrapped into functions. Such code chunks are marked in the two code snippets that follow:
We will wrap most of the highlighted code into individual functions, as follows:
1: show_theme_message 2: show_game_mission 3: occupy_huts 4: process_user_choice 5: reveal_occupants 6: enter_hut
In addition to these six blocks of code, we can also create a few top-level functions to handle all this logic. In Python, the function is created using the def
keyword, followed by the function name and arguments in parentheses. For example, the reveal_occupants
function requires the information about the huts
list. We also need to optionally pass the dotted_line
string if we do not want to recreate it in the function. So, we will pass the hut number idx
, the huts
list, and the dotted_line
string as function arguments. This function can be written as follows:
After this initial work, the original script can be rewritten as:
This is much easier to read now. What we just did is also referred to as refactoring; more on various refactoring techniques in a later chapter. It makes it easier to do changes to the individual methods. For example, if you want to customize the mission statement or scenario description, you do not need to open the main function, run_application
. Similarly, occupy_huts
can be expanded further without any clutter in the main code.
Tip
The initial refactored version of the code is not perfect. There is plenty of room for improvement. Can you reduce the burden of passing the dotted_line
parameter or think of some other way to handle the printing of bold text?
Pseudo code with attack feature – Version 0.0.5
In the previous section, we wrapped the game logic into individual functions. This not only improved the code readability, but also made it easier to maintain. Let's move on and include the new attack()
function in the game. The following steps show the logic of the game with the attack feature included.
While the user wishes to keep playing the game:
- Print game mission
- Create a
huts
list - Randomly place
'enemy'
,'friend',
or'unoccupied'
in 5 huts - Prompt the player to select a hut number
if
the hut has an enemy, do the following:while
the user wishes to continue the attack, use theattack()
method on the enemyAfter each attack, update and show the health of Sir Foo, and of the enemy too;
if enemy health <= 0:
print"You Win".
But,
if Sir Foo health <= 0:
print"You Lose".
else
(hut has a friend or is unoccupied) print"you win"
Initially, Sir Foo and the Orc will have full health. To quantify health, let's assign hit points to each of these characters (or the game units). So, when we say the character has full health, it means it has the maximum possible hit points. Depending on the character, the default number of hit points will vary. The following image shows Sir Foo and the Orc with the default number of hit points, indicated by the Health label:
The bar above the Health label in the image represents a health meter. Essentially, it keeps track of the hit points. In the discussion that follows, we will use the terms hit points and health meter interchangeably. During the combat, either the player or the enemy will get injured. For now, neglect the third possibility where both escape unhurt. An injury will reduce the number of available hit points for the injured unit. In the game, we will assume that in a single attack turn only one of the characters is hit. The following image will help you imagine one such attack turn:
Here, Sir Foo's health meter is shown as the maximum and the Orc has sustained injuries!
Hmm, the Orc thinks he can defeat Sir Foo! This is interesting. Let's develop the game first and then see who has a better chance of winning! |
With this understanding of the problem, let's review the code that implements this feature.
Reviewing the code
Download the source file, ch01_ex02.py
, from the chapter's code bundle and skim through the code. The key logic will be in the attack()
function. We will also need a data structure to keep the health record of Sir Foo and the enemy. Let's start by introducing the following utility functions that take care of some print business:
Now, look at the main function, run_application
, and the supporting function, reset_health_meter
. In addition to introducing the dictionary health_meter
, we have also encapsulated the game logic in play_game
:
At the start of a new game, the values of the health_meter
dictionary are set back to the initial ones by calling reset_health_meter
:
Next, let's review the play_game
function. If the hut has the enemy, the player will be asked if the attack should be continued (the start of the while
loop). Based on the user input, the code calls the attack
function or exits the current game:
The enemy is attacked repetitively using the interactive while
loop, which accepts user input. Execution of the attack
function may result in injury to Sir Foo, or the enemy, or both. It is also possible that no one gets hurt. For simplicity, we will only consider two possibilities: a single attack that will injure either the enemy or Sir Foo. In the previous section, we used the built-in random number generator to randomly determine the occupants of the huts. We can use the same technique to determine who gets hurt:
injured_unit = random.choice(['player', 'enemy'])
But hold on a minute. Sir Foo has something to say: |
We should take into account the chance of an injury to the player and to the enemy. In the attack
function shown next, we will assume that for about 60% of the time, the enemy will get hit and for the remaining 40%, it is Sir Foo who is on the receiving end.
The simplest way is to create a list with 10 elements. This list should have six entries of 'enemy'
and four entries of 'player'
. Then, let random.choice
select an element from this list. You can always introduce a difficulty level in the game and change this distribution:
Once the injured_unit
is selected randomly, the injury
is determined by picking a random number between 10
and 15
, inclusive. Here, we use the random.randint
function. The final important step is to update the health_meter
dictionary for the injured unit by reducing its number of hit points.
Running Attack of the Orcs v0.0.5
We have discussed the most important functions in this game. Review the other supporting functions from the downloaded file. The following screenshot shows the game in action: