Dragon Realm#

Based on: http://inventwithpython.com/invent4thed/chapter5.html

Table of Contents

Introduction#

The game you will create in this chapter is called Dragon Realm. The player decides between two caves which hold either treasure or certain doom.

Part 1: A Script Template: Shebang, Docstring, Scope#

We’re going to start with a bare bones script that will serve as a template for all future scripts.

Follow the instructions in Repl.it Tips to create a new file called dragon_realm.py and change your .replit file to run it. Copy and paste the following code into it.

code
Listing 445 dragon_realm.py#
 1#!/usr/bin/env python3
 2# -*- coding: utf-8 -*-
 3
 4"""
 5Dragon Realm - A game where the player decides between two caves, which hold
 6  either treasure or certain doom.
 7  Inspired by: http://inventwithpython.com/invent4thed/chapter5.html
 8"""
 9
10
11def main():
12    """The Dragon Realm Game"""
13    print("Welcome to Dragon Realm!")
14
15
16# this means that if this script is executed, then
17# the main() function will be called
18if __name__ == '__main__':
19    main()

Part 1.1: Shebang, Encoding#

The very first line of any executable file (script) is the shebang line. The line starts with a #! then is immediately followed (without a space) by the path to the interpreter. In this case it is telling the computer to run this script using python3.

The next line tells Python (as well as some editors) what the encoding to expect. That is, what kinds of characters. This line might be different if, for example, we were going to include Chinese characters.

Listing 446 dragon_realm.py#
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-

Part 1.2: Docstrings#

The first expression in a Python script should always be a Docstring. A Docstring, surrounded by """ or ''', is similar to a comment in that its contents will not be executed. Docstrings however, are stored by the interpreter as documentation for a particular file, module, class, or function.

Listing 447 dragon_realm.py#
 4"""
 5Dragon Realm - A game where the player decides between two caves, which hold
 6  either treasure or certain doom.
 7  Inspired by: http://inventwithpython.com/invent4thed/chapter5.html
 8"""
 9
10

Part 1.3: Scope, __main__ and main()#

Up until now we have been writing all our code in the body of the file. (Aside from a few functions in the PyPet project.) This is what is referred to as the global scope or global namespace.

Scope refers to the place where an identifier (variable or function) can be used. When a variable is defined in the body of the file it is available to everything in the file–globally. When a variable is defined in a function it is only available to the code inside of that function.

It is a good idea to keep the amount of code in the global scope to a minimum. This avoids problems like accidentally reusing the same variable name and causing unintended results.

In order to achieve this, organize code into functions. The convention is to write a function called main() and call it when your script is executed.

Note that main() has a docstring too. This will describe the purpose of the function.

Listing 448 dragon_realm.py#
11def main():
12    """The Dragon Realm Game"""
13    print("Welcome to Dragon Realm!")
14
15
16# this means that if this script is executed, then
17# the main() function will be called
18if __name__ == '__main__':
19    main()

Part 2: Beginning and end#

In this section we’ll add a description of the imaginary game world that will be printed when the player first starts the game.

At the end of the game, we’ll ask the player if they want to continue instead of just exiting.

Part 2.1: Global Variables#

Global variables are called that because they are available to everything in the file. Whereas a variable that is defined inside a function is called a local variable, and it goes away when the function is done executing.

A. Conventions#

You generally want to define them at the top of the file, and name them with ALL_CAPS to help you tell them apart from local variables.

MY_GLOBAL = True

B. Add WIDTH#

The WIDTH global variable is for the number of horizontal characters we want our game to take up.

  1. Add the global variable WIDTH under the docstring and set it to 58 as a starting point. (You can always change it later.)

Listing 449 dragon_realm.py#
 4"""
 5Dragon Realm - A game where the player decides between two caves, which hold
 6  either treasure or certain doom.
 7  Inspired by: http://inventwithpython.com/invent4thed/chapter5.html
 8"""
 9
10WIDTH = 58

Part 2.2: intro()#

In this section we’ll write the intro() function which will print out a paragraph to the player that describes the players surroundings.

A. Multi-line strings#

You can use the docstring syntax to make a string and use it anywhere you would normally use a string. This is useful if you have a multi-line string such as the paragraph we want to print in the intro() function.

This will retain all whitespace–both newlines and indentation.

print("""
  a
    b
      c
        d
