954 88 25MB
English Pages 280 [326]
Playlists
MISSION PYTHON. Copyright © 2018 by Sean McManus. History
All rights reserved. No part of this work may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording, or by
Topics
any information storage or retrieval system, without the prior written permission of the copyright owner and the publisher.
Tutorials
ISBN10: 1593278578
Offers & Deals
ISBN13: 9781593278571
Highlights
Publisher: William Pollock Production Editor: Riley Hoffman
Settings
Cover Illustration: Josh Ellingson Game Illustrations: Rafael Pimenta
Support
Developmental Editor: Liz Chadwick
SignTechnical Reviewer: Daniel Aldred Out
Copyeditor: Anne Marie Walker Compositor: Riley Hoffman Proofreader: Emelie Burnette The following images are reproduced with permission: Figure 11 courtesy of Johnson Space Center, NASA Figure 16 courtesy of NASA/JPLCaltech/UCLA Figure 17 image of Mars courtesy of NASA For information on distribution, translations, or bulk sales, please contact No Starch Press, Inc. directly: No Starch Press, Inc. 245 8th Street, San Francisco, CA 94103 phone: 1.415.863.9900; [email protected] www nostarch com Library of Congress Control Number: 2018950581
No Starch Press and the No Starch Press logo are registered trademarks of No Starch Press, Inc. Other product and company names mentioned herein may be the trademarks of their respective owners. Rather than use a trademark symbol with every occurrence of a trademarked name, we are using the names only in an editorial fashion and to the benefit of the trademark owner, with no intention of infringement of the trademark. The information in this book is distributed on an “As Is” basis, without warranty. While every precaution has been taken in the preparation of this work, neither the author nor No Starch Press, Inc. shall have any liability to any person or entity with respect to any loss or damage caused or alleged to be caused directly or indirectly by the information contained in it.
History
INTRODUCTION
Topics
Tutorials
Offers & Deals
Highlights
Settings
Air is running out. There’s a leak in the space station, so you’ve got to act fast. Can you
find your way to safety? You’ll need to navigate your way around the space station, find Support access cards to unlock doors, and fix your damaged space suit. The adventure has Signbegun! Out
And it starts here: on Earth, at mission command, also known as your computer. This book shows you how to use Python to build a space station on Mars, explore the station, and escape danger in an adventure game complete with graphics. Can you think like an astronaut to make it to safety?
HOW TO USE THIS BOOK By following the instructions in this book, you can build a game called Escape with a map to explore and puzzles to solve. It’s written in Python, a popular programming language that is easy to read. It also uses Pygame Zero, which adds some instructions for managing images and sounds, among other things. Bit by bit, I’ll show you how to make the game and how the main parts of the code work, so you can customize it or build your own games based on my game code. You can also download all the code you need. If you get stuck or just want to jump straight into playing the game and seeing it work, you can do so. All the software you need is free, and I’ve provided instructions for Windows PCs and the Raspberry Pi. I recommend you use the Raspberry Pi 3 or Raspberry Pi 2. The game may run too slowly to enjoy on the Pi Zero, original Model B+, and older models.
There are several different ways you can use the book and the game: Download the game, play it first, and then use the book to understand how it works. This way, you eliminate the risk of seeing any spoilers in the book before you play the game! Although I’ve kept them to a minimum, you might notice a few clues in the code as you read the book. If you get really stuck on a problem in the game, you can try reading the code to work out the solution. In any case, I recommend you run the game at least once to see what you’ll be building and learn how to run your programs. Build the game, and then play it. This book guides you through creating the game from start to finish. As you work your way through the chapters, you’ll add new sections to the game and see how they work. If you can’t get the code working at any point, you can just use my version of the code listing and continue building from there. If you choose this route, avoid making any custom changes to the game until you’ve built it, played it, and finished it. Otherwise, you might accidentally make the game impossible to complete. (It’s okay to make any changes I suggest in the exercises.) Customize the game. When you understand how the program works, you can change it by using your own maps, graphics, objects, and puzzles. The Escape game is set on a space station, but yours could be in the jungle, under the sea, or almost anywhere. You could use the book to build your own version of Escape first, or use my version of the final game and customize that. I’d love to see what you make using the program as a starting point! You can find me on Twitter at @musicandwords or visit my website at www.sean.co.uk.
WHAT’S IN THIS BOOK? Here’s a briefing on what’s in store for you as you embark on your mission. Chapter 1 shows you how to go on a spacewalk. You’ll learn how to use graphics in your Python programs using Pygame Zero and discover some of the basics of making Python programs. Chapter 2 introduces lists, which store much of the information in the Escape game. You’ll see how to use lists to make a map. Chapter 3 shows you how to get parts of a program to repeat and how to use that knowledge to display a map. You’ll also design a room layout for the space station,
using wall pillars and floor tiles. In Chapter 4, you’ll start to build the Escape game, laying down the blueprints for the station. You’ll see how the program understands the station layout and uses it to create the fabric for the rooms, putting the walls and floor in place. In Chapter 5, you’ll learn how to use dictionaries in Python, which are another important way of storing information. You’ll add information for all the objects the game uses, and you’ll see how to create a preview of your own room design. When you extend the program in Chapter 6, you’ll see all the scenery in place and will be able to look at all the rooms. After building the space station, you can move in. In Chapter 7, you’ll add your astronaut character and discover how to move around the rooms and animate movements. Chapter 8 shows you how to polish the game’s graphics with shadows, fading walls, and a new function to draw the rooms that fixes the remaining graphical glitches. When the space station is operational, you can unpack your personal effects. In Chapter 9, you’ll position items the player can examine, pick up, and drop. In Chapter 10, you’ll see how to use and combine items, so you can solve puzzles in the game. The space station is nearly complete. Chapter 11 adds safety doors that restrict access to certain zones. Just as you’re putting your feet up and celebrating a job well done, there’s danger around the corner, as you’ll add moving hazards in Chapter 12. As you work through the book, you’ll complete training missions that give you an opportunity to test your programs and your coding skills. The answers, if you need them, are at the end of each chapter. The appendixes at the back of the book will help you, too. Appendix A contains the listing for the whole game. If you’re not sure where to add a new chunk of code, you can check here. Appendix B contains a table of the most important variables, lists, and dictionaries if you can’t remember what’s stored where, and Appendix C has some debugging tips if a program doesn’t work for you. For more information and supporting resources for the book, visit the book’s website at
www.sean.co.uk/books/missionpython/. You can also find information and resources at https://nostarch.com/missionpython/.
INSTALLING THE SOFTWARE The game uses the Python programming language and Pygame Zero, which is software that makes it easier to handle graphics and sound. You need to install both of these before you begin.
NOTE For updated installation instructions, visit the book’s web page at https://nostarch.com/missionpython/.
INSTALLING THE SOFTWARE ON RASPBERRY PI If you’re using a Raspberry Pi, Python and Pygame Zero are already installed. You can skip ahead to “Downloading the Game Files” on page 7.
INSTALLING PYTHON ON WINDOWS To install the software on a Windows PC, follow these steps: 1. Open your web browser and visit https://www.python.org/downloads/. 2. At the time of this writing, 3.7 is the latest version of Python, but Pygame isn’t available for easy installation on it yet. I recommend you use the latest version of Python 3.6 instead (3.6.6 at the time of writing). You can find old versions of Python farther down the screen on the downloads page (see Figure 1). Save the file on your desktop or somewhere else you can easily find it. (Pygame Zero works only with Python 3, so if you usually use Python 2, you’ll need to switch to Python 3 for this book.)
Figure 1: The Python downloads page 3. When the file has downloaded, doubleclick it to run it. 4. In the window that opens, select the checkbox to Add Python 3.6 to PATH (see Figure 2). 5. Click Install Now.
Figure 2: The Python installer 6. If you’re asked whether you want to allow this application to make changes to your device, click Yes. 7. Python will take a few minutes to install. When it finishes, click Close to complete the installation.
INSTALLING PYGAME ZERO ON WINDOWS Now that you have Python installed on your computer, you can install Pygame Zero. Follow these steps: 1. Hold down the Windows Start key and press R. The Run window should open (see Figure 3). 2. Enter cmd (see Figure 3). Press ENTER or click OK.
Figure 3: The Windows Run dialog box 3. The command line window should open, as shown in Figure 4. Here you can enter instructions for managing files or starting programs. Enter pipinstallpgzero and press ENTER at the end of the line.
Figure 4: The command line window 4. Pygame Zero should start to install. It will take a few moments, and you’ll know it’s finished when your > prompt appears again. 5. If you get an error message saying that pip is not recognized, try installing Python again. You can uninstall Python first by running the installation program again or using the Windows Control panel. Make sure you select the box for the PATH when installing Python (see Figure 2). After you have reinstalled Python, try installing Pygame Zero again. 6. When Pygame Zero has finished downloading and you can type again, enter the following: e c h op r i n t ( " H e l l o ! " )>t e s t . p y 7. This line creates a new file called test.py that contains the instruction print("Hello!"). I’ll explain the print() instruction in Chapter 1, but for now, this is just a quick way to make a test file. Be careful when you enter the parentheses (curved brackets) and quotation marks: if you miss one, the file won’t work properly. 8. Open the test file by entering the following: p g z r u nt e s t . p y 9. After a short delay, a blank window should open with the title Pygame Zero Game. Click the command line window again to bring it to the front: you should see the text Hello! Press CTRLC in the command line window to stop the program.
10. If you want to delete your test program, enter deltest.py.
INSTALLING THE SOFTWARE ON OTHER MACHINES Python and Pygame Zero are available for other computer systems. Pygame Zero has been designed in part to enable games to work across different computers, so the Escape code should run wherever Pygame Zero runs. This book only provides guidance for users of Windows and Raspberry Pi computers. But if you have a different computer, you can download Python at https://www.python.org/downloads/ and can find advice on installing Pygame Zero at http://pygame zero.readthedocs.io/en/latest/installation.html.
DOWNLOADING THE GAME FILES I’ve provided all the program files, sounds, and images you need for the Escape game. You can also download all the listings in the book, so if you can’t get one to work, you can use mine instead. All the book’s content downloads as a single ZIP file called escape.zip.
DOWNLOADING AND UNZIPPING THE FILES ON A RASPBERRY PI To download the game files on a Raspberry Pi, follow these steps, and refer to Figure 5. The numbers in Figure 5 show you where to do each step. ➊ Open your web browser and visit https://nostarch.com/missionpython/. Click the link to download the files. ➋ From your desktop, click the File Manager icon on the taskbar at the top of the screen. ➌ Doubleclick your Downloads folder to open it ➍ Doubleclick the escape.zip file. ➎ Click the Extract Files button to open the Extract Files dialog box. ➏ Change the folder that you’ll extract to so it reads /home/pi/escape. ➐ Ensure that the option is selected to Extract files with full path. ➑ Click Extract.
Figure 5: The steps you should take to unzip your files
UNZIPPING THE FILE ON A WINDOWS PC To unzip the files on a Windows PC, follow these steps. 1. Open your web browser and visit https://nostarch.com/missionpython/. Click the link to download the files. Save the ZIP file on your desktop, in your Documents folder, or somewhere else you can easily find it. 2. Depending on the browser you’re using, the ZIP file might open automatically, or there might be an option to open it at the bottom of the screen. If not, hold down the Windows Start key and press E. The Windows Explorer window should open. Go to the folder where you saved the ZIP file. Doubleclick the ZIP file. 3. Click Extract All at the top of the window. 4. I recommend that you create a folder called escape in your Documents folder and extract the files there. My documents folder is C:\Users\Sean\Documents, so I just typed \escape at the end of the folder name to create a new folder in that folder (see Figure 6). You can use the Browse button to get to your Documents folder first if necessary.
5. Click Extract.
Figure 6: Setting the folder to unzip the game files into
WHAT’S IN THE ZIP FILE The ZIP file you’ve just downloaded contains three folders and a Python program, escape.py (see Figure 7). The Python program is the final version of the Escape game, so you can start playing it right away. The images folder contains all the images you’ll need for the game and other projects in this book. The sounds folder contains the sound effects. In the listings folder, you’ll find all the numbered listings in this book. If you can’t get a program to work, try my version from this folder. You’ll need to copy it from the listings folder first, and then paste it in the escape folder where the escape.py program is now. The reason you do this is because the program needs be alongside the images and sounds folders to work correctly.
Figure 7: The contents of the ZIP file as they might appear in Windows
RUNNING THE GAME When you downloaded Python, another program called IDLE will have been downloaded with it. IDLE is an integrated development environment (IDE), which is software you can use to write programs in Python. You can run some of the listings in this book from the IDLE Python editor using the instructions provided. Most of the programs, though, use Pygame Zero, and you have to run those programs from the command line. Follow the instructions here to run the Escape game and any other Pygame Zero programs.
RUNNING PYGAME ZERO PROGRAMS ON THE RASPBERRY PI If you’re using a Raspberry Pi, follow these steps to run the Escape game: 1. Using the File Manager, go to your escape folder in your pi folder. 2. Click Tools on the menu and select Open Current Folder in Terminal, or you can press F4. The command line window (also known as the shell) should open, as shown in Figure 8. You can enter instructions here for managing files or starting programs.
Figure 8: The command line window on the Raspberry Pi 3. Type in the following command and press ENTER. The game begins! p g z r u ne s c a p e . p y
This is how you run a Pygame Zero program on the Pi. To run the same program again, repeat the last step. To run a different program that’s saved in the same folder, repeat the last step but change the name of the filename after pgzrun. To run a Pygame Zero program in a different folder, follow the steps starting from step 1, but open the command line from the folder with the program you want to run.
RUNNING PYGAME ZERO PROGRAMS IN WINDOWS
RUNNING PYGAME ZERO PROGRAMS IN WINDOWS If you’re using Windows, follow these steps to run the program: 1. Go to your escape folder. (Hold down the Windows Start key and press E to open the Windows Explorer again.) 2. Click the long bar above your files, as shown in Figure 9. Type cmd into this bar and press ENTER.
Figure 9: Finding the path to your Pygame files 3. The command line window will open. Your folder named escape will appear just before the > on the last line, as shown in Figure 10.
Figure 10: The command line window in Windows 4. Type pgzrunescape.py in the command line window. Press ENTER, and the Escape game begins. This is how you run a Pygame Zero program on a Windows computer. You can run the program again by repeating the last step. To run a different program that’s saved in the same folder, repeat the last step but change the name of the filename after pgzrun. To run a Pygame Zero program in a different folder, follow the steps starting from step 1, but open the command line from the folder with the program you want to run.
PLAYING THE GAME
You’re working alone on the space station on Mars, many millions of kilometers from home. The rest of the crew is on a longdistance mission, exploring a canyon for signs of life, and won’t be back for days. The murmuring hum of the life support systems surrounds you. You’re startled when the alarm sounds! There’s a breach in the space station wall, and your air is slowly venting into the Martian atmosphere. You climb quickly but carefully into your space suit, but the computer tells you the suit is damaged. Your life is at risk. Your first priority is to repair your suit and ensure a reliable air supply. Your second priority is to radio for help, but the space station’s radio systems are malfunctioning. Last night the Poodle lander, sent from Earth, crashlanded in the Martian dust. If you can find it, perhaps you can use its radio to issue a distress signal. Use the arrow keys to move around the space station. To examine an object, stand on it and press the spacebar. Alternatively, if the object is something you can’t walk on, press the spacebar while walking into it. To pick up an object, walk onto it and press the G key (for get). To select an object in your inventory, shown at the top of the screen (see Figure 11), press the TAB key to move through the items. To drop the selected object, press D.
Figure 11: Your adventure begins! To use an object, either select it in your inventory or walk onto or into it and press U. You can combine objects or use them together when you press U while you carry one object and stand on the other or while you carry one and walk into the other. You’ll need to work out how to use your limited resources creatively to overcome obstacles and get to safety. Good luck!
History
Topics
1
YOUR FIRST SPACEWALK
Tutorials
Offers & Deals
Highlights
Settings Support
Welcome to the space corps. Your mission is to build the first human outpost on Mars.
Sign Out
For years, the world’s greatest scientists have been sending robots to study it up close. Soon you too will set foot on its dusty surface. Travel to Mars takes between six and eight months, depending on how Earth and Mars are aligned. During the journey, the spaceship risks hitting meteoroids and other space debris. If any damage occurs, you’ll need to put on your spacesuit, go to the airlock, and then step into the void of space to make repairs, similar to the astronaut in Figure 11.
In this chapter, you’ll go on a spacewalk by using Python to move a character around the screen. You’ll launch your first Python program and learn some of the essential Python instructions you’ll need to build the space station later in the book. You’ll also learn how to create a sense of depth by overlapping images, which will prove essential when we create the Escape game in 3D later (starting with our first room mockup in Chapter 3).
Figure 11: NASA astronaut Rick Mastracchio on a 26minute spacewalk in 2010, as photographed by astronaut Clayton Anderson. The spacewalk outside the International Space Station was one of a series to replace coolant tanks. If you haven’t already installed Python and Pygame Zero (Windows users), see “Installing the Software” on page 3. You’ll also need the Escape game files in this chapter. “Downloading the Game Files” on page 7 tells you how to download and unzip those files.
STARTING THE PYTHON EDITOR As I mentioned in the Introduction, in this book we’ll use the Python programming language. A programming language provides a way to write instructions for a computer. Our instructions will tell the computer how to do things like react to a keypress or display an image. We’ll also be using Pygame Zero, which gives Python some additional instructions for handling sound and images. Python comes with the IDLE editor, and we’ll use the editor to create our Python programs. Because you’ve already installed Python, IDLE should now be on your computer as well. The following sections explain how to start IDLE, depending on the type of computer you’re using.
STARTING IDLE IN WINDOWS 10
To start IDLE in Windows 10, follow these steps: 1. Click the Cortana search box at the bottom of the screen, and enter Python in the box. 2. Click IDLE to open it. 3. With IDLE running, rightclick its icon in the taskbar at the bottom of the screen and pin it. Then you can run it from there in the future using a single click.
STARTING IDLE IN WINDOWS 8 To start IDLE in Windows 8, follow these steps: 1. Move your mouse to the top right of the screen to show the Charms bar. 2. Click the Search icon, and enter Python in the box. 3. Click IDLE to open it. 4. With IDLE running, rightclick its icon in the taskbar at the bottom of the screen and pin it. Then you can run it from there in the future using a single click.
STARTING IDLE ON THE RASPBERRY PI To start IDLE on the Raspberry Pi, follow these steps: 1. Click the Programs menu at the top left of the screen. 2. Find the Programming category. 3. Click the Python 3 (IDLE) icon. The Raspberry Pi has both Python 2 and Python 3 installed, but most of the programs in this book will work only in Python 3.
INTRODUCING THE PYTHON SHELL When you start IDLE, you should see the Python shell, as shown in Figure 12. This window is where you can give Python instructions and immediately see the computer respond. The three arrows (>>>) are called a prompt. They tell you that Python is ready for you to enter an instruction.
Figure 12: The Python shell So let’s give Python something to do!
DISPLAYING TEXT For our first instruction, let’s tell Python to display text on the screen. Type the following line and press ENTER: >>> print("Prepare for launch!") As you type, the color of your text will change. It starts off black, but as soon as Python recognizes a command, like print, the text changes color. Figure 13 shows the names of the different parts of the instruction you just entered. The purple word print is the name of a builtin function, which is one of many instructions that are always available in Python. The print() function displays onscreen the information you place between the parentheses (curved brackets). The information between a function’s parentheses is the function’s argument.
Figure 13: The different parts of your first instruction In our first instruction, the print() function’s argument is a string, which is what programmers call a piece of text. (A string can include numbers, but they’re treated as letters, so you can’t do calculations with numbers in a string.) The double quotation
marks ("") show the start and end of the string. Anything you type between double quotation marks will be green, and so will the quotation marks. The colors do more than brighten up the screen: they highlight the different parts of the instruction to help you find mistakes. For example, if your final parenthesis is green, it means you forgot the closing double quote on the string. If you entered the instruction correctly, your computer will display this text: Prepare for launch! The string that was shown in green is now displayed onscreen in blue. All output (information the computer gives to you) appears in blue. If your command didn’t work, check that you did the following: 1. Spelled print correctly. If you did, it will be purple (see Figure 13). 2. Used two parentheses. Other bracket shapes won’t work. 3. Used two double quotes. Don’t use two apostrophes ('') instead of a double quote ("). Although the double quote includes two marks, it’s just one symbol on the keyboard. On a US keyboard, the double quote is in the middle row of letters, on the right, and must be used with the SHIFT key. On a UK keyboard, the double quote is on the 2 key. If you make a mistake typing the text between the double quotes, the instruction will still work, but the computer will display exactly what you typed. For example, try this: >>> print("Prepare for lunch!") It doesn’t matter if you mistype the string now, but be careful when you type a string or an instruction later in the book. Mistakes often prevent a program from working correctly, and it can be hard to track down a mistake in a longer program, even with the color coding.
TRAINING MISSION #1 Can you enter a new instruction to output your name? (You’ll find the answers to the Training Missions in the “Mission Debrief” section at the end
of each chapter.)
OUTPUTTING AND USING NUMBERS So far you’ve used the print() function to output a string, but it can also do calculations and output a number. Enter the following line: >>> print(4 + 1) The computer should output the number 5, the solution to 4+1. Unlike with a string, you don’t use quotes around numbers and calculations. But you still use the parentheses to mark the start and end of the information you want to give the print() function. What happens if you do put quotes around 4+1? Try it! The result is that the computer outputs "4+1" because it doesn’t treat 4 and 1 as numbers. Instead, it treats the argument as a string. You ask it to output "4+1", and it does exactly that! >>> print(4 + 1) 5 >>> print("4 + 1") 4 + 1 Python does the calculation only when you don’t include the quotes. You’ll use the pri nt() function a lot in your programs.
INTRODUCING SCRIPT MODE The shell is great for quick calculations and for short instructions. But for longer sets of instructions, like games, it’s much easier to create programs instead. Programs are repeatable sets of instructions that we save so we can run them whenever we want and change them whenever we need to without retyping them. We’ll build programs using IDLE’s script mode. When you enter instructions in script mode, they don’t run immediately as they do in the shell. Using the menu at the top of the shell window, select File and then select New File to open a blank new window, as shown in Figure 14. The title bar at the top of the window displays Untitled until you save your file and name it. Once you’ve saved your file, the
title bar will display the file’s name. From now on, we’ll use script mode nearly all the time when we’re creating Python code.
Figure 14: Python script mode When you enter instructions in script mode, you can change, add, and delete instructions using the mouse or the arrow keys, so it’s much easier to fix mistakes and build your programs. Starting from Chapter 4, we’ll build the Escape game by adding to it piecebypiece in script mode and testing each new section as we go.
TIP If you’re not sure whether you’re in the shell or the script mode window, look at the title bar at the top. The shell displays Python Shell. The script mode window displays either Untitled or the name of your program.
CREATING THE STARFIELD The first program we’ll write will display the starfield image that we’ll use as the space background for our Spacewalk program. This image is in the images folder within the escape folder. Start by entering Listing 11 into the new blank window in IDLE.
NOTE In this book, I’ll use numbers in circles (like this: ➊) to refer to different bits of code in the explanations so it’s easier for you to follow along. Don’t type these numbers in your program. When you see a number in a circle in the text, refer back to the program listing to see which part of the program I’m talking about.
Listing 11 is a short program, but there are a couple of details that you should pay attention to while you’re typing: the def statement ➍ needs a colon at the end of its line, and the next line ➎ needs to start with four spaces. When you add the colon to the end of the def line and press ENTER, IDLE automatically adds the four spaces at the beginning of the next line for you. listing11.py ➊ # Spacewalk # by Sean McManus # www.sean.co.uk / www.nostarch.com ➋ WIDTH = 800 HEIGHT = 600 ➌ player_x = 600 player_y = 350 ➍ def draw(): ➎ screen.blit(images.backdrop, (0, 0)) Listing 11: See the starfield in Pygame Zero. Select the File menu at the top of the screen and then select Save (from now on, we’ll use a shorthand for menu selections that looks like this: File ▸ Save). In the Save dialog, name your program listing11.py. You need to save your file in the escape folder you set up in the Introduction. This way, it’s in the same folder as the book’s images folder, and Pygame Zero can find the images when you run the program. After you save the file, your escape folder should now contain your listing11.py file and the images folder, as shown in Figure 15 (along with the listings and sounds folders).
Figure 15: Your new Python program and the images folder should be stored in the same place. I’ll explain how the listing11.py program works shortly, but first let’s run the program so we can admire the starfield. The program needs some instructions from Pygame Zero to manage the images, so to use those instructions, we need to run the program using a pgzrun instruction. Whenever we use any instructions from Pygame Zero in a Python program, we need to run it using pgzrun. We’ll type this on the computer’s command line, just like we did in the Introduction to run the Escape game. First, look back at “Running the Game” on page 9, and follow the directions there to open your computer’s command line terminal from your escape folder. Then run the following instruction from the command line: p gz runl i st ing 11.p y
RED ALERT Don’t type this instruction in IDLE: be sure to type it in your Windows or Raspberry Pi command line. The Introduction shows you how.
If all went according to plan, you should be looking at the majesty of space, as shown in Figure 16.
Figure 16: The starfield. The starfield image is courtesy of NASA/JPLCaltech/UCLA and shows star cluster NGC 2259.
USING MY EXAMPLE LISTINGS If you can’t get a program in this book to work, you can use my example program instead. For instance, you can use my listing11.py example and modify it to make your own listing12.py shortly so you can continue following along. You’ll find my programs in the listings folder, which is in the escape folder. Simply open the listings folder in Windows or the Raspberry Pi desktop, find the listing you need, copy it, and then paste it into the escape folder. Then open the copied listing in IDLE and follow along with the next step in the book. When you look at the folder, you should be able to see your Python file and the images folder are in the same place (see Figure 15).
UNDERSTANDING THE PROGRAM SO FAR Most of the instructions you’ll see in this book will work in any Python program. The pr in t( ) function, for example, is always available. To make the programs in this book,
we’re also using Pygame Zero. This adds some new functions and capabilities to Python for creating games, especially for the screen display and sound. Listing 11 introduces our first instructions from Pygame Zero, used to set up the game window and draw the starfield. Let’s take a closer look at how the listing11.py program works. The first few program lines are comments ➊. When you use a # symbol, Python ignores everything after it on the same line, and the line appears in red. The comments help you and other people reading the program understand what a program does and how it works. Next, the program needs to store some information. Programs almost always need to store information that the program uses or needs to refer back to at a later time. For example, in many games, the computer needs to keep track of the score and the player’s position on the screen. Because these details can change (or vary) as the program runs, they’re stored in something called a variable. A variable is a name you give to a piece of information, either a number or some text. To create a variable, you use an instruction like this: va riabl e_ na me=val u e
NOTE Code terms shown in italics are placeholders that would be filled in. Instead of variable_name, you would enter your own variable name.
For example, the following instruction puts the number 500 into the variable score: score = 500 You can name your variables almost anything you want. However, to make your program easy to write and understand, you should choose variable names that describe the information inside each variable. Note that you can’t use names for your variables that Python uses for its language, such as print.
RED ALERT Python is casesensitive, which means it is strict about whether variables use uppercase or lowercase letters. In fact, it treats score, SCORE, and Score as three completely different variables. Make sure you copy my example programs exactly, or they might not work properly.
Listing 11 begins by creating some variables. Pygame Zero uses the WIDTH and HEIGHT variables ➋ to set the size of the game window on the screen. Our window is wider than it is tall because the WIDTH value (800) is bigger than the HEIGHT value (600). Notice that we’ve spelled these variables with capital letters. The capital letters in variable names tell us that they’re constants. A constant is a particular kind of variable with values that aren’t supposed to change after they’ve been set up. The capital letters help other programmers who are looking at the program understand that they shouldn’t let anything else in the program change these variables. The player_x and player_y variables ➌ will store your position on the screen as you carry out your spacewalk. Later in the chapter, we’ll use these variables to draw you on the screen. We then define a function using the def() statement ➍. A function is a group of instructions you can run whenever you need them in your program. You’ve already seen one builtin function called print(). We’ll make our own function in this program called draw() . Pygame Zero will use it to draw the screen display whenever the screen changes.
We define a function using the keyword def ➍, followed by the function name we choose, empty parentheses, and a colon. Sometimes you’ll use a function’s parentheses to contain information for that function, as you’ll see later in this book. We then need to give the function instructions for what it should do. To tell Python which instructions belong to the function, we indent them by four spaces. The screen.blit( ) instruction ➎ from Pygame Zero draws an image on the screen. In the
parentheses, we tell it which image to draw and where to draw it, like this: screen.blit(images.i mage_nam e, (x , y ) ) From the images folder, we’ll use the backdrop.jpg file, which is the starfield. In our
listing11.py program, we refer to it as images.backdrop. We don’t have to use the file’s .jpg extension, because we’re using Pygame Zero to handle the images, and Pygame Zero doesn’t require the extension. Also, the program knows where the image is because all the images must be in the images folder so Pygame Zero can find them. We put the image on the screen at position (0,0) ➌, which is the topleft corner of the screen. The first number, known as the x position, tells the screen.blit() instruction how far from the left edge we want our image to be; the second number, known as the y position, describes how far down we want it to be. The x positions go from 0 on the left edge of the window to 799 on the right edge because our window is 800 pixels wide. Similarly, the y positions run from 0 at the top of the window to 599 at the bottom (see Figure 16). For positions onscreen, we use a tuple, which is just a group of numbers or strings in parentheses, such as (0,0). In a tuple, the numbers are separated with a comma, plus an optional space for readability. The most important thing you need to know about tuples is that you have to take care with the punctuation. Because the tuple uses parentheses, and we put this tuple inside the parentheses for screen.blit(), there are two sets of parentheses here. So you need parentheses around the tuple values, but you also need to close the parentheses for sc re en . blit ( ) after the tuple.
STOPPING YOUR PYGAME ZERO PROGRAM Similar to space, your Pygame Zero program will go on forever. To stop it, click the game window’s close button at the top right (see Figure 16). You can also close the program from the command line window where you entered the pgzrun instruction by pressing CTRLC.
RED ALERT Don’t close the command line window itself. Otherwise, you’ll have to open it again to run another Pygame Zero program. If you do close it by mistake, refer back to “Running the Game” on page 9 to open it again.
ADDING THE PLANET AND SPACESHIP
Let’s bring Mars and the spaceship into view. In IDLE, add the last two lines in Listing 12 to your existing listing 11.py program.
NOTE I’ll use --snip-- in code listings to show you where I’ve left out some code, usually because the code is repeated from before. I’ll also show any repeated code in gray so you can see the new code you need to add more clearly. Don’t add in the repeated code again!
In the following code, I’ve excluded the comments and variable setup to save space and make it easier for you to see the new code. But make sure you keep those instructions in your program. Just add the two new lines at the end. listing12.py s n i p def draw(): screen.blit(images.backdrop, (0, 0)) screen.blit(images.mars, (50, 50)) screen.blit(images.ship, (130, 150)) Listing 12: Adding Mars and the ship Save your updated program as listing12.py by selecting File ▸ Save As. Run your program by switching back to the command line window and entering the command pg zr unlist i n g 12 .p y . Figure 17 shows how the screen should now look, with the red
planet and the spaceship above it.
Figure 17: Mars and the spaceship. The Mars image was taken by the Hubble Space Telescope in 1991.
NOTE If your program doesn’t work as expected, check that all your screen.blit() instructions have exactly four spaces before them and are lined up with each other.
The first of the new instructions places the image mars.jpg at the position (50, 50), which is near the topleft corner of the screen. The second new instruction positions the ship at (130, 150). In each case, the coordinates used are for the topleft corner of the image.
CHANGING PERSPECTIVE: FLYING BEHIND THE PLANET Now let’s look at how we can make the ship fly behind the planet. Swap the order of the last two instructions in IDLE, as shown in Listing 13. To do this, highlight one of the
lines, press CTRLX to cut it, click on a new line, and press CTRLV to paste it in place. You can also use the cut and paste options in the Edit menu at the top of the screen. listing13.py snip def draw(): screen.blit(images.backdrop, (0, 0)) screen.blit(images.ship, (130, 150)) screen.blit(images.mars, (50, 50)) Listing 13: Swapping the order of the planet and ship instructions If the previous version of your program is still running, close it now. Save your new program as listing13.py and run it from the command line by entering pgzrunlisting13.p y . You should see that the spaceship is now behind the planet, as shown in Figure 1
8. If not, make sure you ran the right file (listing13.py), and then check that the instructions in the program are correct. The ship goes behind the planet because the images are added to the screen in the order they are drawn in the program. In our updated program, we draw the starfield, draw the ship, and then draw Mars. Each new image appears on top of the previous one. If two images overlap, the image that was drawn last appears in front of the one drawn earlier.
Figure 18: The spaceship is now behind the planet.
TRAINING MISSION #2 Can you move just one drawing instruction in your program to make the planet and the spaceship disappear? If you’re not sure what to do, experiment by moving the drawing instructions to see what effect it has when you save the program and run it again. Make sure you keep the drawing instructions aligned and indented with four spaces inside the draw() function. When you’re done experimenting, match the instructions in Listing 13 again to bring the ship and Mars back into view.
SPACEWALKING! It’s time to climb out of the underside of the spaceship and begin your spacewalk. Edit your program so it matches Listing 14. But be sure to keep the variable instructions that aren’t shown here the same as they were before. Save the updated program as listing14.py.
listing14.py sn ip def draw(): screen.blit(images.backdrop, (0, 0)) screen.blit(images.mars, (50, 50)) ➊ screen.blit(images.astronaut, (player_x, player_y)) ➋ screen.blit(images.ship, (550, 300)) ➌ def game_loop(): ➍ global player_x, player_y ➎ if keyboard.right: ➏ player_x += 5 ➐ elif keyboard.left: player_x = 5 ➑ elif keyboard.up: player_y = 5 elif keyboard.down: player_y += 5 ➒ clock.schedule_interval(game_loop, 0.03) Listing 14: Adding the spacewalk instructions In this listing, we add a new instruction ➊ to draw the astronaut image at the position in the player_x and player_y variables, which were set up at the start of the program in Listing 11. As you can see, we can use these variable names in place of numbers for the astronaut’s position. The program will use the current numbers stored in these variables to figure out where to put the astronaut every time it is drawn. Note that the order of drawing the images has changed in the program and is now backdrop, Mars, astronaut, and ship. Make sure you change the order of your sc re en . blit ( ) instructions to match this listing.
The astronaut starts off overlapping the ship. Because the astronaut is drawn before the ship, the astronaut will appear to emerge from underneath (behind) the spaceship. We also changed the position of the ship ➋ to the bottomright area of the screen. This gives the astronaut space to fly toward the planet. Run the program by entering pgzrunlisting1-4.py. You should now be able to use the
arrow keys to move freely through space, protected by your spacesuit, as shown in Figure 19. You’ll see that you fly behind the spaceship but in front of Mars and the starfield. The order in which we draw the images creates a simple illusion of depth. When we draw the space station beginning in Chapter 3, we’ll use this drawing technique to create a 3D perspective of each room. We’ll draw the rooms from back to front to create a sense of depth.
Figure 19: You emerge from the ship for your spacewalk.
TRAINING MISSION #3 Can you edit the code to move the spaceship and the astronaut to the top right corner of the screen? You’ll need to change the starting values for p l a y e r _ x and player_y, as well as where the spaceship is drawn. Make sure the
player is “inside” (actually underneath) the ship at the start of the program. Experiment with other positions, too. This is a great way to get familiar with screen positions. Refer back to Figure 16 if you need to.
UNDERSTANDING THE SPACEWALK LISTING
UNDERSTANDING THE SPACEWALK LISTING The spacewalk listing, Listing 14, is interesting because it lets you control part of the program from the keyboard, which will be crucial in the Escape game. Let’s look at how our final spacewalk program works. We build on our earlier listings and add a new function called game_loop() ➌. This function’s job is to change the values of the player_x and player_y variables when you press the arrow keys. Changing the variables enables you to move the astronaut character because those variables position the astronaut when it’s drawn. Before we go on, we need to look at two different types of variables. Variables that are changed inside a function usually belong to that function and can’t be used by other functions. They’re called local variables, and they make it harder for bits of the program to interfere with other bits accidentally and cause errors. But in the spacewalk listing, we need both the draw() and game_loop() functions to use the same player_x and player_y variables, so they need to be global variables, which any part of the program can use. We set up global variables at the start of the program, outside of any functions. To tell Python that the game_loop() function needs to use and change the global variables we set up outside of this function, we use the global command ➍. We put it at the beginning of the function and list the variables we want to use as global variables. Doing this is like overriding the safety feature that stops you from changing variables that weren’t created inside the function. We don’t need to use global in the draw() function, because the draw() function doesn’t need to change those variables. It only needs to look at what those variables contain. We tell the program to use keyboard controls using the if command. With this instruction, we tell Python to do something only if certain conditions are met. We use four spaces to indent the instructions that belong to the if command. That means these instructions are indented by eight spaces in total in Listing 14 because they are also inside the game_loop() function. These instructions run only if the statement after the if command is true. If not, the instructions that belong to the if command are skipped over. It might seem odd to use spaces like this to show which instructions belong together, especially if you’ve used other programming languages, but it makes the programs easy to read. Other languages often need brackets around sets of instructions like this. Python keeps it simple.
We use the if command to check whether the right arrow key is pressed ➎. If it is, we change the value of player_x by adding 5 ➏, moving the astronaut image to the right. The symbols += mean increase by, so the following line increases the number in the player_x variable by 5: player_x += 5 Similarly, -= means decrease by, so the following instruction reduces the number in pla yer_ x by 5:
player_x = 5 If the right arrow key is not pressed, we check whether the left key is pressed. If it is, the program subtracts 5 from the player_x value, moving the astronaut’s position left. To do that, we use an elif command ➐, which is short for “else if.” You can think of else as meaning otherwise here. In plain English, this part of our program means, “If the right arrow key is pressed, add 5 to the x position. Otherwise, if the left key is pressed, subtract 5 from the x position.” We then use elif to check for up and down keypresses in the same way, and change the y position to move the astronaut up or down. The dra w() function uses the player_x and player_y variables for the astronaut’s position, so
changing the numbers in these variables makes the astronaut move on the screen.
TIP If you change the elif command at ➑ to an if command, the program allows you to move up or down at the same time as moving left or right, letting you walk diagonally. That’s fun in the spacewalk program, but we’ll use code similar to this to move around the space station later, and it doesn’t look natural there.
The final instruction ➒ sets the game_loop() function to run every 0.03 seconds using the clock in Pygame Zero, so the program keeps checking for your keypresses and changing your position variables frequently. Note that you don’t put any parentheses after gam e_lo op here. This instruction isn’t indented, because it doesn’t belong to any function.
When the program starts, it runs the instructions that aren’t in any function in the
order they are in the listing, from top to bottom. Therefore, the last line of the program is one of the first to run after the variables are set up. This last line starts the game_loop() function running. The draw() function runs automatically whenever the screen needs updating. This is a feature of Pygame Zero.
TRAINING MISSION #4 Let’s fit some new thrusters to the spacesuit. Can you work out how to make the astronaut move faster in the up and down directions than it does in the left and right directions? Each keypress in the up or down direction should make the space suit move more than a keypress in the left or right direction.
Enjoy the breathtaking views as you take your spacewalk and conduct any essential repairs to your ship. We’ll reconvene in Chapter 2, where you’ll learn some procedures that will help you stay safe in space.
ARE YOU FIT TO FLY? Check the following boxes to confirm that you’ve learned the key lessons in this chapter. If you’re not sure about something, flip back through the chapter and give the topic another look.
You use IDLE’s script mode to create a program that you can save, edit, and run again. Enter script mode by selecting File ▸ New File or edit an existing file by selecting File ▸ Open.
Strings are pieces of text in code. Double quotes mark the start and end of a string. A string can include numbers, but they’re treated as letters.
Variables store information, either numbers or strings.
The print() function outputs information on the screen. You can use it for strings, numbers, calculations, or the values of variables.
The # symbol in a program marks a comment. Python ignores anything on the same line after a #, and comments can be a handy reminder for you and anyone you share your code with.
Use the WIDTH and HEIGHT variables to set the size of your game window.
To run a Pygame Zero program, open the command line from the folder your Python program is in, and then enter pgzrun filename.py in the command line to run it.
A function is a group of instructions you can run whenever you want your program to use the instructions. Pygame Zero uses the draw() function to draw or update the game screen.
Use screen.blit(images.image_name,(x, y)) to draw an image at position (x, y) on the screen. The x and yaxes are numbered starting at 0 in the topleft corner.
A tuple is a group of numbers or strings in parentheses, separated by a comma. The contents of a tuple can’t be changed by the program after they’ve been set up.
To end your Pygame Zero program, click the window’s close button or press CTRLC in the command line window.
If images overlap, the image you drew last in the program appears at the front.
The elif command is short for “else if.” Use it to combine if conditions so that only one set of instructions can run. In our program, we use it to stop the player from moving in two directions at the same time.
If we want to change a variable inside a function and use it in a different function, we need to use a global variable. We set it up outside of the functions and use the global keyword inside a function when we plan to change the variable there.
We can set a function to run at regular intervals using the clock feature in Pygame Zero.
Playlists
History
Topics
2
LISTS CAN SAVE YOUR LIFE
Tutorials
Offers & Deals
Highlights
Settings Support
Astronauts live by lists. The safety checklists they use help make sure all systems are
Sign Out
working before they entrust their lives to those systems. For example, emergency checklists tell the astronauts what to do in dire situations to prevent them from panicking. Procedural checklists confirm that they’re using their equipment correctly so nothing breaks and prevents them from returning home. These lists just might save their lives one day. In this chapter, you’ll learn how to manage lists in Python and how to use them for checklists, maps, and almost anything in the universe. When you build the Escape game, you’ll use lists to store information about the space station layout.
MAKING YOUR FIRST LIST: THE TAKE-OFF CHECKLIST Takeoff is one of the most dangerous aspects of space travel. When you’re strapped to a rocket, you want to doublecheck everything before it launches. A simple checklist for takeoff might contain the following steps:
Put on suit
Seal hatch
Check cabin pressure
Fasten seatbelt Python has the perfect way to store this information: the Python list is like a variable that stores multiple items. As you’ll see, you can use it for numbers and text as well as a combination of both. Let’s make a list in Python called take_off_checklist for our astronauts to use. Because we’re just practicing with a short example, we’ll enter the code in the Python shell rather than creating a program. (If you need a refresher on how to use the Python shell, see “Introducing the Python Shell” on page 15.) Enter the following in the IDLE shell, pressing ENTER at the end of each line to start a new line in the list: >>> take_off_checklist = ["Put on suit", "Seal hatch", "Check cabin pressure", "Fasten seatbelt"]
RED ALERT Make sure the brackets, quote marks, and commas in your code are precise. If you get any errors, enter the list code again, and doublecheck that the brackets, quotes, and commas are in the correct places. To avoid having to retype the code, use your mouse to highlight the text in the shell, rightclick the text, select Copy, rightclick again, and select Paste.
Let’s take a closer look at how the take_off_checklist list is made. You mark the start of the list with an opening square bracket. Python knows the list is not finished until it detects the final closing square bracket. This means you can press ENTER at the end of each line to continue typing the instruction, and Python will know you’re not finished until you’ve given it the final bracket. Quote marks tell Python that you’re giving it some text and where each piece of text starts and ends. Each entry needs its own opening and closing quote marks. You also need to separate the different pieces of text with commas. The last entry doesn’t need a comma after it, because there isn’t another list item following it.
SEEING YOUR LIST
SEEING YOUR LIST To see your checklist, you can use the print() function, as we did in Chapter 1. Add the name of your list to the print() function, like this: >>> print(take_off_checklist) ['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt'] You don’t need quotes around take_off_checklist, because it’s the name of a list, not a piece of text. If you do put quotes around it, Python will just write the text ta ke _o f f_ch e c k li s t onscreen instead of giving you back your list. Try it to see what
happens.
ADDING AND REMOVING ITEMS Even after you’ve created a list, you can add an item to it using the append() command. The word append means to add something at the end (think of an appendix, at the end of a book). You use the append() command like this: >>> take_off_checklist.append("Tell Mission Control checks are complete") You enter the name of the list (without quote marks) followed by a period and the ap pe nd ( ) command, and then put the item to add in parentheses. The item will be added
to the end of the list, as you’ll see when you print the list again: >>> print(take_off_checklist) ['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell Mission Control checks are complete'] You can also take items out of the list using the remove() command. Let’s remove the Seal ha tc h item:
>>> take_off_checklist.remove("Seal hatch") >>> print(take_off_checklist) ['Put on suit', 'Check cabin pressure', 'Fasten seatbelt', 'Tell Mission Control checks are complete'] Again, you enter the name of the list followed by a period and the remove() command, and then specify the item you want to remove inside the parentheses.
RED ALERT When you’re removing an item from a list, make sure what you type matches the item exactly, including capital letters and any punctuation. Otherwise, Python won’t recognize it and will give you an error.
USING INDEX NUMBERS Hmm, we should probably put the Sealhatch check back into the list before anyone at Mission Control notices. You can insert an item in a specific position in the list by using that item’s index number. The index is the position of the item in the list. Python starts counting at 0, not 1, so the first item in the list always has an index of 0, the second item has an index of 1, and so on.
INSERTING AN ITEM Using the position index, we’ll put Sealhatch back where it belongs: >>> take_off_checklist.insert(1, "Seal hatch") >>> print(take_off_checklist) ['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell Mission Control checks are complete'] Phew! I think we got away with it. Because the index starts at 0, when we inserted Seal hatch , we placed it at position 1, the second item in the list. The rest of the list items
shifted down in the list to make room, increasing their index numbers, as shown in Figure 21.
Figure 21: Inserting an item at index 1. Top row: before insertion. Bottom row: after insertion.
ACCESSING AN INDIVIDUAL ITEM
You can also access a particular item in a list using the list name with the index number of the item you want to access in square brackets. For example, to print particular items in the list, you can enter the following: >>> print(take_off_checklist[0]) Put on suit >>> print(take_off_checklist[1]) Seal hatch >>> print(take_off_checklist[2]) Check cabin pressure Now you can see individual items in the list!
RED ALERT Don’t mix up your brackets. Roughly speaking: Use square brackets when you’re telling Python which list item to use. Use parentheses when you’re doing something to the list or items in it, such as printing the list or appending items to it. Every opening bracket needs a closing bracket of the same type.
REPLACING AN ITEM You can also replace an item if you know its index number. Simply enter the list name followed by the index of the item you want to replace, and then use an equal sign (=) to tell Python what you want at that index, like this: >>> take_off_checklist[3] = "Take a selfie" >>> print(take_off_checklist) ['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Take a selfie', 'Tell Mission Control checks are complete'] The old item at index 3 is removed and replaced with the new item. Be aware that when you replace an item, Python forgets the original. Recall your training to put it back, like this: >>> take_off_checklist[3] = "Fasten seatbelt" >>> print(take_off_checklist)
['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell Mission Control checks are complete']
DELETING AN ITEM If you know where an item is in a list, you can delete it using its index number too, like this: >>> del take_off_checklist[2] >>> print(take_off_checklist) ['Put on suit', 'Seal hatch', 'Fasten seatbelt', 'Tell Mission Control checks are complete'] The "Checkcabinpressure" item disappears from the list.
TRAINING MISSION #1 It’s time to practice your skills! We just deleted item 2 in the list. Can you insert it back into the list in the correct position? Print the list to check that it worked.
CREATING THE SPACEWALK CHECKLIST As you know from Chapter 1, another dangerous activity for an astronaut is venturing out into the black vacuum of space with just a suit to protect you and provide oxygen. Here is a checklist to help keep you safe when you’re spacewalking:
Put on suit
Check oxygen
Seal helmet
Test radio
Open airlock Let’s make this checklist into a Python list. We’ll call it spacewalk_checklist, like this:
>>> spacewalk_checklist = ["Put on suit", "Check oxygen", "Seal helmet", "Test radio", "Open airlock"] Remember to be careful with the commas and brackets.
TRAINING MISSION #2 It’s always a good idea to test your code so you know it’s working as it should. Can you try printing all the list items to check that they’re in the right place?
A LIST OF LISTS: THE FLIGHT MANUAL We have two checklists now: one for takeoff and one for spacewalking. We can organize them by putting them into another list to create our “flight manual.” Think of the flight manual as a folder that contains two sheets of paper, and each piece of paper has one list on it.
MAKING A LIST OF LISTS Here is how we make the flight manual list of lists: >>> flight_manual = [take_off_checklist, spacewalk_checklist] We give IDLE the flight_manual list name, use the equal sign (=), and then add the two lists we want to put in the flight_manual list inside square brackets. As we did earlier when making lists, we separate the two items with a comma. The new flight_manual list has two items in it: the take_off_checklist and the spacewalk_checklist. When you print f light_ man ua l , it looks like this:
>>> print(flight_manual) [['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell Mission Control checks are complete'], ['Put on suit', 'Check oxygen', 'Seal helmet', 'Test radio', 'Open airlock']]
TIP Remember that you don’t need to use quote marks around list names; you use them only when you’re entering text into a list.
RED ALERT If you don’t see 'Checkcabinpressure' in your list, it’s because you skipped Training Mission #1. To make it easier to follow along, I recommend you go back and complete that mission. You can check the training mission answers at the end of the chapter if you need to.
The output looks messy! To work out what’s going on, look closely at the brackets. Square brackets mark the start and end of each list. If you strip out the list items, the output looks like this: [ [ fi r s tli stishere ], [ secondlisti she re ] ] In the middle, you can see where the first list ends with a closed bracket followed by a comma before the next list begins with an opening bracket. So what happens when you try to print the first item in the flight_manual list? >>> print(flight_manual[0]) The first item is the take_off_checklist, so the output looks like this: ['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell Mission Control checks are complete']
TRAINING MISSION #3 Try adding other checklists to flight_manual and printing them. For example, you could add a checklist for landing on a planet or docking with another
spaceship.
FINDING AN ITEM IN THE FLIGHT MANUAL If you want to look at a particular item in one of the lists in flight_manual, you must give Python two pieces of information: the list the item is in, and the index of the item in the list, in that order. For each piece of information, you can use index numbers, like this: >>> print(flight_manual[0][1]) Seal hatch Check your result against the printout of your checklist higher up in the shell. The Seal ha tc h item is in the first list (index 0), which is the take_off_checklist, and it’s the second
item in that list (index 1). Those are the two index numbers we used to find it. Let’s choose an item from the second list: >>> print(flight_manual[1][3]) Test radio This time, we’re printing from the second list (index 1), and from that list, we’re printing the fourth item (index 3). Although it might seem confusing that Python starts counting at 0, soon it will become second nature to subtract one from the position number you want. Be careful that you don’t end up buying one fewer of everything when you go shopping!
TIP To print a list or variable on the screen, you can leave out the print() command when you’re typing into the shell, like so: >>> flight_manual[0][2] ‘Check cabin pressure’ This only works in the shell, though, and not in a program. Often, you’ll have many ways to do the same thing in Python. This book focuses on the techniques that will most help you make the Escape game. As you learn
Python, you’ll find your own style and preferences.
COMBINING LISTS You can join two lists using a plus sign (+) to combine them into a single list. Let’s make a list of all the skills needed for takeoff and spacewalking and call it skills_list: >>> skills_list = take_off_checklist + spacewalk_checklist >>> print(skills_list) ['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell Mission Control checks are complete', 'Put on suit', 'Check oxygen', 'Seal helmet', 'Test radio', 'Open airlock'] The output you see here is a single list containing the skills astronauts need from the two lists we already made. We can also add more skills to the list by entering the combined list’s name and using += to add single items or other lists to the end of it. (In Chapter 1, you learned how to use += to add a number to a variable’s value.) Few people get to go into space, so a big part of an astronaut’s role is to share that experience. Let’s add a list called pr_list for public relations (PR) skills that an astronaut might need. I think there might be a place for selfie skills after all! >>> pr_list = ["Taking a selfie", "Delivering lectures", "Doing TV interviews", "Meeting the public"] >>> skills_list += pr_list >>> print(skills_list) ['Put on suit', 'Seal hatch', 'Check cabin pressure', 'Fasten seatbelt', 'Tell Mission Control checks are complete', 'Put on suit', 'Check oxygen', 'Seal helmet', 'Test radio', 'Open airlock', 'Taking a selfie', 'Delivering lectures', 'Doing TV interviews', 'Meeting the public'] The skills_list now has the items from pr_list added. The skills_list is still just a single list with individual items in it, unlike flight_manual, which has two separate lists inside it.
TIP You might have noticed that this code line: skills_list += pr_list is just a shorter way of writing this: skills_list = skills_list + pr_list It’s a very useful shortcut!
MAKING MAPS FROM LISTS: THE EMERGENCY ROOM Navigation is an essential skill for an astronaut. You must always know where you are, where your nearest sanctuary is, and even where the air is so you’re always ready in an emergency. The Escape game will keep a map of the room the player is in, so it can draw the room correctly and enable the player to interact with objects. Let’s look at how we can use lists to make a map of the emergency supplies room.
MAKING THE MAP Now that you know how to manage lists and lists inside lists, you can make maps. This time, we’ll create a program rather than working in the shell. At the top of the Python window, select File ▸ New File to open a new window. Enter Listing 21 into your new program window: listing21.py room_map = [ [1, 0, 0, 0, 0], [0, 0, 0, 2, 0], [0, 0, 0, 0, 0], [0, 3, 0, 0, 0], [0, 0, 0, 0, 4] ] print(room_map)
Listing 21: Setting up the emergency room Note that you don’t need a comma at the end of the last line in the list. This program creates and displays a list called room_map. Our new emergency room is five meters by five meters. The room_map list contains five lists. Each of those lists contains five numbers, which represent one row of the map. I’ve lined up the numbers in the code so it looks like the grid shown in Figure 22, which shows a map of the room. Compare the diagram and the program; you’ll see that the first list is for the top row, the second list is for the second row, and so on. A 0 represents an empty space in the grid, and the numbers 1 to 4 are for various emergency items in the room. The numbers we’ll use in this chapter represent the following items:
Figure 22: Our first simple map 1. Fertilizer 2. Spare oxygen tanks 3. Scissors 4. Toothpaste 5. Emergency blankets 6. Emergency radio
RED ALERT Make sure your brackets and commas are in the correct places. One reason for putting Listing 21 into a program instead of typing it into the shell is so
you can easily make corrections if you make a mistake.
Click File ▸ Save and save your program as listing21.py. This program doesn’t use Pygame Zero, so we can run it from IDLE. Click Run in the menu bar at the top of the window, and then click Run Module. You should see the following output in the shell window: [[1, 0, 0, 0, 0], [0, 0, 0, 2, 0], [0, 0, 0, 0, 0], [0, 3, 0, 0, 0], [0, 0, 0, 0, 4]] It’s hard to work out what you’re looking at when the list is shown like this, which is why I lined up the numbers in a grid in the program listing. But this shell output is the same map and the same data, so everything is where it should be: it’s just being presented in a different way. In Chapter 3, you’ll learn how to print this map data so it looks more like the listing we created.
FINDING AN EMERGENCY ITEM To find out what item is at a particular point in the map, you need to give Python a coordinate to check. The coordinates are a combination of the y position (from top to bottom) and the x position (from left to right), in that order. The y position will be the list in room_map you want to check (the row in the grid). The x position will be the item in that list you want to look at (the column) (see Figure 23). As always, remember that index numbers start at 0.
Figure 23: The ycoordinate indicates the list we want to look at. The xcoordinate
indicates the item in that list.
RED ALERT If you’ve used coordinates before, you know that you usually put the x coordinate before the ycoordinate. We’re doing the opposite here because it makes the code simpler. If we put x first, we would have to make each list in r oo m_ ma p represent a column of the map, from top to bottom, instead of a row,
from left to right. That would make the map look wrong in our code: the map would be on its side and a mirror image, which would be very confusing! Just remember that our map coordinates use y and then x.
Let’s work through an example: we’ll find out what item is at the position marked 2 on our simple map diagram. We need to know the following: The 2 is in the second row (from top to bottom), so it’s in the second list in room_map. The index starts at 0, so we subtract 1 from 2 to get the index number for the y position, which is 1. Use Figure 23 to check this index number: the index numbers for the rows are on the left of the grid in red. The 2 is in the fourth column (from left to right) of the list. Again, we subtract 1 to get the index number for the x position, which is 3. Use Figure 23 to check this index number as well. The index numbers for the columns are shown across the top of the grid in red. Go to the shell and enter the following print() command to view the number in that position on the map: >>> print(room_map[1][3]) 2 As expected, the result is the number 2, which happens to be spare oxygen tanks. You’ve successfully navigated your first map!
TRAINING MISSION #4
Try to predict the output before you enter the following command into the shell: >>> print(room_map[3][1]) Refer to the map in Figure 22 and your code listing to make your prediction. If you need more help, look at Figure 23. Then check your answer by entering the instruction in the shell.
SWAPPING ITEMS IN THE ROOM You can also change items in the room. Let’s check which item is at the topleft position of the map, using the shell again: >>> print(room_map[0][0]) 1 The 1 is fertilizer. We don’t need fertilizer in the emergency room, so let’s change that item to emergency blankets in the map. We’ll use a 5 to represent them. Remember how we used an equal sign (=) to change the value of an item in a list? We can do the same to change the number in the map, like this: >>> room_map[0][0] = 5 We enter the coordinates and then enter a new number to replace the original number. We can check that the code worked by printing the value at that coordinate again, which was 1 a moment ago. Let’s also print room_map and confirm that the emergency blankets appear in the correct position: >>> print(room_map[0][0]) 5 >>> print(room_map) [[5, 0, 0, 0, 0], [0, 0, 0, 2, 0], [0, 0, 0, 0, 0], [0, 3, 0, 0, 0], [0, 0, 0, 0, 4]] Perfect! The emergency blankets are stored in the topleft corner of the room. Item 5 is the first item in the first list.
TRAINING MISSION #5 Space is precious in the emergency room! Replace the toothpaste (4) with an emergency radio (6). You’ll need to find the coordinates of the 4 first and then enter the command to change it. Refer to Figure 22 and Figure 23 if you need more help with the index numbers.
In the Escape game, the room_map list is used to remember the items in the room the player is currently in. The map stores the number of the object that appears at each position on the map, or a 0 if the floor space is empty. The rooms in the game will be bigger than this 5 × 5 grid, so the size of the room_map will vary depending on the width and height of the room the player is in.
ARE YOU FIT TO FLY? Check the following boxes to confirm that you’ve learned the key lessons in this chapter.
Python lists store words, numbers, or a mixture of both.
To see an item in a list, use its index number in square brackets: for example, print(take_off_checklist[2]) .
The append() function adds items to the end of a list.
The remove() function removes items from a list: for example, spacewalk _checklist.remove("Sealhelmet") .
You can use index numbers to delete or insert an item at a particular position in a list.
Index numbers start at 0.
You can change an item in a list using the equal sign (=): for example, take_off_checklist[3]="Testcomms" .
You can make a list that contains other lists to build a simple map.
You can check which item is in your map using coordinates: for example, use room_map[ ycoordinate ][ xcoordinate ] .
Be sure to use y first and then x for your coordinates. In space, everything is upside down.
The coordinates are index numbers, so both start at 0, not 1.
You can use += to add an item to a list, or to join two lists.
History
Topics
3
REPEAT AFTER ME
Tutorials
Offers & Deals
Highlights
Settings Support
Everyone talks about the heroism and glamour of space travel, but some of it is routine,
Sign Out
repetitive work. When you’re cleaning, gardening in the space station greenhouse, or exercising to keep your strength up, you’re following detailed plans designed to keep the team safe and the space station operating. Luckily, robots take care of some of the drudgery, and they never complain about having to repeat themselves. Whether you’re programming robots or building games, the loop is one of your basic programming building blocks. A loop is a section of a program that repeats: sometimes it repeats a set number of times, and sometimes it continues until a particular event takes place. Sometimes, you’ll even set a loop to keep going forever. In this chapter, you’ll learn how to use loops to repeat instructions a certain number of times in your programs. You’ll use loops, along with your knowledge of lists, to display a map and draw a 3D room image.
DISPLAYING MAPS WITH LOOPS In the Escape game, we’ll use loops extensively. Often, we’ll use them to pull information from a list and perform some action on it. Let’s start by using loops to display a text map.
MAKING THE ROOM MAP
MAKING THE ROOM MAP We’ll make a new map for the example in this chapter and use 1 to represent a wall and 0 to represent a floor space. Our room has a wall all the way around the edge and a
pillar near the middle. The pillar is the same as a section of wall, so it’s also marked with a 1. I’ve chosen its position so it looks good when we draw a 3D room later in this chapter. The room doesn’t have any other objects, so we won’t use any other numbers at this time. In IDLE, open a new Python program, and enter the code in Listing 31, saving it as listing31.py: listing31.py room_map = [ [1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 1] ] print(room_map) Listing 31: Adding the room map data This program creates a list called room_map that contains seven other lists. Each list starts and ends with square brackets and is separated from the next list with a comma. As you learned in Chapter 2, the last list doesn’t need a comma after it. Each list represents a row of the map. Run the program by clicking Run ▸ Run Module and you should see the following in the shell window: [[1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 1]] As you saw in Chapter 2, printing the map list shows you all the rows run together, which isn’t a useful way to view a map. We’ll use a loop to display the map in a way that is much easier to read.
DISPLAYING THE MAP WITH A LOOP
To display the map in rows and columns, delete the last line of your program and add the two new lines shown in Listing 32. As before, don’t type in the grayedout lines— just use them to find your place in the program. Save your program as listing32.py. listing32.py sn ip [1, 0, 0, 0, 1], [1, 1, 1, 1, 1] ] ➊ for y in range(7): ➋ print(room_map[y]) Listing 32: Using a loop to display the room map
RED ALERT Remember to place a colon at the end of the first new line! The program won’t work without it. The second new line should be indented with four spaces to show Python which instructions you want to repeat. If you add the colon at the end of the for line, the spaces are added automatically for you when you press ENTER to go to the next line.
When you run the program again, you should see the following in the shell: [1, 1, 1, 1, 1] [1, 0, 0, 0, 1] [1, 0, 1, 0, 1] [1, 0, 0, 0, 1] [1, 0, 0, 0, 1] [1, 0, 0, 0, 1] [1, 1, 1, 1, 1] That’s a more useful way to view a map. Now you can easily see that a wall (represented by 1s) runs all around the edge. So how does the code work? The for command ➊ is the engine here. It’s a loop command that tells Python to repeat a piece of code a certain number of times. Listing 32 tells Python to repeat the print() instruction for each item
in our room_map list ➋. Each item in room_map is a list containing one row of the map, so printing them separately displays our map one row at a time, resulting in this organized display. Let’s break down the code in more detail. We use the range() function to create a sequence of numbers. With range(7), we tell Python to generate a sequence of numbers up to, but not including, 7. Why does it leave out the last number? That’s just how the ra ng e( ) function works! If we give the range() function just one number, Python assumes
we want to start counting at 0. So range(7) creates the sequence of numbers 0, 1, 2, 3, 4, 5, and 6. Each time the code repeats, the variable in the for command takes the next item from the sequence. In this case, the y variable takes on the values 0, 1, 2, 3, 4, 5, and 6 in turn. This matches the index numbers in room_map perfectly. I’ve chosen y as the variable name because we’re using it to represent which map row we want to display, and the row on the map is referred to as the ycoordinate. The print(room_map[y]) command ➋ is indented four spaces, telling Python that this is the chunk of code we want our for loop ➊ to repeat. The first time through the loop, y has a value of 0, so print(room_map[y]) prints the first item in room_map, which is a list containing the data for the first row of the map. The second time through, y has a value of 1, so print(room_map[y]) prints the second row. The code repeats until it’s printed all seven lists inside room_map.
TRAINING MISSION #1 In an emergency situation on the space station, you might need to issue a distress signal. Write a simple program to print the word Mayday! three times only, using a loop. If you’re stuck, start with Listing 32, used for printing the map. You just need to change what the program prints and how many times it loops the print code.
LOOP THE LOOP Our map output is getting better, but it still has a couple of limitations. One is that the
commas and brackets make it look cluttered. The other limitation is that we can’t do anything with the individual wall panels or spaces in the room. We’ll need to be able to handle whatever is at each position in the room separately, so we can display its image correctly. To do that, we’ll need to use more loops.
NESTING LOOPS TO GET ROOM COORDINATES The listing32.py program uses a loop to extract each row of the map. Now we need to use another loop to examine each position in the row, so we can access the objects there individually. Doing so will enable us to have full control over how the items are displayed. You just saw that we can repeat a piece of code inside a loop. We can also put a loop inside another loop, which is known as a nested loop. To see how this works, we’ll first use this technique to print the coordinates for each space in the room. Edit your code to match Listing 33: listing33.py snip [1, 0, 0, 0, 1], [1, 1, 1, 1, 1] ] ➊ for y in range(7): ➋ for x in range(5): ➌ print("y=", y, "x=", x) ➍ print() Listing 33: Printing the coordinates
RED ALERT As every astronaut knows, space can be dangerous. Spaces can, too. If the indentation in a loop is wrong, the program won’t work correctly. Indent the first print() command ➌ with eight spaces so it’s part of the inner x loop. Make sure the final print() instruction ➍ is lined up with the second for command ➋ (with four spaces of indentation) so it stays in the outer loop. When you start a new line, Python indents it the same as the previous one, but you can delete the indentation when you no longer need it.
Save your program as listing33.py and run the program by clicking Run ▸ Run Module. You’ll see the following output: y= 0 x= 0 y= 0 x= 1 y= 0 x= 2 y= 0 x= 3 y= 0 x= 4 y= 1 x= 0 y= 1 x= 1 y= 1 x= 2 y= 1 x= 3 y= 1 x= 4 y= 2 x= 0 y= 2 x= 1 y= 2 x= 2 s ni p The output continues and ends on y=6x=4. We’ve set up the y loop the same as before so it repeats seven times ➊, once for each number from 0 to 6, putting that value into the y variable. This is what is different in our program this time: inside the y loop, we start a new for loop that uses the x variable and gives it a range of five values, from 0 to 4 ➋. The first time through the y loop, y is 0, and x then takes the values 0, 1, 2, 3, and 4 in turn while y is 0. The second time through the y loop, y is 1. We start a new x loop, and it takes the values 0, 1, 2, 3, and 4 again while y is 1. This looping keeps going until y is 6 and x is 4. You can see how the loops work when you look at the program’s output: inside the x loop, we print the values for y and x each time the x loop repeats ➌. When the x loop finishes, we print a blank line ➍ before the next repeat of the y loop. We do this by leaving the print() function’s parentheses empty. The blank line breaks up where the y loop repeats, and the output shows you what the values of x and y are each time through the inner x loop. As you can see, this program outputs the y and xcoordinates of every position in the room.
TIP We’ve used the variable names y and x in our loops, but those variable names don’t affect the way the program runs. You could call them sausages and eggs, and the program would work just the same. It wouldn’t be as easy to understand, though. Because we’re getting x and ycoordinates, it makes sense to use x and y for our variable names.
CLEANING UP THE MAP We’ll use the coordinates in the loops to print our map without any brackets and commas. Edit your program to change the inner nested loop as shown in Listing 34: listing34.py s n i p for y in range(7): for x in range(5): print(room_map[y][x], end="") print() Listing 34: Tidying up the map display Save your program as listing34.py and run the program by clicking Run ▸ Run Module. You should see the following in the shell: 11111 10001 10101 10001 10001 10001 11111 That map is much cleaner and easier to understand. It works by going through the coordinates in the same way the program in Listing 33 did. It takes each row in turn using the y loop, and then uses the x loop to get each position in that row. This time,
instead of printing the coordinates, we look at what is in the room_map at each position, and print that. As you learned in Chapter 2, you can pull any item out of the map using coordinates in the form room_map[ycoordinate][xcoordinate]. The way we’ve formatted the output means the map resembles the room: we put all the numbers from one row together, and only start a new line on the screen when we start a new row of the map (a new repeat of the y loop). The print() instruction inside the x loop finishes with end="" (with no space between the quote marks) to stop it from starting a new line after each number. Otherwise, by default, the print() function would end each piece of output by adding a code that starts a new line. But instead, we tell it to put nothing ("") at the end. As a result, all the items from one complete run of the x loop (from 0 to 4) appear on the same line. After each row is printed, we use an empty print() command to start a new line. Because we indent this command with only four spaces, it belongs to the y loop and is not part of the code that repeats in the x loop. That means it runs only once each time through the y loop, after the x loop has finished printing a row of numbers.
TRAINING MISSION #2 The final print() command is indented using four spaces. See what happens when you indent it eight spaces, and then see what happens if you don’t indent it at all. In each case, record how many times it runs and how the indentation changes the output.
DISPLAYING A 3D ROOM IMAGE You now know enough about maps to display a 3D room image. In Chapter 1, you learned how to use Pygame Zero to place images on the screen. Let’s combine that knowledge with your newfound skills in getting data from the room_map, so we can display our map with images instead of 0s and 1s. Click File ▸ New File to start a new file in Python, and then enter the code in Listing 35. You can copy the room_map data from your most recent program for this chapter. listing35.py
room_map = [ [1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 1] ] ➊ WIDTH = 800 # window size ➋ HEIGHT = 800 top_left_x = 100 top_left_y = 150 ➌ DEMO_OBJECTS = [images.floor, images.pillar] room_height = 7 room_width = 5 ➍ def draw(): for y in range(room_height): for x in range(room_width): ➎ image_to_draw = DEMO_OBJECTS[room_map[y][x]] ➏ screen.blit(image_to_draw, (top_left_x + (x*30), top_left_y + (y*30) image_to_draw.get_height())) Listing 35: Code for displaying the room in 3D Save the program as listing35.py. You need to save it in your escape folder, because the program will use the files inside the images folder stored there. Don’t save your file inside the images folder: your file should be alongside it instead. If you haven’t downloaded the Escape game files yet, see “Downloading the Game Files” on page 7 for download instructions. The listing35.py program uses Pygame Zero, so you need to go to the command line and enter the instruction pgzrunlisting3-5.py to run the program. See “Running the Game” on page 9 for advice on running programs that use Pygame Zero, including the final Escape game.
The listing35.py program uses the Escape game’s image files to create an image of a room. Figure 31 shows the room with its single pillar. The Escape game uses a simplified 3D perspective where we can see the front and top surfaces of an object. Objects at the front and back of the room are drawn at the same size. When you created the spacewalk simulator in Chapter 1, you saw how the order in which objects are drawn determines which ones are in front of the others. In the Escape game and Listing 35, the objects are drawn from the back of the room to the front, enabling us to create the 3D effect. Objects nearer to the viewer (sitting at their computer) appear to be in front of those at the back of the room.
Figure 31: Your first 3D room (left) and the same room with the parts labeled (right)
UNDERSTANDING HOW THE ROOM IS DRAWN Now let’s look at how the listing35.py program works. Much of the program will be familiar to you from Chapters 1 and 2 . The WIDTH ➊ and HEIGHT ➋ variables hold the size of the window, and we use the draw() function to tell Pygame Zero what to draw onscreen ➍. The y and x loops come from Listing 34 earlier in this chapter and give us coordinates for each space in the room. Instead of using numbers in the range() functions to tell Python how many times to repeat our y and x loops, we’re using the new variables room_height and room_width. These variables store the size of our room map and tell Python how many times to repeat the loops. For example, if we changed the room_height variable to 10, the y loop would repeat 10 times and work through 10 rows of the map. The room_width variable controls how
many times the x loop repeats in the same way, so we can display wider rooms.
RED ALERT If you use room widths and heights that are bigger than the actual room_map data, you’ll cause an error.
The listing35.py program uses two images from the images folder: a floor tile (with the filename floor.png) and a wall pillar (called pillar.png), as shown in Figure 32. A PNG (Portable Network Graphics) is a type of image file that Pygame Zero can use. PNG enables parts of the image to be seethrough, which is important for our 3D game perspective. Otherwise, we wouldn’t be able to see the background scenery through the gaps in a plant, for example, and the astronaut would look like they had a square halo around them.
Figure 32: The images used to make your first 3D room Inside the draw() function ➍, we use y and x loops to look at each position in the room map in turn. As you saw earlier, we can find the number at each position in the map by accessing room_map[y][x]. In this map, that number will be either 1 for a wall pillar or 0 for an empty floor space. Instead of printing the number onscreen, as we did before, we use the number to look up an image of the item in the DEMO_OBJECTS list ➎. That list contains our two images ➌: the floor tile is at index position 0, and the wall pillar is at index position 1. If the room_map contains a 1 at the position we’re looking at, for example, we’ll take the item at list index 1 in the DEMO_OBJECTS list, which is the wall pillar image. We store that image in the variable image_to_draw ➎. We then use screen.blit() to draw this image onscreen, giving it the x and y coordinate of the pixel on the screen where we want to draw it ➏. This instruction extends over three
lines to make it easier to read. The amount of indentation on the second and third lines doesn’t matter, because these lines are surrounded by the screen.blit() parentheses.
WORKING OUT WHERE TO DRAW EACH ITEM To figure out where to draw each image that makes up the room, we need to do a calculation at ➏. We’ll look at how that calculation works, but before we do, I’ll explain how the space station was designed. All the images are designed to fit a grid. The units we use for measuring images on a computer are called pixels and are the size of the smallest dot you can see on your screen. We’ll call each square of the grid a tile. Each tile is 30 pixels across the screen and 30 pixels down the screen. It’s the same size as one floor tile. We position objects in terms of tiles, so a chair might be 4 tiles down and 4 tiles across, measured from the topleft corner. Figure 33 shows the room we’ve just created with a grid laid on top. Each floor tile and pillar is one tile wide. The pillar is tall, so it covers three tile spaces: the front surface of the wall pillar is two tiles tall, and the top surface of the pillar covers another tile space.
Figure 33: The tile grid overlaid on your first room The top_left_x and top_left_y variables store the coordinates at which we want to start drawing the first image of the room in the window. We never change these variables in this chapter. I’ve chosen to start drawing where x is 100 and y is 150 so we have a bit of a border around the room image. To work out where to draw a piece of wall or floor, we need to convert our map positions (which range from 0 to 4 in the x direction, for example) into pixel positions in
the window. Each tile space is 30 pixels square, so we multiply the x loop number by 30 and add it to the top_left_x position to get the xcoordinate for the image. In Python, the * symbol is for multiplication. The top_left_x value is 100, so the first image is drawn at 100+(0*30), which is 100. The second image is drawn at 100+(1*30), which is 130, one tile position to the right of the first. The third image is drawn at 100+(2*30), which is 160. These positions ensure that the images sit perfectly side by side. The y position is calculated in a similar way. We use top_left_y as the starting position vertically and add y*30 to it to make the images join together precisely. The difference is that we subtract the height of the image we’re drawing, so we ensure that the images align at the same point at the bottom. As a result, tall objects can rise out of a tile space and obscure any scenery or floor tiles behind them, making the room display look threedimensional. If we didn’t align the images at the bottom, they would all align at the top, which would destroy the 3D effect. The second and third rows of floor tiles would cover up the front surface of the back wall, for example.
TRAINING MISSION #3 Now that you know how to display a 3D room, try to adjust the map to change the room layout, adding new pillars or floor spaces. You can edit the room_map data to add new rows or columns to the map. Remember to change
the room_height and room_width variables too. Perhaps try making a room with more rows and adding a doorway by replacing the 1s used for pillars with 0s. In the final Escape game, each doorway will be three spaces. For best results, design rooms with an odd width and height so you can center the door in the wall. Figure 34 shows a room I designed with a width and height of 9. You can try copying my design if you like. I’ve added a grid to make it easier to work out the data for the room_map list. The wall pillars rise two tiles out of the floor, so the grid shown is 11 tiles high. Look at the bottoms of the pillars, not the tops, to work out where to position them. See the end of the chapter for the code to make this room.
Figure 34: One possible new room design
In the real Escape game, the tall wall pillars will only be used at the edges of the rooms. They can look a bit odd in the middle of the room, especially if they touch the back wall. When we add shadows to the game later in the book, objects in the middle of the room won’t look like they’re floating in space, which is a risk of this way of simulating a 3D perspective.
ARE YOU FIT TO FLY? Check the following boxes to confirm that you’ve learned the key lessons in this chapter.
The for loop repeats a section of code a set number of times.
The range() function creates a sequence of numbers.
You can use range() to tell a for loop how many times to repeat.
The colon at the end of the for line is essential.
To show Python which lines to repeat in the loop, indent the lines using four spaces.
A loop inside another loop is called a nested loop.
Images are aligned at the bottom to create a 3D illusion with tall objects rising up from the floor. The room_height and room_width variables store the room size in Escape and are used to set up the loop that displays the room.
4
History
Topics
CREATING THE SPACE STATION
Tutorials
Offers & Deals
Highlights
Settings Support
SignIn Out this chapter, you’ll build the map for your space station on Mars. Using the simple
Explorer code that you’ll add in this chapter, you’ll be able to look at the walls of each room and start to find your bearings. We’ll use lists, loops, and the techniques you learned in Chapters 1, 2 , and 3 to create the map data and display the room in 3D.
AUTOMATING THE MAP MAKING PROCESS
The problem with our current room_mapdata is that there’s a lot of it. The Escape game includes 50 locations. If you had to enter room_mapdata for every location, it would take ages and be hugely inefficient. As an example, if each room consisted of 9 × 9 tiles, we would have 81 data items per room, or 4,050 data items in total. Just the room data would take up 10 pages of this book. Much of that data is repeated: 0s mark the floor and exits, and 1s mark the walls at the edges. You know from Chapter 3 that we can use loops to efficiently manage repetition.
We can use that knowledge to make a program that will generate the room_mapdata automatically when we give it certain information, such as the room size and the location of the exits.
HOW THE AUTOMATIC MAP MAKER WORKS
HOW THE AUTOMATIC MAP MAKER WORKS The Escape program will work like this: when the player visits a room, our code will take the data for that room (its size and exit positions) and convert it into the room_map data. The room_map data will include columns and rows that represent the floor, walls around the edge, and gaps where the exits should be. Eventually, we’ll use the room_map data to draw the room with the floor and walls in the correct place. Figure 41 shows the map for the space station. I’ll refer to each location as a room, although numbers 1 to 25 are sectors on the planet surface within the station compound, similar to a garden. Numbers 26 to 50 are rooms inside the space station. The indoor layout is a simple maze with many corridors, deadends, and rooms to explore. When you make your own maps, try to create winding paths and corners to explore, even if the map isn’t very big. Be sure to reward players for their exploration by placing a useful or appealing item at the end of each corridor. Players also often feel more comfortable travelling from left to right as they explore a game world, so the player’s character will start on the left of the map in room 31. Outside, players can walk anywhere, but a fence will stop them from leaving the station compound (or wandering off the game map). Due to the claustrophobic atmosphere inside the space station, players will experience a sense of freedom outside with space to roam.
Figure 41: The space station map When you’re playing the final Escape game, you can refer to this map, but you might find it more enjoyable to explore without a map or to make your own. This map doesn’t show where the doors are, which in the final game will stop players from accessing some parts of the map until they find the right key cards.
CREATING THE MAP DATA Let’s create the map data. The rooms in our space station will all join up, so we only need to store the location of an exit from one side of the wall. For instance, an exit on the right of room 31 and an exit on the left of room 32 would be the same doorway connecting the two rooms. We don’t need to specify that exit for both rooms. For each room in the map, we’ll store whether it has an exit at the top or on the right. The program can work out on its own whether an exit exists at the bottom or on the left (as I’ll explain shortly). This approach also ensures that the map is consistent and no exits seem to vanish after you walk through them. If you can go one way through an exit, you can always go back the other way. Each room in the map needs the following data: A short description of the room. Height in tiles, which is the size of the room from top to bottom on the screen. (This has nothing to do with the distance from floor to ceiling.) Width in tiles, which is the size of the room from left to right across the screen. Whether or not there is an exit at the top (True or False). Whether or not there is an exit on the right (True or False).
TIP Tr ue and False are known as Boolean values. In Python, these values must
start with a capital letter, and they don’t need quotes around them, because they’re not strings.
We call the unit we use to measure the room size a tile because it’s the same size as a floor tile. As you learned in Chapter 3, a tile will be our basic unit of measurement for all objects. For instance, a single object in the room, such as a chair or a cabinet, will often be the size of one tile. In Chapter 3 (see Figure 31 and Listing 35), we made a room map that had seven rows with five list items in each row, so that room would be seven tiles high and five tiles wide. Having rooms of different sizes adds variety to the map: some rooms can be narrow like corridors, and some can be expansive like community rooms. To fit in our game window, the maximum size of a room is 15 tiles high by 25 tiles wide. Large rooms or rooms with lots of objects in them might run more slowly on older computers, though. Here’s an example of the data for room 26: it’s a narrow room 13 tiles high and 5 tiles wide with an exit at the top but none to the right (see the map in Figure 41). ["The airlock", 13, 5, True, False] We give the room a name (or description), numbers for the height and width respectively, and True and False values for whether the top and right edges have an exit. In this game, each wall can have only one exit, and that exit will be automatically positioned in the middle of the wall. When the program makes the room_map data for room 27 next door, it will check room 26 to see whether it has an exit on the right. Because room 26 has no exit on the right, the program will know that room 27 has no left exit. We’ll store the lists of data for each room in a list called GAME_MAP.
WRITING THE GAME_MAP CODE Click File ▸ New File to start a new file in Python. Enter the code from Listing 41 to start building the space station. Save your listing as listing41.py.
TIP Remember to save your work regularly when you’re typing a long program. As in many applications, you can press CTRLS to save in IDLE.
listing41.py # Escape A Python Adventure # by Sean McManus / www.sean.co.uk # Typed in by PUT YOUR NAME HERE import time, random, math ############### ## VARIABLES ## ############### WIDTH = 800 # window size HEIGHT = 800 #PLAYER variables ➊ PLAYER_NAME = "Sean" # change this to your name! FRIEND1_NAME = "Karen" # change this to a friend's name! FRIEND2_NAME = "Leo" # change this to another friend's name! current_room = 31 # start room = 31 ➋ top_left_x = 100 top_left_y = 150 ➌ DEMO_OBJECTS = [images.floor, images.pillar, images.soil] ############### ## MAP ## ############### ➍ MAP_WIDTH = 5 MAP_HEIGHT = 10 MAP_SIZE = MAP_WIDTH * MAP_HEIGHT ➎ GAME_MAP = [ ["Room 0 where unused objects are kept", 0, 0, False, False] ] outdoor_rooms = range(1, 26) ➏ for planetsectors in range(1, 26): #rooms 1 to 25 are generated here GAME_MAP.append( ["The dusty planet surface", 13, 13, True, True] )
➐ GAME_MAP += [ #["Room name", height, width, Top exit?, Right exit?] ["The airlock", 13, 5, True, False], # room 26 ["The engineering lab", 13, 13, False, False], # room 27 ["Poodle Mission Control", 9, 13, False, True], # room 28 ["The viewing gallery", 9, 15, False, False], # room 29 ["The crew's bathroom", 5, 5, False, False], # room 30 ["The airlock entry bay", 7, 11, True, True], # room 31 ["Left elbow room", 9, 7, True, False], # room 32 ["Right elbow room", 7, 13, True, True], # room 33 ["The science lab", 13, 13, False, True], # room 34 ["The greenhouse", 13, 13, True, False], # room 35 [PLAYER_NAME + "'s sleeping quarters", 9, 11, False, False], # room 36 ["West corridor", 15, 5, True, True], # room 37 ["The briefing room", 7, 13, False, True], # room 38 ["The crew's community room", 11, 13, True, False], # room 39 ["Main Mission Control", 14, 14, False, False], # room 40 ["The sick bay", 12, 7, True, False], # room 41 ["West corridor", 9, 7, True, False], # room 42 ["Utilities control room", 9, 9, False, True], # room 43 ["Systems engineering bay", 9, 11, False, False], # room 44 ["Security portal to Mission Control", 7, 7, True, False], # room 45 ➑ [FRIEND1_NAME + "'s sleeping quarters", 9, 11, True, True], # room 46 [FRIEND2_NAME + "'s sleeping quarters", 9, 11, True, True], # room 47 ["The pipeworks", 13, 11, True, False], # room 48 ["The chief scientist's office", 9, 7, True, True], # room 49 ["The robot workshop", 9, 11, True, False] # room 50 ] # simple sanity check on map above to check data entry ➒ assert len(GAME_MAP)1 == MAP_SIZE, "Map size and GAME_MAP don't match" Listing 41: The GAME_MAP data Let’s take a closer look at this code for setting out the room map data. Keep in mind that as we build the Escape game, we’ll keep adding to the program. To help you find your way around the program, I’ll mark the different sections with headings like this:
############### ## VARIABLES ## ############### The # symbol marks a comment and tells Python to ignore anything after it on the same line, so the game will work with or without these comments. The comments will make it easier to figure out where you are in the code and where you need to add new instructions as the program gets bigger. I’ve drawn boxes using the comment symbols to make the headings stand out as you scroll through the program code. Three astronauts are based on the space station, and you can personalize their names in the code ➊. Change the PLAYER_NAME to your own, and add the names of two friends for the FRIEND1_NAME and FRIEND2_NAME variables. Throughout the code, we’ll use these variables wherever we need to use the name of one of your friends: for example, each astronaut has their own sleeping quarters. We need to set up these variables now because we’ll use them to set up some of the room descriptions later in this program. Who will you take with you to Mars? The program also sets up some variables we’ll need at the end of this chapter to draw our room: the top_left_x and top_left_y variables ➋ specify where to start drawing the room; and the DEMO_OBJECTS list contains the images to use ➌. First, we set up variables to contain the height, width, and overall size of the map in tiles ➍. We create the GAME_MAP list ➎ and give it the data for room 0: this room is for storing items that aren’t in the game yet because the player hasn’t discovered or created them. It’s not a real room the player can visit. We then use a loop ➏ to add the same data for each of the 25 planet surface rooms that make up the grounds of the compound. The range(1,26) function is used to repeat 25 times. The first number is the one we want to start at, and the second is the number we want to finish at plus one (range() doesn’t include the last number you give it, remember). Each time through the loop, the program adds the same data to the end of GAM E_MA P because all the planet surface “rooms” are the same size and have exits in every
direction. The data for every surface room looks like this: ["The dusty planet surface", 13, 13, True, True] When this loop finishes, GAME_MAP will include room 0 and also have the same “dusty planet surface” data for rooms 1 to 25. We also set up the outdoor_rooms range to store the
room numbers 1 to 25. We’ll use this range when we need to check whether a room is inside or outside the space station. Finally, we add rooms 26 to 50 to GAME_MAP ➐. We do this by using += to add a new list to the end of GAME_MAP. That new list includes the data for the remaining rooms. Each of these rooms will be different, so we need to enter the data for them separately. You saw the information for room 26 earlier: the data contains the room name, its height and width, and whether it has exits at the top and the right. Each piece of room data is a list, so it has square brackets at the start and end. At the end of each piece of room data (except the last one), we must use a comma to separate it from the next one. I’ve also put the room number in a comment at the end of each line to help keep track of the room numbers. These comments will be helpful as you develop the game. It’s good practice to annotate your code like this so you can understand it when you revisit it. Rooms 46 and 47 add the variables FRIEND1_NAME and FRIEND2_NAME to the room description, so you have two rooms called something like “Karen’s sleeping quarters,” using your friends’ names ➑. As well as using the + symbol to add numbers and combine lists, you can also use it to combine strings. At the end of listing41.py, we perform a simple check using assert() to make sure the map data makes sense ➒. We check whether the length of the GAME_MAP (the number of rooms in the map data) is the same as the size of the map, which we calculated at ➍ by multiplying its width by its height. If it’s not, it means we’re missing some data or have too much. We have to subtract 1 from the length of GAME_MAP because it also includes room 0, which we didn’t include when we calculated the map size. This check won’t catch all errors, but it can tell you whether you missed a line of the map data when entering it. Wherever possible, I’ll try to include simple tests like this to help you check for any errors as you enter the program code.
TESTING AND DEBUGGING THE CODE Run listing41.py by clicking Run ▸ Run Module or press F5 (the keyboard shortcut). Nothing much should happen. The shell window should just display a message that says "RESTART:" together with your filename. The reason is that all we’ve asked the program to do is set up some variables and a list, so there is nothing to see. But if you made a mistake entering the listing, you might also see a red error message in the shell window. If you do get an error, doublecheck the following details:
Are the quote marks in the right place? Strings are in green in the Python program window, so look for large areas of green, which suggest you didn’t close your string. If room descriptions are in black, you didn’t open the string. Both indicate a missing quote mark. Are you using the correct brackets and parentheses in the proper places? In this listing, square brackets surround list items, and parentheses (curved brackets) are used for functions, such as range() and append(). Curly brackets {…} are not used at all. Are you missing any brackets or parentheses? A simple way to check is to count the number of opening and closing brackets and parentheses. Every opening bracket or parenthesis should have a closing bracket or parenthesis of the same shape. You have to close brackets and parentheses in the reverse order of how you opened them. If you have an opening parenthesis and then an opening square bracket, you must close them first with a closing square bracket and then a closing parenthesis. This format is correct: ( [ … ] ). This format is wrong: ( [ … ) ]. Are your commas in the correct place? Remember that each list for a room in GAME_MA P must have a comma after the closing square bracket to separate it from the
next room’s data (except for the last room).
TIP Why not ask a friend to help you build the game? Programmers often work in pairs to help each other with ideas and, perhaps most importantly, have two pairs of eyes checking everything. You can take turns typing too!
GENERATING ROOMS FROM THE DATA Now the space station map is stored in our GAME_MAP list. The next step is to add the function that takes the data for the current room from GAME_MAP and expands it into the room_map list that the Escape game will use to see what’s at each position in the room. The room_map list always stores information about the room the player is currently in. When
the player enters a different room, we replace the data in room_map with the map of the new room. Later in the book, we’ll add scenery and props to the room_map, so the player
has items to interact with too. The room_map data is made by a function we’ll create called generate_map(), shown in Listing 42. Add the code in Listing 42 to the end of Listing 41. The grayed out code shows you where Listing 41 ends. Make sure all the indentation is correct. The indentation determines whether code belongs to the get_floor_type() or generate_map() function, and some code is indented further to tell Python which if or for command it belongs to. Save your program as listing42.py and click Run ▸ Run Module to run it and check for any error messages in the shell.
RED ALERT Don’t start a new program with the code in Listing 42: make sure you add Listing 42 to the end of Listing 41. As you follow along in this book, you’ll increasingly add to your existing program to build the Escape game.
listing42.py sn ip # simple sanity check on map above to check data entry assert len(GAME_MAP)1 == MAP_SIZE, "Map size and GAME_MAP don't match"
############### ## MAKE MAP ## ############### ➊ def get_floor_type(): if current_room in outdoor_rooms: return 2 # soil else: return 0 # tiled floor def generate_map():
# This function makes the map for the current room, # using room data, scenery data and prop data. global room_map, room_width, room_height, room_name, hazard_map global top_left_x, top_left_y, wall_transparency_frame ➋ room_data = GAME_MAP[current_room] room_name = room_data[0] room_height = room_data[1] room_width = room_data[2] ➌ floor_type = get_floor_type() if current_room in range(1, 21): bottom_edge = 2 #soil side_edge = 2 #soil if current_room in range(21, 26): bottom_edge = 1 #wall side_edge = 2 #soil if current_room > 25: bottom_edge = 1 #wall side_edge = 1 #wall # Create top line of room map. ➍ room_map=[[side_edge] * room_width] # Add middle lines of room map (wall, floor to fill width, wall). ➎ for y in range(room_height 2): room_map.append([side_edge] + [floor_type]*(room_width 2) + [side_edge]) # Add bottom line of room map. ➏ room_map.append([bottom_edge] * room_width) # Add doorways. ➐ middle_row = int(room_height / 2) middle_column = int(room_width / 2) ➑ if room_data[4]: # If exit at right of this room room_map[middle_row][room_width 1] = floor_type room_map[middle_row+1][room_width 1] = floor_type room_map[middle_row1][room_width 1] = floor_type ➒ if current_room % MAP_WIDTH != 1: # If room is not on left of map
room_to_left = GAME_MAP[current_room 1] # If room on the left has a right exit, add left exit in this room if room_to_left[4]: room_map[middle_row][0] = floor_type room_map[middle_row + 1][0] = floor_type room_map[middle_row 1][0] = floor_type ➓ if room_data[3]: # If exit at top of this room room_map[0][middle_column] = floor_type room_map[0][middle_column + 1] = floor_type room_map[0][middle_column 1] = floor_type if current_room >> print([1] * 10) [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] The [1] in the print() instruction is a list that contains just one item. When we multiply it by 10, we get a list that contains that item 10 times. In our program, we multiply the edge type we’re using by the width of the room ➍. If the top edge has an exit in it, we’ll add that shortly. The middle rows of the room are made using a loop ➎ that adds each row in turn to the end of room_map. All the middle rows in a room are the same and are made up of the following: 1. An edge tile (either wall or soil) for the left side of the room. 2. The floor in the middle. We can use our shortcut again here. We multiply the fl oo r_ t yp e by the size of the space in the middle of the room. That is the room_width
minus 2 because there are two edge spaces. 3. The edge piece at the right side. The bottom line is then added ➏ and is generated in the same way as the top line.
ADDING EXITS Next, we add exits in the walls where required. We’ll put the exits in the middle of the walls, so we start by figuring out where the middle row and middle column are ➐ by
dividing the room height and width by 2. Sometimes this calculation results in a number with a decimal. We need a whole number for our index positions, so we use the int () function to remove the decimal part ➐. The int() function converts a decimal
number into a whole number (an integer). We check for a right exit first ➑. Remember that room_data contains the information for this room, which was originally taken from GAME_MAP. The value room_data[4] tells us whether there is an exit on the right of this room. This instruction: if room_data[4]: is shorthand for this instruction: if room_data[4] == True: We use == to check whether two things are the same. One reason that Boolean values are often a great choice to use for your data is that they make the code easier to read and write, as this example shows. When there is a right exit, we change three positions in the middle of the right wall from the edge type to the floor type, making a gap in the wall there. The value room_width1 finds the x position on the right edge: we subtract 1 because index numbers start at 0.
In Figure 42, for example, you can see that the room width is 11 tiles, but the index position for the right wall is 10. On the planet surface, this code doesn’t change anything, because there’s no wall there to put a gap in. But it’s simpler to let the program add the floor tiles anyway so we don’t have to write code for special cases. Before we check whether we need an exit for the left wall, we make sure the room isn’t on the left edge of the map where there can be no exit ➒. The % operator gives us the remainder when we divide one number by another. If we divide the current room number by the map width, 5, using the % operator, we’ll get a 1 if the room is on the left edge. The left edge room numbers are 1, 6, 11, 16, 21, 26, 31, 36, 41, and 46. So we only continue checking for a left exit if the remainder is not 1 (!= means “is not equal to”). To see whether we need an exit on the left in this room, we work out which room is on the other side of that wall by subtracting 1 from the current room number. Then we check whether that room has a right exit. If so, our current room needs a left exit, and we add it.
The exits at the top and bottom are added in a similar way ➓. We check room_data directly to see whether there’s an exit at the top of the room, and if so, we add a gap in that wall. We can check the room below as well to see whether there should be a bottom exit in the room.
TESTING THE PROGRAM When you run the program, you can confirm that you don’t see any errors in the Python shell. You can also check that the program is working by generating the map and then printing it from the shell, like this: >>> generate_map() >>> print(room_map) [[1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]] The current_room variable is set by default to be room 31, the starting room in the game, so that is the room_map data that prints. From our GAME_MAP data (and Figure 42) we can see that this room has 7 rows and 11 columns, and our output confirms that we have 7 lists, each containing 11 numbers: perfect. What’s more, we can see that the first row features four wall pillars, three empty spaces, and then four more wall pillars, so the function has put an exit here as we would expect. Three of the lists have a 0 as their last number too, indicating an exit on the right. It looks like the program is working!
TRAINING MISSION #1 You can change the value of current_room from the shell to print a different room. Try entering different values for the room, regenerating the map, and printing it. Check the output against the map and the GAME_MAP code to make sure the results match what you expect. Here is one example: >>> current_room = 45 >>> generate_map() >>> print(room_map) [[1, 1, 0, 0, 0, 1, 1], [1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 1], [1, 0, 0, 0, 0, 0, 1],
[1, 1, 0, 0, 0, 1, 1]] What happens when you enter a value for one of the planet surface rooms?
EXPLORING THE SPACE STATION IN 3D Let’s turn our room maps into rooms! We’ll combine the code we created for turning room maps into 3D rooms in Chapter 3 with our code for extracting the room map from the game map. Then we can tour the space station and start to get our bearings. The Explorer feature of our program will enable us to view all the rooms on the space station. We’ll give it its own EXPLORER section in the program. It’s a temporary measure to enable us to quickly see results. We’ll replace the Explorer with better code for viewing rooms in Chapters 7 and 8 . Add the code in Listing 43 to the end of your program for Listing 42, after the instructions shown in gray. Then save the program as listing43.py. Remember to save it with your other programs for this book in the escape folder so the images folder is in the right place (see “Downloading the Game Files” on page 7). listing43.py room_map[room_height1][middle_column] = floor_type room_map[room_height1][middle_column + 1] = floor_type room_map[room_height1][middle_column 1] = floor_type ############### ## EXPLORER ## ############### def draw(): global room_height, room_width, room_map ➊ generate_map() screen.clear() ➋ for y in range(room_height): for x in range(room_width): image_to_draw = DEMO_OBJECTS[room_map[y][x]]
screen.blit(image_to_draw, (top_left_x + (x*30), top_left_y + (y*30) image_to_draw.get_height())) ➌ def movement(): global current_room old_room = current_room if keyboard.left: current_room = 1 if keyboard.right: current_room += 1 if keyboard.up: current_room = MAP_WIDTH if keyboard.down: current_room += MAP_WIDTH ➍ if current_room > 50: ➎ current_room = 50 if current_room ) 50 ➍ it’s reset to 50 ➎. In this code, I’ve lowered the sensitivity
of the keys to reduce the risk of whizzing through the rooms too fast. If you find the controls unresponsive or sluggish, you might need to press the keys for slightly longer. Explore the space station and compare what you see on screen with the map in Figure 41. If you see any errors, go back to the GAME_MAP data to check the data, and then take another look at the generate_map() function to make sure it’s been entered correctly. To help you follow the map, when you move to a new room, its number will appear in the command line window where you entered the pgzrun command, as shown in Figure 45.
Figure 45: The command line window tells you which room you’re entering. Also, check that exits exist from both sides: if you go through a door and it isn’t there when you look from the other side, generate_map() has been entered incorrectly. Follow along on the map first to make sure you’re not going off the edge of the map and coming back on the other side before you start debugging. It’s worth taking the time to make sure your map data and functions are all correct at this point, because broken map data can make it impossible to complete the Escape game!
TRAINING MISSION #2 To enjoy playing Escape and solving the puzzles, I recommend that you use the data I’ve provided for the game map. It’s best not to change the data until you’ve completed playing the game and have decided to redesign it. Otherwise, objects might be in locations you can’t reach, making the game impossible to complete. However, you can safely extend the map. The easiest way to do so is to add another row of rooms at the bottom of the map, making sure a door connects at least one of the new rooms to the existing bottom row of the map. Remember to change the MAP_HEIGHT variable. You’ll also need to change the number 50 in the Explorer code (listing43.py) to your highest room number
(see ➍ and ➎). Why not add a corridor now?
MAKING YOUR OWN MAPS After you’ve finished building and playing Escape, you can customize the map or design your own game layouts using this code. If you want to add your own map data for rooms 1 to 25, delete the code that generates their data automatically (see ➏ in Listing 41). You can then add your own data for these rooms. Alternatively, if you don’t want to use the planet surface locations, just block the exit to them. The exit onto the planet surface is in room 26. Change that room’s entry in the GAM E_MA P list so it doesn’t have a top exit. You can use room numbers starting at room 26
and extend the map downward to make a game that is completely indoors. As a result, you won’t need to make any code changes to account for the planet surface. If you remove a doorway from the Escape game map (including the one in room 26), you might also need to remove a door. Some of the exits at the top and bottom of the room will have doors that seal them off. (We’ll add doors to the Escape game in Chapter 11.)
ARE YOU FIT TO FLY? Check the following boxes to confirm that you’ve learned the key lessons in this chapter.
The GAME_MAP list stores the main map data for Escape.
The GAME_MAP only needs to store the exit at the top and right of a room.
When the player visits a room, the generate_map() function makes the room_map list for the current room. The room_map list describes where the walls and objects are in the room.
Locations 1 to 25 are on the planet surface, and a loop generates their map data. Locations 26 to 50 are the space station rooms, and you need to input their data manually.
We use comments to help us find our way around the Escape program listing.
When adding data using a program in script mode, you can use the shell to check the contents of lists and variables to make sure the program is working correctly. Remember to run the program first to set up the data!
The Explorer code enables you to look at every room in the game map using the arrow keys.
It’s important to make sure the game map matches Figure 41. Otherwise, it might not be possible for players to complete the Escape game. You can use the Explorer program to do this.
History
Topics
5
PREPARING THE SPACE STATION EQUIPMENT
Tutorials
Offers & Deals
Highlights
Settings Support
Now that the space station walls are in place, we can start installing the equipment.
Sign Out
We’ll need detailed information about the different pieces of equipment, including the furniture, survival systems, and experimental machinery. In this chapter, you’ll add information about all the items on the space station, including their images and descriptions. You’ll also experiment with designing your own room and view it using the Explorer program you created in Chapter 4.
CREATING A SIMPLE PLANETS DICTIONARY To store the information about the space station equipment, we’ll use a programming concept called dictionaries. A dictionary is a bit like a list but with a builtin search engine. Let’s take a closer look at how it works.
UNDERSTANDING THE DIFFERENCE BETWEEN A LIST AND A DICTIONARY As with a paper dictionary, you can use a word or phrase to look up information in a Python dictionary. That word or phrase is called the key, and the information linked to the key is called the value. Unlike in a paper dictionary, the entries in a Python dictionary can be in any order. They don’t have to be alphabetical. Python can go
directly to the entry you need, wherever it is. Imagine you have a list that contains information about previous space missions. You could get the first item from that list by using this line: print(mission_info[0]) If mission_info was a dictionary instead of a list, you could use a mission name instead of an index number to get the information on that mission, like this: print(mission_info["Apollo 11"]) The key can be a word or phrase but can also be a number. We’ll start by using words because it’s easier to understand the difference between a list and a dictionary that way.
MAKING AN ASTRONOMY CHEAT SHEET DICTIONARY All astronauts need a good understanding of the solar system, so let’s learn about the planets as we build our first dictionary. We’ll use the planet names as the keys and connect each name to information about that planet. Take a look at Listing 51, which creates a dictionary called planets. When you make a dictionary, you use curly brackets {} to mark the start and end of it, instead of the square brackets you use for a list. Each entry in the dictionary is made up of the key, followed by a colon and then the information for that entry. As with a list, we separate the entries with commas and put double quotes around pieces of text. Open a new file in IDLE (File ▸ New File) and enter the following program. Save it as listing51.py. listing51.py planets = { "Mercury": "The smallest planet, nearest the Sun", "Venus": "Venus takes 243 days to rotate", "Earth": "The only planet known to have native life", "Mars": "The Red Planet is the second smallest planet", "Jupiter": "The largest planet, Jupiter is a gas giant", "Saturn": "The second largest planet is a gas giant",
"Uranus": "An ice giant with a ring system", "Neptune": "An ice giant and farthest from the Sun" } ➊ while True: ➋ query = input("Which planet would you like information on? ") ➌ print(planets[query]) Listing 51: Your first dictionary program This program doesn’t use Pygame Zero, so you can run it by clicking Run ▸ Run Module at the top of the IDLE window. (It will still work if you run it using pgzrun, but it’s easier to use the menu.) When you run the program, it asks you which planet you want information on using the input() builtin function ➋. Try entering Earth or Jupiter for the planet name. Which planet would you like information on? Earth The only planet known to have native life Which planet would you like information on? Jupiter The largest planet, Jupiter is a gas giant Whichever planet name you enter is stored in the variable query. That variable is then used to look up the information for that planet in the planets dictionary ➌. Instead of using an index number inside the square brackets as we did with a list, we use the word we entered to get the information, and that word is stored in the variable query. In Python, we can use a while ➊ loop to repeat a set of instructions. Unlike a for loop, which we use to repeat a certain number of times, a while loop usually repeats until something changes. Often in a game, the while command will check a variable to decide whether to keep repeating instructions. For example, the instruction whilelives>0 could keep a game going until the player runs out of lives. When the lives variable changes to 0, the instructions in the loop would stop repeating. The while True command we use in listing51.py will keep repeating forever, because it means “while True is True,” which is always. For this while True command to work, make sure you capitalize True and place a colon at the end of the line. Under the while command, we use four spaces to indent the instructions that should repeat. Here, we’ve indented the lines that ask you for a planet name and then give you
the planet information, so they’re the instructions that repeat. After you enter a planet name and get the information, the program asks you for another planet name, and another, forever. Or until you stop the program by pressing CTRLC, at least. Although this program works, it isn’t complete yet. You might get an unhelpful error if you enter a planet name that isn’t in the dictionary. Let’s fix the code so it returns a useful message instead.
ERROR-PROOFING THE DICTIONARY When you enter a key that isn’t in the dictionary, you’ll see an error message. Python looks for an exact match. So, if you try to look up something that isn’t in the dictionary or make even a tiny spelling mistake, you won’t get the information you want. Dictionary keys, like variable names, are case sensitive, so if you type earth instead of Ea rt h , the program will crash. If you enter a planet that doesn’t exist, this is what
happens: Which planet would you like information on? Pluto Traceback (most recent call last): File "C:\Users\Sean\Documents\Escape\listing51.py", line 13, in print(planets[query]) KeyError: 'Pluto' >>> Poor Pluto! After 76 years of service, it was disqualified as a planet in 2006, so it’s not in our planets dictionary.
TRAINING MISSION #1 Can you add an entry for Pluto in the dictionary? Pay special attention to the position of the quotes, colon, and comma. You can add it at any position in the dictionary.
When the program looks for an item in the dictionary that isn’t there, it stops the program and drops you back at the Python shell prompt. To avoid this, we need the program to check whether the word entered is one of the keys in the dictionary before it tries to use it.
You can see which keys are in the dictionary by entering the dictionary name followed by a dot and keys(). The technical jargon for this is a method. Roughly speaking, a method is a set of instructions that you can attach to a piece of data using a period. Run the following code in the Python shell: >>> print(planets.keys()) dict_keys(['Mars', 'Pluto', 'Jupiter', 'Earth', 'Uranus', 'Saturn', 'Mercury', 'Neptune', 'Venus']) You might notice something odd here. When I completed Training Mission #1, I added Pluto to the dictionary as the last item. But in this output, it’s in second place in my list of keys. When you add items to a list, they’re placed at the end, but in a dictionary, that is not always the case. It depends on which version of Python you’re using. (The latest version does keep dictionary items in the same order you added them.) As mentioned earlier, the order of the keys in the dictionary doesn’t matter, though. Python figures out where the keys are in the dictionary, so you never need to think about it. To stop the program from crashing when a user asks for information on a planet that isn’t in the dictionary, modify your program with the new lines shown in Listing 52. listing52.py sn ip while True: query = input("Which planet would you like information on? ") ➊ if query in planets.keys(): ➋ print(planets[query]) else: ➌ print("No data available! Sorry!") Listing 52: Error proofing the dictionary lookup Save the program as listing52.py, and run it by clicking Run ▸ Run Module. Check that it works by entering a planet correctly, and then enter another planet that isn’t in the list of keys. Here’s an example: Which planet would you like information on? Venus Venus takes 243 days to rotate Which planet would you like information on? Tatooine
No data available! Sorry! We protect our program from crashing by making it check whether the key in query exists in the dictionary before the program tries to use it ➊. If the key does exist, we use the query as we did before ➋. Otherwise, we send a message to users telling them that we don’t have that information in our dictionary ➌. Now the program is much friendlier.
PUTTING LISTS INSIDE DICTIONARIES Our planet dictionary is a bit limited at the moment. What if we want to add extra information, such as whether the planet has rings and how many moons it has? To do so, we can use a list to store multiple pieces of information about a planet and then put that list inside the dictionary. For example, here is a new entry for Venus: "Venus": ["Venus takes 243 days to rotate", False, 0] The square brackets mark the start and end of the list, and there are three items in the list: a short description, a True or False value that indicates whether or not the planet has rings, and the number of moons it has. Because Venus doesn’t have rings, the second entry is False. It also doesn’t have any moons, so the third entry is 0.
RED ALERT T ru e and False values need to start with a capital letter and shouldn’t be in
quotes. The words turn orange when you type them correctly in IDLE.
Change your dictionary code so each key has a list, as shown in Listing 53, keeping the rest of the code the same. Remember that dictionary entries are separated by commas, so there’s a comma after the closing bracket for all the lists except the last one. Save your updated program as listing53.py. I’ve slipped in information for Pluto too. Some speculate that Pluto might have rings, and exploration continues. By the time you read this book, our understanding of Pluto might have changed.
listing53.py planets = { "Mercury": ["The smallest planet, nearest the Sun", False, 0], "Venus": ["Venus takes 243 days to rotate", False, 0], "Earth": ["The only planet known to have native life", False, 1], "Mars": ["The second smallest planet", False, 2], "Jupiter": ["The largest planet, a gas giant", True, 67], "Saturn": ["The second largest planet is a gas giant", True, 62], "Uranus": ["An ice giant with a ring system", True, 27], "Neptune": ["An ice giant and farthest from the Sun", True, 14], "Pluto": ["Largest dwarf planet in the Solar System", False, 5] } snip Listing 53: Putting a list in a dictionary Run the program by selecting Run ▸ Run Module. Now when you ask for information on a planet, the program should display the entire list for that planet: Which planet would you like information on? Venus ['Venus takes 243 days to rotate', False, 0] Which planet would you like information on? Mars ['The second smallest planet', False, 2]
EXTRACTING INFORMATION FROM A LIST INSIDE A DICTIONARY We know how to get a list of information from a dictionary, so the next step is to get individual pieces of information from that list. For example, the False entry doesn’t mean much by itself. If we can separate it from the list, we can add an explanation beside it so the results are easier to understand. We previously used lists inside lists for the room map in Chapter 4. Now, as then, we’ll use index numbers to get items from a list in a dictionary. Because planets[query] is the entire list, we can see just the description (the first item in the list) by using planets[query][0]. We can see whether it has rings or not by using pl an et s [que r y ] [1 ] . Briefly, here is what we’re doing:
1. We’re using the planet name, stored in the variable query, to access a particular list from the planets dictionary.
2. We’re using an index number to take an individual item from that list. Modify your program to look like Listing 54. As before, change only the lines that are not grayed out. Save your program as listing54.py, and run it by clicking Run ▸ Run Module. listing54.py sn ip while True: query = input("Which planet would you like information on? ") if query in planets.keys(): ➊ print(planets[query][0]) ➋ print("Does it have rings? ", planets[query][1]) else: print("Databanks empty. Sorry!") Listing 54: Displaying information from the list stored in the dictionary When you run the listing54.py program, you should see something like the following: Which planet would you like information on? Earth The only planet known to have native life Does it have rings? False Which planet would you like information on? Saturn The second largest planet is a gas giant Does it have rings? True This should work for every planet in the dictionary! When you enter the name of a planet that’s in the dictionary, the program now prints the first item from its list of information, which is the description ➊. On the next line, the program asks itself whether that planet has rings and then shows you the True or Fa ls e answer, which is the second item in that planet’s list of information ➋. You can
display some text and some data using the same print() instruction, by separating them with a comma. The display is much clearer than printing the entire list, and the information is easier to understand.
TRAINING MISSION #2 Can you modify the program to also tell you how many moons the planet has?
MAKING THE SPACE STATION OBJECTS DICTIONARY Let’s put our knowledge of how to use dictionaries, and lists inside dictionaries, to use in the space station. With all the furniture, life support equipment, tools, and personal effects required on the space station, there’s a lot of information to keep track of. We’ll use a dictionary called objects to store information about all the different items in the game. We’ll use numbers as the keys for the objects. It’s simpler than using a word for each object. Also, using numbers makes it easier to understand the room map if you want to print it as we did in Chapter 4. There’s less risk of mistyping, too. When we create the code for the puzzles later, it’ll be less obvious what the solution is, which means there will be fewer spoilers if you’re building the game before playing it. You might remember that we used the numbers 0, 1, and 2 to represent floor tiles, wall pillars, and soil in Chapter 4. We’ll continue using those numbers for those items, and the rest of the objects will use numbers 3 to 81. Each entry in the dictionary is a list containing information about the item, similar to how we made the planets dictionary earlier in this chapter. The lists contain the following information for each object: An object image file Different objects can use the same image file. For example, all the access cards use the same image. A shadow image file We use shadows to enhance the 3D perspective in the game. The two standard shadows are images.full_shadow, which fills a full tile space and is for larger objects, and images.half_shadow, which fills half a tile space for smaller objects. Objects with a distinctive outline, such as the cactus, have their own shadow image file that is used only for that object. Some items, like the chair, have the shadow within the image file. Some items have no shadow, like the crater and any items the player can carry. When an image has no shadow, we write None where its shadow filename belongs in the dictionary. The word None is a special data type in Python. Like with True and False, you don’t need any quotes around it, and it should start with a
capital letter. When you enter it correctly, None turns orange in the code. A long description A long description is displayed when you examine or select an object while playing the game. Some of the long descriptions include clues, and others simply describe the environment. A short description Typically just a few words, such as “an access card,” a short description is shown onscreen when you do something with the object while playing the game. For example, “You have dropped an access card.” A short description is only required for items that the player can pick up or use, such as an access card or the vending machine. The game can reuse items in the objects dictionary. For example, if a room is made of 60 or more identical wall pillars, the game can just reuse the same wall pillar object. It only needs to be in the dictionary once. There are some items that use the same image files but have other differences, which means we must store them separately in the dictionary. For example, the access cards have different descriptions depending on who they belong to, and the doors have different descriptions to tell you which key to use. Each access card and door has its own entry in the objects dictionary.
ADDING THE FIRST OBJECTS IN ESCAPE Open listing43.py, which you created in Chapter 4. This listing contains the game map and the code to generate the room map. We’ll add to this program to continue building the Escape game. First, we need to set up some additional variables. Before the adventure begins, a research craft, called the Poodle lander, crashlands on the planet surface. We’ll store coordinates for a random crash site in these new variables. We’ll add these variables now because the map object (number 27) will require them for its description. Add the new lines in Listing 55 to the VARIABLES section, marked out with a hashed box, in your existing listing43.py file. I recommend adding them at the end of your other variables, just above where the MAP section begins, so your listing and my listing are consistent. Save your program as listing55.py. The program won’t do anything new if you run it now, but if you want to try it, enter pgzrunlisting5-5.py. listing55.py
snip ############### ## VARIABLES ## ############### snip DEMO_OBJECTS = [images.floor, images.pillar, images.soil] LANDER_SECTOR = random.randint(1, 24) LANDER_X = random.randint(2, 11) LANDER_Y = random.randint(2, 11) ############### ## MAP ## ############### snip Listing 55: Adding the crash site location variables These new instructions create variables to remember the sector (or room number) the Poodle landed on, and its x and y position in that sector. The instructions use the ran dom. ra ndi n t() function, which picks a random number between the two numbers you
give it. These instructions run once at the start of the game, so the lander location is different each time you play but doesn’t change during the game. Now let’s add the first chunk of the objects data, shown in Listing 56. This section provides the data for objects 0 to 12. Because the player cannot pick up or use these objects, they don’t have a short description. Place this section of the listing just above the MAKEMAP section of your existing program (listing55.py). To help you find your way around the listing, you can press CTRLF in IDLE to search for a particular word or phrase. For example, try searching for make map to see where to start adding the code in Listing 56. After searching, click Close on the search dialog box. Remember that if you get lost in the listing, you can always refer to the complete game listing in Appendix A. If you prefer not to type the data, use the file datachapter5.py, in the listings folder. It contains the objects dictionary, so you can copy and paste it into your program. You can
start by just pasting the first 12 items. listing56.py snip assert len(GAME_MAP)1 == MAP_SIZE, "Map size and GAME_MAP don't match" ############### ## OBJECTS ## ############### objects = { 0: [images.floor, None, "The floor is shiny and clean"], 1: [images.pillar, images.full_shadow, "The wall is smooth and cold"], 2: [images.soil, None, "It's like a desert. Or should that be dessert?"], 3: [images.pillar_low, images.half_shadow, "The wall is smooth and cold"], 4: [images.bed, images.half_shadow, "A tidy and comfortable bed"], 5: [images.table, images.half_shadow, "It's made from strong plastic."], 6: [images.chair_left, None, "A chair with a soft cushion"], 7: [images.chair_right, None, "A chair with a soft cushion"], 8: [images.bookcase_tall, images.full_shadow, "Bookshelves, stacked with reference books"], 9: [images.bookcase_small, images.half_shadow, "Bookshelves, stacked with reference books"], 10: [images.cabinet, images.half_shadow, "A small locker, for storing personal items"], 11: [images.desk_computer, images.half_shadow, "A computer. Use it to run life support diagnostics"], 12: [images.plant, images.plant_shadow, "A spaceberry plant, grown here"] } ############### ## MAKE MAP ## ############### snip Listing 56: Adding the first objects
Remember that the colors of the code can help you spot errors. If your text sections aren’t green, you’ve left out the opening double quotes. If there is too much green, you might have forgotten the closing double quotes. Some of the lists continue on the next line, and Python knows the list isn’t complete until it sees the closing bracket. If you struggle to get any of the listings to work, you can use my version of the code (see “Using My Example Listings” on page 21) and pick up the project from any point. Listing 56 looks similar to our earlier planets dictionary: we use curly brackets to mark the start and end of the dictionary, and each entry in the dictionary is a list so it is inside square brackets. The main difference is that this time the key is a number instead of a word. Save your new program as listing56.py. This program uses Pygame Zero for the graphics, so you need to run your new program by entering pgzrunlisting5-6.py. It should work the same as it did previously because we’ve added new data but haven’t done anything with that data yet. It’s worth running the program anyway, because if you see an error message in the command line window, you can fix the new code before you go any further.
VIEWING OBJECTS WITH THE SPACE STATION EXPLORER To see the objects, we have to tell the game to use the new dictionary. Change the following line in the EXPLORER part of the program from this: image_to_draw = DEMO_OBJECTS[room_map[y][x]] to the following: image_to_draw = objects[room_map[y][x]][0] This small change makes the Explorer code use our new objects dictionary instead of the DE MO _O B JECT S list we told it to use previously.
Notice that we’re now using lowercase letters instead of capital letters. In this program, I use capital letters for constants whose values won’t change. The DEMO_OBJECTS list never changed: it was only used for looking up the image filenames. But the objects dictionary will sometimes have its content changed as the game is played. The other difference is that [0] is on the end of the line now. This is because when we pull an item from the objects dictionary, it gives us an entire list of information. But we
just want to use the image here, which is the first item in that list, so we use the index number [0] to extract it. Save the program and run it again, and you should see that the rooms look the same as before. That’s because we haven’t added any new objects yet, and we kept the object numbers for the floor, wall, and soil the same as the index numbers we were using for them before.
DESIGNING A ROOM Let’s add some items to the room display. In the EXPLORER section of the code, add the new lines shown in Listing 57: listing57.py s n i p ############### ## EXPLORER ## ############### def draw(): global room_height, room_width, room_map print(current_room) generate_map() screen.clear() room_map[2][4] = 7 room_map[2][6] = 6 room_map[1][1] = 8 room_map[1][2] = 9 room_map[1][8] = 12 room_map[1][9] = 9 s n i p Listing 57: Adding some objects in the room display These new instructions add objects to the room_map list at different positions before the room is displayed. Remember that room_map uses the ycoordinate before the xcoordinate. The first index
number says how far from the back of the room the objects are; the smaller the number the nearer to the back they are. The smallest useful number is usually 1 because the wall is in row 0. The second number says how far across the room the objects are, from left to right. There’s usually a wall in column 0, so 1 is the smallest useful number for this position too. The number on the other side of the equal sign is the key for a particular object. You can check which object each number represents by looking at the objects dictionary in Listing 56. So this line: room_map[1][1] = 8 places object 8 (a tall bookcase) into the topleft corner of the room. And this line: room_map[2][6] = 6 places a chair (object 6) three rows from the top and seven positions from the left. (Remember that index numbers start at 0.) Save your program as listing57.py and enter pgzrunlisting5-7.py to run it. Figure 51 shows what you should see now.
Figure 51: Cozy! Some objects displayed in the space station Explorer program Because the Explorer program is just a demo, some things don’t work yet. For example,
some objects have a black square under them because there’s no floor tile there. Also, all the rooms look the same because we’ve coded the objects into the EXPLORER section, so they appear in every room we display. This means you can’t view all the rooms anymore, because the objects won’t fit in some of them. As a result, you can’t use the arrow keys to look at all the rooms. The program doesn’t display wide objects such as the bed correctly yet, either. We’ll fix all of these problems later, but we can continue building and testing the space station in the meantime.
TRAINING MISSION #3 Experiment with the code you’ve added to the Explorer program to reposition the furniture to your liking. Playing with this code is a great way to learn how to position objects in the rooms. If you want to play with a bigger room, change the value of current_room in the VARIABLES section from 31 to 40 (which is the biggest room in the game). Save your program as mission53.py and run it using pgzrunmission5-3.py. You’ll need to keep a safe copy of the existing Explorer code (listing57.py) to use in Training Mission #4.
ADDING THE REST OF THE OBJECTS So far we’ve added objects 0 to 12 to the objects dictionary. There are 81 objects in total in the game, so let’s add the rest now by adding the new lines in Listing 58. Remember to add a comma after item 12 before adding the rest of the items in the dictionary. When the same filename or a similar description is used for more than one object, you can just copy and paste it. To copy code, click and hold down the mouse button at the beginning of the chunk of code, move the mouse to highlight it, and then press CTRLC. Then click the mouse where you want to paste that code and press CTRLV. Remember too that you can copy and paste the whole dictionary from the datachapter5.py file if you want to save time typing. Save the program as listing58.py. You can test that the program still works by entering p gzrunlis ti ng 5-8 .p y , although you won’t see anything new yet.
This is Listing 58: listing58.py ###############
## OBJECTS ## ############### objects = { 0: [images.floor, None, "The floor is shiny and clean"], sni p 12: [images.plant, images.plant_shadow, "A spaceberry plant, grown locally"], ➊ 13: [images.electrical1, images.half_shadow, "Electrical systems used for powering the space station"], 14: [images.electrical2, images.half_shadow, "Electrical systems used for powering the space station"], 15: [images.cactus, images.cactus_shadow, "Ouch! Careful on the cactus!"], 16: [images.shrub, images.shrub_shadow, "A space lettuce. A bit limp, but amazing it's growing here!"], 17: [images.pipes1, images.pipes1_shadow, "Water purification pipes"], 18: [images.pipes2, images.pipes2_shadow, "Pipes for the life support systems"], 19: [images.pipes3, images.pipes3_shadow, "Pipes for the life support systems"], ➋ 20: [images.door, images.door_shadow, "Safety door. Opens automatically \ for astronauts in functioning spacesuits."], 21: [images.door, images.door_shadow, "The airlock door. \ For safety reasons, it requires two person operation."], 22: [images.door, images.door_shadow, "A locked door. It needs " \ + PLAYER_NAME + "'s access card"], 23: [images.door, images.door_shadow, "A locked door. It needs " \ + FRIEND1_NAME + "'s access card"], 24: [images.door, images.door_shadow, "A locked door. It needs " \ + FRIEND2_NAME + "'s access card"], 25: [images.door, images.door_shadow, "A locked door. It is opened from Main Mission Control"], 26: [images.door, images.door_shadow, "A locked door in the engineering bay."], ➌ 27: [images.map, images.full_shadow, "The screen says the crash site was Sector: " \ + str(LANDER_SECTOR) + " // X: " + str(LANDER_X) + \ " // Y: " + str(LANDER_Y)], 28: [images.rock_large, images.rock_large_shadow,
"A rock. Its coarse surface feels like a whetstone", "the rock"], 29: [images.rock_small, images.rock_small_shadow, "A small but heavy piece of Martian rock"], 30: [images.crater, None, "A crater in the planet surface"], 31: [images.fence, None, "A fine gauze fence. It helps protect the station from dust storms"], 32: [images.contraption, images.contraption_shadow, "One of the scientific experiments. It gently vibrates"], 33: [images.robot_arm, images.robot_arm_shadow, "A robot arm, used for heavy lifting"], 34: [images.toilet, images.half_shadow, "A sparkling clean toilet"], 35: [images.sink, None, "A sink with running water", "the taps"], 36: [images.globe, images.globe_shadow, "A giant globe of the planet. It gently glows from inside"], 37: [images.science_lab_table, None, "A table of experiments, analyzing the planet soil and dust"], 38: [images.vending_machine, images.full_shadow, "A vending machine. It requires a credit.", "the vending machine"], 39: [images.floor_pad, None, "A pressure sensor to make sure nobody goes out alone."], 40: [images.rescue_ship, images.rescue_ship_shadow, "A rescue ship!"], 41: [images.mission_control_desk, images.mission_control_desk_shadow, \ "Mission Control stations."], 42: [images.button, images.button_shadow, "The button for opening the timelocked door in engineering."], 43: [images.whiteboard, images.full_shadow, "The whiteboard is used in brainstorms and planning meetings."], 44: [images.window, images.full_shadow, "The window provides a view out onto the planet surface."], 45: [images.robot, images.robot_shadow, "A cleaning robot, turned off."], 46: [images.robot2, images.robot2_shadow, "A planet surface exploration robot, awaiting setup."], 47: [images.rocket, images.rocket_shadow, "A 1person craft in repair."], 48: [images.toxic_floor, None, "Toxic floor do not walk on!"], 49: [images.drone, None, "A delivery drone"], 50: [images.energy_ball, None, "An energy ball dangerous!"], 51: [images.energy_ball2, None, "An energy ball dangerous!"], 52: [images.computer, images.computer_shadow, "A computer workstation, for managing space station systems."],
53: [images.clipboard, None, "A clipboard. Someone has doodled on it.", "the clipboard"], 54: [images.bubble_gum, None, "A piece of sticky bubble gum. Spaceberry flavour.", "bubble gum"], 55: [images.yoyo, None, "A toy made of fine, strong string and plastic. \ Used for antigrav experiments.", PLAYER_NAME + "'s yoyo"], 56: [images.thread, None, "A piece of fine, strong string", "a piece of string"], 57: [images.needle, None, "A sharp needle from a cactus plant", "a cactus needle"], 58: [images.threaded_needle, None, "A cactus needle, spearing a length of string", "needle and string"], 59: [images.canister, None, "The air canister has a leak.", "a leaky air canister"], 60: [images.canister, None, "It looks like the seal will hold!", "a sealed air canister"], 61: [images.mirror, None, "The mirror throws a circle of light on the walls.", "a mirror"], 62: [images.bin_empty, None, "A rarely used bin, made of light plastic", "a bin"], 63: [images.bin_full, None, "A heavy bin full of water", "a bin full of water"], 64: [images.rags, None, "An oily rag. Pick it up by a corner if you must!", "an oily rag"], 65: [images.hammer, None, "A hammer. Maybe good for cracking things open...", "a hammer"], 66: [images.spoon, None, "A large serving spoon", "a spoon"], 67: [images.food_pouch, None, "A dehydrated food pouch. It needs water.", "a dry food pack"], 68: [images.food, None, "A food pouch. Use it to get 100% energy.", "readytoeat food"], 69: [images.book, None, "The book has the words 'Don't Panic' on the \ cover in large, friendly letters", "a book"], 70: [images.mp3_player, None, "An MP3 player, with all the latest tunes", "an MP3 player"], 71: [images.lander, None, "The Poodle, a small space exploration craft. \ Its black box has a radio sealed inside.", "the Poodle lander"], 72: [images.radio, None, "A radio communications system, from the \ Poodle", "a communications radio"],
73: [images.gps_module, None, "A GPS Module", "a GPS module"], 74: [images.positioning_system, None, "Part of a positioning system. \ Needs a GPS module.", "a positioning interface"], 75: [images.positioning_system, None, "A working positioning system", "a positioning computer"], 76: [images.scissors, None, "Scissors. They're too blunt to cut \ anything. Can you sharpen them?", "blunt scissors"], 77: [images.scissors, None, "Razorsharp scissors. Careful!", "sharpened scissors"], 78: [images.credit, None, "A small coin for the station's vending systems", "a station credit"], 79: [images.access_card, None, "This access card belongs to " + PLAYER_NAME, "an access card"], 80: [images.access_card, None, "This access card belongs to " + FRIEND1_NAME, "an access card"], 81: [images.access_card, None, "This access card belongs to " + FRIEND2_NAME, "an access card"] } ➍ items_player_may_carry = list(range(53, 82)) # Numbers below are for floor, pressure pad, soil, toxic floor. ➎ items_player_may_stand_on = items_player_may_carry + [0, 39, 2, 48] ############### ## MAKE MAP ## ############### sn ip Listing 58: Completing the objects data for the Escape game Some of the lists for objects extend over more than one line in the program ➊. This is fine because Python knows the list isn’t complete until it sees a closing bracket. To break a string (or any other piece of code) over more than one line, you can use a \ at the end of the line ➋. The line breaks in listing58.py are just there to make the code fit onto the book page neatly: onscreen, the code can extend to the right if you want it to. Object 27 is a map showing the Poodle’s crash site. Its long description includes the variables that you set in Listing 55 for the Poodle’s position. The str() function is used
to convert the numbers in those variables into strings so they can be combined with other strings to make up the long description ➌. We’ve also set up some additional lists we’ll need in the game: items_player_may_carry stores the numbers of the objects the player can pick up ➍. These are objects 53 to 81. Because they’re grouped together, we can set up the items_player_may_carry list using a range. A range is a sequence of numbers that starts from the first number given and finishes at the one before the last number. (We used ranges in Chapter 3.) We turn that range into a list using list(range(53to82)), which makes a list of all the numbers from 53 to 81. If you add more objects that a player can carry later, you can add them to the end of this list. For example, to add new objects numbered 89 and 93 that the player can carry, use ite ms_p la yer _ may_ car ry=li st ( ra ng e ( 54,82 ))+[8 9 ,93 ] . You can also add new objects to the
end of the objects list and just extend the range used to set up items_player_may_carry. The other new list is items_player_may_stand_on, which specifies whether a player is allowed to stand on a particular item ➎. Players can only stand on objects small enough to be picked up and on the different types of floor. We make this list by adding the object numbers for the different floor types to the items_player_may_carry list. After you’ve entered Listing 58, you’ve completed the OBJECTS section of the Escape game! But we haven’t put the objects into the game map yet. We’ll start to do that in Chapter 6.
TRAINING MISSION #4 Experiment with some of the new objects you just added to the game. By modifying the code, can you . . .
Swap the tall bookcase for a bin (object 62)? Swap the spaceberry plant for a small rock (object 29)? Swap the chair on the right for a patch of toxic floor (object 48)? To understand which instruction places which object, you can either use the coordinates in your existing code or look up the object numbers in the objects dictionary (onscreen or in the listings in this chapter). Run your program to make sure it works.
ARE YOU FIT TO FLY? Check the following boxes to confirm that you’ve learned the key lessons in this chapter.
To get information from a dictionary, you use the key for that information. The key can be a word or a number and can also be stored in a variable.
If you try to use a key that isn’t in the dictionary, you’ll cause an error.
To avoid an error, check whether the key is in the dictionary before the program tries to use it.
You can put lists inside dictionaries. Then you can use the dictionary key followed by the list index to get a particular item from the list. For example: planets["Earth"][1].
The Escape game uses the objects dictionary to store information about all the objects in the game. Each item in the dictionary is a list.
You can use the index number of that list to access the object’s image file, shadow image file, and long and short description.
6
History
Topics
INSTALLING THE SPACE STATION EQUIPMENT
Tutorials
Offers & Deals
Highlights
Settings Support
In Chapter 5, you prepared information about all the equipment you’ll use on your
Sign Out
mission. In this chapter, you’ll install some of that equipment in the space station and use the Explorer to view any room or planet surface location. This is your first chance to explore the design of the Mars base that will become your home.
UNDERSTANDING THE DICTIONARY FOR THE SCENERY DATA There are two different types of objects on the space station: Scenery is the equipment that stays in the same place throughout the Escape game and includes furniture, pipes, and electronic equipment. Props are items that can appear, disappear, or move around during the game. They include things the player can create and pick up. Props also include doors, which appear in the room when they’re closed and disappear when they’re open. The data for positioning scenery and the data for props are stored separately and organized differently. In this chapter, we’ll just add the scenery data.
Our program already knows the image and description to use for all the objects in the game, because they’re in the objects dictionary you created in Chapter 5. Now we’ll tell the program where to put the scenery objects in the space station. To do that, we’ll create a new dictionary called scenery. This is how we’ll structure the entry for one room: r oo mn um b er : [[object number, y, x], [object number, y, x]] The key for the dictionary will be the room number. For each room number, the dictionary stores a list, with a square bracket at the start and the end of it. Each item in that list is another list that tells the program where in the room to put one object. Here, I’ve made one object red and the other green so you can see where they start and end. These are the three pieces of information you need for each object: The object number This is the same as the number that is used as the key in the ob je ct s dictionary. For example, number 5 represents a table.
The object’s y position This is the object’s position in the room, from back to front. The back wall is usually in row 0, so we typically start placing objects at 1. The largest useful number will be the room height minus 2: we subtract 1 because the map positions start at 0 and subtract another 1 for the space the front wall occupies. In practice, it’s a good idea to leave a bit more space at the front of the room, because the front wall can obscure other items. You can check the size of the room in the GAME_MAP code you added in Chapter 4. The object’s x position This tells the program how far across the room from left to right the object should be. Again, a wall is usually in position 0. The largest useful number will generally be the room width minus 2. To get a better understanding of these numbers, let’s take a look at Figure 61, which shows one of the rooms on the space station as a screenshot and a map. In this image, the sink (S) is in the second row from the back, so its y position is 1. Remember that the wall in the first row at the back is in position y = 0. The sink’s x position is 3. There are two other tile spaces to the left of it, and the wall is in position x = 0.
Figure 61: An example space station room as seen in the game (left) and represented by a map (right). T = toilet, S = sink, P = player. Let’s look at the data for this room. Don’t enter this code yet. I’ll give you all the scenery data shortly. scenery = { snip 30: [[34,1,1], [35,1,3]], snip } This code tells the program about the objects in room 30. Room 30 has object number 34, a toilet, in the topleft corner at position y = 1 and x = 1, and object number 35, a sink, at position number y = 1 and x = 3, quite close to the toilet. You can have the same object in the room several times by adding a list for each position and using the same object number for them. For example, you could fill the room with toilets in different positions if you wanted to, although that would be a rather bizarre thing to do. You don’t need to include the walls in the scenery data, because the program automatically adds them to the room when it creates the room_map list, as you’ve already seen. Even though putting the information for each item into a list means adding more brackets, it’s much easier to understand the data at a glance. The brackets help you see how many items are in the room, which numbers are the object numbers, and which are the position numbers.
ADDING THE SCENERY DATA
ADDING THE SCENERY DATA Open listing58.py, the final listing in Chapter 5. This listing contains your game map and objects data. Now we’ll add the scenery data to it. Listing 61 shows the scenery data. Add this new SCENERY section before the MAKEMAP section. Make sure the placement of the brackets and commas is correct. Remember that each piece of scenery needs a list of three numbers, and each list is separated with a comma too. If you prefer not to type all the data in, use the file datachapter6.py, which is in the listings folder. It contains the scenery dictionary for you to copy and paste into your program. listing61.py sn ip items_player_may_stand_on = items_player_may_carry + [0, 39, 2, 48] ############### ## SCENERY ## ############### # Scenery describes objects that cannot move between rooms. # room number: [[object number, y position, x position]...] scenery = { 26: [[39,8,2]], 27: [[33,5,5], [33,1,1], [33,1,8], [47,5,2], [47,3,10], [47,9,8], [42,1,6]], 28: [[27,0,3], [41,4,3], [41,4,7]], 29: [[7,2,6], [6,2,8], [12,1,13], [44,0,1], [36,4,10], [10,1,1], [19,4,2], [17,4,4]], 30: [[34,1,1], [35,1,3]], 31: [[11,1,1], [19,1,8], [46,1,3]], 32: [[48,2,2], [48,2,3], [48,2,4], [48,3,2], [48,3,3], [48,3,4], [48,4,2], [48,4,3], [48,4,4]], 33: [[13,1,1], [13,1,3], [13,1,8], [13,1,10], [48,2,1], [48,2,7], [48,3,6], [48,3,3]], 34: [[37,2,2], [32,6,7], [37,10,4], [28,5,3]], 35: [[16,2,9], [16,2,2], [16,3,3], [16,3,8], [16,8,9], [16,8,2], [16,1,8], [16,1,3], [12,8,6], [12,9,4], [12,9,8],
[15,4,6], [12,7,1], [12,7,11]], 36: [[4,3,1], [9,1,7], [8,1,8], [8,1,9], [5,5,4], [6,5,7], [10,1,1], [12,1,2]], 37: [[48,3,1], [48,3,2], [48,7,1], [48,5,2], [48,5,3], [48,7,2], [48,9,2], [48,9,3], [48,11,1], [48,11,2]], 38: [[43,0,2], [6,2,2], [6,3,5], [6,4,7], [6,2,9], [45,1,10]], 39: [[38,1,1], [7,3,4], [7,6,4], [5,3,6], [5,6,6], [6,3,9], [6,6,9], [45,1,11], [12,1,8], [12,1,4]], 40: [[41,5,3], [41,5,7], [41,9,3], [41,9,7], [13,1,1], [13,1,3], [42,1,12]], 41: [[4,3,1], [10,3,5], [4,5,1], [10,5,5], [4,7,1], [10,7,5], [12,1,1], [12,1,5]], 44: [[46,4,3], [46,4,5], [18,1,1], [19,1,3], [19,1,5], [52,4,7], [14,1,8]], 45: [[48,2,1], [48,2,2], [48,3,3], [48,3,4], [48,1,4], [48,1,1]], 46: [[10,1,1], [4,1,2], [8,1,7], [9,1,8], [8,1,9], [5,4,3], [7,3,2]], 47: [[9,1,1], [9,1,2], [10,1,3], [12,1,7], [5,4,4], [6,4,7], [4,1,8]], 48: [[17,4,1], [17,4,2], [17,4,3], [17,4,4], [17,4,5], [17,4,6], [17,4,7], [17,8,1], [17,8,2], [17,8,3], [17,8,4], [17,8,5], [17,8,6], [17,8,7], [14,1,1]], 49: [[14,2,2], [14,2,4], [7,5,1], [5,5,3], [48,3,3], [48,3,4]], 50: [[45,4,8], [11,1,1], [13,1,8], [33,2,1], [46,4,6]] } checksum = 0 check_counter = 0 for key, room_scenery_list in scenery.items(): for scenery_item_list in room_scenery_list: ➊ checksum += (scenery_item_list[0] * key + scenery_item_list[1] * (key + 1) + scenery_item_list[2] * (key + 2)) check_counter += 1 print(check_counter, "scenery items") ➋ assert check_counter == 161, "Expected 161 scenery items" ➌ assert checksum == 200095, "Error in scenery data" print("Scenery checksum: " + str(checksum)) ############### ## MAKE MAP ## ###############
sn ip Listing 61: Adding the scenery data Save your listing as listing61.py, and run it using pgzrunlisting6-1.py in the command line. We’ve added some data, but we haven’t told the program to do anything with it, so you won’t see any change. But if you made a mistake entering the data, the program should stop and display the message Errorin scenerydata. In this case, go back and doublecheck your code against the book. Check that you entered the checksum number correctly first! ➌ The second half of this listing is a safety measure, called a checksum. It checks that all the data is present and correct by making a calculation involving the data and then checking the result against the correct answer. If there’s a mistake in the data you’ve entered, this bit of code will stop the program until you fix it. This stops your game from running with bugs in it. (Some errors could get through, but this code catches most mistakes.) The program uses the assert instruction to check the data. The first instruction checks that the program has the right number of data items. If it doesn’t, the program stops and shows an error message ➋. The program also checks whether the checksum (the result from the calculation) is the expected number, and if it isn’t, it stops the program ➌. Notice that one of the instructions in Listing 61 spreads across three lines ➊: Python knows we haven’t finished the instruction until we close the final parenthesis.
TIP If you want to change the scenery data, to redesign rooms or to add your own rooms, you will need to turn off the checksum. This is because the calculation based on your changed data will be different, so the checksum will fail and the program won’t run. You can simply put a # symbol before the two lines that start with assert ➋➌ to switch them off. As you know, the # symbol is used for a comment, and Python ignores everything after it on the same line. It can be a handy off switch when you’re building or testing programs.
ADDING THE PERIMETER FENCE FOR THE PLANET SURFACE
You might have noticed that we haven’t added any scenery for rooms 1 to 25 yet. Our data starts at room 26. As you might remember, the first 25 locations are outside on the planet surface. For simplicity, we’ll still call them rooms, although they have no walls. Figure 62 shows rooms 1 to 25 on the map. A fence, shown as a dotted line in Figure 6 2, surrounds the outside of these rooms. The fence stops people from wandering out of the compound and off the game map.
Figure 62: Adding the fence around the planet surface locations We need to add fences at the following locations: On the left in rooms 1, 6, 11, 16, and 21 At the top in rooms 1, 2, 3, 4, and 5 On the right in rooms 5, 10, 15, 20, 25 Each outside room has one item of planet surface scenery too, which is randomly chosen from a small selection of suitable items that includes rocks, shrubs, and craters. For the game, it doesn’t matter where these items are placed, so they can also be randomly positioned. Listing 62 shows the code that generates the random planet surface scenery and adds the fences. Add the code to the end of the SCENERY section you just created, and save your program as listing62.py. You can use pgzrunlisting6-2.py to check whether the program reports any errors. listing62.py s n i p
print("Scenery checksum: " + str(checksum)) for room in range(1, 26): # Add random scenery in planet locations. ➊ if room != 13: # Skip room 13. ➋ scenery_item = random.choice([16, 28, 29, 30]) ➌ scenery[room] = [[scenery_item, random.randint(2, 10), random.randint(2, 10)]] # Use loops to add fences to the planet surface rooms. ➍ for room_coordinate in range(0, 13): ➎ for room_number in [1, 2, 3, 4, 5]: # Add top fence ➏ scenery[room_number] += [[31, 0, room_coordinate]] ➐ for room_number in [1, 6, 11, 16, 21]: # Add left fence ➑ scenery[room_number] += [[31, room_coordinate, 0]] for room_number in [5, 10, 15, 20, 25]: # Add right fence ➒ scenery[room_number] += [[31, room_coordinate, 12]] ➓ del scenery[21][1] # Delete last fence panel in Room 21 del scenery[25][1] # Delete last fence panel in Room 25 ############### ## MAKE MAP ## ############### sn ip Listing 62: Generating random planet surface scenery You don’t need to understand this code to enjoy building and playing Escape, but if you want to dig deeper, I’ll explain the code in more detail. The first section in Listing 62 adds the random scenery. For each room, random.choice() ➋ chooses a scenery item randomly. In the same way that random.randint() gave us a random number (like rolling dice), random.choice() gives us a random item (like a grab bag or lucky dip game). The item is chosen from the list [16,28,29,30]. Those object numbers represent a shrub, a large rock, a small rock, and a crater, respectively. We also add a new entry to the scenery dictionary for the room ➌. This entry contains the random scenery item and random y and x positions for that item. The y and x
positions place the item inside the room but not too near the edge. The != operator ➊ means “not equal to,” so scenery is added only if the room number is not 13. Who knows? Maybe it’ll be useful to have an empty space on the planet surface when you’re on your mission… In the second part of Listing 62, we add the fences. All the planet surface locations are 13 tiles high and 13 tiles wide, so we can use one loop ➍ to add the top and side fences. The loop’s variable, room_coordinate, counts from 0 to 12, and each time around the loop, fence panels are put in place at the top and the sides of the appropriate rooms. Inside the room_coordinate loop, there are three loops for the room_number. The first roo m_nu mb er loop ➎ adds a fence along the top row of the top rooms. Instead of using a ran ge() , this time we’re looping through a list. Each time through the list, the variable roo m_nu mb er takes the next number from the list [1,2,3,4,5]. We add a piece of scenery
to the scenery list for the room, using += ➏. This is scenery item 31 (a fence), in the top row of the room (at position y = 0). The room_coordinate value is used for the x position. This puts the top fence into rooms 1 to 5, in the top row of those rooms. There are two other room_number loops inside the room_coordinate loop. The first one adds the left fence to rooms 1, 6, 11, 16, and 21 ➐. This time, the program uses the roo m_co or din a te variable for the y position and uses 0 for the x position ➑. This puts fence
panels along the left edge of those rooms. The second loop adds the right edge fence to rooms 5, 10, 15, 20, and 25. This also uses the room_coordinate for the y position of the fence panel but uses 12 for the x coordinate, putting a fence along the right edge of those rooms ➒. We don’t want side fence panels where the outside area joins the space station wall. Figure 63 shows a map of room 21. The bottomleft corner of the room should be wall, so there shouldn’t be a fence panel here. The loops we used just added a fence panel here, though, so we use an instruction ➓ to delete the last item of scenery added to this room, and to room 25, which is on the other side of the compound (see Figure 62). It’s easier to add these two panels and take them out again than it is to write code that avoids putting these fence panels in. The index number -1 is a handy shortcut for referring to the last item in a list.
Figure 63: Map showing how the fence touches the wall in an outside room next to the space station Using random scenery and loops to position fences enables us to have a large area to explore without having to type in data for over 200 fence panels and scenery items.
TIP If you’re customizing the game and don’t want to add random scenery or fences in rooms 1 to 25, you can delete the code sections shown in Listing 6 2.
LOADING THE SCENERY INTO EACH ROOM Now that we’ve added scenery data to the program, let’s add some code so we can see the scenery in the space station! You might remember that the generate_map() function creates the room_map list for the room you’re currently exploring. The room_map list is used to display and navigate the room.
So far, the generate_map() function just calculates the size of the room and where the doors are, and places the floor and walls. We need to add some code to extract the scenery from our new dictionary and add it to the room_map. But first, we’ll make one small but important adjustment to the program. In the VARIABLES section, near the start of the program, add the new line shown in Listing 63. Save your program as listing6 3.py. listing63.py s n i p ############### ## VARIABLES ## ############### s n i p LANDER_SECTOR = random.randint(1, 24) LANDER_X = random.randint(2, 11) LANDER_Y = random.randint(2, 11) TILE_SIZE = 30 ############### ## MAP ## ############### s n i p Listing 63: Setting up the TILE_SIZE variable This line creates a variable to store the size of a tile. Using it makes the program easier to read because we can replace the number 30 with a more meaningful phrase. Instead of seeing the number 30 in the code and having to remember what it represents, we can see the words TILESIZE instead, which gives us a hint about what the code is doing. Next, find the MAKEMAP section of the program: it comes before the EXPLORER section. Add Listing 64 to the end of the MAKEMAP section to place the scenery in the current room. All the code in Listing 64 belongs to the generate_map() function, so we need to indent the
first line by four spaces and then indent the remaining lines as shown. Save your program as listing64.py. listing64.py sn ip def generate_map(): sn ip ➊ if current_room in scenery: ➋ for this_scenery in scenery[current_room]: ➌ scenery_number = this_scenery[0] ➍ scenery_y = this_scenery[1] ➎ scenery_x = this_scenery[2] ➏ room_map[scenery_y][scenery_x] = scenery_number ➐ image_here = objects[scenery_number][0] ➑ image_width = image_here.get_width() ➒ image_width_in_tiles = int(image_width / TILE_SIZE) ➓ for tile_number in range(1, image_width_in_tiles): room_map[scenery_y][scenery_x + tile_number] = 255 ############### ## EXPLORER ## ############### sn ip Listing 64: Additional code for generate_map() that adds the scenery for the current room to the room_map list Let’s break this down. The line at ➊ checks whether there’s an entry for the current room in the scenery dictionary. This check is essential because some rooms in our game might not have any scenery, and if we try to use a dictionary key that doesn’t exist, Python stops with an error. We then set up a loop ➋ that cycles through the scenery items for the room and copies them into a list called this_scenery. The first time through the loop, this_scenery contains
the list for the first scenery item. The second time, it contains the list for the second item, and so on until it reaches the final scenery item for the current room. Each scenery item has a list containing its object number, y position, and x position. The program extracts these details from this_scenery using index numbers and puts them into variables called scenery_number ➌, scenery_y ➍, and scenery_x ➎. Now the program has all the information it needs to add the scenery item to room_map. You might remember that room_map stores the object number of the item in each position in the room. It uses the y position and x position in the room as list indexes. This program uses the scenery_y and scenery_x values as list indexes to put the item sce nery _n umb e r into room_map ➏.
If all our objects were one tile wide, that is all we would need to do. But some objects are wider and cover several tiles. For example, a wide object positioned in one tile might cover two more tiles to its right, but at the moment, the program only sees it in that one tile. We need to add something to room_map in those additional spaces so the program knows the player can’t walk on those tiles. I’ve used the number 255 to represent a space that doesn’t have an object in it but also cannot be walked on. Why the number 255? It’s a large enough number to give you space to add many more objects to the game if you want to, allowing for 254 items in the objects dictionary. Also, it feels like a nice number to me: it’s the highest number you can write with one byte of data (that mattered when I started writing games in the 1980s, and the computer only had about 65,000 bytes of memory to store all its data, graphics, and program code). First, we need to figure out how wide an image is so we know how many tiles it fills. We use scenery_number as the dictionary key to get information about the object from the obj ects dictionary ➐. We know the objects dictionary returns a list of information, the
first item of which is the image. So we use the index 0 to extract the image and put it into the variable image_here. Then we can use Pygame Zero to find out the width of an image by adding get_width() after its name ➑. We put that number into a variable called image_width. Because we need to know how many tiles the image covers, the program divides the image width (in pixels) by the tile size, 30, and makes it an integer (a whole number) ➒. We must convert the number to an integer because we’re going to use it in the range() function ➓, which can only take integers. If we didn’t convert the number, the width would be a
floatingpoint number—a number with a decimal point. Finally, we set up a loop that adds the value 255 in the spaces to the right of the scenery item, wherever the tile is covered ➓. If an image is 90 pixels wide, we divide it by the tile size of 30 and store the result, 3, in ima ge_w id th_ i n_ti les . Then the loop counts to 2 using range() because we give it a range of
1 to image_width_in_tiles ➓. We add the loop numbers to the x position of the object, and those positions in room_map are marked with 255. Large objects that cover three tiles now have 255 in the next two spaces to the right. Now our program contains all the scenery and can add it to the room_map, ready for display. Next, we’ll make some small changes to the EXPLORER section so we can tour the space station.
UPDATING THE EXPLORER TO TOUR THE SPACE STATION The EXPLORER part of the program lets you view all the rooms on the space station and move around the map using the arrow keys. Let’s update that section so you can see all the scenery in place. If your Explorer code includes any lines for adding scenery to the room_map, you’ll need to switch them off now. Although they’re a good way to experiment with a room design, they force the same scenery into every room and override the real room designs. Because these lines might include your ideas for room designs, rather than deleting them, you can comment them out so Python will ignore them. Click and drag the mouse to highlight all the lines at once, and then click Format ▸ Comment Out Region (or use the keyboard shortcut ALT3). Comment symbols will be added at the start of the highlighted lines, as shown in Listing 65: listing65.py s n i p ############### ## EXPLORER ## ############### def draw(): global room_height, room_width, room_map
print(current_room) generate_map() screen.clear() ## room_map[2][4] = 7 ## room_map[2][6] = 6 ## room_map[1][1] = 8 ## room_map[1][2] = 9 ## room_map[1][8] = 12 ## room_map[1][9] = 10 snip Listing 65: Commenting out code in the EXPLORER section Now we need to make a small change to the code that displays the room so it doesn’t try to draw an image for a floor space marked with 255. That space will be covered by an image to the left of it, and we don’t have an entry in the objects dictionary for 255. Listing 66 shows the new line you need to add to the EXPLORER part of the program where indicated. The if statement makes sure the instructions that draw an object run only if the object number is not (!=) 255. After adding the line, indent the existing code that comes after it by four spaces. The indentation tells Python that those instructions belong to the if instruction. You can either type four spaces at the start of the next two lines, or you can highlight them and click Format ▸ Indent Region. listing66.py s n i p ############### ## EXPLORER ## ############### s n i p for y in range(room_height): for x in range(room_width): if room_map[y][x] != 255: image_to_draw = objects[room_map[y][x]][0]
screen.blit(image_to_draw, (top_left_x + (x*30), top_left_y + (y*30) image_to_draw.get_height())) s n i p Listing 66: Updating the Explorer so it doesn’t try to show image 255 Now you’re ready to take a tour of the base. Save the program as listing66.py and run it by entering pgzrunlisting6-6.py. Use the arrow keys to move around the map and familiarize yourself with the layout of the space station. As before, the Explorer program allows you to move any direction around the map, even if a wall would block your path when playing the game. All the scenery should be in place in the rooms. Wide objects should display correctly now, and you should be able to view all the rooms again because of the changes you made earlier in Listing 65. Some objects will still have a black square under them because there’s no floor tile underneath, but we’ll fix that in Chapter 8. The space station map and scenery are now complete. It’s time to move into the space station. In the next chapter, you’ll teleport down to the surface and set foot on Mars at last.
TRAINING MISSION #1 Can you add your own room design to the scenery data? Room number 43 has been left empty for you to fill. It is 9 × 9 tiles in size, so you can place objects in positions 1 to 7 in each direction (remember the wall!). You could base your design on a room you created previously in the Explorer in Chapter 5 or invent a new layout. Remember that you need to turn off the a ss er t instructions to stop the checksum complaining when the scenery
numbers don’t add up. Your program’s objects dictionary (shown in Chapter 5) tells you the number of each object. Use object numbers between 1 and 47 to ensure that you don’t create any problems now that might affect the code when you complete and play the Escape game later. If you get stuck, try building my example, which is shown in the Mission Debrief on page 110. Change the value of current_room in the VARIABLES section to 43 so you can see your redesigned room when you first run the program.
Remember to change current_room back to 31 when you’ve finished.
ARE YOU FIT TO FLY? Check the following boxes to confirm that you’ve learned the key lessons in this chapter.
Items that cannot move during the Escape game are called scenery.
The scenery dictionary uses the room number as its key and provides a list of the fixed items in each room.
Each scenery item is stored as a list containing the object number, y position, and x position.
Checksums check whether the data has been changed or entered incorrectly.
Loops can be used to add items to the scenery dictionary. Some scenery can be positioned randomly, too.
The generate_map() function takes items that are in the current room from the scenery dictionary and puts them in the room_map list. Then the items can be displayed in the room.
The number 255 in room_map represents a space that is covered by a wide object, when the object doesn’t start in that space.
History
Topics
7
MOVING INTO THE SPACE STATION
Tutorials
Offers & Deals
Highlights
Settings Support
Now that we’ve outfitted the space station with scenery, life support systems, and other
Sign Out
equipment, it’s time to move in. In this chapter, you’ll see yourself in the space station for the first time, and you’ll be able to move around and explore the rooms. You might feel a bit stiff from the journey to begin with, but you’ll soon be walking all over the base. You’ll discover how to animate the astronaut and use the keyboard controls to move them around. You’ll also add code to enable the astronaut to move between rooms. Is there life on Mars? There is now.
ARRIVING ON THE SPACE STATION We’ll use Listing 66 from Chapter 6 as a starting point in this chapter, so open listing66.py. We’ll add code to show you in your space suit in the space station. Eventually, you’ll be able to move around using the arrow keys.
DISABLING THE ROOM NAVIGATION CONTROLS IN THE EXPLORER SECTION So far, we’ve been using the arrow keys in the EXPLORER section to show different rooms on the map. We’re going to start using those keys to move the astronaut around the
rooms. First, we need to disable the existing controls. Scroll down to the EXPLORER part of the program and highlight the instructions shown in Listing 71. Click Format ▸ Comment Out Region to turn those instructions into comments so the program will ignore them. (You can also just delete them if you prefer.) Save your program as listing71.py. listing71.py s n i p ##def movement(): ## global current_room ## old_room = current_room ## ## if keyboard.left: ## current_room = 1 ## if keyboard.right: ## current_room += 1 ## if keyboard.up: ## current_room = MAP_WIDTH ## if keyboard.down: ## current_room += MAP_WIDTH ## ## if current_room > 50: ## current_room = 50 ## if current_room 0: player_frame += 1 time.sleep(0.05) if player_frame == 5: player_frame = 0 player_offset_x = 0 player_offset_y = 0 ➎ # save player's current position old_player_x = player_x old_player_y = player_y ➏ # move if key is pressed if player_frame == 0: if keyboard.right: from_player_x = player_x from_player_y = player_y player_x += 1 player_direction = "right" player_frame = 1 elif keyboard.left: #elif stops player making diagonal movements from_player_x = player_x from_player_y = player_y player_x = 1 player_direction = "left" player_frame = 1 elif keyboard.up: from_player_x = player_x from_player_y = player_y player_y = 1 player_direction = "up" player_frame = 1 elif keyboard.down: from_player_x = player_x
from_player_y = player_y player_y += 1 player_direction = "down" player_frame = 1 ➐ # If the player is standing somewhere they shouldn't, move them back. # Keep the 2 comments below you'll need them later if room_map[player_y][player_x] not in items_player_may_stand_on: #\ # or hazard_map[player_y][player_x] != 0: player_x = old_player_x player_y = old_player_y ➑ player_frame = 0 ➒ if player_direction == "right" and player_frame > 0: player_offset_x = 1 + (0.25 * player_frame) if player_direction == "left" and player_frame > 0: player_offset_x = 1 (0.25 * player_frame) if player_direction == "up" and player_frame > 0: player_offset_y = 1 (0.25 * player_frame) if player_direction == "down" and player_frame > 0: player_offset_y = 1 + (0.25 * player_frame) ############### ## EXPLORER ## ############### sn ip Listing 74: Adding player movement At the very end of the program, you also need to add a new section called START, which will make the game_loop() function run every 0.03 seconds. Listing 75 shows you the lines to add. This instruction isn’t indented, because it doesn’t belong to a function. Python runs the instructions that aren’t inside a function in the order they appear in the program, from top to bottom. This instruction runs after all the variables, map, scenery, and prop data have been set up and the functions have been defined in the instructions above. Save your program as listing75.py. listing75.py
snip ############### ## START ## ############### clock.schedule_interval(game_loop, 0.03) snip Listing 75: Setting the game_loop() function to run regularly Run the program using pgzrunlisting7-5.py. You should be in the room (as shown in Figure 71) and be able to move using the arrow keys! You might notice your legs disappear when you walk up the screen. This is a side effect of teleportation that will wear off when we improve the code for drawing rooms in Chapter 8. At this point, the program won’t work properly if you walk out the door, but it should stop you from walking through walls or furniture. If you can walk through objects, doublecheck the new code you just added. If you still have problems, carefully check the line that sets up the items_player_may_stand_on list at the end of the OBJECTS part of the program.
UNDERSTANDING THE MOVEMENT CODE If you want to play the game and customize it with your own designs, you don’t need to understand how the code in this chapter works. You can simply replace the images and the data for maps, scenery, and props. This movement code, and the code for moving between rooms, which you’ll add later in this chapter, should keep working. However, if you want to understand how the code works and want to see how you could add animation to your programs, I’ll break it down now. This code is the real engine of the game, so in many ways it’s the most exciting bit! If you’re getting a sense of déjà vu, it’s because you’ve already seen much of this code. In Chapter 2, for your spacewalk, you used code to change the player’s position using keyboard controls and a function called game_loop() to control movement. Let’s refresh our memories and see what’s new in Listing 74. In Listing 74, we define a function called game_loop() ➊ at the start of this new section. The clock.schedule_interval() function we added at the end of the program (see Listing 7 5) makes this game_loop() function run every 0.03 seconds. Each time the game_loop()
function runs, it checks whether you’ve pressed an arrow key or are walking and, if so, updates your position. At the start of game_loop(), we tell Python which variables are global variables ➋ (see “Understanding the Spacewalk Listing” on page 27 for a refresher on why we need to do this). Some of these aren’t used yet, but we’ll need them later. Then we check the game_over variable. If it’s set to True ➌, the game_loop() function finishes without running any of its other instructions because the game is over. This variable stops the player from moving when the game ends. For now, it won’t do anything, because nothing in our program causes the game to end. The game_loop() function checks whether the player is already walking ➍. It takes four animation frames to walk one tile across the screen. If the player is moving, the pla yer_ fr ame variable contains a number between 1 and 4, which represents the
animation frame being used. If the player is walking, the program increases the pla yer_ fr ame variable by 1 to move to the next animation frame. That means the draw()
function in the EXPLORER section will show the next animation frame the next time it runs. When player_frame reaches 5, it means all the animation frames have been shown and the animation has ended. In that case, the program resets player_frame to 0 to end the animation. When the animation ends, the program also resets the player_offset_x and pla yer_ of fse t _y variables. I’ll tell you what these do in a minute.
Next, we see whether the player has pressed a key to start a new walking animation. Before we let the player move, we save their current position ➎ by storing the x position in the variable old_player_x and the y position in the variable old_player_y. We will use these variables to move the player back if they try to walk somewhere they shouldn’t, such as into a wall pillar. The program then uses a familiar block of code to change the player’s x and y position variables if an arrow key is pressed ➏. We measure the player’s position in tiles, the same units we use for positioning the scenery. This is different from when we used pixels as the measurement in Chapter 1. When the player presses the right arrow key, the program adds 1 to the x position. If the player presses the left arrow key, it subtracts 1. We use similar code to change the y position if the player presses the up or down arrow keys. When the player moves, the global variables from_player_x and from_player_y store the
position the player is walking from. These variables will be used later to check whether the player has been hit by a hazard while walking. The player_direction variable is also set to the direction they’re moving, and the player_frame is set to 1, the first frame in the animation sequence. As in Chapter 1, we use elif to combine our checks for a keypress. This ensures the player cannot change the x and y positions at the same time to move diagonally. In our 3D room, walking diagonally would enable the player to walk through obstacles, squeezing through impossible gaps. After moving the player, we check whether the new position puts them somewhere they’re allowed to be ➐. We do this by using room_map to see what item is in the position they’re standing at and checking it against the list items_player_may_stand_on. There is some code I’ve commented out here too, which we’ll need to enable later to stop players from walking through hazards. We can use the keyword in to check whether something is in a list. By using the keyword not with it, we can see whether something is missing from a list. The following line means “If the number in the map where the player is standing isn’t in the list of items, the player is allowed to stand on . . .” if room_map[player_y][player_x] not in items_player_may_stand_on: If the player is standing on something that isn’t in the items_player_may_stand_on list, we reset the player’s x and y positions to their position before they moved. All of this happens so fast that the player doesn’t notice anything. If they try to walk into a wall, it looks like they never went anywhere! This is a simpler way of stopping the player from walking through walls than checking whether each movement is allowed before making it. The program also sets the player_frame variable to 0 if the player’s position must be reset ➑. This turns off the player animation again. When you press the right arrow key, the astronaut steps one tile to the right. It takes four frames to animate this, so the astronaut is displayed at positions that are partway across the tile while this animation plays out. The player_offset_x and player_offset_y variables are used to work out where to draw the astronaut. These variables are calculated at the end of the game_loop() function ➒. The draw() function (see Listing 73) multiplies the offset values by the size of a tile (30 pixels) because images are drawn in
pixels. For example, if the offset is 0.25 tiles, the astronaut is drawn roughly 7 pixels away from the center of the new tile. The computer will round the number because you can’t position something using half a pixel. Look at the left side of Figure 72. For the first animation frame when the astronaut is walking left, we need to add threequarters of a tile to the player’s new tile position (0.75). For the second animation frame, we add half a tile (0.5) to the player’s new tile position before drawing it. For the third animation frame, we add a quarter of a tile to the player’s new tile position.
Figure 72: Understanding how the astronaut is positioned during the animation We can calculate these offset numbers using the frame number. Here’s the calculation for walking left: player_offset_x = 1 (0.25 * player_frame) Check that this calculation makes sense by working out the numbers on your own. For example, here is the calculation when the animation frame is 2:
0.25 × 2 = 0.5 1 − 0.5 = 0.5 In Figure 72, 0.5 is the correct offset for frame 2. When the player walks right, we need to subtract part of a tile from the player’s position, so the offsets are negative. Look at the right side of Figure 72. For frame 1, adding −0.75 puts the astronaut threequarters of a tile to the left of their new position. We can work out the x offset for walking right using the frame number too. Here’s the formula: player_offset_x = 1 + (0.25 * player_frame)
TRAINING MISSION #1 Can you check that the formula works? Use it to find the offset values for frames 1 and 3, and check that they match the offset values in Figure 72.
The offsets for the y direction work the same. When the astronaut is moving up, we calculate the y offset using the same formula as the left offset. When the astronaut is moving down, we calculate the y offset using the same formula as the right offset. In summary, the game_loop() function does this: If you’re not walking, it starts the walking animation when you press a key. If you are walking, it works out the next animation frame and the positions partway across the tile to use when drawing you. If you’ve reached the end of the animation sequence, it resets it so you can move again. The movement is fluid, so if you hold down a key, you’ll cycle through animation frames 1 to 4 and won’t see the standing position until you stop walking.
MOVING BETWEEN ROOMS Now that you’re on your feet, you’ll want to explore the space station fully. Let’s add
some code to the game_loop() function that lets you walk into the next room. Add the new code in Listing 76, which goes after we check for keypresses and before we check whether the player is standing somewhere they shouldn’t be. Make sure you include the instructions with comment symbols (#) at the start. We’ll need them later. The grayedout lines in Listing 76 show you where to add the new code. Save your program as listing76.py. Run it using pgzrunlisting7-6.py and then walk around the space station! This is a good time to look around, before the doors are fitted and certain areas are locked down. listing76.py sn ip def game_loop(): sn ip player_direction = "down" player_frame = 1 # check for exiting the room ➊ if player_x == room_width: # through door on RIGHT #clock.unschedule(hazard_move) ➋ current_room += 1 ➌ generate_map() ➍ player_x = 0 # enter at left ➎ player_y = int(room_height / 2) # enter at door ➏ player_frame = 0 ➐ #start_room() ➑ return ➒ if player_x == 1: # through door on LEFT #clock.unschedule(hazard_move) current_room = 1 generate_map() player_x = room_width 1 # enter at right player_y = int(room_height / 2) # enter at door player_frame = 0 #start_room() return
➓ if player_y == room_height: # through door at BOTTOM #clock.unschedule(hazard_move) current_room += MAP_WIDTH generate_map() player_y = 0 # enter at top player_x = int(room_width / 2) # enter at door player_frame = 0 #start_room() return if player_y == 1: # through door at TOP #clock.unschedule(hazard_move) current_room = MAP_WIDTH generate_map() player_y = room_height 1 # enter at bottom player_x = int(room_width / 2) # enter at door player_frame = 0 #start_room() return # If the player is standing somewhere they shouldn't, move them back. if room_map[player_y][player_x] not in items_player_may_stand_on: #\ # or hazard_map[player_y][player_x] != 0: player_x = old_player_x sn ip Listing 76: Enabling the player to move between rooms To see how this code works, let’s use an example room map. Figure 73 shows a room 9 tiles wide and 9 tiles high with exits on each wall. We’ll use this image to understand the player’s position when they’ve left the room. As you know, the positions on the map are numbered starting at 0 in the top left. The yellow squares show where the player might be if they walked out of the room: If the player’s y position is −1, they’ve walked out of the top exit. If the player’s x position is −1, they’ve walked out of the left exit.
If the player’s y position is the same as the room_height variable, they’ve walked out of the bottom. The tile positions are numbered starting at 0, so if the player goes into row 9 in a room that has 9 rows, they’ve already left the room. Similarly, if the player’s x position is the same as the room_width variable, they’ve walked out of the right exit.
Figure 73: Working out whether the player has walked through an exit The new code lines check whether the player position means they’ve walked out of the room. If the player’s x position is the same as room_width ➊, they’re outside the door on the right, as shown in Figure 73. When a player leaves the room, we need to change the number of the room they’re in, which is stored in the current_room variable. When they go through a door on the right,
the room number increases by 1 ➋. Look at the room map again (flip back to Figure 41 on page 60) to see that this makes sense: room numbers increase from left to right. For example, if the player is in room 33 and walks through the exit on the right, they end up in room 34. The program then generates a new room_map list ➌ to use in displaying and navigating the new room. The player is repositioned at the opposite side of the room ➍, so it looks like they’ve walked through the doorway. If the player exits to the right of the room, they enter the next room from the left ➍. Rooms are lots of different sizes, so we also need to change the player’s y position to put them in the middle of the doorway. Otherwise, the player might emerge from a wall! We set the player’s position to be half the height of the room ➎, which means they’re right in the middle of the doorway. When they enter the room, we reset the player animation, too ➏. I’ve included a couple of features here that we’ll need later, so make sure you include the clock.unschedule(hazard_move) ➊ and start_room() ➐ instructions. The start_room() function will display the room name when the player enters a new room. We’ll talk about those instructions more later in the book. Finally, the return instruction exits the game_loop() function ➑. Any further instructions in the function won’t run this time around. When the function starts again, it will start from the top as usual. The next code block ➒ checks whether the player went through the left door. To go through the left door, the program does the following: Checks whether the player_x variable contains -1 (see Figure 73). Subtracts 1 from the current room number to go into the room on the left. Sets the player’s x position to be just inside the doorway on the right. This position is the room_width minus 1. (You can check this in Figure 73. In a room that has a ro om _width of 9, the player’s x position should be 8.)
Sets the player’s y position to the middle using the room_height. This is the same approach as walking through the right exit. The same code structure is used for the top and bottom exits ➓. However, the program
checks the player’s y position to see if they used an exit and sets their new position to enter through a top or bottom doorway. This time, we change the room number by 5 instead of 1 because that’s how many rooms wide the game map is (see Figure 41). For example, if you’re in room 37 and you go through the top exit, you end up in room 32 (which is 37 minus 5). If you’re in room 37 and go through the bottom exit, you end up in room 42 (37 plus 5). We stored the number 5 in the variable MAP_WIDTH earlier, and the program uses it here. Now you’re able to freely explore the space station. In the next chapter, we’ll fix the remaining few bugs in the room display.
ARE YOU FIT TO FLY? Check the following boxes to confirm that you’ve learned the key lessons in this chapter.
The player’s position in the Escape game is measured in tiles, just like the scenery.
The game_loop() function controls player movement and is scheduled to run every 0.03 seconds.
If the player moves somewhere they aren’t allowed to be, they’re put back in their previous position so fast you won’t see them move.
The program checks the player’s x and y positions to see whether they’ve walked out of an exit. If they have, they’ll appear in the middle of the opposite exit in the next room.
The animation frames are stored in the PLAYER dictionary and have a list of images for each direction. The dictionary key is the direction name, and an index number gets the particular frame needed.
Frame 0 is the standingstill position. Frames 1, 2, 3, and 4 show the astronaut walking.
The game_loop() function increases the animation frame number used when the player is walking.
The player_offset_x and player_offset_y variables are used to position the astronaut correctly when walking into a new tile.
8
History
Topics
REPAIRING THE SPACE STATION
Tutorials
Offers & Deals
Highlights
Settings Support
While wandering around the space station, you must have noticed that some things
Sign Out
don’t look quite right. To get the program up and running quickly, we used the EXPLORER section to display the rooms. However, it has a few drawbacks: Sometimes a blank space is shown beneath the scenery because there’s no floor there. When you walk to the front of the room, the front wall hides the astronaut.
The astronaut’s legs disappear when walking to the back of the screen. The rooms are all drawn in the top left of the game window. This makes it look uneven and inconsistent, because there’s much more space on the right of the rooms than on the left, and wider rooms leave less space on the right than narrow rooms do. There are no shadows, making it harder to understand the position of objects in the room. In this chapter, we’ll fix these glitches and also add a function for displaying messages
at the top of the window. These messages will give players information about the space station and their progress in the game. As you read through the chapter, you’ll learn how to send information to a Python function and discover how to draw rectangles using Pygame Zero. By the end of the chapter, the space station will look great!
SENDING INFORMATION TO A FUNCTION For the first time, we’ll need to send information to a function. You’ve already seen how to send information to the print() function by putting it between the parentheses. For example, you can output a message like this: print("Learn your emergency evacuation drill") When that instruction runs, the print() function receives information you put in the brackets, and displays it in the command line window or the Python shell. We can also send information to functions we’ve made.
CREATING A FUNCTION THAT RECEIVES INFORMATION To experiment with functions, we’ll build a function that adds two numbers that we send it. Click File ▸ New to open a new window, and enter the program in Listing 81. listing81.py ➊ def add(first_number, second_number): ➋ total = first_number + second_number ➌ print(first_number, "+", second_number, "=", total) ➍ add(5, 7) add(2012, 137) add(1234, 4321) Listing 81: Sending information to a function Save the program as listing81.py. Because it doesn’t use any Pygame Zero features, you can run it by clicking Run ▸ Run Module or by pressing F5. (If you do run it using Pygame Zero, the results will appear in the command line window, and the game
window will be empty.) When you run the program, you should see the following output: 5 + 7 = 12 2012 + 137 = 2149 1234 + 4321 = 5555 We create a new function called add() ➊. After we’ve defined add(), we can run it by using its name ➍ and send it numbers by putting them in the parentheses, using commas between them ➍. The function will then add those two numbers.
HOW IT WORKS To enable the function to receive the numbers, we give it two variables to store the numbers in when we define it. I’ve called them first_number and second_number ➊ to make the program easier to understand, but the variable names could be anything. These are local variables: they only work inside this function. When you use the function, it takes the first item it receives and puts it into the variable fir st_n um ber . The second item goes into second_number.
Of course, it doesn’t matter which order you add two numbers in, so it doesn’t matter what order you send the numbers in. The instructions add(5,7) and add(7,5) give the same result. But some functions will need you to send the information in the same order the function expects to receive it. For example, if the function were subtracting numbers, you’d get a different result if you sent the numbers in the wrong order. The only way to know what information a function expects to receive is to take a look at its code. The body of the function is quite simple. It creates a new variable called total, which stores the result of adding the two numbers ➋. The program then prints a line that contains the first number, a plus sign, the second number, an equal sign, and the total ➌. In the last three instructions, we send the function three pairs of numbers to add ➍. This simple demonstration shows you how information (or arguments) can be sent to a function. You can make functions that take more arguments than just two, and even take lists, dictionaries, or images. Functions make it easy to reuse sets of instructions,
and sending arguments means we can reuse those instructions with different information. For example, Listing 81 uses the same print() instruction three times, to display the sum of three different number pairs. In this case, we’ve avoided repeating the print() instruction and the one that sets up the total variable. More sophisticated functions can avoid repeating a lot of code, and this can make the program much easier to write and understand.
TRAINING MISSION #1 Try modifying the program to subtract one number from another rather than adding. What happens when you change the order of the numbers you send to the new function? You might want to change more than just the calculation to make sure the function is easy to use.
Now we’re ready to add some new functions to Escape to draw objects on the space station.
ADDING VARIABLES FOR SHADOWS, WALL TRANSPARENCY, AND COLORS To fix our space station, we’ll create new display functions for the Escape game, using our newfound knowledge of functions. Before we make these new functions, we need to set up new variables for the functions to use. Open listing76.py, the last listing you saved in Chapter 7. Find the VARIABLES section near the start of the program, and add the new lines shown in Listing 82. Save the program as listing82.py. As always, it’s a good idea to run the program (using pgzrun l isting 8-2 .p y ) to check for any new errors.
listing82.py sn ip ############### ## VARIABLES ## ############### sn ip
player_image = PLAYER[player_direction][player_frame] player_offset_x, player_offset_y = 0, 0 ➊ PLAYER_SHADOW = { "left": [images.spacesuit_left_shadow, images.spacesuit_left_1_shadow, images.spacesuit_left_2_shadow, images.spacesuit_left_3_shadow, images.spacesuit_left_3_shadow ], "right": [images.spacesuit_right_shadow, images.spacesuit_right_1_shadow, images.spacesuit_right_2_shadow, images.spacesuit_right_3_shadow, images.spacesuit_right_3_shadow ], "up": [images.spacesuit_back_shadow, images.spacesuit_back_1_shadow, images.spacesuit_back_2_shadow, images.spacesuit_back_3_shadow, images.spacesuit_back_3_shadow ], "down": [images.spacesuit_front_shadow, images.spacesuit_front_1_shadow, images.spacesuit_front_2_shadow, images.spacesuit_front_3_shadow, images.spacesuit_front_3_shadow ] } ➋ player_image_shadow = PLAYER_SHADOW["down"][0] ➌ PILLARS = [ images.pillar, images.pillar_95, images.pillar_80, images.pillar_60, images.pillar_50 ] ➍ wall_transparency_frame = 0 ➎ BLACK = (0, 0, 0) BLUE = (0, 155, 255) YELLOW = (255, 255, 0) WHITE = (255, 255, 255) GREEN = (0, 255, 0) RED = (128, 0, 0)
############### ## MAP ## ############### sn ip Listing 82: Adding the variables needed for the new display functions We add a PLAYER_SHADOW dictionary ➊ that’s similar to the PLAYER dictionary. It contains animation frames for the astronaut’s shadow on the floor. As the astronaut moves, the shadow also changes shape. The player_image_shadow ➋ stores the astronaut’s current shadow, like the player_image variable that stores the astronaut’s current animation frame (or the standing image). Later in this chapter, we’ll add animation that fades out the front wall when you walk behind it so you can still see the astronaut. Here, we set up a list of the animation frames ➌ and a wall_transparency_frame variable to remember the one that’s being shown now ➍. You’ll learn more about how these work later on. We’ve also set up some names that we can use to refer to color numbers ➎. Colors in Pygame Zero are stored as tuples. A tuple is like a list whose content you can’t change, and it uses parentheses instead of square brackets. You’ve seen tuples used for coordinates when drawing on the screen (see Chapter 1). Colors are stored as three numbers that specify the amount of red, green, and blue in the color, in that order. The scale for each color ranges from 0 to 255. This color is bright red: (255, 0, 0) The red is at its maximum (255), and there’s no green (0) or blue (0) in the color. Because we’ve set up these color variables, we can now use the name BLACK instead of using the tuple (0,0,0) to represent black. Using color names will make the program easier to read. Table 81 shows you some of the color combinations that you might want to use in your programs. You can also try different numbers to invent your own colors. Table 81: Some Example RGB Color Numbers
Red
GreenBlue
Description
255
0
0
Bright red
0
255
0
Bright green
0
0
255
Bright blue
0
0
50
Very dark blue (nearly black!)
255
255
255
White (all the colors at maximum strength)
255
255
150
Creamy yellow (slightly less blue than white)
230
230
230
Silver (a slightly toned-down white)
200
150
200
Lilac
255
100
0
Orange (maximum red with a dash of green)
255
105
180
Pink
DELETING THE EXPLORER SECTION We need to add a new DISPLAY section with some new functions that will improve the game’s appearance onscreen. The EXPLORER section has enabled us to get up and running quickly, but we’re going to build a new and better draw() function in this chapter that
replaces the one we’ve used so far. To avoid any problems caused by EXPLORER code still being in the program, we’re going to remove it. Your EXPLORER section might have more or fewer lines than mine does in Figure 81, depending on whether you deleted some of it in earlier chapters. To delete the entire EXPLORER section, follow these steps: 1. Find the EXPLORER part of the program near the end of the code. 2. Click the start of the EXPLORER comment box, hold down the mouse button, and drag the mouse to the bottom of the section (see Figure 81). The section ends just above where the START section begins. 3. Press DELETE or BACKSPACE on the keyboard. There’s one instruction in the EXPLORER section that we still need: it runs the generate_map() function to set up the room map for the first room. You’ll need to add that instruction to the end of the program as a single line, as shown in Listing 83. listing83.py s n i p ############### ## START ## ############### clock.schedule_interval(game_loop, 0.03) generate_map() Listing 83: Generating the map for the first room The generate_map() line will run after the variables have been set up and will make the map for the current room.
Figure 81: Deleting the EXPLORER section Save your new listing as listing83.py and run it using pgzrunlisting8-3.py. If all is going to plan, you should see no error messages in the command line window. The game window shows the inky blackness of space because we haven’t added the new code to draw anything yet.
ADDING THE DISPLAY SECTION Now we’ll add the new DISPLAY section to replace the deleted EXPLORER section. This section contains most of the code for updating the screen display. It includes code for drawing the room, showing messages, and changing the transparency of the front wall.
ADDING THE FUNCTIONS FOR DRAWING OBJECTS
ADDING THE FUNCTIONS FOR DRAWING OBJECTS First, we’ll make some functions to draw an object, a shadow, or the player at a particular tile position. Between the GAMELOOP and START sections, add the new DISPLAY section shown in Listing 84 to your program. Save this program as listing84.py and run it using pgzrunlisting8-4.py. Again, you won’t see anything in the game window yet. If there are any errors in the command line window, you can use them to help you fix the program. It’s better to test as you add code to the program than to add a lot of code and not know where the mistakes might be. listing84.py sn ip if player_direction == "down" and player_frame > 0: player_offset_y = 1 + (0.25 * player_frame) ############### ## DISPLAY ## ############### ➊ def draw_image(image, y, x): ➋ screen.blit( image, (top_left_x + (x * TILE_SIZE), top_left_y + (y * TILE_SIZE) image.get_height()) ) ➌ def draw_shadow(image, y, x): screen.blit( image, (top_left_x + (x * TILE_SIZE), top_left_y + (y * TILE_SIZE)) ) def draw_player(): ➍ player_image = PLAYER[player_direction][player_frame] ➎ draw_image(player_image, player_y + player_offset_y, player_x + player_offset_x)
➏ player_image_shadow = PLAYER_SHADOW[player_direction][player_frame] ➐ draw_shadow(player_image_shadow, player_y + player_offset_y, player_x + player_offset_x) ############### ## START ## ############### clock.schedule_interval(game_loop, 0.03) generate_map() Listing 84: Adding the first functions in the DISPLAY section The first new function, draw_image() ➊, draws a given image on the screen. When we use it, we give it the image we want to draw and the y and x tile positions of the object in the room. The function will work out where on the screen to draw the image (the pixel position), based on the tile position in the room. For example, we might use the function like this: draw_image(player_image, 5, 2) This line draws the player image at position y = 5 and x = 2 in the room. When we define the draw_image() function, we set it up to give the image the name image, put the y position into the y variable, and put the x position into the x variable ➊. Although the draw_image() function is several lines long, its only instruction is sc re en . blit ( ) , which draws the image at the position we specify ➋. This instruction is
virtually the same as the one we used in the old EXPLORER section, so take a look at Chapter 3 for a refresher on how it works.
TIP Make sure all the parentheses are in the correct places. You need a pair around all the screen.blit() arguments and another pair around the y and x positions because they make up a single tuple. You also need a pair around the multiplication parts of the position calculations. If the program doesn’t work, start checking for errors by counting the opening and closing parentheses to make sure you have the same number of each of them.
We then add a new draw_shadow() function ➌. This is similar to the function for drawing an image, except that the image’s height is not subtracted when calculating its onscreen position. This is what places the shadow below the main image. Figure 82 shows the astronaut and their shadow based on the same tile position. Remember that the y position given to screen.blit() is for the top edge of the image.
Figure 82: Working out the position of the image and the shadow The third new function, draw_player(), draws the astronaut. First, it puts the correct astronaut animation frame into player_image ➍. It then uses the new draw_image() function to draw it ➎. The draw_image() function requires the following arguments: The variable player_image, which contains the image to draw. The result after adding the global variables for player_y and player_offset_y. This is the y position in tiles, which might include a decimal part (such as 5.25). The result after adding player_x and player_offset_x for the x position in tiles. (See “Understanding the Movement Code” on page 119 for more information on how the offset variables are used for animation.) We use similar code to draw the player’s shadow: the correct animation frame from the P L A Y E R _ S H A D O W dictionary is put into player_image_shadow ➏. Then the draw_shadow() function is
used to draw it ➐. The draw_shadow() function uses the same tile positions as the d r a w _ i m a g e ( ) function.
DRAWING THE ROOM Now that we’ve created the functions for drawing objects and the player, we can add the code to draw the room. The new draw() function in Listing 85 adds shadows for scenery and the player, and fixes the visual glitches we saw previously.
Add the new code at the end of the DISPLAY section, save your program as listing85.py, and run it using pgzrunlisting8-5.py. As if you’ve flicked the lights on, the shadows appear in front of the objects. The game won’t look quite right yet because all the rooms will be drawn in the top left of the window, and sometimes a room won’t be cleared properly when you leave it. We’ll fix this in a moment. At this point, you shouldn’t see any error messages. listing85.py sn ip def draw_player(): player_image = PLAYER[player_direction][player_frame] draw_image(player_image, player_y + player_offset_y, player_x + player_offset_x) player_image_shadow = PLAYER_SHADOW[player_direction][player_frame] draw_shadow(player_image_shadow, player_y + player_offset_y, player_x + player_offset_x) def draw(): if game_over: return ➊ # Clear the game arena area. box = Rect((0, 150), (800, 600)) screen.draw.filled_rect(box, RED) box = Rect ((0, 0), (800, top_left_y + (room_height 1)*30)) ➋ screen.surface.set_clip(box) floor_type = get_floor_type() ➌ for y in range(room_height): # Lay down floor tiles, then items on floor. for x in range(room_width): draw_image(objects[floor_type][0], y, x) # Next line enables shadows to fall on top of objects on floor if room_map[y][x] in items_player_may_stand_on: draw_image(objects[room_map[y][x]][0], y, x) ➍ # Pressure pad in room 26 is added here, so props can go on top of it. if current_room == 26:
draw_image(objects[39][0], 8, 2) image_on_pad = room_map[8][2] if image_on_pad > 0: draw_image(objects[image_on_pad][0], 8, 2) ➎ for y in range(room_height): for x in range(room_width): item_here = room_map[y][x] # Player cannot walk on 255: it marks spaces used by wide objects. if item_here not in items_player_may_stand_on + [255]: image = objects[item_here][0] ➏ if (current_room in outdoor_rooms and y == room_height 1 and room_map[y][x] == 1) or \ (current_room not in outdoor_rooms and y == room_height 1 and room_map[y][x] == 1 and x > 0 and x 0: player_offset_x = 1 + (0.25 * player_frame) snip Listing 1213: Reducing the player’s energy when they walk on the toxic floor
MAKING THE FINISHING TOUCHES The game is now nearly complete. Before you embark on your exploration of the space station, we need to remove some of the instructions we used while building and testing the game.
DISABLING THE TELEPORTER Mission rules forbid the use of the teleporter once your work on the space station begins. Find its instructions in the game_loop() function, highlight them using your mouse, and then click Format ▸ Comment Out Region to disable them. Your code should now look like Listing 1214. listing1214.py s n i p #### Teleporter for testing #### Remove this section for the real game ## if keyboard.x: ## current_room = int(input("Enter room number:")) ## player_x = 2 ## player_y = 2 ## generate_map() ## start_room() ## sounds.teleport.play() #### Teleport section ends s n i p Listing 1214: The teleporter is turned off.
CLEANING UP THE DATA
CLEANING UP THE DATA While testing the game, you might have changed the contents of some of the variables and lists. The game should look like Figure 125 when it begins. If it doesn’t, look at the VA RI AB L ES section of the program and make sure the current_room variable is set to 31.
Figure 125: The start of your mission If you’re carrying more than your yoyo, look at the PROPS part of the program and check that this line is correct: in_my_pockets = [55]
YOUR ADVENTURE BEGINS It’s an exciting moment: your training is complete; the space station is ready; and your mission on Mars is about to begin. Let’s set a scifi fanfare to play when the game starts. Listing 1215 shows the final instruction you’ll add to Escape.
listing1215.py snip clock.schedule_unique(alarm, 10) clock.schedule_interval(air_countdown, 11) # A higher number gives a longer time limit. sounds.mission.play() # Intro music Listing 1215: A scifi fanfare plays when the game begins. Save your final program as escape.py. You can now play the game using pgzrunescape.py. See “Playing the Game” on page 11 for instructions. Congratulations on completing the space station construction. You’ve truly earned your place on this mission. It’s time to begin your work on the planet’s surface!
YOUR NEXT MISSION: CUSTOMIZING THE GAME Did you make it to safety in the Escape game? That was a close shave! For your next mission, try customizing the game. There are different ways to use this book, so you might already have made some customizations as you built the game. Here are some suggestions for modifying the game, starting with the easiest: Change the names of the characters in the game to those of your friends. See Listing 41 on page 63 in Chapter 4. Customize the images. You can edit our images, or create your own. The game includes a whiteboard image that you can edit using your favorite art package. If you make your images the same size as ours, use the same filenames, and store them in the images folder, they should just drop into the game world with no problem. Redesign the room layouts. Chapter 6 explains how scenery is positioned in a room. Add your own objects to the game. Start by creating their images. Props should be 30 pixels square. Scenery items can be bigger and should touch the left and right sides of their tile spaces so that it doesn’t look odd when the player can’t get closer to the scenery than the tile next door. (For example, if your image is 30, 60, or 90 pixels wide and touches the ground at both sides, it should look fine.) You need to add the new items in the objects dictionary (see Chapter 5). For help positioning scenery, see Chapter 6. For advice on positioning props, see Chapter 9.
Create your own space station map (see Chapter 4). Use the game engine to make your own game. You can replace the images and maps, and code your own puzzles to make a new game based on the Escape code. The USEOBJECTS section is where the game puzzles are programmed. It details what happens when objects are used, individually or in combination with other objects. It might be useful to keep the code for combining objects (recipes) and just update it (see Chapter 10); keep the code for displaying standard responses (see Chapter 10); and keep the code for opening doors (see Chapter 11). If you make any changes that affect room 26, you’ll need to disable the code for its pressure pad (see Chapter 11). Bear in mind that any changes you make might break the puzzles in the original Escape game, making it impossible to complete. For example, it might become impossible to find important tools. I recommend saving any changes you make separately, so you can always come back to the original code.
SHARING YOUR CUSTOMIZATIONS I’d love to hear about your customizations! You can find me on Twitter at @musicandwords or visit my website at www.sean.co.uk, which includes bonus content for the book. If you share your modified Escape game with others or share your own games built using its code, sounds, or images, please credit this book and its author, and make it clear that you’ve modified the code. Thank you!
ARE YOU FIT TO FLY? Check the following boxes to confirm that you’ve learned the key lessons in this chapter.
You can use Pygame Zero to draw text with a shadow underneath it and can adjust the size of the text displayed.
You can play a sound multiple times by putting the number of times in parentheses in its sounds.sound_name.play() instruction.
The moving hazards’ directions are numbered from 1 at the top, moving clockwise.
To create a movement pattern for a hazard, you provide the number you want to add to its direction number when it hits something.
The deplete_energy() function reduces the player’s energy. Hazards use their own room map called hazard_map. This enables them to more easily move over objects on the floor.
Before playing the game, check that the starting variables are correct.
A
Playlists
History
ESCAPE: THE COMPLETE GAME LISTING
Topics
Tutorials
Offers & Deals
Highlights
Settings
This appendix shows the final listing for the Escape game. You can use it as a reference to see where to place particular functions and sections, or read through it if you want to Support see the whole listing in one place. This listing doesn’t include the temporary sections Signyou wrote while building the game, such as the Out EXPLORER section. It just contains the code
that is in the final game. Remember that you can also download the escape.py listing and read it in IDLE, which lets you search it by pressing CTRLF. I’ve changed PLAYER_NAME to “Captain” in this listing. When you’re building or customizing the game, you can use your own name (see ➊ in Listing 41 on page 63). To test this project, I rebuilt the game using the instructions in this book. This game listing has been copied from game code that has been tested to completion on Windows, the Raspberry Pi 3 Model B+, and the Raspberry Pi 2 Model B. # Escape A Python Adventure # by Sean McManus / www.sean.co.uk # Art by Rafael Pimenta # Typed in by PUT YOUR NAME HERE
.
import time, random, math ############### ## VARIABLES ## ############### WIDTH = 800 #window size HEIGHT = 800 #PLAYER variables PLAYER_NAME = "Captain" # change this to your name! FRIEND1_NAME = "Karen" # change this to a friend's name! FRIEND2_NAME = "Leo" # change this to another friend's name! current_room = 31 # start room = 31 top_left_x = 100 top_left_y = 150 DEMO_OBJECTS = [images.floor, images.pillar, images.soil] LANDER_SECTOR = random.randint(1, 24) LANDER_X = random.randint(2, 11) LANDER_Y = random.randint(2, 11) TILE_SIZE = 30 player_y, player_x = 2, 5 game_over = False PLAYER = { "left": [images.spacesuit_left, images.spacesuit_left_1, images.spacesuit_left_2, images.spacesuit_left_3, images.spacesuit_left_4 ], "right": [images.spacesuit_right, images.spacesuit_right_1, images.spacesuit_right_2, images.spacesuit_right_3, images.spacesuit_right_4 ],
"up": [images.spacesuit_back, images.spacesuit_back_1, images.spacesuit_back_2, images.spacesuit_back_3, images.spacesuit_back_4 ], "down": [images.spacesuit_front, images.spacesuit_front_1, images.spacesuit_front_2, images.spacesuit_front_3, images.spacesuit_front_4 ] } player_direction = "down" player_frame = 0 player_image = PLAYER[player_direction][player_frame] player_offset_x, player_offset_y = 0, 0 PLAYER_SHADOW = { "left": [images.spacesuit_left_shadow, images.spacesuit_left_1_shadow, images.spacesuit_left_2_shadow, images.spacesuit_left_3_shadow, images.spacesuit_left_3_shadow ], "right": [images.spacesuit_right_shadow, images.spacesuit_right_1_shadow, images.spacesuit_right_2_shadow, images.spacesuit_right_3_shadow, images.spacesuit_right_3_shadow ], "up": [images.spacesuit_back_shadow, images.spacesuit_back_1_shadow, images.spacesuit_back_2_shadow, images.spacesuit_back_3_shadow, images.spacesuit_back_3_shadow ], "down": [images.spacesuit_front_shadow, images.spacesuit_front_1_shadow, images.spacesuit_front_2_shadow, images.spacesuit_front_3_shadow, images.spacesuit_front_3_shadow ] } player_image_shadow = PLAYER_SHADOW["down"][0] PILLARS = [ images.pillar, images.pillar_95, images.pillar_80, images.pillar_60, images.pillar_50
] wall_transparency_frame = 0 BLACK = (0, 0, 0) BLUE = (0, 155, 255) YELLOW = (255, 255, 0) WHITE = (255, 255, 255) GREEN = (0, 255, 0) RED = (128, 0, 0) air, energy = 100, 100 suit_stitched, air_fixed = False, False launch_frame = 0
############### ## MAP ## ############### MAP_WIDTH = 5 MAP_HEIGHT = 10 MAP_SIZE = MAP_WIDTH * MAP_HEIGHT GAME_MAP = [ ["Room 0 where unused objects are kept", 0, 0, False, False] ] outdoor_rooms = range(1, 26) for planetsectors in range(1, 26): #rooms 1 to 25 are generated here GAME_MAP.append( ["The dusty planet surface", 13, 13, True, True] ) GAME_MAP += [ #["Room name", height, width, Top exit?, Right exit?] ["The airlock", 13, 5, True, False], # room 26 ["The engineering lab", 13, 13, False, False], # room 27 ["Poodle Mission Control", 9, 13, False, True], # room 28 ["The viewing gallery", 9, 15, False, False], # room 29 ["The crew's bathroom", 5, 5, False, False], # room 30 ["The airlock entry bay", 7, 11, True, True], # room 31 ["Left elbow room", 9, 7, True, False], # room 32
["Right elbow room", 7, 13, True, True], # room 33 ["The science lab", 13, 13, False, True], # room 34 ["The greenhouse", 13, 13, True, False], # room 35 [PLAYER_NAME + "'s sleeping quarters", 9, 11, False, False], # room 36 ["West corridor", 15, 5, True, True], # room 37 ["The briefing room", 7, 13, False, True], # room 38 ["The crew's community room", 11, 13, True, False], # room 39 ["Main Mission Control", 14, 14, False, False], # room 40 ["The sick bay", 12, 7, True, False], # room 41 ["West corridor", 9, 7, True, False], # room 42 ["Utilities control room", 9, 9, False, True], # room 43 ["Systems engineering bay", 9, 11, False, False], # room 44 ["Security portal to Mission Control", 7, 7, True, False], # room 45 [FRIEND1_NAME + "'s sleeping quarters", 9, 11, True, True], # room 46 [FRIEND2_NAME + "'s sleeping quarters", 9, 11, True, True], # room 47 ["The pipeworks", 13, 11, True, False], # room 48 ["The chief scientist's office", 9, 7, True, True], # room 49 ["The robot workshop", 9, 11, True, False] # room 50 ] #simple sanity check on map above to check data entry assert len(GAME_MAP)1 == MAP_SIZE, "Map size and GAME_MAP don't match"
############### ## OBJECTS ## ############### objects = { 0: [images.floor, None, "The floor is shiny and clean"], 1: [images.pillar, images.full_shadow, "The wall is smooth and cold"], 2: [images.soil, None, "It's like a desert. Or should that be dessert?"], 3: [images.pillar_low, images.half_shadow, "The wall is smooth and cold"], 4: [images.bed, images.half_shadow, "A tidy and comfortable bed"], 5: [images.table, images.half_shadow, "It's made from strong plastic."], 6: [images.chair_left, None, "A chair with a soft cushion"], 7: [images.chair_right, None, "A chair with a soft cushion"], 8: [images.bookcase_tall, images.full_shadow, "Bookshelves, stacked with reference books"],
9: [images.bookcase_small, images.half_shadow, "Bookshelves, stacked with reference books"], 10: [images.cabinet, images.half_shadow, "A small locker, for storing personal items"], 11: [images.desk_computer, images.half_shadow, "A computer. Use it to run life support diagnostics"], 12: [images.plant, images.plant_shadow, "A spaceberry plant, grown here"], 13: [images.electrical1, images.half_shadow, "Electrical systems used for powering the space station"], 14: [images.electrical2, images.half_shadow, "Electrical systems used for powering the space station"], 15: [images.cactus, images.cactus_shadow, "Ouch! Careful on the cactus!"], 16: [images.shrub, images.shrub_shadow, "A space lettuce. A bit limp, but amazing it's growing here!"], 17: [images.pipes1, images.pipes1_shadow, "Water purification pipes"], 18: [images.pipes2, images.pipes2_shadow, "Pipes for the life support systems"], 19: [images.pipes3, images.pipes3_shadow, "Pipes for the life support systems"], 20: [images.door, images.door_shadow, "Safety door. Opens automatically \ for astronauts in functioning spacesuits."], 21: [images.door, images.door_shadow, "The airlock door. \ For safety reasons, it requires two person operation."], 22: [images.door, images.door_shadow, "A locked door. It needs " \ + PLAYER_NAME + "'s access card"], 23: [images.door, images.door_shadow, "A locked door. It needs " \ + FRIEND1_NAME + "'s access card"], 24: [images.door, images.door_shadow, "A locked door. It needs " \ + FRIEND2_NAME + "'s access card"], 25: [images.door, images.door_shadow, "A locked door. It is opened from Main Mission Control"], 26: [images.door, images.door_shadow, "A locked door in the engineering bay."], 27: [images.map, images.full_shadow, "The screen says the crash site was Sector: " \ + str(LANDER_SECTOR) + " // X: " + str(LANDER_X) + \ " // Y: " + str(LANDER_Y)], 28: [images.rock_large, images.rock_large_shadow, "A rock. Its coarse surface feels like a whetstone", "the rock"],
29: [images.rock_small, images.rock_small_shadow, "A small but heavy piece of Martian rock"], 30: [images.crater, None, "A crater in the planet surface"], 31: [images.fence, None, "A fine gauze fence. It helps protect the station from dust storms"], 32: [images.contraption, images.contraption_shadow, "One of the scientific experiments. It gently vibrates"], 33: [images.robot_arm, images.robot_arm_shadow, "A robot arm, used for heavy lifting"], 34: [images.toilet, images.half_shadow, "A sparkling clean toilet"], 35: [images.sink, None, "A sink with running water", "the taps"], 36: [images.globe, images.globe_shadow, "A giant globe of the planet. It gently glows from inside"], 37: [images.science_lab_table, None, "A table of experiments, analyzing the planet soil and dust"], 38: [images.vending_machine, images.full_shadow, "A vending machine. It requires a credit.", "the vending machine"], 39: [images.floor_pad, None, "A pressure sensor to make sure nobody goes out alone."], 40: [images.rescue_ship, images.rescue_ship_shadow, "A rescue ship!"], 41: [images.mission_control_desk, images.mission_control_desk_shadow, \ "Mission Control stations."], 42: [images.button, images.button_shadow, "The button for opening the timelocked door in engineering."], 43: [images.whiteboard, images.full_shadow, "The whiteboard is used in brainstorms and planning meetings."], 44: [images.window, images.full_shadow, "The window provides a view out onto the planet surface."], 45: [images.robot, images.robot_shadow, "A cleaning robot, turned off."], 46: [images.robot2, images.robot2_shadow, "A planet surface exploration robot, awaiting setup."], 47: [images.rocket, images.rocket_shadow, "A 1person craft in repair."], 48: [images.toxic_floor, None, "Toxic floor do not walk on!"], 49: [images.drone, None, "A delivery drone"], 50: [images.energy_ball, None, "An energy ball dangerous!"], 51: [images.energy_ball2, None, "An energy ball dangerous!"], 52: [images.computer, images.computer_shadow, "A computer workstation, for managing space station systems."], 53: [images.clipboard, None,
"A clipboard. Someone has doodled on it.", "the clipboard"], 54: [images.bubble_gum, None, "A piece of sticky bubble gum. Spaceberry flavour.", "bubble gum"], 55: [images.yoyo, None, "A toy made of fine, strong string and plastic. \ Used for antigrav experiments.", PLAYER_NAME + "'s yoyo"], 56: [images.thread, None, "A piece of fine, strong string", "a piece of string"], 57: [images.needle, None, "A sharp needle from a cactus plant", "a cactus needle"], 58: [images.threaded_needle, None, "A cactus needle, spearing a length of string", "needle and string"], 59: [images.canister, None, "The air canister has a leak.", "a leaky air canister"], 60: [images.canister, None, "It looks like the seal will hold!", "a sealed air canister"], 61: [images.mirror, None, "The mirror throws a circle of light on the walls.", "a mirror"], 62: [images.bin_empty, None, "A rarely used bin, made of light plastic", "a bin"], 63: [images.bin_full, None, "A heavy bin full of water", "a bin full of water"], 64: [images.rags, None, "An oily rag. Pick it up by a corner if you must!", "an oily rag"], 65: [images.hammer, None, "A hammer. Maybe good for cracking things open...", "a hammer"], 66: [images.spoon, None, "A large serving spoon", "a spoon"], 67: [images.food_pouch, None, "A dehydrated food pouch. It needs water.", "a dry food pack"], 68: [images.food, None, "A food pouch. Use it to get 100% energy.", "readytoeat food"], 69: [images.book, None, "The book has the words 'Don't Panic' on the \ cover in large, friendly letters", "a book"], 70: [images.mp3_player, None, "An MP3 player, with all the latest tunes", "an MP3 player"], 71: [images.lander, None, "The Poodle, a small space exploration craft. \ Its black box has a radio sealed inside.", "the Poodle lander"], 72: [images.radio, None, "A radio communications system, from the \ Poodle", "a communications radio"], 73: [images.gps_module, None, "A GPS Module", "a GPS module"],
74: [images.positioning_system, None, "Part of a positioning system. \ Needs a GPS module.", "a positioning interface"], 75: [images.positioning_system, None, "A working positioning system", "a positioning computer"], 76: [images.scissors, None, "Scissors. They're too blunt to cut \ anything. Can you sharpen them?", "blunt scissors"], 77: [images.scissors, None, "Razorsharp scissors. Careful!", "sharpened scissors"], 78: [images.credit, None, "A small coin for the station's vending systems", "a station credit"], 79: [images.access_card, None, "This access card belongs to " + PLAYER_NAME, "an access card"], 80: [images.access_card, None, "This access card belongs to " + FRIEND1_NAME, "an access card"], 81: [images.access_card, None, "This access card belongs to " + FRIEND2_NAME, "an access card"] } items_player_may_carry = list(range(53, 82)) # Numbers below are for floor, pressure pad, soil, toxic floor. items_player_may_stand_on = items_player_may_carry + [0, 39, 2, 48]
############### ## SCENERY ## ############### # Scenery describes objects that cannot move between rooms. # room number: [[object number, y position, x position]...] scenery = { 26: [[39,8,2]], 27: [[33,5,5], [33,1,1], [33,1,8], [47,5,2], [47,3,10], [47,9,8], [42,1,6]], 28: [[27,0,3], [41,4,3], [41,4,7]], 29: [[7,2,6], [6,2,8], [12,1,13], [44,0,1], [36,4,10], [10,1,1], [19,4,2], [17,4,4]], 30: [[34,1,1], [35,1,3]], 31: [[11,1,1], [19,1,8], [46,1,3]],
32: [[48,2,2], [48,2,3], [48,2,4], [48,3,2], [48,3,3], [48,3,4], [48,4,2], [48,4,3], [48,4,4]], 33: [[13,1,1], [13,1,3], [13,1,8], [13,1,10], [48,2,1], [48,2,7], [48,3,6], [48,3,3]], 34: [[37,2,2], [32,6,7], [37,10,4], [28,5,3]], 35: [[16,2,9], [16,2,2], [16,3,3], [16,3,8], [16,8,9], [16,8,2], [16,1,8], [16,1,3], [12,8,6], [12,9,4], [12,9,8], [15,4,6], [12,7,1], [12,7,11]], 36: [[4,3,1], [9,1,7], [8,1,8], [8,1,9], [5,5,4], [6,5,7], [10,1,1], [12,1,2]], 37: [[48,3,1], [48,3,2], [48,7,1], [48,5,2], [48,5,3], [48,7,2], [48,9,2], [48,9,3], [48,11,1], [48,11,2]], 38: [[43,0,2], [6,2,2], [6,3,5], [6,4,7], [6,2,9], [45,1,10]], 39: [[38,1,1], [7,3,4], [7,6,4], [5,3,6], [5,6,6], [6,3,9], [6,6,9], [45,1,11], [12,1,8], [12,1,4]], 40: [[41,5,3], [41,5,7], [41,9,3], [41,9,7], [13,1,1], [13,1,3], [42,1,12]], 41: [[4,3,1], [10,3,5], [4,5,1], [10,5,5], [4,7,1], [10,7,5], [12,1,1], [12,1,5]], 44: [[46,4,3], [46,4,5], [18,1,1], [19,1,3], [19,1,5], [52,4,7], [14,1,8]], 45: [[48,2,1], [48,2,2], [48,3,3], [48,3,4], [48,1,4], [48,1,1]], 46: [[10,1,1], [4,1,2], [8,1,7], [9,1,8], [8,1,9], [5,4,3], [7,3,2]], 47: [[9,1,1], [9,1,2], [10,1,3], [12,1,7], [5,4,4], [6,4,7], [4,1,8]], 48: [[17,4,1], [17,4,2], [17,4,3], [17,4,4], [17,4,5], [17,4,6], [17,4,7], [17,8,1], [17,8,2], [17,8,3], [17,8,4], [17,8,5], [17,8,6], [17,8,7], [14,1,1]], 49: [[14,2,2], [14,2,4], [7,5,1], [5,5,3], [48,3,3], [48,3,4]], 50: [[45,4,8], [11,1,1], [13,1,8], [33,2,1], [46,4,6]] } checksum = 0 check_counter = 0 for key, room_scenery_list in scenery.items(): for scenery_item_list in room_scenery_list: checksum += (scenery_item_list[0] * key + scenery_item_list[1] * (key + 1) + scenery_item_list[2] * (key + 2)) check_counter += 1
print(check_counter, "scenery items") assert check_counter == 161, "Expected 161 scenery items" assert checksum == 200095, "Error in scenery data" print("Scenery checksum: " + str(checksum)) for room in range(1, 26): # Add random scenery in planet locations. if room != 13: # Skip room 13. scenery_item = random.choice([16, 28, 29, 30]) scenery[room] = [[scenery_item, random.randint(2, 10), random.randint(2, 10)]] # Use loops to add fences to the planet surface rooms. for room_coordinate in range(0, 13): for room_number in [1, 2, 3, 4, 5]: # Add top fence scenery[room_number] += [[31, 0, room_coordinate]] for room_number in [1, 6, 11, 16, 21]: # Add left fence scenery[room_number] += [[31, room_coordinate, 0]] for room_number in [5, 10, 15, 20, 25]: # Add right fence scenery[room_number] += [[31, room_coordinate, 12]] del scenery[21][1] # Delete last fence panel in Room 21 del scenery[25][1] # Delete last fence panel in Room 25
############### ## MAKE MAP ## ############### def get_floor_type(): if current_room in outdoor_rooms: return 2 # soil else: return 0 # tiled floor def generate_map(): # This function makes the map for the current room, # using room data, scenery data and prop data. global room_map, room_width, room_height, room_name, hazard_map global top_left_x, top_left_y, wall_transparency_frame
room_data = GAME_MAP[current_room] room_name = room_data[0] room_height = room_data[1] room_width = room_data[2] floor_type = get_floor_type() if current_room in range(1, 21): bottom_edge = 2 #soil side_edge = 2 #soil if current_room in range(21, 26): bottom_edge = 1 #wall side_edge = 2 #soil if current_room > 25: bottom_edge = 1 #wall side_edge = 1 #wall # Create top line of room map. room_map=[[side_edge] * room_width] # Add middle lines of room map (wall, floor to fill width, wall). for y in range(room_height 2): room_map.append([side_edge] + [floor_type]*(room_width 2) + [side_edge]) # Add bottom line of room map. room_map.append([bottom_edge] * room_width) # Add doorways. middle_row = int(room_height / 2) middle_column = int(room_width / 2) if room_data[4]: # If exit at right of this room room_map[middle_row][room_width 1] = floor_type room_map[middle_row+1][room_width 1] = floor_type room_map[middle_row1][room_width 1] = floor_type if current_room % MAP_WIDTH != 1: # If room is not on left of map room_to_left = GAME_MAP[current_room 1] # If room on the left has a right exit, add left exit in this room if room_to_left[4]: room_map[middle_row][0] = floor_type
room_map[middle_row + 1][0] = floor_type room_map[middle_row 1][0] = floor_type if room_data[3]: # If exit at top of this room room_map[0][middle_column] = floor_type room_map[0][middle_column + 1] = floor_type room_map[0][middle_column 1] = floor_type if current_room 0: player_frame += 1 time.sleep(0.05) if player_frame == 5: player_frame = 0 player_offset_x = 0 player_offset_y = 0 # save player's current position old_player_x = player_x old_player_y = player_y # move if key is pressed if player_frame == 0: if keyboard.right: from_player_x = player_x from_player_y = player_y player_x += 1 player_direction = "right" player_frame = 1 elif keyboard.left: #elif stops player making diagonal movements from_player_x = player_x from_player_y = player_y player_x = 1 player_direction = "left" player_frame = 1 elif keyboard.up: from_player_x = player_x from_player_y = player_y player_y = 1 player_direction = "up" player_frame = 1 elif keyboard.down: from_player_x = player_x from_player_y = player_y player_y += 1 player_direction = "down" player_frame = 1
# check for exiting the room if player_x == room_width: # through door on RIGHT clock.unschedule(hazard_move) current_room += 1 generate_map() player_x = 0 # enter at left player_y = int(room_height / 2) # enter at door player_frame = 0 start_room() return if player_x == 1: # through door on LEFT clock.unschedule(hazard_move) current_room = 1 generate_map() player_x = room_width 1 # enter at right player_y = int(room_height / 2) # enter at door player_frame = 0 start_room() return if player_y == room_height: # through door at BOTTOM clock.unschedule(hazard_move) current_room += MAP_WIDTH generate_map() player_y = 0 # enter at top player_x = int(room_width / 2) # enter at door player_frame = 0 start_room() return if player_y == 1: # through door at TOP clock.unschedule(hazard_move) current_room = MAP_WIDTH generate_map() player_y = room_height 1 # enter at bottom player_x = int(room_width / 2) # enter at door player_frame = 0 start_room()
return if keyboard.g: pick_up_object() if keyboard.tab and len(in_my_pockets) > 0: selected_item += 1 if selected_item > len(in_my_pockets) 1: selected_item = 0 item_carrying = in_my_pockets[selected_item] display_inventory() if keyboard.d and item_carrying: drop_object(old_player_y, old_player_x) if keyboard.space: examine_object() if keyboard.u: use_object() #### Teleporter for testing #### Remove this section for the real game ## if keyboard.x: ## current_room = int(input("Enter room number:")) ## player_x = 2 ## player_y = 2 ## generate_map() ## start_room() ## sounds.teleport.play() #### Teleport section ends # If the player is standing somewhere they shouldn't, move them back. if room_map[player_y][player_x] not in items_player_may_stand_on \ or hazard_map[player_y][player_x] != 0: player_x = old_player_x player_y = old_player_y player_frame = 0
if room_map[player_y][player_x] == 48: # toxic floor deplete_energy(1) if player_direction == "right" and player_frame > 0: player_offset_x = 1 + (0.25 * player_frame) if player_direction == "left" and player_frame > 0: player_offset_x = 1 (0.25 * player_frame) if player_direction == "up" and player_frame > 0: player_offset_y = 1 (0.25 * player_frame) if player_direction == "down" and player_frame > 0: player_offset_y = 1 + (0.25 * player_frame)
############### ## DISPLAY ## ############### def draw_image(image, y, x): screen.blit( image, (top_left_x + (x * TILE_SIZE), top_left_y + (y * TILE_SIZE) image.get_height()) ) def draw_shadow(image, y, x): screen.blit( image, (top_left_x + (x * TILE_SIZE), top_left_y + (y * TILE_SIZE)) ) def draw_player(): player_image = PLAYER[player_direction][player_frame] draw_image(player_image, player_y + player_offset_y, player_x + player_offset_x) player_image_shadow = PLAYER_SHADOW[player_direction][player_frame] draw_shadow(player_image_shadow, player_y + player_offset_y, player_x + player_offset_x)
def draw(): if game_over: return # Clear the game arena area. box = Rect((0, 150), (800, 600)) screen.draw.filled_rect(box, RED) box = Rect ((0, 0), (800, top_left_y + (room_height 1)*30)) screen.surface.set_clip(box) floor_type = get_floor_type() for y in range(room_height): # Lay down floor tiles, then items on floor. for x in range(room_width): draw_image(objects[floor_type][0], y, x) # Next line enables shadows to fall on top of objects on floor if room_map[y][x] in items_player_may_stand_on: draw_image(objects[room_map[y][x]][0], y, x) # Pressure pad in room 26 is added here, so props can go on top of it. if current_room == 26: draw_image(objects[39][0], 8, 2) image_on_pad = room_map[8][2] if image_on_pad > 0: draw_image(objects[image_on_pad][0], 8, 2) for y in range(room_height): for x in range(room_width): item_here = room_map[y][x] # Player cannot walk on 255: it marks spaces used by wide objects. if item_here not in items_player_may_stand_on + [255]: image = objects[item_here][0] if (current_room in outdoor_rooms and y == room_height 1 and room_map[y][x] == 1) or \ (current_room not in outdoor_rooms and y == room_height 1 and room_map[y][x] == 1 and x > 0 and x