Chapter 18
Keep Going

Image

You now have a functioning (albeit limited) text-based adventure game. Using libraries and functions, lists and dictionaries, and classes, you have everything you need to really build a complete and comprehensive game. In this chapter, we’ll give you some ideas about where to go next and pointers to help you get there.

Health and Lives

Many games track player lives. When players die, they lose a life and continue playing. The game is over when no lives are left. Our player class has properties and methods for using lives, and you can use them in your game.

Newer games also track player health and often combine doing so with tracking lives. How could you do this in your game?

For starters, you’d need to update the player class with a couple of properties. Here’s what our updated class looks like:

    # Properties
    name = "Adventurer"
    livesLeft = 3
    boulderVisits = 0
    maxHealth = 100
    health = maxHealth

You’ll see that two properties have been added: maxHealth and health.

maxHealth stores the maximum amount of health a player can have at any time. We’ll need this value for all sorts of calculations, and rather than hard code the value all over the place, we do so once, in a property that other code can refer to. This way, if we needed to change the value, we’d do so once, in one place only. Here maxHealth is 100, but you can change it to whatever value your game needs.

health stores the current player health level. When the game starts, the player needs to be at full health, so the health variable is initialized to maxHealth. Of course, if your game works differently—maybe the player starts at half health and has to locate items to increase health—you change the initialization as needed.

Now we need methods to work with lives and health. We’ll start with methods to add and remove lives.

If your game allows a player to add lives (maybe by finding an item, using a potion, buying a heart, etc.), you’ll need a way to update the player system with this information. This addLives() method gets added to the player class:

    # Add lives
    def addLife(self, lives=1):
        # Increment lives
        self.livesLeft += lives
        # And fill up health
        self.health = self.maxHealth

When executed, the addLife() function does two things. The first line increases the value of the livesLeft property, effectively giving the player another life. Many games also restore player health when a life is added, and the second line in the function restores player health back to maxHealth. If you don’t want this to happen, just remove that second line.

As this function is a method in a class, the first argument is always self (as we discussed in Chapter 16).

But look at that second argument. Does it look different to you? What does lives=1 mean? That =1 provides a default value for argument lives (which ­actually makes the lives argument optional).

In our game we only need to add a single life at a time, so we could have simply written addLife() like this:

    def addLife(self):
        self.livesLeft += 1
        self.health = self.maxHealth

Call addLife(), and it increments livesLeft by 1 (that’s what += 1 does). Simple.

But what if at some point in the future we needed the code to allow users to add multiple lives at once? We’d need another function that accepted the number of lives to add as an argument. Something like this:

    def addLives(self, lives):
        self.livesLeft += lives
        self.health = self.maxHealth

True, we may not need this functionality now, but at some point we may, and then we’d have two very similar functions.

Anticipating possible future needs, we created a single addLife() function that supports both use cases. How does it do this? By accepting an optional ­argument—one that has a default value that is used if no explicit argument value is passed. So, if your code called the method like this:

p.addLife()

without passing a value for lives, then Python would use the default value of 1 for that argument value. But if the code did this:

p.addLife(3)

then the passed value of 3 would be used.

We may never need the ability to add multiple lives. But, with barely any extra code, we can support that use case, and we’ve added no complexity to how the function gets called either (p.addLife() works perfectly). So, why not?

Now we need a method to remove a life:

    # Lose lives
    def loseLife(self, lives=1):
        # Decrement lives
        self.livesLeft -= lives
        # Make sure didn't go below 0
        if self.livesLeft < 0:
            # It did, so set to 0
            self.livesLeft = 0
        # If no lives
        if self.livesLeft == 0:
            # No health either
            self.health = 0
        # If lives left
        elif self.livesLeft >= 1:
            # Reset health to full
            self.health = self.maxHealth

loseLife() looks more complicated, but it really isn’t.

Like the addLife() function, it accepts an optional number of lives, and it defaults to 1.