"""
)
  a
    b
      c
        d

B. Newlines#

The backslash (\) in a string tells Python that the next character has special meaning. \n, for example, is for a new line

print("a\nb\nc\n")
a
b
c

C. Add intro()#

Let’s use the concepts we just learned to define the intro() function.

  1. Define the intro() function

  2. Add a docstring describing what the function does

  3. Using docstring syntax, print the intro description copied from below (or make up your own!)

Listing 450 dragon_realm.py: intro()#
13def intro():
14    """Display the introduction description to the player"""
15    print("""You are in a land full of dragons. In front of you,
16you see two caves. In one cave, the dragon is friendly
17and will share his treasure with you. The other dragon
18is greedy and hungry, and will eat you on sight.\n""")

2.3. Keep playing#

In this section we’ll ask the player if they want to keep playing when the game ends, instead of just exiting.

A. input()#

To make a program interactive, we need to get feedback from the user. To do this we call the input() function and whatever the user types will be returned.

The syntax is:

VAR = input(PROMPT)

In this example, whatever the user typed before hitting enter would be assigned to the variable response.

Listing 451 example#
response = input("Are you sure? ")

The user would see something like this:

Listing 452 prompt#
Are you sure? ▉

Be sure to always add the space at the end of the prompt string, otherwise there will be no space between the text the user sees and their cursor.

We’ll use the input() function to ask the player if they want to keep playing.

B. Objects and str.lower()#

In Python, all values are objects. An object is data that can have values and functions attached to it. An objects values are called attributes and its functions are called methods.

For example, list objects have a method .count() which will tell you how many of a certain thing the list contains.

votes = ["red", "blue", "red", "red", "blue", "blue"]
votes.count("blue")
3

If you want to know what methods or attributes an object has you can use the dir() function and pass it either an object or a type. (Just scroll past all the ones that start with __ for now.)

dir(str)
['__add__',
 '__class__',
 '__contains__',
 '__delattr__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__getnewargs__',
 '__gt__',
 '__hash__',
 '__init__',
 '__init_subclass__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__mod__',
 '__mul__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__rmod__',
 '__rmul__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'capitalize',
 'casefold',
 'center',
 'count',
 'encode',
 'endswith',
 'expandtabs',
 'find',
 'format',
 'format_map',
 'index',
 'isalnum',
 'isalpha',
 'isascii',
 'isdecimal',
 'isdigit',
 'isidentifier',
 'islower',
 'isnumeric',
 'isprintable',
 'isspace',
 'istitle',
 'isupper',
 'join',
 'ljust',
 'lower',
 'lstrip',
 'maketrans',
 'partition',
 'replace',
 'rfind',
 'rindex',
 'rjust',
 'rpartition',
 'rsplit',
 'rstrip',
 'split',
 'splitlines',
 'startswith',
 'strip',
 'swapcase',
 'title',
 'translate',
 'upper',
 'zfill']

You can use the help() function to get more information about a method.

You can use either the object (like "hello") or the type (like str) followed by a dot and the method name.

help(str.lower)
Help on method_descriptor:

lower(self, /)
    Return a copy of the string converted to lowercase.

You can see that str objects have a .lower() method. Let’s try it out.

"OH HAI THERE".lower()
'oh hai there'

We’ll use the .lower() function to change the player’s answer to lower case. That way we’ll understand if they type "YES", "Yes" or "yes".

B. Boolean and Membership Operators#

In programming, sometimes we don’t just want to see if something is the same as something else, but the same as a couple of things.

One way that we could do this would be using the or boolean operator.

answer = "Y"
answer.lower() == "y" or answer.lower() == "yes"
True

We’re going to learn to use the in operator to easily check if a value is a member of a sequence.

The syntax is:

<value> in <sequence>

For example:

answer = "Y"
answer.lower() in ["y", "yes"]
True

We’ll use the in operator to check the player’s answer against multiple acceptable answers.

C. Repeating strings#

Python provides an easy way to repeat a string using multiplication. Just use the * operator to multiply a str by an int.

"echo... " * 3
'echo... echo... echo... '

This is a handy way to quickly make a visual line that is whatever width you want.

"~" * 30
'~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~'

We’ll use this to print a line at the start of each game.

E. while loops#

Sometimes we want our program to repeat the same steps over and over again. One way to do this is to use a while loop which will repeat the same block of code as long as a certain condition is met.

The syntax is:

while CONDITION:
    BODY

CONDITION

an expression evaluated in a boolean context that determines if the loop keeps going

BODY

statements to execute each iteration

In this example if the user hits enter before typing anything, they’ll be asked again repeatedly until they do.

Listing 453 example#
answer = ""

while not answer:
  answer = input("Your choice: ")

Caution

Beware of infinite loops!

Infinite loops happen when the condition is always met. While not inherently bad, they can wreak havoc on your system when unintended.

This is a simple example of a while True infinite loop.

Listing 454 example#
import time

while True:
  print("this is the song that never ends...")
  time.sleep(1)

We’ll use a while loop in main() to let the user keep playing if they want to.

F. In main()#

  1. Change your docstring to reflect the way main() will work when we’re done

  2. Add a variable answer and set it to "yes"

  3. Add a while loop that will keep going as long as lower cased answer matches any of a list of the strings ["y", "yes"]

  4. In the while loop:

    • Print a line by multiplying a dash by WIDTH. Add a newline at the end

    • Call the intro() function

    • Ask the user if they want to play again using the input() function and assign the result to the variable answer

Listing 455 dragon_realm.py: main()#
21def main():
22    """Keep playing the game until the user doesn't say yes"""
23    print("Welcome to Dragon Realm!")
24    again = "yes"
25    while again.lower() in ["y", "yes"]:
26        print("-" * WIDTH, "\n")
27        intro()
28        again = input("Play again? ")

