Based on: http://inventwithpython.com/invent4thed/chapter5.html
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
- Part 2: Print intro() and Keep Playing
- Part 3: Player, choose() a Cave
- Part 4: Player, enter() Your Cave
- Part 5: Prettier output with describe()
- Part 6: Wrap text using the textwrap module
- Part 7: Pick the Friendly Dragon
- Part 8: The Dragon Acts
- Make it Your Own
- What we've learned
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.
dragon_realm.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Dragon Realm - A game where the player decides between two caves, which hold
either treasure or certain doom.
Inspired by: http://inventwithpython.com/invent4thed/chapter5.html
"""
def main():
"""The Dragon Realm Game"""
print("Welcome to Dragon Realm!")
# this means that if this script is executed, then
# the main() function will be called
if __name__ == '__main__':
main()
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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
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.
"""
Dragon Realm - A game where the player decides between two caves, which hold
either treasure or certain doom.
Inspired by: http://inventwithpython.com/invent4thed/chapter5.html
"""
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 varible 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 accedentally 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.
def main():
"""The Dragon Realm Game"""
print("Welcome to Dragon Realm!")
# this means that if this script is executed, then
# the main() function will be called
if __name__ == '__main__':
main()
Add the WIDTH
global variable, the intro()
function, and change the
main()
function.
dragon_realm.py
WIDTH = 58
def intro():
"""Display the introduction description to the player"""
print("""You are in a land full of dragons. In front of you,
you see two caves. In one cave, the dragon is friendly
and will share his treasure with you. The other dragon
is greedy and hungry, and will eat you on sight.""")
print()
def main():
"""Keep playing the game until the user doesn't say yes"""
print("Welcome to Dragon Realm!")
again = "yes"
while again.lower() in ["y", "yes"]:
print("-" * WIDTH, "\n")
intro()
again = input("Play again? ")
In programming a variable that cannot be changed is called a constant.
Python does not provide any way to make sure that a variable is never changed,
but the convention is to define variables that you intend to never be changed
at the top of the file and to name them with ALL_CAPS
.
Here we're defining the width of the screen (or really, just the line) to 58.
WIDTH = 58
The intro()
function will print out a paragraph to the player that describes
the surroundings. It starts with a docstring to say what the function does.
Because the intro text is long it will be on several lines. One way to do this
is to use the same """
syntax of a docstring. This will retain all
whitespace--both newlines and indentation. That is why lines 4-6 of the
function are not indented. If they were, the indention would be printed too.
The intro string ends with a \n
. The backslash (\
) tells the interpreter
that the next character has special meaning. In this case, \n
means add a
newline.
def intro():
"""Display the introduction description to the player"""
print("""You are in a land full of dragons. In front of you,
you see two caves. In one cave, the dragon is friendly
and will share his treasure with you. The other dragon
is greedy and hungry, and will eat you on sight.\n""")
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.
Python provides a nifty way to find out more informatin about an object--the
dir()
function. In your console start the Python shell by typing python3
,
then try typing dir("")
This will tell you all of the attributes and
functions available for strings. Then you can type help(str.lower)
to get
more information about the lower()
function.
again.lower()
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.lower() == "y" or answer.lower() == "yes"
But then answer.lower()
would be called twice. Instead, we're going to use the
in
membership operator. The syntax is <value> in <sequence>
which will result
in a True or False value. It will check if the value
is a member of the
sequence
. In this case the sequence is a list of answers ["y", "yes"]
.
So the following is has exactly the same result as above.
again.lower() in ["y", "yes"]
An easy way to repeat a string is by using the *
operator. At your Python
shell, try typing ""hello " * 3"
.
Here, we use the *
operator to repeat the -
character to easily print a
line, then add an extra newline at the end.
print("-" * WIDTH, "\n")
We need to 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, then add the choose()
and valid_cave()
functions.
dragon_realm.py
CAVES = ["right", "left"]
def valid_cave(response):
"""Return True if response is in the list of valid CAVES"""
return response in CAVES
def choose():
"""Prompt the player to choose "right" or "left" then return response."""
cave = ""
while not valid_cave(cave):
print("Do you enter the cave on the right or left?")
cave = input("(right, left): ").lower()
if cave in ["q", "quit", "exit"]:
exit()
if not valid_cave(cave):
print('Type "right" or "left". \n')
print()
return cave
Then edit your main()
function to add cave = choose()
.
while again.lower() in ["y", "yes"]:
print("-" * WIDTH, "\n")
intro()
cave = choose()
again = input("Play again? ")
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
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()
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.
dragon_realm.py
Above your global variables, import the time
module.
import time
Add a global variable DELAY
DELAY = 1
Add the enter()
function
def enter(cave):
messages = [
"You approach the cave...",
"It is dark and spooky...",
"A large dragon jumps out in front of you!",
"He opens his jaws and...",
]
for message in messages:
print(message)
time.sleep(DELAY)
And change your main()
function to add the enter()
line
while again.lower() in ["y", "yes"]:
print("-" * WIDTH, "\n")
intro()
cave = choose()
enter(cave)
again = input("Play again? ")
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.
dragon_realm.py
Add a describe()
function
def describe(message):
print(" ", message)
Then change your intro()
function to call it instead of print()
.
def intro():
"""Display the introduction description to the player"""
describe("""You are in a land full of dragons. In front of you,
you see two caves. In one cave, the dragon is friendly
and will share his treasure with you. The other dragon
is greedy and hungry, and will eat you on sight.\n""")
And call describe()
in your message()
function
for message in messages:
describe(message)
time.sleep(DELAY)
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.
dragon_realm.py
Import the textwrap
module
import textwrap
Add a global variable WRAP
WRAP = 50
Then change your describe()
function
def describe(message):
for line in textwrap.wrap(message, WRAP):
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.
def intro():
"""Display the introduction description to the player"""
describe("""You are in a land full of dragons. In front of you,
you see two caves. In one cave, the dragon is friendly
and will share his treasure with you. The other dragon
is greedy and hungry, and will eat you on sight.""")
print()
Next we need to randomly pick a dragon to be the friendly one.
dragon_realm.py
Import the random
module
import random
Add a is_friendly()
function
def is_friendly(dragon):
"""Return True if dragon is in the randomly chosen friendly one"""
friendly = random.randint(0, 1)
print("The friendly dragon is:", CAVES[friendly])
return dragon == CAVES[friendly]
Add a line to the end of your enter()
function to save the resulting value
nature = is_friendly(cave)
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]
Finally, we'll tell the player what the dragon does.
dragon_realm.py
Add a dragon()
function
def dragon(is_friendly):
"""Print the dragon action for a friendly or unfriendly dragon"""
actions = {
# friendlyness: action
True: "Gives you his treasure!",
False: "Gobbles you down in one bite!",
}
print()
describe(actions[is_friendly])
print()
Add a line to the end of your enter()
function to call it
dragon(nature)
Finally, remove or comment out the print()
line in your is_friendly()
function
# print("The friendly dragon is:", CAVES[friendly])
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.
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.
- The shebang
- Docstrings as documentation
- Scope,
main()
, and global variables - Docstrings as multiline strings
- Objects
str.lower()
,time.sleep()
andtextwrap.wrap()
- Repeating strings with
*
- Escape characters and
\n
- Method chaining
- List indexes and more dictionary key types
- Conditional expessions evaluate to boolean values
- The
not
,in
andor
operators