The first thing it does is decrement the livesLeft property. But, unlike when we add lives, when we subtract lives, we’ll want to make sure we didn’t inadvertently go below 0. This code addresses that:

        # Make sure didn't go below 0
        if self.livesLeft < 0:
            # It did, so set to 0
            self.livesLeft = 0

Now if livesLeft goes below 0, it gets set back to 0. Now users can’t have negative lives! Phew!

The rest of the code sets health as needed. Each time the player starts using a new life, health is reset back to maxHealth. But if there are no lives left, then we set health to 0.

We can now add and delete lives as needed. Next, we need to do the same for health. Let’s start with a method to get the current health value:

    # Get health value
    def getHealth(self):
        return self.health

getHealth() is a simple function: It just returns the current value of the health property.

So, on to adding and losing health. Here are the two new methods:

    # Add health
    def addHealth(self, health):
        self.health += health
        # Make sure not over maxHealth
        if self.health > self.maxHealth:
            # Went too high, reset to max
            self.health=self.maxHealth

    # Lose health
    def loseHealth(self, health):
        self.health -= health
        # Make sure not < 0
        if self.health < 0:
            # Lose a life
            self.loseLife()

addHealth() accepts an argument that tells the function how much health to add. It then uses self.health += health to add the passed health amount to the health property. An if statement then makes sure that health isn’t larger than maxHealth (which could happen if health were at 75 and the user did some action that added 50, for example). If health is greater than maxHealth, it gets reset to maxHealth.

loseHealth() does the opposite: It subtracts from the health property. If doing so makes health go below 0, then the function calls loseLife() (which, as you saw previously, handles decrementing livesLeft and sets health as needed).

With these properties and methods, you have everything you need to support lives and health in your game. So what should you do next?

  • Modify your game code to give players ways to add and lose lives.

  • You also need ways to use and gain health.

  • You previously added a way to display inventory contents, and you’ll probably want to do the same to display life and health status.

  • If you really want to up the sophistication level, you can display a health meter. You’ll want to write a function that accepts a meter maximum and the current level—the two things you’ll need in order to display a progress meter. You could even color the meter: green if the current level is 50% or more of the meter maximum, yellow if it is between 25% and 50%, and red if the value is less than 25%.

Shopping for Items

Buying items is a popular and common game feature. Some items may be required (meaning the game can’t be finished without having procured them), and some may be optional (they improve the game experience but are not required to complete the game). Allowing players to buy items is a good way to add variability to a game. By buying different items, players can impact game play.

We added support for coins in our inventory system. But we never added any way to do anything with the coins.

So, how could you support shopping in your code? There are a few things you’ll need to do:

  • Players need a way to obtain coins. You can perhaps give them coins when they solve a puzzle. Or enemies can drop coins when they are beaten. You can hide coins in various locations and add them to the player’s inventory when found. (If you do this, you need to decide what happens if the player revisits that location. Do they get more coins, or is it a one-time deal?)

  • You need to decide if the store is always available as an option or whether it’s in specific game locations.

  • You need a way to display a list of things available for purchase.

  • And, finally, when the player purchases an item, you need to remove those coins from the inventory and add the item they bought.

There is no single way to implement these tasks, but we’ll show you one way to create a list of items for purchase and how to display them to the player.

This is the code for Items.py:

############################################
# Items
# Items that may be purchased
############################################

items = [
    {
        "id":"health",
        "description":"Health restoration potion.",
        "key":"H",
        "cost":100
    },
    {
        "id":"blaster",
        "description":"Laser blaster.",
        "key":"B",
        "cost":250
    },
    {
        "id":"grenades",
        "description":"3 Space Grenades.",
        "key":"G",
        "cost":300
    },
    {
        "id":"shield",
        "description":"Shield which halves enemy damage.",
        "key":"S",
        "cost":500
    },
    {
        "id":"life",
        "description":"Additional life.",
        "key":"L",
        "cost":1000},
]