Part 3: Player, choose() a Cave#

In this section we will prompt the player to choose a cave, then make sure their response is a valid cave.

Add a global variable CAVES to the top of your script where WIDTH is defined.

Listing 456 dragon_realm.py: global variables#
1WIDTH = 58
2CAVES = ["right", "left"]

Add the valid_cave() function

Listing 457 dragon_realm.py: valid_cave()#
1def valid_cave(response):
2    """Return True if response is in the list of valid CAVES"""
3    return response in CAVES

Add the choose() function

Listing 458 dragon_realm.py: choose()#
 1def choose():
 2    """Prompt the player to choose "right" or "left" then return response."""
 3    cave = ""
 4    while not valid_cave(cave):
 5        print("Do you enter the cave on the right or left?")
 6        cave = input("(right, left): ").lower()
 7
 8        if cave in ["q", "quit", "exit"]:
 9            exit()
10
11        if not valid_cave(cave):
12            print('Type "right" or "left". \n')
13
14    print()
15    return cave

Edit your main() function to add cave = choose().

Listing 459 dragon_realm.py: main()#
1def main():
2    """Keep playing the game until the user doesn't say yes"""
3    print("Welcome to Dragon Realm!")
4    again = "yes"
5    while again.lower() in ["y", "yes"]:
6        print("-" * WIDTH, "\n")
7        intro()
8        cave = choose()
9        again = input("Play again? ")

Part 3.1: Conditionals Expressions Resolve to Boolean Values#

We have used conditional expressions in if-statements

if a == b:
  ...

And we have used conditional expressions in while-statements

while a < b:
  ...

A key think to understand is that a conditional expression always resolves to a Boolean value, either True or False.

>>> 2*2 == 4
True
>>> "5" == str(5)
True
>>> import random
>>> 5 < random.randint(0, 10)
False
>>> 57 in [ range(0, 10) ]
False

That means that we can treat a conditional expression as just another value. Which is why we can return the result of this conditional in the valid_cave() function.

return response in CAVES

Part 3.2: Method Chaining#

Since all values are objects in Python, all values may have methods. Method chaining is a way to take advantage of that to write less code.

In this case, since input() always returns a string, we can call lower() from the return result of input() by chaining them together with a dot.

    # these two lines of code...
    cave = input("(right, left): ")
    cave = cave.lower()

    # have the same result as this one
    cave = input("(right, left): ").lower()

Part 4: Enter the Cave#

Now that the player has picked a cave, it’s time to tell them what happens when they enter it. We’ll add a new enter() function and use the sleep() function in the time module to add a delay between messages.

Above your global variables, import the time module.

Listing 460 dragon_realm.py: imports#
1"""
2Dragon Realm - A game where the player decides between two caves, which hold
3  either treasure or certain doom.
4  Inspired by: http://inventwithpython.com/invent4thed/chapter5.html
5"""
6
7import time

Add a global variable DELAY

Listing 461 dragon_realm.py: global variables#
 1"""
 2Dragon Realm - A game where the player decides between two caves, which hold
 3  either treasure or certain doom.
 4  Inspired by: http://inventwithpython.com/invent4thed/chapter5.html
 5"""
 6
 7import time
 8
 9WIDTH = 58
10DELAY = 1
11CAVES = ["right", "left"]

Add the enter() function

Listing 462 dragon_realm.py: enter()#
 1def enter(cave):
 2    messages = [
 3        "You approach the cave...",
 4        "It is dark and spooky...",
 5        "A large dragon jumps out in front of you!",
 6        "He opens his jaws and...",
 7    ]
 8
 9    for message in messages:
10        print(message)
11        time.sleep(DELAY)

And change your main() function to call enter()

Listing 463 dragon_realm.py: main()#
 1def main():
 2    """Keep playing the game until the user doesn't say yes"""
 3    print("Welcome to Dragon Realm!")
 4    again = "yes"
 5    while again.lower() in ["y", "yes"]:
 6        print("-" * WIDTH, "\n")
 7        intro()
 8        cave = choose()
 9        enter(cave)
10        again = input("Play again? ")

Part 5: Prettier output with describe()#

It is getting a little hard to tell which lines of the game are description and which parts are prompts. Lets make that clearer by indenting the text. To do that we’re going to add a function describe() which we’ll use to print anything that is not related to getting input.

Add a describe() function

Listing 464 dragon_realm.py: describe()#
1def describe(message):
2    print("  ", message)

Then change your intro() function to call it instead of print().

Listing 465 dragon_realm.py: intro()#
1def intro():
2    """Display the introduction description to the player"""
3    describe("""You are in a land full of dragons. In front of you,
4you see two caves. In one cave, the dragon is friendly
5and will share his treasure with you. The other dragon
6is greedy and hungry, and will eat you on sight.\n""")

And call describe() in enter()

Listing 466 dragon_realm.py: enter()#
 1def enter(cave):
 2    messages = [
 3        "You approach the cave...",
 4        "It is dark and spooky...",
 5        "A large dragon jumps out in front of you!",
 6        "He opens his jaws and...",
 7    ]
 8
 9    for message in messages:
10        describe(message)
11        time.sleep(DELAY)

Part 6: Wrap text using the textwrap module#

That looks nicer, but the intro looks funky because only the first line is indented. Let’s fix that by using the textwrap module’s wrap() function. It takes two arguments, the string to wrap, and the width to wrap it to. It returns a list where each item in the list is one line of the string.