When creating a list of items that can be purchased you’ll want to think about how this information will be used. You’ll need to check that the player has enough money to buy an item, so you’ll need to access the cost. You’ll want a clear description of the item. And so on.

To this end, you create a list of dictionaries. Each item in a dictionary is one item that can be purchased. If you need to add more items to the game, you just add items to this dictionary. And then you write supporting functions. Here’s one example, which gets the items that are available for purchase:

# Get available items
# Return in format used by getUserChoice()
def getItems():
    # Variable for result
    result = []
    # Loop through items
    for item in items:
        # Create empty list
        i=[]
        # Add key
        i.append(item["key"])
        # Add description + cost
        i.append(item["description"]+" ("+str(item["cost"])+")")
        # Add this item to the result
        result.append(i)
    # Return it
    return result

getItems() is an interesting function. As you will recall from Chapter 14, the getUserChoice() function expects choices to be passed as a list of lists. Your list of items to be purchased is a list of dictionaries. So, getItems() loops through the items and creates the list of lists that getUserChoice() expects. How does it do this?

The function starts by creating an empty list to store the result, like this:

result = []

It then uses a for loop to loop through the items. Each time it loops, it creates another temporary list variable to store this particular item, like this:

# Create empty list
i=[]

It then needs to add two items into this list. The first is the menu letter (what the player enters to select a menu option), and the second is the menu text. To add the first, we do this:

# Add key
i.append(item["key"])

The append() function adds an item to a list, and here it adds the value key for the current item.

The menu text is comprised of the description and the cost, like this:

# Add description + cost
i.append(item["description"]+" ("+str(item["cost"])+")")

Again, append() adds this to the temporary list. When done, the list (for the first item) will look like ['H', 'Health restoration potion. (100)'], which is exactly what we need.

This item is then added to the result:

# Add this item to the result
result.append(i)

This continues for each item, and then the completed result is returned.

Now the list of items can be displayed using the existing menu function, perhaps like this:

# Display shopping list menu
choice=Utils.getUserChoice(Items.getItems())

Clever, huh?

If you plan to implement items and shopping, here are some things to consider:

  • Give players ways to obtain coins. (We’ll show you one idea next.)

  • Decide how the player displays the store and items.

  • Possibly put shopping in a loop so that the player can keep buying things until they are done.

  • You may want to limit the shopping to show only items the player can afford. You could modify getItems() to accept the number of coins the player has, and then, when you loop to build the list of items, have an if statement that checks the cost and only append ones the player can afford.

  • Some items purchased will need to be added to the inventory. If the player buys health or lives, you call the appropriate player() methods instead.

Random Events

You may want your game to be linear, meaning that things happen in a specific order every time the game is played. Or you may want to introduce variability into the game, so that each time it is played, things are a little different. There is no right or wrong way to implement game play. You, as the coder, get to decide what your game does.

If you do want to introduce variability, you can employ random events. You know how to use the random library; we did that together way back in Chapter 3. You can use random anywhere you want to introduce a random event. For example, code like this would find 100 coins on the ground and would add them to the inventory:

# 100 coins show up 1 in 4 times
if random.randrange(1, 5) == 1:
    # Tell player
    print("You see 100 coins on the ground.")
    print("You pick them up.")
    # Add to inventory
    inv.takeCoins(100)

This code would find coins about one in four times. How? Because the random.randrange(1, 5) returns a random number from 1 to 4 (the 5 is not included, as you will recall). This means that approximately a quarter of the time the code will return 1, a quarter of the time it’ll return 2, and so on. The if statement checks to see if randrange() returned 1, which it does about one in four times, and if yes, the coins are found and picked up. So, a quarter of the time that this function runs, the user will get coins. Want it to be one in three times? Just change the range to be 1, 4.

If you do this often enough, you might want to write a function to determine if a random event should occur or not. Look at this function (which could be added to Utils.py):