Import the textwrap module

Listing 467 dragon_realm.py: imports#
1"""
2Dragon Realm - A game where the player decides between two caves, which hold
3  either treasure or certain doom.
4  Inspired by: http://inventwithpython.com/invent4thed/chapter5.html
5"""
6
7import time
8import textwrap

Add a global variable WRAP

Listing 468 dragon_realm.py: global variables#
 1"""
 2Dragon Realm - A game where the player decides between two caves, which hold
 3  either treasure or certain doom.
 4  Inspired by: http://inventwithpython.com/invent4thed/chapter5.html
 5"""
 6
 7import time
 8import textwrap
 9
10WIDTH = 58
11WRAP = 50
12DELAY = 1
13CAVES = ["right", "left"]

Then change your describe() function

Listing 469 dragon_realm.py: describe()#
1def describe(message):
2    for line in textwrap.wrap(message, WRAP):
3        print("  ", line)

The wrap() function strips trailing newlines so we’ll need to change the intro() function. Remove the \n and add a print() statement to the end of the function.

Listing 470 dragon_realm.py: intro()#
1def intro():
2    """Display the introduction description to the player"""
3    describe("""You are in a land full of dragons. In front of you,
4you see two caves. In one cave, the dragon is friendly
5and will share his treasure with you. The other dragon
6is greedy and hungry, and will eat you on sight.""")
7    print()

Part 7: Pick the Friendly Dragon#

Next we need to randomly pick a dragon to be the friendly one.

Import the random module

Listing 471 dragon_realm.py: imports#
1"""
2Dragon Realm - A game where the player decides between two caves, which hold
3  either treasure or certain doom.
4  Inspired by: http://inventwithpython.com/invent4thed/chapter5.html
5"""
6
7import time
8import textwrap
9import random

Add a is_friendly() function

Listing 472 dragon_realm.py: is_friendly()#
1def is_friendly(dragon):
2    """Return True if dragon is in the randomly chosen friendly one"""
3    friendly = random.randint(0, 1)
4    print("The friendly dragon is:", CAVES[friendly])
5    return dragon == CAVES[friendly]

Add a line to the end of your enter() function to save the resulting value

Listing 473 dragon_realm.py: enter()#
 1def enter(cave):
 2    messages = [
 3        "You approach the cave...",
 4        "It is dark and spooky...",
 5        "A large dragon jumps out in front of you!",
 6        "He opens his jaws and...",
 7    ]
 8
 9    for message in messages:
10        describe(message)
11        time.sleep(DELAY)
12
13    nature = is_friendly(cave)

Part 7.1 Accessing List Elements#

You may recall that dictionaries have keys. Dictionary elements can be accessed by adding square brackets to the end of the variable name containing the key.

>>> car = {
  "brand": "Ford",
  "model": "Mustang",
  "year": 1964
}
>>> print(car["brand"])
Ford

List elements have an index number which always starts at 0 and increases for each element in the list. List elements can be accessed by their index number the same way that dictionary elements can. Since the index is always a number, don’t use quotes.

>>> brands = [ "Ford", "Chevrolet", "Honda" ]
>>> print(brands[1])
Chevrolet

List elements are in the order they are added in, unless changed by the programmer.

>>> letters = [ 3, 2, 1 ]
>>> print(letters[0])
3

>>> letters = [ "a", "b", "z", "c", "d" ]
>>> print(letters[2])
z

The CAVES list defined earlier contains the elements [ "right", "left" ], which means that the value of CAVES[0] is "right" and the value of CAVES[1] is "left".

Here we generate a random number between 0 and 1 to use as the index in the CAVES list, so CAVES[friendly] will be either "right" or "left".

Then we compare it to the value of dragon. dragon == CAVES[friendly] will resolve to either True or False. That is the value that the function returns.

    friendly = random.randint(0, 1)
    print("The friendly dragon is:", CAVES[friendly])
    return dragon == CAVES[friendly]