# Should a random event occur?
# Pass it a frequency, 2=1 out 2, 3=1 out of 3, etc.)
def randomEvent(freq):
    return True if random.randrange(0, freq)==0 else False

randomEvent() accepts a single argument, which tells the function how often the event should occur. Pass it 4, and it’ll return True one in four times and False the other three times. Pass it 2, and half the time it’ll return True and half the time it’ll return False. The logic is the same as in the previous example, but this time it is put into a user-defined function.

By using this function you don’t really have less code than you’d have by using randrange() right inside your app. But using a user-defined function like this is still a good idea. For starters, it keeps your code cleaner. In addition, it allows you to easily change how randomness works; just change one function rather than lots of individual pieces of code.

So how would you use the function? Like this:

# 100 coins show up 1 in 4 times
if Utils.randomEvent(4):
    # Tell player
    print("You see 100 coins on the ground.")
    print("You pick them up.")
    # Add to inventory
    inv.takeCoins(100)

As you can see, the result is the same, but the code is a bit cleaner. Utils.randomEvent(4) returns True approximately one in four times, and when that happens, the code under if is executed, and the player gets the coins.

And you can add even more randomness if you’d like. Look at this example:

# 1-100 coins show up 1 in 4 times
if Utils.randomEvent(4):
    # Pick a random number of coins
    coins=random.randrange(1, 101)
    # Tell player
    print("You see", coins, "coins on the ground.")
    print("You pick them up.")
    # Add to inventory
    inv.takeCoins(coins)

Now there is a one in four chance of finding a random number of coins (between 1 and 100). Randomness is a great way to make your game more interesting.

Battling Enemies

Battling enemies is another common game feature. Unfortunately, this one is a bit trickier to implement. That said, we’ll give you some pointers to get you started.

Like items, enemies have properties. So you’ll want an Enemies.py file, perhaps something like this one:

############################################
# Enemies
# Defines enemies, supporting functions
############################################

# List of enemies
# Each needs a short name, a description,
# strength (higher number = need to do more damage to kill),
# and defense (lower number = easier to hit)
enemies = [
    {
        "id":"slug",
        "description":"Space slug",
        "strength":10,
        "damageMin":1,
        "damageMax":3,
        "defense":2
    },
    {
        "id":"eel",
        "description":"Radioactive eel",
        "strength":50,
        "damageMin":10,
        "damageMax":15,
        "defense":1
    },
    {
        "id":"alien",
        "description":"Green tentacled alien",
        "strength":25,
        "damageMin":5,
        "damageMax":10,
        "defense":3
    }
]

This code defines and organizes enemies. Each has an id and a description, and then there is data that describes the enemy’s abilities and behavior. strength is how much damage the player must inflict to kill the enemy. damageMin and damageMax are the range of values for the amount of damage the enemy can inflict on the player. (We wanted a range so that each time the enemy attacks, the actual damage inflicted will be random using that range, but you could easily change it to a fixed damage value.) defense is how well the enemy can avoid attacks from the player; "defense":3 means that there is a one-in-three chance of the enemy dodging an attack.

You get the idea. You don’t have to use these specific settings, and you can make any setting fixed or a range, as needed. The key is that all enemy data needs to be cleanly defined and organized.

Your code could then get a specific enemy by using the id key. Or if you wanted a random enemy, you could do something like this:

# Get a random enemy
def getRandomEnemy():
    # Return a random enemy
    return random.choice(enemies)