Part 8: The Dragon Acts#

Finally, we’ll tell the player what the dragon does.

Edit Your Script

Add a dragon() function

Listing 474 dragon_realm.py: dragon()#
 1def dragon(is_friendly):
 2    """Print the dragon action for a friendly or unfriendly dragon"""
 3    actions = {
 4        # friendlyness: action
 5        True: "Gives you his treasure!",
 6        False: "Gobbles you down in one bite!",
 7    }
 8    print()
 9    describe(actions[is_friendly])
10    print()

Add a line to the end of your enter() function to call it

Listing 475 dragon_realm.py: enter()#
 1def enter(cave):
 2    messages = [
 3        "You approach the cave...",
 4        "It is dark and spooky...",
 5        "A large dragon jumps out in front of you!",
 6        "He opens his jaws and...",
 7    ]
 8
 9    for message in messages:
10        describe(message)
11        time.sleep(DELAY)
12
13    nature = is_friendly(cave)
14    dragon(nature)

Finally, remove or comment out the print() line in your is_friendly() function

Listing 476 dragon_realm.py: is_friendly()#
1def is_friendly(dragon):
2    """Return True if dragon is in the randomly chosen friendly one"""
3    friendly = random.randint(0, 1)
4    #  print("The friendly dragon is:", CAVES[friendly])
5    return dragon == CAVES[friendly]

Part 8.1: Dictionary Keys#

In the past we’ve used strings for dictionary keys, but other types can be other keys too.

Ints and Floats can be keys.

>>> dewey = {
...   610: "Medicine & health",
...   610.3: "Medical encyclopedias",
...   610.6: "Medical organizations & professions",
...   610.72: "Medical research",
...   610.9: "Geography and history of medicine",
... }
>>>
>>> dewey[610]
'Medicine & health'
>>> dewey[610.6]
'Medical organizations & professions'

Booleans can be keys

>>> d = { True: "true", False: "false" }
>>> d[True]
'true'

However, True and False are equal to 1 and 0 respectively, so you can’t mix them.

>>> d = { True: "true", False: "false", 1: "one", 0: "zero" }
>>> d[True]
'one'
>>> d[False]
'zero'
>>> d[1]
'one'
>>> d[0]
'zero'

Here we’re using booleans as keys in the actions dictionary, then looking them up using actions[is_friendly].

    actions = {
        # friendlyness: action
        True: "Gives you his treasure!",
        False: "Gobbles you down in one bite!",
    }

We could have written the dictionary like this:

    actions = {
        "friendly": "Gives you his treasure!",
        "unfriendly": "Gobbles you down in one bite!",
    }

But then we would have had to add an if-statement based on the boolean value of the is_friendly variable.


Make it Your Own#

Change the game to make it your own. Here are some ideas.

  • Add a third cave in the middle with a silly dragon who does a little jig.

  • Make a dictionary for each dragon and give them other details like names, colors, sizes, if they breath fire. Print the additional values when you walk in the cave.

  • If the player gets treasure from the dragon, add another level to the game. Perhaps the player encounters a well and can either make a wish with coin, or take a drink. Randomly decide if the wish is granted, or if the water is poisoned.

  • Add a rarely occurring event that may sometimes take place instead of the usual friendly/unfriendly actions. Perhaps the dragon transforms into a toad, or it falls in love with you. Calculate it by using some datetime information (like, it only occurs on Friday the 13th or after midnight on odd numbered days) combined with some randomness.

What We’ve Learned#

  • The shebang

  • Docstrings as documentation

  • Scope, main(), and global variables

  • Docstrings as multi-line strings

  • Objects

  • str.lower(), time.sleep() and textwrap.wrap()

  • Repeating strings with *

  • Escape characters and \n

  • Method chaining

  • List indexes and more dictionary key types

  • Conditional expressions evaluate to boolean values

  • The not, in and or operators