That said, that’s the easy part. The trickier part is the actual battle mechanics. There are lots of ways to do this, and a popular one is turn-based battling, kinda like Pokémon battles. To do this:

  • One side goes first (you could randomize that if you’d like), and then each side takes turns launching an attack.

  • You could allow users to pick which weapon to attack with, based on what they have in their inventory. Different weapons would inflict different amounts of damage, and some may be single-use weapons (use it and then it’s gone from the inventory).

  • You may want to allow the player to use potions or somehow improve their health mid-battle.

  • Based on weapons used, range of damage inflicted, and ability to dodge, eventually one side will win. If the enemy strength is reduced to 0, then the player wins. If the player runs out of health (or lives), then the enemy wins.

  • What happens when the battle is over? That’s up to you. Beating the enemy may be part of the player’s journey that allows the user to progress (­basically walking past the enemy). Or the enemy may drop items for the user to pick up and put in the inventory. Or, well, you’re the coder, so you decide!

As we said, this one is tricky. Not all games need battling enemies. If yours does, you’ll want to spend time carefully planning.

Saving and Restoring

Some games let players save their progress and then restore it later. You might do this when the game is too long for players to complete all at once. Or players may want to save their progress before trying something new (like battling an enemy) so that if they die, they can restore back to where they were.

So long as your game data is well organized (in dictionaries and classes, for example, as opposed to scattered in variables all over the place), then you’ll find that Python makes this quite easy. And the magic is a Python library named pickle.

How does saving and restoring work? To save your game, you need to do the following:

  1. Create a single variable that contains all of the data you need to save.

  2. Data gets saved in a file on the computer, so pick a file name to use.

  3. Use pickle to serialize the data and save it to the file.

Restoring your game is the reverse:

  1. Read data from the saved file.

  2. Deserialize it with pickle.

  3. Save the data back to the right variables.

Image New Term

Serializing In Python, serializing data means taking an internal Python object (variables, lists, classes, dictionaries, etc.) and turning it into a string of bytes that can be stored. Deserializing is the opposite: turning a string of bytes back into a Python object.

Ok, so let’s see how this works. Look at this code:

# Imports
from os import path
import pickle

# Data file
saveDataFile = "savedGame.p"

The first import is the library used to access files on your computer. You need that to actually save and read the saved file. The second line imports the pickle library. Then you create a variable named saveDataFile, which contains the name of the file that will actually hold the saved game data.

As for saving data, look at this code:

# Create a data object to store both data sets.
db = {
    "inv":inv,
    "player":player
}

# Save it
pickle.dump(db, open(saveDataFile, "wb"))

When you save game data, you want all of the data in one big variable. Here we create a dictionary named db (for database), and inside it we save our inv and player variables (assuming that these are the names we are using).

The next line of code actually saves the data. pickle.dump() gets passed the data to save (the db variable) and the open file to save it to. The "wb" argument tells dump() to write binary data (as opposed to simple text data). That’s all there is to it. Game data saved!

To restore game data, we just do the opposite:

# Now read back saved file
if path.isfile(saveDataFile):
    db = pickle.load(open(saveDataFile, "rb"))
    inv = db["inv"]
    player = db["player"]

This code first uses path.isfile() to verify that the saved game file exists. If it does, it uses pickle.load() to read and deserialize the data and then puts the restored data back in the original inv and player variables. Here we use an "rb" argument which tells dump() to read binary data.

As you can see, saving and restoring data in Python is really easy. We mean that. We’ve done this in other programming languages, and most can’t do this in just a few lines of code.

If you do want to support save and restore, here are some things to keep in mind:

  • What we did here—copying all objects to be saved into a single save ­variable—is the cleanest and easiest way to save data. You don’t want to be saving and restoring lots of variables. Trust us on this one.

  • The variable you save should contain everything you need to restore the game to a specific state.

  • Decide if you want a single save file or multiple files. A single file is simpler to work with but will only let you restore to a specific point in game play. Multiple save files will let players restore as they need, but you’ll need a way to let them pick which to restore.

  • You may want to check to see if there are any saved games at game startup. If you detect a saved game, ask the user if they want to restore it.

Summary

In this chapter, we’ve introduced lots of ideas and techniques that you can use to really take your text-based adventure game to the next level. In Part III, we’ll use these skills as we tackle graphics-based games.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset