Coding Games With Pygame Zero & Python

Student workbook (2nd edition)

Richard Smith

Contents

For the instructor

This book contains all the example programs used in my CoderDojo class to teach Python programming. The primary goal of the class is to teach programming using action games to make learning more interesting. Some of the examples are entirely focused on introducing new language concepts or showing how the Pygame Zero API works, but most are a mixture of both.

The intention is that each program in the example chapters is brief, complete and introduces only one or two new concepts.

  • The programs are short so that students can feasibly type them in during the class. Typing for themselves is very valuable for beginners, because it helps them learn to type, and to type precisely without mistakes (which show up as syntax errors). Even if it takes them a while, they should feel proud when they type a program correctly in the end!1
  • Each program is complete to avoid the confusion caused by worksheets that tell a student to add lines to a program piecemeal. If they make an error or omit a step they may never recover and not produce a working program. Also being complete and separate means the whole class can be told to skip to the same program and it won’t matter so much that some of them didn’t complete all the prior programs.
  • Introducing ideas one or two at a time allows the students to learn mostly through doing and observing the output of each program rather than memorizing. To this end the order of the programs within each chapter has been carefully selected. The chapter ordering is more loose and it is possible to skip back and forth between chapters. If the text quizzes or the Python fundamentals are boring the students, skip ahead to the graphical games, then come back to the earlier examples when necessary.

Following each program are some ideas for how students can modify the program. Hopefully they will go further and modify these programs into their own unique games! The difficulty of these suggestions varies, to accommodate students of different age and ability.

This book may be suitable for self-teaching by a motivated learner, but does not attempt to be comprehensive or give detailed explanations because it is intended to be used in a class with an instructor who will fill in the gaps as needed by the students.

The second edition

I discovered two important things from feedback from the first edition:

  1. It’s not possible to learn coding with only a single lesson each week. Therefore if that is all we have, the time is better spent inspiring interest in the subject rather than memorizing syntax or wrapping their heads around abstract concepts. Those who are motivated to learn more in their own time will require more syntax and concepts, of course, and hence the Fundamentals chapter has been greatly expanded for their use.
  2. Many students wanted to create larger games than the simple examples given in the first edition. It was always the intention that they absorb the knowledge from the examples and then go on to greater larger games of their own but some will require more hand-holding.

Therefore there are four new chapters of tutorial style examples. These are intended to be done in class, with the initial code file given to the students to save time. They are intended to create a sense of accomplishment as each subsequent modifcation improves the game, and also to allow lots of scope for student creativity and customisation.

As noted above, tutorials can become confusing, but I hope the class instructors can resolve any issues.

1. For writing essays nowadays people can use voice input and touch-screen input, but for programming the majority of programmers still type on a keyboard. There’s no sign of this changing in the immediate future, so practising keyboard skills now is still very important for the future. Students can practise at home with a program such as TuxType. That being said, many programs contain similar code, so it’s also a useful skill for them be able to copy and paste from their other programs.

Chapter 1 Overview of Python

1.1 Comments

A computer program is intended to be understood by both humans and computers. However to make it easier for the humans, it can also contain comments written in English.

# A comment looks like this

Python ignores comments. They provide explanations for the human readers.

1.2 Literals

A Python program can contain any number and any string of text surrounded by quotes.

Examples:

5 1.23 “Hello” ‘Barry’

1.3 Keywords

Every computer language has a number of keywords that you will need to learn along with their meanings. Fortunately they look like English words and there are only a few of them in Python. You could tick them off as you meet them.

False None True and as assert async await
break class continue def del elif else except
finally for from global if import in is
lambda nonlocal not or pass raise return try
while with yield

1.4 Built-ins

Python also comes with a large number of functions. The most common ones are built-in and always available, much like the keywords. Here is a list of them, just for the sake of completeness, but you probably won’t ever use them all, and when you do use one you will probably look it up in the documentation. So you don’t need to remember these.

abs all any ascii bin bool
breakpoint bytearray bytes callable chr classmethod
compile complex copyright credits delattr dict
dir divmod enumerate eval exec exit
filter float format frozenset getattr globals
hasattr hash help hex id input
int isinstance issubclass iter len license
list locals map max memoryview min
next object oct open ord pow
print property quit range repr reversed
round set setattr slice sorted staticmethod
str sum super tuple type vars
zip

Once you understand all of these you effectively understand all of the Python language. By the end of this book you will be familiar with at least 20 keywords / functions which is enough to create a huge variety of programs.

1.5 Libraries

There are many more functions available (too many to list here), but not everyone will need them, so they are kept in libraries. Some libraries are supplied with Python. You can use their functions only after first importing the relevant library module. For example, if you want a random number, import the random library:

from random import randint
print(randint(0,10))

Other libraries are not supplied with Python and must be downloaded separately, such as the Minecraft, Pygame and Richlib libraries.

1.6 Names

You will see many words in a program that appear to be English words and yet they are not literals, keywords or library functions. These are names chosen by the programmer. For example, if the program needs to record a score and store it in a variable, the programmer might choose to give that variable the name score:

score = 1
print("Score: ", score)

Python has no understanding of what score means. It only cares that the same word is used every time. So a different programmer might decide to write the program like this:

points = 1
print("Score: ", points)

A programmer who doesn’t like typing might use a shorter, less descriptive name:

p = 1
print("Score: ", p)

However the programmer must be consistent. This would not work:

points = 1
print("Score: ", score)

1.7 Whitespace

Python is unusual in that it cares about whitespace, i.e. what you get when you press the tab key or the space bar on the keyboard.

Python programs are arranged in blocks of lines. Every line in a block must have the same amount of whitespace preceding it - the indentation. See Program 2.19 for an example.

Chapter 2 Python Fundamentals

There are some exercises here. Each exercise will ask you to write a program. The solution is often on the following page - do not turn the page until you have attempted your own solution! Save each program in a separate file.

2.1 The REPL

REPL stands for Read Evaluate Print Loop. In Mu you access it via the REPL button. It appears at the bottom of the window. It’s a special mode in which you type an instruction to Python and Python executes it immediately (no need to click RUN) and displays the result (no need to type print()). It’s useful for doing calculations and trying things out, but it won’t save what you type, so you will only want to use it for very short programs.

images/figures/repl

2.2 Arithmetic operators

Python understands several operators from maths. You can use them in your programs, or just enter these examples at the REPL to use Python as a calculator, as in the screenshot above.

Operator Symbol Example Result
Addition + 20 + 10 30
Subtraction - 20 - 10 10
Multiplication * 20 * 10 200
Division / 20 / 10 2

There are some more advanced operators in Program 2.18

2.3 Variables

A variable is a place in the computer’s memory where data is stored. You can name a variable whatever you like; you should try to make the name descriptive. There are many types of variable but Python sets the type for us automatically when we store data in the variable. (Unlike in many other languages, we do not need to specify the type.) The types we will see most often are whole numbers (integers) and strings of text.

We create a variable and assign a value to it using the = operator. Note this is different from the == operator which is used for comparisons.

We use the print() function to print the value of our variables. It will print any type of data (numbers, strings, both literals and variables) provided each item is separated with a comma (,).

Program 2.1: Variable assignment
my_number = 7
my_string = "hello"
print(my_string, my_number)

We can use a variable anywhere we would use a literal number or string. The value of the variable will be retrieved from the computer’s memory and substituted for the variable in any expression.

Program 2.2: Adding two variables together
apples = 27
pears = 33
fruits = apples + pears
print("Number of fruits:", fruits)
Exercise 2.1.

Copy Program 2.2, but also add 17 bananas to the calculation of fruits.

We can store a new value in the same variable. The old value will be forgotten.

Program 2.3: Overwriting a variable with a new value
apples = 27
apples = 40
print("Number of apples:", apples)
Exercise 2.2.

What do you think Program 2.3 will print? If you aren’t sure, type it in.

More usefully, we can take the old value, modify it, then store it back in the same variable.

Program 2.4: Modifying a variable
x = 5
x = x * 10
x = x + 7
print(x)
Exercise 2.3.

What will Program 2.4 print? Change the numbers in the program. Use a division / operation. Then ask your friend to predict what the new program will print. Was he right?

You will often see this used for counting:

Program 2.5: Counting
total = 0
total = total + 1
total = total + 1
total = total + 1
print(x)
Exercise 2.4.

What is the total count of Program 2.5 ?

See Program 2.18 for a quicker way of writing this.

2.4 Input

Program 2.2 is not very useful if the number of apples changes. This would require the programmer to change the program. We can improve it by allowing the user of the program to change the numbers. The input() function allows the user to type a string which can be different every time the program is run.

my_string = input()
print(my_string)

Sometimes we want the user to type in a number rather than a string. We can combine the int() function with the input() function to convert the string to a number.

Program 2.6: Getting input from user
print("Enter a number")
my_number = int(input())
print("Double your number is", my_number * 2)
Exercise 2.5.

Copy Program 2.2 but use input() to ask the user to enter the number of apples and pears.

2.5 Booleans

A boolean is another type of variable that is not a string or a number. It can have only two possible values: True or False. In some languages and in electronics you may see these represented as 0 and 1.

Booleans are used by keywords such as if and while. In an if statement, the indented code block is only run if the boolean is True.

sunny = True
if a:
    print("Let's go to the park")

You could write it like this:

sunny = True
if sunny==True:
    print("Let's go to the park")

but that would be redundant because if always tests if the boolean is True.

If the boolean is not true, and if you write an else clause, the indented code block under else is run instead.

sunny = False
if sunny:
    print("Let's go to the park")
else:
    print("We must stay at home")

2.6 Comparison operators

Comparison operators take two numbers, strings or other variables, compare them, and then return a boolean True or False from them.

Operator Symbol
Equal ==
Not equal !=
Less than <
Less than or equal <=
Greater than >
Greater than or equal >=
Program 2.7: Comparisons: greater than, lesser than, equal to .
if 7 < 9:
    print("7 is less than 9")

a = 10
b = 5

if a == b:
    print("a is equal to b")

if a < b:
    print("a is less than b")

if a > b:
    print("a is greater than b")

2.7 Boolean logic

The and, or and not operators operate on booleans and return new boolean values.

Program 2.8: Boolean operators .
a = True
b = False

if a:
    print("a is true")

if a and b:
    print("a and b are both true")

if a or b:
    print("either a or b is true")
Exercise 2.6.

Change the values of a and b in Progam 2.8 and see what output is printed by different combinations of True and False.

2.7.1 Or

Only people older than 12 or taller than 150cm are allowed to ride the rollercoaster. This program checks whether people are allowed to ride.

print("How old are you?")
age = int(input())
print("How tall are you?")
height = int(input())
if age > 12:
    print("You can ride")
elif height > 150:
    print("You can ride")
else:
    print("YOU MAY NOT RIDE, GO AWAY!")

Boolean operators combine two truth values together. The or operator is True if either of its operands is true. Try this example:

a = True
b = False
print(a or b)
Exercise 2.7.

Use the or operator to make the rollercoaster program shorter by combining the two tests into one test.

A possible solution:

print("How old are you?")
age = int(input())
print("How tall are you?")
height = int(input())
if age > 12 or height > 150:
    print("You can ride")
else:
    print("YOU MAY NOT RIDE, GO AWAY!")

2.7.2 And

The and operator is True if both of its operands is true. Try this example:

a = True
b = False
print(a and b)
Exercise 2.8.

The rollercoaster is only allowed to run on days when the temperature is less than 30 degrees. Extend the program to ask the temperature and use the and operator to only allow riding when less than 30 degrees.

A possible solution:

print("How old are you?")
age = int(input())
print("How tall are you?")
height = int(input())
print("What is the temperature?")
temp = int(input())
if (age > 12 or height > 150) and temp < 30:
    print("You can ride")
else:
    print("YOU MAY NOT RIDE, GO AWAY!")

Note that we have put brackets around the or expression. This ensures it is calculated first and the result of that calculation is then used in the and expression. This is the same way you use the BODMAS rule to decide the order of operations in maths.

2.7.3 Not

The not operator is True if its operand is False. If its operand is False then it is True. Try this example:

a = True
b = False
print(not a)
print(not b)

We can get a user input and convert it to a boolean like this:

print("Is it raining? Y/N")
if input() == "Y":
    raining = True
else:
    raining = False
Exercise 2.9.

Change the program so that you can only ride the rollercoaster if it is not raining.

Possible solution:

print("Is it raining? Y/N")
if input() == "Y":
    raining = True
else:
    raining = False
print("How old are you?")
age = int(input())
print("How tall are you?")
height = int(input())
print("What is the temperature?")
temp = int(input())
if (age > 12 or height > 150) and temp < 30 and not raining:
    print("You can ride")
else:
    print("YOU MAY NOT RIDE, GO AWAY!")

2.8 For loops

A for loop repeats a block of code a number of times. A variable is created which we can use to find the current number within the loop. Here the variable is called x but you can name it whatever you like. Run this program:

 for x in range(0, 11):
     print(x)

You can also change the step of the loop. Run this program:

 for x in range(0, 11, 2):
     print(x)

2.8.1 Nested loops

It is often useful to put one loop inside another loop.

Program 2.9: Nested for loop
for a in range(0, 6):
    for b in range(0, 6):
        print(a, "times", b, "is", a * b)
Exercise 2.10.

Write a program which prints out the 12 times table.

2.8.2 Incrementing a variable in a loop

A baker has three customers. He asks them each how many cakes they want so he knows how many he must bake. He writes this program.

 total = 0
 print("Customer", 1, "how many cakes do you want?")
 cakes = int(input())
 total = total + cakes
 print("Customer", 2, "how many cakes do you want?")
 cakes = int(input())
 total = total + cakes
 print("Customer", 3, "how many cakes do you want?")
 cakes = int(input())
 total = total + cakes
 print("I will bake", total, "cakes!")
Exercise 2.11.

This program is longer than it needs to be. Write your own program that does the same thing using a for loop. It should be only 6 (or fewer) lines long.

Program 2.10: Possible solution to baker program exercise
 total=0
 for x in range(1, 4):
     print("Customer", x, "how many cakes do you want?")
     cakes = int(input())
     total = total + cakes
 print("I will bake", total, "cakes!")
Exercise 2.12.

The baker gets a fourth customer. Change Program 2.10 so it works for 4 customers.

Exercise 2.13.

The baker has a different number of customers every day. Change the program so it asks how many customers there are. Store the number typed by the user in a variable called c. Change the loop so it works for c customers rather than 4 customers.

Program 2.11: Possible solution to variable number of customers exercise
 print("How many customers are there today?")
 c = int(input())
 total=0
 for x in range(1, c+1):
     print("Customer", x, "how many cakes do you want?")
     cakes = int(input())
     total = total + cakes
 print("I will bake", total, "cakes!")
Exercise 2.14.

If a customer orders 12 cakes, he gets an extra cake for free. Use an if statement to check cakes > 12. If so, add one more cake.

2.9 Array lists

Variables can be stored together in a list. Most languages call this an array so try to remember that word also.1

Program 2.12: Array lists
# a is a list of integers

a = [74, 53, 21]

# b is a list of strings

b = ["hello", "goodbye"]

# You can take a single element from the list.
print(a[2])

# You can use a for loop to print every element.
for x in a:
    print(x)

2.9.1 Looping over lists

Rather than the user typing in data, your program might be supplied with data in a list. Here is a list of prices - a shopping list. Note we don’t use a currency symbol except when we print the price.

prices = [3.49, 9.99, 2.50, 20.00]
for x in range(0, 4):
    print("item costs £", prices[x])

In this program x is used an index for the array. Note that indices begin at 0 rather than 1. If the array contains 4 elements then the final element will have index 3, nor 4.

However, for can directly give you all the array values without the need for an index or to specify the size of the range:

Program 2.13: A shopping list
prices = [3.49, 9.99, 2.50, 20.00]
for price in prices:
    print("item costs £", price)
Exercise 2.15.

Change the Program 2.13 so that it prints the total price of all the items added together.

Program 2.14: Possible way of calculating the total cost of shopping list
prices = [3.49, 9.99, 2.50, 20.00]
total = 0
for price in prices:
    print("item costs £", price)
    total = total + price
print("shopping total", total)

There is a problem with solution, can you see what it is when you run it?

The problem is that we are using floating point numbers for the prices and floating point maths in the computer is not entirely accurate, so the answer will be very slightly wrong. One way to fix this is to round the result to two decimal places using the round() function:

print("shopping total", round(total,2))

This works for a short list, but if the list was millions of items long it might not give the right result. Can you think of a better way?

Instead of storing the number of pounds, store the the number of pennies. Britain no longer has a half-penny, so the numbers will always be whole numbers - integers - and no floating points will be needed for the addition.

Program 2.15: Better way of calculating the total cost of shopping list
prices = [349, 999, 250, 2000]
total = 0
for price in prices:
    print("item costs £", price/100)
    total = total + price
print("shopping total", total/100)
Exercise 2.16.

Conditional discount. Any item that costs more than £10 will be discounted by 20 percent. Use an if statement to check if the price is more than 1000 pennies. If it is, multiply the price by 0.8 to reduce it before you add it to the total.

Program 2.16: Possible way of discounting shopping list
prices = [349, 999, 250, 2000]
total = 0
for price in prices:
    print("item costs £", price/100)
    if price > 1000:
        price = price * 0.8
        print("  item discounted to", price/100)
    total = total + price
print("shopping total", total/100)

2.10 Functions

You may have seen specially named functions that are called by Pygame: draw() and update(). However, you can define a function named whatever you like and call it yourself.

Functions are useful for many reasons. The simplest is that they make your program look more organized. They also enable you re-use code without needing to copy it and risk making mistakes. When your programs get longer they enable you to create abstractions so you only have to think about what function you want to call and don’t need to remember the details of the code inside the function.

Program 2.17: Functions
def my_func():
    print("This is my function")
    print("Imagine there was lots of code here"
          " that you didnt want to type 3 times")


my_func()
my_func()
my_func()

2.11 Shortcuts

Here are quicker ways of doing basic things. You may have noticed some of these being used already.

Program 2.18: Shortcuts
# f is an easy way to insert variables into strings
score = 56
name = "Richard"
message = f"{name} scored {score} points"
print(message)

# += is an easy way to increase the value of a variable
score = score + 10  # hard way
score += 10         # easy way
print(score)

# double / means whole number division, no decimals
x = 76 // 10
# MODULO is the percent sign %. It means do division and take the remainder.
remainder = 76 % 10
print(f"76 divided by 10 is {x} and the remainder is {remainder}")

WIDTH = 500
a = 502
b = 502
# Modulo is often used as a shortcut to reset a number back
# to zero if it gets too big.  So instead of:
if a > WIDTH:
    a = a - WIDTH
# You could simply do:
b = b % WIDTH
print(a, b)

# input() takes a string argument which it prints out.
# Instead of:
print("Enter a number")
num = input()
# You can have a single line:
num = input("Enter a number")

2.12 Indentation

Code is arranged in blocks. For example, a function consists of a one line declaration followed by a block of several lines of code. Similarly, all the lines of a loop form one block. A conditional has a block of code following the if statement (and optionally blocks after the elif and else. )

Many languages use {} or () to delimit a block. However Python is unusual: each block begins with : and then all the lines of the block are indented by the same amount of whitespace (tabs or spaces). The block ends when the indentation ends.

Blocks can be nested inside other blocks.

Program 2.19: Can you predict what this program will print?
def test():
    print("entering function block")
    for i in range(0,10):
        print("entering for loop block")
        if i == 5:
            print("in if block")
        print("leaving for loop block")
    print("leaving function block")
print("not in any block")
test()

2.13 Global variables

A variable defined inside a function has local scope: it cannot be used outside of the function. If you want to use the same variable in different functions then you must define it outside the functions, in the global scope. However, if you attempt to modify the value of the global variable inside a function you will get an error, or - even worse - you will create a local variable with the same name as the global variable and your changes to the global variable will be silently lost.

You must explicitly tell Python that you want to use a global variable with the global keyword.

Program 2.20: Try removing line 3 and see what happens
a = 10
def my_function():
    global a
    a=20
my_function()
print(a)

2.14 Dictionaries

A dictionary (called a HashMap in some languages) stores pairs of values. You can use the first value to look-up the second, just like how you look-up a word in a dictionary to find its meaning. Here is a dictionary of the ages of my friends:

friends = {'richard': 96, 'john': 12, 'paul': 8}
print("What is your name?")
name = input()
age = friends[name]
print("Your age is", age)
Exercise 2.17.

Change the program so it contains 5 of your friends’ ages.

2.14.1 Counting

Here is a loop that prints out all the ages:

friends = {'richard': 96, 'john': 12, 'paul': 8}
for name, age in friends.items():
    print(name, "is age", age)
Exercise 2.18.

Can you add an if statement to only print the ages of friends older than 10?

Possible solution:

friends = {'richard': 96, 'john': 12, 'paul': 8}
for name, age in friends.items():
    if age > 10:
        print(name, "is age", age)
Exercise 2.19.

Now add a count variable that counts how many of the friends are older than 10. Print the number at the end.

Possible solution:

friends = {'richard': 96, 'john': 12, 'paul': 8}
count = 0
for name, age in friends.items():
    if age > 10:
        count = count + 1
print("friends older than 10:",count)

2.14.2 Combining tests

Exercise 2.20.

Use the and operator together with the < and > operators to only count friends between the ages of 11 to 13.

Possible solution:

friends = {'richard': 96, 'john': 12, 'paul': 8}
count = 0
for name, age in friends.items():
    if age > 10 and age < 14:
        count = count + 1
print("friends age 11 to 13 :",count)

2.14.3 Finding

We make a variable oldest that will contain the oldest age in the list.

friends = {'richard': 96, 'john': 12, 'paul': 8}
oldest = 0
for name, age in friends.items():
    if age > oldest:
        oldest = age
print("oldest age", oldest)
Exercise 2.21.

Make a variable youngest that will contain the youngest age in the list. Print the youngest at the end.

Possible solution:

friends = {'richard': 96, 'john': 12, 'paul': 8}
oldest = 0
youngest = 100
for name, age in friends.items():
    if age > oldest:
        oldest = age
    if age < youngest:
        youngest = age
print("oldest age", oldest)
print("youngest age", youngest)

2.14.4 Finding names

Exercise 2.22.

As well as the ages, print the names of the youngest and oldest friends.

Possible solution:

friends = {'richard': 96, 'john': 12, 'paul': 8}
oldest = 0
youngest = 100
for name, age in friends.items():
    if age > oldest:
        oldest = age
        oldname = name
    if age < youngest:
        youngest = age
        youngname = name
print("oldest friend", oldname)
print("youngest friend", youngname)

2.14.5 Find the average

Exercise 2.23.

Create a total variable. Add each age to the total. At the end, calculate the average by dividing the total by the number of friends.

Possible solution:

friends = {'richard': 96, 'john': 12, 'paul': 8}
total = 0
for name, age in friends.items():
    total = total + age
average = total / 3
print("average age is ", average)

2.15 Bugs

Fixing bugs can feel frustrating but all programmers must wrestle with them. The simplest (but still annoying!) are syntax errors. The computer is not very intelligent and is unable to guess what you mean, so you must type the programs in this book exactly as they appear. A single wrong letter or space will prevent them from working.

A particular issue in Python is that indentation must be correct.

Program 2.21: Buggy program
x = 10
 y = 11
z = 12
print(x,y,z)
Exercise 2.24.

Can you spot and fix the bug in Program 2.21?

Program 2.22: More bugs
def myfunction:
    print "hello"

myfunction()
Exercise 2.25.

Program 2.22 has two bugs to fix.

1. There are other kinds of list that are not arrays but this need not concern the beginner.

Chapter 3 Text-based quiz games

These programs can be entered using any text editor, but I suggest using the Mu editor because it comes with Python, Pygame Zero and other libraries all pre-installed in one easy download.

3.1 Hello, world

The traditional first program used to make sure Python is working and that we can run programs.

Program 3.1: Hello, world
print("Hello world")

# This line is a comment, you dont have to type these!

If using the Mu editor:

  1. Click the mode button and make sure the mode is set to Python3.
  2. Type in the program.
  3. Click Save and enter a name for the program.
  4. Click Run.

3.2 Getting input from the keyboard

This program will pause and wait for you to enter some text with the keyboard, followed by the return key. The text you enter is stored in a variable, x.

Program 3.2: Getting input from the keyboard
print("Enter your name:")
x = input()
print("Hello", x)
if x == "richard":
    print("That is a very cool name")
Exercise 3.1.

Add some names of your friends and display a different message for each friend.

3.3 Making decisions: if, elif, else

This is how to add another name to Program 3.2

Program 3.3: Decisions: if, elif, else
print("Enter your name:")
x = input()
print("Hello", x)
if x == "richard":
    print("That is a very cool name")
elif x == "nick":
    print("That is a rubbish name")
else:
    print("I do not know your name", x)

Program 3.3 is very similar to Program 3.2. The new lines have been highlighted. You can either modify Program 3.2, or else create a new file and use copy and paste to copy the code from the old program into the new.

3.4 A random maths question

Program 3.4: A random maths question .
import random

n = random.randint(0, 10)

print("What is", n, "plus 7?")
g = int(input())  # Why do we use int() here?
if g == n + 7:
    print("Correct")
else:
    print("Wrong")
Exercise 3.2.

Add some more questions, e.g.

  • Instead of 7, use another random number.
  • Use a bigger random number.
  • Multiply (use *), divide (use /) or subtract (use -) numbers.
Exercise 3.3.

Print how many questions the player got correct at the end.

3.5 Keeping score

We create a score variable to record how many questions the player answered correctly.

Program 3.5: Keeping score .
score = 0

print("What is 1+1 ?")
g = int(input())
if g == 2:
    print("Correct")
    score = score + 1

print("What is 35-25 ?")
g = int(input())
if g == 10:
    print("Correct")
    score = score + 1

print("Your score:", score)

3.6 Guessing game with a loop

This while loop goes round and round forever … or until the player gets a correct answer, and then it breaks out of the loop. Note that everything in the loop is indented.

Program 3.6: Guessing game with a loop .
import random

n = random.randint(0, 10)

while True:
    print("I am thinking of a number, can you guess what it is?")
    g = int(input())
    if g == n:
        break
    else:
        print("Wrong")
print("Correct!")
Exercise 3.4.

Give a hint to the player when they are wrong. Was their guess too high or too low?

Exercise 3.5.

Print how many guesses they took to get it right at the end.

3.7 Improved guessing game

Program 3.6 with a hint whether the guess is greater or lesser than the answer.

Program 3.7: Improved guessing game .
import random

n = random.randint(0, 100)
guesses = 0

while True:
    guesses = guesses + 1
    print("I am thinking of a number, can you guess what it is?")
    g = int(input())
    if g == n:
        break
    elif g < n:
        print("Too low")
    elif g > n:
        print("Too high")
print("Correct! You took", guesses, "guesses.")

Chapter 4 Drawing graphics

To create graphics for our games we will use the Pygame Zero library1. You will find the documentation on the website useful!

The smallest square that can be displayed on a monitor is called a pixel. This diagram shows a close-up view of a window that is 40 pixels wide and 40 pixels high. At normal size you will not see the grid lines.

images/figures/pixelgrid

We can refer to any pixel by giving two co-ordinates, (x,y) Make sure you understand co-ordinates before moving on because everything we do in Pygame Zero will use it. (In maths this called a ‘Cartesian coordinate system’).

4.1 Lines and circles

If are using the Mu editor, Pygame Zero is built-in, but you must remember to click ‘Mode’ and select ‘Pygame Zero’ before running your program!

If you are using a different editor, instructions are online2

Program 4.1: Lines and circles
WIDTH = 500  # What are these units? What if we change them?
HEIGHT = 500  # What if we delete this line?


def draw():
    screen.clear()
    screen.draw.circle((250, 250), 50, "white")
    screen.draw.filled_circle((250, 100), 50, "red")
    screen.draw.line((150, 20), (150, 450), "purple")
    screen.draw.line((150, 20), (350, 20), "purple")
Exercise 4.1.

Finish drawing this picture

Exercise 4.2.

Draw your own picture.

4.2 Moving rectangles

To make things move we need to add the special update() function. We don’t need to write our own loop because Pygame Zero calls this function for us in its own loop, over and over, many times per second.

Program 4.2: Moving rectangles
WIDTH = 500
HEIGHT = 500

box = Rect((20, 20), (50, 50))

def draw():
    screen.clear()
    screen.draw.filled_rect(box, "red")


def update():
    box.x = box.x + 2
    if box.x > WIDTH:
        box.x = 0
Exercise 4.3.

Make the box move faster.

Exercise 4.4.

Make the box move in different directions.

Exercise 4.5.

Make two boxes with different colours.

4.3 Actor sprites

Actor sprites are very similar to boxes! Click Images to see the folder of image files available. alien.png should already be there, but for other images you must add the files yourself.

You could use Microsoft Paint which comes with Windows but I recommend you download and install Krita3.

Program 4.3: Actor sprites
WIDTH = 500
HEIGHT = 500

alien = Actor('alien')
alien.x = 0
alien.y = 50


def draw():
    screen.clear()
    alien.draw()


def update():
    alien.x += 2
    if alien.x > WIDTH:
        alien.x = 0
Exercise 4.6.

Draw or download your own image to use instead of alien.

4.4 Background image

We are going to add a background image to Program 4.3

Click Images to see the folder of image files available.

You must create or download a picture to use a background. Save it as background.png in the directory mu_code/images. It should be the same size as the window, 500×500 pixels and it must be in .png format.

Program 4.4: Background
WIDTH = 500
HEIGHT = 500

alien = Actor('alien')
alien.x = 0
alien.y = 50

background = Actor('background')

def draw():
    screen.clear()
    background.draw()
    alien.draw()


def update():
    alien.x += 2
    if alien.x > WIDTH:
        alien.x = 0
Exercise 4.7.

Create a picture to use a background. Save it as background.png. Run the program.

4.5 Keyboard input

The alien moves when you press the cursor keys.

Program 4.5: Keyboard input
alien = Actor('alien')
alien.pos = (0, 50)

def draw():
    screen.clear()
    alien.draw()

def update():
    if keyboard.right:
        alien.x = alien.x + 2
    elif keyboard.left:
        alien.x = alien.x - 2
Exercise 4.8.

Make the alien move up and down as well as left and right.

Exercise 4.9.

Use the more concise += operator to change the alien.x value (see Program 2.18).

Exercise 4.10.

Use the or operator to allow WASD keys to move the alien in addition to the cursor keys (see Program 2.8).

1. https://pygame-zero.readthedocs.io
2. https://pygame-zero.readthedocs.io/en/stable/ide-mode.html
3. https://krita.org

Chapter 5 Arcade games

5.1 Collisions

Arcade games need to know when one Actor sprite has hit another Actor sprite. Most of this code is copied from Program 4.2 and Program 4.5.

Program 5.1: Collisions
WIDTH = 500
HEIGHT = 500

alien = Actor("alien")
alien.pos = (400, 50)
box = Rect((20, 20), (100, 100))

def draw():
    screen.clear()
    screen.draw.filled_rect(box, "red")
    alien.draw()

def update():
    if keyboard.right:
        alien.x = alien.x + 2
    elif keyboard.left:
        alien.x = alien.x - 2
    box.x = box.x + 2
    if box.x > WIDTH:
        box.x = 0
    if alien.colliderect(box):
        print("hit")
Exercise 5.1.

Add vertical movement (as you did in Exercise 4.8).

Exercise 5.2. Advanced

Make the box chase the alien.

Exercise 5.3. Advanced

Print number of times the box hits the alien (i.e. the score).

5.2 Chase

Instead of moving constantly to the right we can make the movement conditional with an if statement so the box chases the alien. Most of this code is copied from Program 5.1. New lines are highlighted. We have also changed what happens when the box catches the alien: the program now exits and you must run it again to play again. This may not be what you want in your game!

Program 5.2: Alien chase
WIDTH = 500
HEIGHT = 500

alien = Actor("alien")
alien.pos = (400, 50)
box = Rect((20, 20), (100, 100))

def draw():
    screen.clear()
    screen.draw.filled_rect(box, "red")
    alien.draw()

def update():
    if keyboard.right:
        alien.x = alien.x + 2
    elif keyboard.left:
        alien.x = alien.x - 2
    if box.x < alien.x:
        box.x = box.x + 1
    if box.x > alien.x:
        box.x = box.x - 1
    if alien.colliderect(box):
        exit()
Exercise 5.4.

Add vertical movement (as you did in Exercise 4.8).

Exercise 5.5. Advanced

Draw a new enemy image. Save it as enemy.png in your mu_code/images folder. Load it as an Actor('enemy') instead of the Rect().

5.3 Powerup

Instead of an enemy the box here is a powerup that the player must collect. When he does it disappears and moves to a new location.

Program 5.3: Collect the powerups
WIDTH = 500
HEIGHT = 500
import random

alien = Actor("alien")
alien.pos = (400, 50)
box = Rect((20, 20), (100, 100))
score = 0

def draw():
    screen.clear()
    screen.draw.filled_rect(box, "green")
    alien.draw()

def update():
    global score
    if keyboard.right:
        alien.x = alien.x + 2
    elif keyboard.left:
        alien.x = alien.x - 2
    if alien.colliderect(box):
        box.x = random.randint(0, WIDTH)
        box.y = random.randint(0, HEIGHT)
        score = score + 1
        print("Score:", score)
Exercise 5.6.

Add vertical movement (as you did in Exercise 4.8).

Exercise 5.7.

Draw a new powerup image. Save it as powerup.png in your mu_code/images folder. Load it as an Actor('powerup') instead of the Rect().

Exercise 5.8. Advanced

Combine this program with the enemy from Program 5.2 and the background from Program 4.4 and whatever else you want to make your own game.

5.4 Sound and animation

Pygame Zero comes with one other image alien_hurt.png and one sound eep.wav. If you want more you will have to add them to the sounds and images folders.

Most of this code is copied from Program 5.1

Program 5.4: Sound and animation upon collision
WIDTH = 500
HEIGHT = 500
alien = Actor("alien")
alien.pos = (0, 50)
box = Rect((20, 20), (100, 100))

def draw():
    screen.clear()
    screen.draw.filled_rect(box, "red")
    alien.draw()
def update():
    if keyboard.right:
        alien.x = alien.x + 2
    elif keyboard.left:
        alien.x = alien.x - 2
    box.x = box.x + 2
    if box.x > WIDTH:
        box.x = 0
# PLAY SOUND AND SHOW IMAGE WHEN HIT
    if alien.colliderect(box):
        alien.image = 'alien_hurt'
        sounds.eep.play()
    else:
        alien.image = 'alien'
Exercise 5.9.

Record your own sound effect and add it to the game.

Exercise 5.10. Advanced

Add more boxes or sprites that move in different ways for the player to avoid.

Exercise 5.11. Advanced

Add a second alien controlled by different keys or gamepad for player 2.

5.5 Mouse clicks

This uses a function call-back for event-based input. It is similiar to Program 5.4 but:

See Program 2.17 for more about functions.

Program 5.5: Getting input from mouse clicks
alien = Actor("alien")
alien.pos = (0, 50)
score = 0

def draw():
    screen.clear()
    alien.draw()
    screen.draw.text("Score "+str(score), (0,0))

def update():
    if keyboard.right:
        alien.x = alien.x + 2
    elif keyboard.left:
        alien.x = alien.x - 2
    alien.image = 'alien'

def on_mouse_down(pos, button):
    global score
    if button == mouse.LEFT and alien.collidepoint(pos):
        alien.image = 'alien_hurt'
        sounds.eep.play()
        score = score + 1

5.6 Mouse movement

Program 5.6: Getting input from mouse movement
# wiggle your mouse around the screen!

alien = Actor("alien")

def draw():
    screen.clear()
    alien.draw()

def on_mouse_move(pos):
    alien.pos = pos
Exercise 5.12.

What happens if you delete line 8 and replace it with this:

     animate(alien, pos=pos, duration=1, tween='bounce_end')
Exercise 5.13.

What happens if you change on_mouse_move to on_mouse_down?

Exercise 5.14. Advanced

Make a game with one alien controlled by mouse and another controlled by keyboard

Chapter 6 Improving your games

Here are some tips that will enable you to enhance your games.

6.1 Joystick input

You may call them gamepads or controllers, but Pygame calls them joysticks.

Some controllers have different inputs and some are not compatible at all so don’t be surprised if this doesnt work properly! PS4 and Xbox One controllers connected by USB cable seems to work best. Use Program 12.3 to test yours and find out what inputs it has. Note: if you run this program with no controller plugged in you will get an error.

Program 6.1: Joystick input
import pygame
WIDTH = 500
HEIGHT = 500

joystick = pygame.joystick.Joystick(0)
joystick.init()

alien = Actor("alien")
alien.pos = (0, 50)

def draw():
    screen.clear()
    alien.draw()

def update():
    if keyboard.right or joystick.get_axis(0) > 0.1:
        alien.x = alien.x + 2
    elif keyboard.left or joystick.get_axis(0) < -0.1:
        alien.x = alien.x - 2
Exercise 6.1. Optional, if you have a controller

Make the alien move up and down as well as left and right using the controller. Do the same for any other examples that use the keyboard!

6.2 Colours

Note that sometimes colour is spelt color in American programs.

Instead of using ready made colours like ‘red’, ‘yellow’, etc. you can create your own color with a tuple of three numbers. The numbers must be between 0 - 255 and represent how much red, green and blue to mix together.

Program 6.2: RGB colours
# my_colour = (0,0,0) # makes black
# my_colour = (255,255,255) # makes white

red_amount = 0
green_amount = 0
blue_amount = 0

def draw():
    my_colour = (red_amount, green_amount, blue_amount)
    screen.fill(my_colour)

# This function makes the colour change every frame
# Remove it if you just want to see a static colour.
def update():
    global red_amount
    red_amount += 1
    red_amount = red_amount % 255
Exercise 6.2. Advanced

Change the green and blue amounts to make different colours.

Exercise 6.3.

Make a gray colour.

Exercise 6.4. Advanced

Make random colours.

6.3 Using loops

The loops from Program 3.6 are useful for graphical games too! Here we draw red circles using a for loop.

We draw green circles using a loop within another loop.

Program 6.3: Loops are useful
WIDTH = 500
HEIGHT = 500

def draw():
    screen.clear()
    for x in range(0, WIDTH, 40):
        screen.draw.filled_circle((x, 20), 20, "red")

    for x in range(0, WIDTH, 40):
        for y in range(60, HEIGHT, 40):
            screen.draw.filled_circle((x, y), 10, "green")
Exercise 6.5.

import random at the top of the program and then make the positions random, e.g.

x = random.randint(0, 100)
Exercise 6.6. Advanced

Learn about RGB colour and make random colours (see Program 6.2).

Exercise 6.7. Advanced

Create a timer variable and change colours based on time (see Program 7.5)

6.4 Fullscreen mode

Sometimes it is nice to play your game using the entire screen rather than in a window. Add these lines to any game to enable fullscreen mode. Then press F to go fullscreen, ESCAPE to quit.

Program 6.4: Fullscreen mode
import pygame

WIDTH = 500
HEIGHT = 500

alien = Actor("alien")

def draw():
    screen.clear()
    alien.draw()

def update():
    if keyboard.f:
        pygame.display.set_mode((WIDTH, HEIGHT), pygame.FULLSCREEN)
    if keyboard.escape:
        exit()

6.5 Displaying the score

This game shows how you can keep the score in a variable and print it on to the game screen. You can display any other messages to the player the same way.

Program 6.5: Keeping score in a variable and displaying it
WIDTH = 500
HEIGHT = 500

score = 0

def draw():
    screen.clear()
    screen.draw.text(f"Player 1 score: {score}", (0, 0))
    screen.draw.textbox("Hello mum", Rect(50, 50, 200, 200))

# This is another special function that is called by Pygame automatically
# each time a key is pressed. That way player cannot just hold down key!

def on_key_down(key):
    global score
    if key == keys.SPACE:
        score += 1
Exercise 6.8.

Make the score text larger.

Exercise 6.9. Advanced

Add a second player who presses a different key and show their score too.

Exercise 6.10.

Add text to one of your other games.

Chapter 7 More advanced games

These games demonstrate some essential building blocks you will need to make more advanced games of your own.

7.1 Lists

We introduced lists in Program 2.12. In this game, we create an empty list [] and use a loop to fill it with alien Actors.

We again use loops to draw all the aliens and move all the aliens in the list. When the mouse is clicked we add a new alien to the list.

Program 7.1: Lists are useful in games!
WIDTH = 500
HEIGHT = 500

aliens = []
for i in range(0,10):
    aliens.append(Actor('alien', (i*30, i*30)))

def draw():
    screen.clear()
    for alien in aliens:
        alien.draw()

def update():
    for alien in aliens:
        alien.x += 2
        if alien.x > WIDTH:
            alien.x = 0

def on_mouse_down(pos, button):
    aliens.append(Actor('alien', pos))
Exercise 7.1. Advanced

Go back to a previous game (e.g. Program 5.1) and add a list of bullets that move up the screen. When the player presses the spacebar to shoot, add a new bullet to the list.

7.2 Animation

Animation is another use for functions. (See Program 2.17) We define our own function and then ask Pygame to call it back every 0.2 seconds. Most of this code is from Program 5.1.

Program 7.2: Animation
alien = Actor("alien")
alien.pos = (200, 200)

def draw():
    screen.clear()
    alien.draw()

def update():
    if keyboard.right:
        alien.x = alien.x + 2
    elif keyboard.left:
        alien.x = alien.x - 2

images = ["alien_hurt", "alien"]
image_counter = 0

def animateAlien():
    global image_counter
    alien.image = images[image_counter % len(images)]
    image_counter += 1

clock.schedule_interval(animateAlien, 0.2)
Exercise 7.2.

Make the alien animate more quickly.

Exercise 7.3. Advanced

Add another image to the list of images.

Exercise 7.4. Advanced

Draw your own animation, e.g. a man walking left which plays when the left key is pressed

7.3 Simple physics

Here we draw a ball and move it, like we did in Program 4.2. But instead of moving it by a fixed number of pixels, we store the number of pixels to move in variables, vx and vy. These are velocities, i.e. speed in a fixed direction. vx is the speed in the horizontal direction and vy is the speed in the vertical direction. This allow us to change the velocity. Here we reverse the velocity when the ball hits the edge of the screen.

Program 7.3: Simple physics: velocity
WIDTH = 500
HEIGHT = 500

ball = Rect((200, 400), (20, 20))
vx = 1
vy = 1

def draw():
    screen.clear()
    screen.draw.filled_rect(ball, "red")

def update():
    global vx, vy
    ball.x += vx
    ball.y += vy
    if ball.right > WIDTH or ball.left < 0:
        vx = -vx
    if ball.bottom > HEIGHT or ball.top < 0:
        vy = -vy
Exercise 7.5. Advanced

Make the ball move faster by increasing its velocity each time it hits the sides.

7.4 Bat and ball game

Pong is the classic bat and ball game.

Program 7.4: Pong
WIDTH = 500
HEIGHT = 500

ball = Rect((150, 400), (20, 20))
bat = Rect((200, 480), (60, 20))
vx = 4
vy = 4

def draw():
    screen.clear()
    screen.draw.filled_rect(ball, "red")
    screen.draw.filled_rect(bat, "white")

def update():
    global vx, vy
    ball.x += vx
    ball.y += vy
    if ball.right > WIDTH or ball.left < 0:
        vx = -vx
    if ball.colliderect(bat) or ball.top < 0:
        vy = -vy
    if ball.bottom > HEIGHT:
        exit()
    if(keyboard.right):
        bat.x += 2
    elif(keyboard.left):
        bat.x -= 2
Exercise 7.6.

Make the ball move more quickly.

Exercise 7.7. Advanced

Add another bat at the top of the screen for player 2.

Exercise 7.8. Advanced

Add bricks (Rects) that disappear when the ball hits them.

7.5 Timer

The update() and draw() functions are called by Pygame many times every second. Each time draw() is called we say it draws one frame. The exact number of frames per second is called the framerate and it will vary from one computer to another. Therefore counting frames is not the most reliable way of keeping time.

Fortunately Pygame can tell us exactly how much many seconds have passed since the last frame in a parameter to our update function. We use this delta time to keep a timer.

Program 7.5: Timer
timer = 0

def update(dt):
    global timer
    timer += dt


def draw():
    screen.clear()
    screen.draw.text(f"Time passed: {timer}", (0, 0))
    if timer > 5:
        screen.draw.textbox("Time's up!", Rect(50, 50, 200, 200))
Exercise 7.9.

Make the timer count down, not up.

Exercise 7.10. Advanced

Add a timer to one of your other games.

Exercise 7.11. Advanced

Add a timer to Program 2.12 that deletes one of the aliens when the timer runs out, then starts the timer again.

7.6 Callbacks: another kind of timer

Pygame has its own clock which we can use by asking it to callback one of our functions at a certain time, or regularly over and over at an interval.

Program 7.6: Timer with callback functions
import random

aliens = []

def add_alien():
    aliens.append(
        Actor("alien", (random.randint(0,500), random.randint(0,500)))
    )

# call add_alien once, 0.5 seconds from now
clock.schedule(add_alien, 0.5)

# call add_alien over and over again, ever 2 seconds
clock.schedule_interval(add_alien, 2.0)

def draw():
    screen.clear()
    for alien in aliens:
        alien.draw()
Exercise 7.12.

Make the aliens appear more often.

Exercise 7.13. Advanced

Use len(aliens) to print how many aliens there are

Exercise 7.14. Advanced

When there are too many aliens, stop adding them using this code:

       clock.unschedule(add_alien)

Chapter 8 Tutorial: Chase game

In this chapter we will build a complete chase game together, step by step. The Python we will use is very simple: just conditionals and loops.

The techniques here should be familiar to you because we used them in Program 4.4, 4.5, 5.1 and 5.2

Now we will show you how to put them all together in one program.

8.1 Moving Actor over a background

We must create two image files for our game. You can use a program such as Krita1 to draw them and save them in the mu_code/images folder (accessible with the images button in Mu). One is the player, called player.png. It should be small, about 64×64 pixels. Ideally it should have a transparent background.

The other is the background for the game itself. It can look like whatever you want, but it must be the same size as the game window, which will be 600×600 pixels.

images/figures/krita
Figure 8.1: New image in Krita, 600×600 pixels
Program 8.1: Chase game
WIDTH = 600
HEIGHT = 600

background = Actor("background")
player = Actor("player")
player.x = 200
player.y = 200

def draw():
    screen.clear()
    background.draw()
    player.draw()

def update():
    if keyboard.right:
        player.x = player.x + 4
    if keyboard.left:
        player.x = player.x - 4
    if keyboard.down:
        player.y = player.y + 4
    if keyboard.up:
        player.y = player.y - 4
Exercise 8.1.

Run the program and move the Actor around with the keys.

8.2 Screen wrap-around

One problem you will soon find with the program is that you can move off the edge of the screen and get lost. One way to solve this would be to stop movement at the screen edges. Instead we going to make the player teleport to the opposite edge when he leaves the screen. Add this code to the end of the program, and make sure it is indented so that it becomes part of the update() function.

    if player.x > WIDTH:
        player.x = 0
    if player.x < 0:
        player.x = WIDTH
    if player.y < 0:
        player.y = HEIGHT
    if player.y > HEIGHT:
        player.y = 0
Exercise 8.2.

Change the code so that the player stops at the edges rather than wraps-around.

8.3 Enemy chases the player

Let’s add an enemy to chase the player. At the top of the program, create a variable to store the enemy Actor:

enemy = Actor("alien")

At the end of the draw() function (but still indented so part of the function), draw the enemy. Remember there is only ever one single draw() function. No program has two. All drawing goes inside this function.

Here is the complete function including the new line at the end:

def draw():
    screen.clear()
    background.draw()
    player.draw()
    enemy.draw()

At the end of the update() function (but still indented so part of the function), add these lines to move the enemy:

    if enemy.x < player.x:
        enemy.x = enemy.x + 1
    if enemy.x > player.x:
        enemy.x = enemy.x - 1
    if enemy.y < player.y:
        enemy.y = enemy.y + 1
    if enemy.y > player.y:
        enemy.y = enemy.y - 1
    if player.colliderect(enemy):
        exit()
Exercise 8.3.

Run the program verify the enemy chases the player.

Exercise 8.4.

Make the enemy faster so the game is more difficult.

Exercise 8.5.

Create an image file enemy.png and save it in the images folder. Change the code so it loads "enemy" instead of "alien".

8.4 Collecting items

Create a small image file coin.png and save it in the images folder. It should look like a coin or something else you would like to collect. We will also need a variable to store the score, i.e. number of coins collected.

coin = Actor("coin", pos=(300,300))
score = 0

At the end of the draw() function (but still indented so part of the function), draw the coin. Remember there is only ever one single draw() function. No program has two. All drawing goes inside this function.

Here is the complete function including the new line at the end:

def draw():
    screen.clear()
    background.draw()
    player.draw()
    enemy.draw()
    coin.draw()

At the end of the update() function (but still indented so part of the function), add these lines to move the coin when it is collected:

    if coin.colliderect(player):
        coin.x = random.randint(0, WIDTH)
        coin.y = random.randint(0, HEIGHT)
        score = score + 1
        print("Score:", score)
Exercise 8.6.

Run the program and collect a coin. What happens?

NameError: name 'random' is not defined

This happens because we are using a function randint() to get a random number. This function is not build-in to Python; it is part of the random library. So at the top of the program, add the first line:

import random
Exercise 8.7.

Run the program again and collect a coin. Does it work now?

No!

UnboundLocalError: local variable 'score' referenced before assignment

You will get an error because score is a global variable and we are trying to modify it inside the update() function. Therefore at the top of the update() function, add this line to declare to Python our intention to modify a global variable:

global score

It must be the first line in the function and it must be indented. The lines surrounding it should look like this:

def update():
    global score
    if keyboard.right:
Exercise 8.8.

Run the program again and verify it works!

8.5 Player 2

We can make any game into a two player game. At the top of the program, create a variable to store the Actor for the second player:

player2 = Actor("alien")

At the end of the draw() function (but still indented so part of the function), draw the enemy. Here is the complete function with the new line at the end:

def draw():
    screen.clear()
    background.draw()
    player.draw()
    enemy.draw()
    coin.draw()
    player2.draw()

At the end of the update() function (but still indented so part of the function), add these lines to move the second player:

    if keyboard.d:
        player2.x = player2.x + 4
    if keyboard.a:
        player2.x = player2.x - 4
    if keyboard.s:
        player2.y = player2.y + 4
    if keyboard.w:
        player2.y = player2.y - 4
    if player.colliderect(player2):
        exit()
Exercise 8.9. Advanced

Create a variable score2 and store the score for player two, i.e. it goes up when he collides with a coin.

8.6 Showing the score on the screen

At the end of the draw() function (but still indented so part of the function), draw a title on the screen:

    screen.draw.text("My game", (200,0), color='red')

The draw.text() function is not like print() - it can only print strings of text, not numbers. Therefore we must convert the score into a string. Add these lines to the end of the draw() function:

    score_string = str(score)
    screen.draw.text(score_string, (0,0), color='green')
Exercise 8.10.

Change the colour of the text.

Exercise 8.11. Advanced

Display the word “Score: “ before the score.

Exercise 8.12. Advanced

When the score reaches 10, show a message on the screen to congratulate the player

8.7 Timer

Add a variable at the top of the program (but preferably after any import statements) to store the number of seconds of time remaining in the game:

time = 20

Pygame Zero calls our update() function many times per second. We can ask it to tell us how much time has passed by adding a parameter to the function, delta. We then subtract this from the remaining time. Modify update() so the first lines look like this:

def update(delta):
    global score, time
    time = time - delta
    if time <= 0:
         exit()

We can also display the time on the screen. At the end of the draw() function (but still indented so part of the function) add these lines:

    time_string = str(time)
    screen.draw.text(time_string, (50,0), color='green')
Exercise 8.13.

Run the program. Could the displayed time be improved?

We don’t need to see the decimal places in the time. Modify those lines to use the round() function, like this:

    time_string = str(round(time))
    screen.draw.text(time_string, (50,0), color='green')

8.8 Finished game

Here is the finished game with all the changes included:

Program 8.2: Finished chase game
import random

WIDTH = 600
HEIGHT = 600

background = Actor("background")
player = Actor("player")
player.x = 200
player.y = 200

enemy = Actor("alien")
player2 = Actor("player")
coin = Actor("alien", pos=(300,300))
score = 0
time = 20


def draw():
    screen.clear()
    background.draw()
    player.draw()
    enemy.draw()
    player2.draw()
    coin.draw()
    screen.draw.text("My game", (200,0), color='red')
    score_string = str(score)
    screen.draw.text(score_string, (0,0), color='green')
    time_string = str(round(time))
    screen.draw.text(time_string, (50,0), color='green')

def update(delta):
    global score, time
    time = time - delta
    if time <= 0:
        exit()
    if keyboard.right:
        player.x = player.x + 4
    if keyboard.left:
        player.x = player.x - 4
    if keyboard.down:
        player.y = player.y + 4
    if keyboard.up:
        player.y = player.y - 4

    if player.x > WIDTH:
        player.x = 0
    if player.x < 0:
        player.x = WIDTH
    if player.y < 0:
        player.y = HEIGHT
    if player.y > HEIGHT:
        player.y = 0

    if enemy.x < player.x:
        enemy.x = enemy.x + 1
    if enemy.x > player.x:
        enemy.x = enemy.x - 1
    if enemy.y < player.y:
        enemy.y = enemy.y + 1
    if enemy.y > player.y:
        enemy.y = enemy.y - 1
    if player.colliderect(enemy):
        exit()

    if keyboard.d:
        player2.x = player2.x + 4
    if keyboard.a:
        player2.x = player2.x - 4
    if keyboard.s:
        player2.y = player2.y + 4
    if keyboard.w:
        player2.y = player2.y - 4
    if player.colliderect(player2):
        exit()

    if coin.colliderect(player):
        coin.x = random.randint(0, WIDTH)
        coin.y = random.randint(0, HEIGHT)
        score = score + 1
        print("Score:", score)

8.9 Ideas for extension

There are many things you could add to this game.

1. https://krita.org

Chapter 9 Tutorial: Maze game

In this chapter we will build a maze game together, step by step. The Python we will use is quite simple: mostly just conditionals and loops. The technique of creating a tilemap is common in games and after seeing it here you should be able to incorporate it into your own projects.

images/figures/mazegame
Figure 9.1: Maze game

9.1 Tilemap

A tilemap uses a small number of images (the tiles) and draws them many times to build a much larger game level (the map). This saves you from creating a lot of artwork and makes it very easy to change the design of the level on a whim. Here we create a maze level.

We must create three image files for the tiles: empty.png, wall.png and goal.png and save them in the mu_code/images folder (accessible with the images button in Mu).

They must each be 64×64 pixels. For the player we will use the built in alien image.

Program 9.1: Drawing a tilemap
TILE_SIZE = 64
WIDTH = TILE_SIZE * 8
HEIGHT = TILE_SIZE * 8

tiles = ['empty', 'wall', 'goal']

maze = [
    [1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 1, 2, 0, 1],
    [1, 0, 1, 0, 1, 1, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1]
]

player = Actor("alien", anchor=(0, 0), pos=(1 * TILE_SIZE, 1 * TILE_SIZE))

def draw():
    screen.clear()
    for row in range(len(maze)):
        for column in range(len(maze[row])):
            x = column * TILE_SIZE
            y = row * TILE_SIZE
            tile = tiles[maze[row][column]]
            screen.blit(tile, (x, y))
    player.draw()

The filenames of the tile images are stored in a list, tiles. The level design is stored in a list of lists, more commonly called a two dimensional array. There are 8 rows and 8 columns in the array. If you change the size of the array you will need to change the WIDTH and HEIGHT values too. The numbers in the maze array refers to elements in the tiles array. So 0 means empty and 1 means wall, etc.

images/figures/maze2
Figure 9.2: Tile grid

To draw the maze we use a for loop within another for loop. The outer loop iterates over the rows and the inner loop iterates over the columns, i.e. the elements of the row.

Exercise 9.1.

Create your own images empty.png, wall.png and goal.png and run the program.

Exercise 9.2.

The player appears at the top left of the maze at row 1 column 1. Change the pos parameter so he appears at the bottom instead.

Exercise 9.3.

Change the design of the maze by changing the numbers in the maze array.

Exercise 9.4. Advanced

Make the maze bigger.

9.2 Moving the player

Add this code to the end of the program:

def on_key_down(key):
    row = int(player.y / TILE_SIZE)
    column = int(player.x / TILE_SIZE)
    if key == keys.UP:
        row = row - 1
    if key == keys.DOWN:
        row = row + 1
    if key == keys.LEFT:
        column = column - 1
    if key == keys.RIGHT:
        column = column + 1
    player.x = column * TILE_SIZE
    player.y = row * TILE_SIZE

This function will be called automatically by Pygame, like draw() and update(). However on_key_down() is not called every frame; it is only called when the player presses a key. The key that was pressed is passed to the function in the key parameter.

Exercise 9.5.

Run the program and move the player. Are there any problems with the movement?

9.3 Restricting where the player can move

Delete the last two lines of the program and replace them with this modified version:

    tile = tiles[maze[row][column]]
    if tile == 'empty':
        player.x = column * TILE_SIZE
        player.y = row * TILE_SIZE
    elif tile == 'goal':
        print("Well done")
        exit()
Exercise 9.6.

Run the program and check that the player now only moves if the tile is empty.

Exercise 9.7.

Check that the game ends when the player reaches the goal.

9.4 Animate the movement of the player

First, the alien Actor is bit too big. Draw a new image of size 64×64 pixels and save it as player.png in the images folder. In the program, change the line:

player = Actor("alien", anchor=(0, 0), pos=(1 * TILE_SIZE, 1 * TILE_SIZE))

to

player = Actor("player", anchor=(0, 0), pos=(1 * TILE_SIZE, 1 * TILE_SIZE))

Next, the movement of the Actor is sudden and jerky. Luckily Pygame includes a function to do smooth movement for us automatically. Find these lines of the program:

    if tile == 'empty':
          player.x = column * TILE_SIZE
          player.y = row * TILE_SIZE

replace them with:

    if tile == 'empty':
        x = column * TILE_SIZE
        y = row * TILE_SIZE
        animate(player, duration=0.1, pos=(x, y))
Exercise 9.8.

Verify the player image has changed and moves smoothly.

9.5 Create an enemy

We will create a simple enemy that moves up and down. Add this code near the top just above the draw() function.

enemy = Actor("enemy", anchor=(0, 0), pos=(3 * TILE_SIZE, 6 * TILE_SIZE))
enemy.yv = -1

To make the enemy visible, add this line at the end of the draw() function, after the player is drawn:

  enemy.draw()

enemy.yv is the velocity in the y-axis direction (up and down). Add these lines to end of the program (still inside the on_key_down() function) to make the enemy move and reverse velocity when it hits a wall.

    # enemy movement
    row = int(enemy.y / TILE_SIZE)
    column = int(enemy.x / TILE_SIZE)
    row = row + enemy.yv
    tile = tiles[maze[row][column]]
    if not tile == 'wall':
        x = column * TILE_SIZE
        y = row * TILE_SIZE
        animate(enemy, duration=0.1, pos=(x, y))
    else:
        enemy.yv = enemy.yv * -1
    if enemy.colliderect(player):
        print("You died")
        exit()
Exercise 9.9.

Verify that the enemy moves up and down and kills the player.

Exercise 9.10. Advanced

Make another enemy that moves horizontally (left and right).

Exercise 9.11. Advanced

The collision detection is quite lenient (i.e. buggy) because it only tests for collisions between the enemy and player when a key is pressed. Define a new function called update() and move the collisions detection there so that is called every frame.

9.6 A locked door and a key

We will add two new tiles to the game. Draw images door.png and key.png and save them in images folder.

Find the tiles list near the top and change it to include the new images, and modify the maze with some number 3s and 4s where you want to new tiles to appear. Mine looks like this:

tiles = ['empty', 'wall', 'goal', 'door', 'key']
unlock = 0

maze = [
    [1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 1, 2, 0, 1],
    [1, 0, 1, 0, 1, 1, 3, 1],
    [1, 0, 1, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 0, 1],
    [1, 0, 1, 4, 1, 0, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1]
]

At the top of the program, create a new variable to store the number of keys the player is carrying:

unlock = 0

Find the if statement where we test for goal:

   if tile == 'goal':
        print("Well done")
        exit()

Modify it like this to also test for the key and door tiles. Since we are modifying a global variable inside a function we must declare it.

    global unlock
    if tile == 'goal':
        print("Well done")
        exit()
    elif tile == 'key':
        unlock = unlock + 1
        maze[row][column] = 0 # 0 is 'empty' tile
    elif tile == 'door' and unlock > 0:
        unlock = unlock - 1
        maze[row][column] = 0 # 0 is 'empty' tile

9.7 Finished game

Here is the finished game with all the changes included:

Program 9.2: Finished maze game
TILE_SIZE = 64
WIDTH = TILE_SIZE * 8
HEIGHT = TILE_SIZE * 8

tiles = ['empty', 'wall', 'goal', 'door', 'key']
unlock = 0

maze = [
    [1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 1, 2, 0, 1],
    [1, 0, 1, 0, 1, 1, 3, 1],
    [1, 0, 1, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 0, 1],
    [1, 0, 1, 4, 1, 0, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1]
]

player = Actor("player", anchor=(0, 0), pos=(1 * TILE_SIZE, 1 * TILE_SIZE))
enemy = Actor("enemy", anchor=(0, 0), pos=(3 * TILE_SIZE, 6 * TILE_SIZE))
enemy.yv = -1

def draw():
    screen.clear()
    for row in range(len(maze)):
        for column in range(len(maze[row])):
            x = column * TILE_SIZE
            y = row * TILE_SIZE
            tile = tiles[maze[row][column]]
            screen.blit(tile, (x, y))
    player.draw()
    enemy.draw()

def on_key_down(key):
    # player movement
    row = int(player.y / TILE_SIZE)
    column = int(player.x / TILE_SIZE)
    if key == keys.UP:
        row = row - 1
    if key == keys.DOWN:
        row = row + 1
    if key == keys.LEFT:
        column = column - 1
    if key == keys.RIGHT:
        column = column + 1
    tile = tiles[maze[row][column]]
    if tile == 'empty':
        x = column * TILE_SIZE
        y = row * TILE_SIZE
        animate(player, duration=0.1, pos=(x, y))
    global unlock
    if tile == 'goal':
        print("Well done")
        exit()
    elif tile == 'key':
        unlock = unlock + 1
        maze[row][column] = 0 # 0 is 'empty' tile
    elif tile == 'door' and unlock > 0:
        unlock = unlock - 1
        maze[row][column] = 0 # 0 is 'empty' tile

    # enemy movement
    row = int(enemy.y / TILE_SIZE)
    column = int(enemy.x / TILE_SIZE)
    row = row + enemy.yv
    tile = tiles[maze[row][column]]
    if not tile == 'wall':
        x = column * TILE_SIZE
        y = row * TILE_SIZE
        animate(enemy, duration=0.1, pos=(x, y))
    else:
        enemy.yv = enemy.yv * -1
    if enemy.colliderect(player):
        print("You died")
        exit()

9.8 Ideas for extension

However that is not the end! There are many things you could add to this game.

Chapter 10 Tutorial: Shooting game

In this chapter we will build a shooting game together, step by step. The Python we will use is: conditionals, loops, lists and functions.

10.1 Step 1: Decide what Actors you will need

Our game will need these Actors, so we must create images for all of them and save them as .png files in the images folder.

variable name image file name image size description
player player.png 64x64 player’s spaceship
background background.png 600x800 image of stars
enemies (list) enemy.png 64x64 evil alien
bullets (list) bullet.png 16x16 fired by the player
bombs (list) bomb.png 16x16 dropped by the enemies

The player and background variables will contain Actors. The others are lists which we initialize to the empty list []. Actors will be appended to the lists later.

Program 10.1: Shooter game part 1 of 4
import random

WIDTH = 600
HEIGHT = 800
MAX_BULLETS = 3

level = 1
lives = 3
score = 0

background = Actor("background")
player = Actor("player", (200, 580))
enemies = []
bullets = []
bombs = []

10.2 Step 2: Draw your Actors

Every Pygame game needs an draw() function, and it should draw all the Actors we created above.

Program 10.2: Shooter game part 2 of 4
def draw():
    screen.clear()
    background.draw()
    player.draw()
    for enemy in enemies:
        enemy.draw()
    for bullet in bullets:
        bullet.draw()
    for bomb in bombs:
        bomb.draw()
    draw_text()

10.3 Step 3: Move your Actors

Every Pygame game needs an update() function to move the Actors, check for collisions, etc.

Program 10.3: Shooter game part 3 of 4
def update(delta):
    move_player()
    move_bullets()
    move_enemies()
    create_bombs()
    move_bombs()
    check_for_end_of_level()
Exercise 10.1.

Create the png image files (player.png, background.png, bullet.png, bomb.png, enemy.png). Type in program 10.1, 10.2 and 10.3 into a single file. Save the file. Why doesn’t it run?

10.4 Step 4: Define your functions

Python cannot call a function that has not yet been defined. Therefore we must at least provide empty, dummy versions of our functions that don’t do anything so we can fill them in later. However Python cannot define a completely empty function - it must contain at least one line. Therefore we use the pass keyword to create a line that doesn’t do anything.

Program 10.4: Shooter game part 4 of 4
def move_player():
    pass

def move_enemies():
    pass

def move_bullets():
    pass

def create_bombs():
    pass

def move_bombs():
    pass

def check_for_end_of_level():
    pass

def draw_text():
    pass
Exercise 10.2.

Add listing 10.4 to the end of the file. Verify the game now runs and you can see the player at the bottom of the screen. (He can’t move yet.)

10.5 Create enemies

Add this new function to the end of the program, and then call it immediately. It uses a loop within a loop to create enemy Actors and put them in the enemies list. The reason we put this in a function is we will need to call it again at the start of each level.

def create_enemies():
    for x in range(0, 600, 60):
        for y in range(0, 200, 60):
            enemy = Actor("enemy", (x, y))
            enemy.vx = level * 2
            enemies.append(enemy)


create_enemies()

10.6 Move the player

Replace the move_player() dummy function definition with this. Remember there can only be one function with a given name. There cannot be two move_player() function definitions.

def move_player():
    if keyboard.right:
        player.x = player.x + 5
    if keyboard.left:
        player.x = player.x - 5
    if player.x > WIDTH:
        player.x = WIDTH
    if player.x < 0:
        player.x = 0

10.7 Move the enemies

Replace the move_enemies() dummy function definition with this:

def move_enemies():
    global score
    for enemy in enemies:
        enemy.x = enemy.x + enemy.vx
        if enemy.x > WIDTH or enemy.x < 0:
            enemy.vx = -enemy.vx
            animate(enemy, duration=0.1, y=enemy.y + 60)
        for bullet in bullets:
            if bullet.colliderect(enemy):
                enemies.remove(enemy)
                bullets.remove(bullet)
                score = score + 1
        if enemy.colliderect(player):
            exit()

10.8 Draw text on the screen

Replace the draw_text() dummy function definition with this:

def draw_text():
    screen.draw.text("Level " + str(level), (0, 0), color="red")
    screen.draw.text("Score " + str(score), (100, 0), color="red")
    screen.draw.text("Lives " + str(lives), (200, 0), color="red")

10.9 Player bullets

Add this new function to the end of the program. Pygame will call it for us automatically when a key is pressed.

def on_key_down(key):
    if key == keys.SPACE and len(bullets) < MAX_BULLETS:
        bullet = Actor("bullet", pos=(player.x, player.y))
        bullets.append(bullet)

Replace the move_bullets() dummy function definition with this:

def move_bullets():
    for bullet in bullets:
        bullet.y = bullet.y - 6
        if bullet.y < 0:
            bullets.remove(bullet)

10.10 Enemy bombs

Replace the create_bombs() dummy function definition with this:

def create_bombs():
    if random.randint(0, 100 - level * 6) == 0:
        enemy = random.choice(enemies)
        bomb = Actor("bomb", enemy.pos)
        bombs.append(bomb)

Replace the move_bombs() dummy function definition with this:

def move_bombs():
    global lives
    for bomb in bombs:
        bomb.y = bomb.y + 10
        if bomb.colliderect(player):
            bombs.remove(bomb)
            lives = lives - 1
            if lives == 0:
                exit()

10.11 Check for end of level

Replace the check_for_end_of_level() dummy function definition with this:

def check_for_end_of_level():
    global level
    if len(enemies) == 0:
        level = level + 1
        create_enemies()

10.12 Ideas for extension

Chapter 11 Tutorial: Race game

In this chapter we will build a racing game together, step by step. The Python we will use is: conditionals, loops, lists, functions and tuples. We will show use of velocity, high score and a title screen.

11.1 Basic game

Similar to the shooter game, we will begin with a complete program listing but with empty bodies for some of the functions that we will fill in later. (Python will not run a program with completely empty functions, so they just contain pass to indicate to Python they do nothing.)

images/figures/race
Figure 11.1: Race game

Like the shooter program, we begin we three things:

  1. Definitions of global variables.
  2. A draw() function.
  3. An update() function.

These functions now check a boolean variable playing. If False then instead of drawing/updating the game we show the title screen.

The only really complicated part of this program is how we store the shape of the tunnel the player is racing down. lines is a list of tuples. A tuple is like a list but cannot be modified and can be unpacked into separate variables. Each tuple will represent one horizontal line of the screen. It will have three values, x, x2 and color, representing the position of the left wall, the gap between the left wall and the right wall and the colour of the wall.

Program 11.1: Basic skeleton of race game
import random
import math

WIDTH = 600
HEIGHT = 600

player = Actor("alien", (300, 580))
player.vx = 0   # horizontal velocity
player.vy = 1   # vertical velocity

lines = []          # list of tuples of horizontal lines of walls
wall_gradient = -3  # steepness of wall
left_wall_x = 200   # x-coordinate of wall
distance = 0        # how far player has travelled
time = 15           # time left until game ends
playing = False     # True when in game, False when on title screen
best_distance = 0   # remember the highest distance scored

def draw():
    screen.clear()
    if playing: # we are in game
        for i in range(0, len(lines)): # draw the walls
            x, x2, color = lines[i]
            screen.draw.line((0, i), (x, i), color)
            screen.draw.line((x + x2, i), (WIDTH, i), color)
        player.draw()
    else:   # we are on title screen
        screen.draw.text("PRESS SPACE TO START",
            (150, 300),color="green",fontsize=40)
        screen.draw.text("BEST DISTANCE: "+str(int(best_distance / 10)),
            (170, 400), color="green", fontsize=40)
    screen.draw.text("SPEED: " + str(int(player.vy)),
        (0, 0), color="green", fontsize=40)
    screen.draw.text("DISTANCE: " + str(int(distance / 10)),
        (200, 0), color="green", fontsize=40)
    screen.draw.text("TIME: " + str(int(time)),
        (480, 0), color="green", fontsize=40)


def update(delta):
    global playing, distance, time
    if playing:
        wall_collisions()
        scroll_walls()
        generate_lines()
        player_input()
        timer(delta)
    elif keyboard.space:
        playing = True
        distance = 0
        time = 10


def player_input():
    pass

def generate_lines():
    pass

generate_lines()

def scroll_walls():
    pass

def wall_collisions():
    pass

def timer(delta):
    pass

def on_mouse_move(pos):
    pass
Exercise 11.1.

Run the program. Verify it has a title screen and you can start the game and see the player. (That is all it will do until we fill in the remaining functions.)

11.2 Player input

Replace the definiton of player_input() with this:

def player_input():
    if keyboard.up:
        player.vy += 0.1
    if keyboard.down:
        player.vy -= 0.1
        if player.vy < 1:
            player.vy = 1
    if keyboard.right:
        player.vx += 0.4
    if keyboard.left:
        player.vx -= 0.4
    player.x += player.vx
Exercise 11.2.

Run the program. Verify the player can move left and right and has momentum. Try adjusting the speed or making a limit so you can’t go too fast.

11.3 Generate the walls

We already have code to draw the walls, but currently lines is empty so nothing gets drawn. Replace the function generate_lines() with this. Note that we immediately call generate_lines() after defining it to generate the walls for the start of the game.

def generate_lines():
    global wall_gradient, left_wall_x
    gap_width = 300 + math.sin(distance / 3000) * 100
    while len(lines) < HEIGHT:
        pretty_colour = (255, 0, 0)
        lines.insert(0, (left_wall_x, gap_width, pretty_colour))
        left_wall_x += wall_gradient
        if left_wall_x < 0:
            left_wall_x = 0
            wall_gradient = random.random() * 2 + 0.1
        elif left_wall_x + gap_width > WIDTH:
            left_wall_x = WIDTH - gap_width
            wall_gradient = -random.random() * 2 - 0.1

generate_lines()
Exercise 11.3. Advanced

Run the program. Change the colour of the walls from red to green.

11.4 Make the walls colourful

Modify the line that sets the colour of the generated line to this:

        pretty_colour = (255, min(left_wall_x, 255), min(time * 20, 255))

11.5 Scrolling

Modify the scroll_walls() function so it removes lines from the bottom of the screen according to the player’s vertical velocity.

def scroll_walls():
    global distance
    for i in range(0, int(player.vy)):
        lines.pop()
        distance += 1
Exercise 11.4.

Modify scroll_walls() as above and check that the player can now accelerate forward.

Exercise 11.5. Advanced

Change the amount of the forward acceleration to make the game faster or slower.

11.6 Wall collisions

Currently the player can move through the walls - we don’t want to allow this. Also we want the player to lose all their velocity each time they collide as a penalty.

def wall_collisions():
    a, b, c = lines[-1]
    if player.x < a:
        player.x += 5
        player.vx = player.vx * -0.5
        player.vy = 0
    if player.x > a + b:
        player.x -= 5
        player.vx = player.vx * -0.5
        player.vy = 0
Exercise 11.6.

Modify wall_collisions() as above and check that the player now bounces off the walls.

Exercise 11.7. Advanced

Make the collision more bouncy, i.e. the player bounces further when he hits the wall.

11.7 Timer

Currently the player has infinite time. We want decrease the time variable by how much time has passed and end the game when time runs out.

def timer(delta):
    global time, playing, best_distance
    time -= delta
    if time < 0:
        playing = False
        if distance > best_distance:
            best_distance = distance
Exercise 11.8.

Modify the timer() function as above. Verify the game ends after 15 seconds.

Exercise 11.9.

Make the game last for 30 seconds.

11.8 Mouse movement

The game is easier but perhaps more fun if you can play it with mouse. Pygame will call this function for us automatically.

def on_mouse_move(pos):
    x, y = pos
    player.x = x
    player.vy = (HEIGHT - y) / 20
Exercise 11.10.

Modify the on_mouse_move() function as above. How does the player accelerate using the mouse?

11.9 Ideas for extension

Chapter 12 Advanced topics

12.1 Instructor note

This is the beginning of my attempts to teach object oriented programming, but I wouldn’t attempt this with young students since it requires abstract thinking.

12.2 Classes

You’ve already been using class types provided by Pygame Zero, e.g. Rect and Actor. But if we want to store velocity as in Program 7.3 we find these classes do not include vx and vy variables inside them by default. We have to remember to add a vx and vy every time we create an Actor.

So let’s create our own class, called Sprite, that is the same as Actor but with these variables included.

Program 12.1: Classes
WIDTH = 500
HEIGHT = 500

class Sprite(Actor):
    vx = 1
    vy = 1

ball = Sprite('alien')

def draw():
    screen.clear()
    ball.draw()

def update():
    ball.x += ball.vx
    ball.y += ball.vy
    if ball.right > WIDTH or ball.left < 0:
        ball.vx = -ball.vx
    if ball.bottom > HEIGHT or ball.top < 0:
        ball.vy = -ball.vy

12.3 Methods

Classes can contain functions (called methods) as well as variables. Methods are the best place to modify the class’s variables.

Program 12.2: Class methods
WIDTH = 500
HEIGHT = 500

class Sprite(Actor):
    vx = 1
    vy = 1

    def update(self):
        self.x += self.vx
        self.y += self.vy
        if self.right > WIDTH or self.left < 0:
            self.vx = -self.vx
        if self.bottom > HEIGHT or self.top < 0:
            self.vy = -self.vy

ball = Sprite("alien")

def draw():
    screen.clear()
    ball.draw()

def update():
    ball.update()

12.4 Joystick tester

This program demonstrates using joysticks and for loops, but is mainly included to help you test the input from your controllers.

(I don’t suggest typing this one yourself.)

Program 12.3: Joystick tester
import pygame

def update():
    screen.clear()
    joystick_count = pygame.joystick.get_count()
    y = 0
    for i in range(joystick_count):
        joystick = pygame.joystick.Joystick(i)
        joystick.init()
        name = joystick.get_name()
        axes = joystick.get_numaxes()
        buttons = joystick.get_numbuttons()
        hats = joystick.get_numhats()
        screen.draw.text(
            "Joystick {} name: {} axes:{} buttons:{} hats:{}".format(
                i, name, axes, buttons, hats), (0, y))
        y += 14
        for i in range(axes):
            axis = joystick.get_axis(i)
            screen.draw.text("Axis {} value: {:>6.3f}".format(i, axis),
                             (20, y))
            y += 14
        for i in range(buttons):
            button = joystick.get_button(i)
            screen.draw.text("Button {:>2} value: {}".format(i, button),
                             (20, y))
            y += 14
        for i in range(hats):
            hat = joystick.get_hat(i)
            screen.draw.text("Hat {} value: {}".format(i, str(hat)),
                             (20, y))
            y += 14

12.5 Distributing your Pygame Zero games

This is often tricky to get working, but you can distribute your games to people who don’t have Python or Mu installed. You can put them on a USB stick, or a website for people to download, or even on itch.io for people to buy.

  1. Install the full version of python from www.python.org.
  2. Edit your game source code (using Mu). We will assume your source is in a file NAME_OF_GAME.py. At the top of the file add the line:
    import pgzrun
    

    At the bottom of the file add the line:

    pgzrun.go()
    

    Any time in the program you use draw.text() you must specify a font, e.g. add parameter fontname="arial"

Save the file.

  1. Copy any fonts you have used into the fonts folder and rename them to lowercase names, e.g. arial.ttf
  2. Open a command prompt (Click start menu and type cmd.exe)
  3. Enter your mu_code folder. At the prompt type:

    cd mu_code

  4. Install pyinstaller and pgzero. At the command prompt type:

    pip install pgzero pyinstaller

  5. You should find the pgzero folder at:

    C:\Users\YOURNAME\AppData\\Local\\Programs

    Python\Python37\Lib\site-packages\pgzero

    This is a hidden folder so you must enable hidden folders in Windows Explorer to see it.

    Copy the pgzero folder into your mu_code folder.

    You should find your mu_code folder at: C:\Users\YOURNAME\mu_code

  6. Create the executable. At the command prompt type:

    pyinstaller NAME_OF_GAME.py –distpath . –add-data "pgzero;pgzero" –add-data "images;images" –add-data "fonts;fonts" –add-data "sounds;sounds" –add-data "music;music" –onefile –noconfirm –windowed –clean

    This will generate a program called NAME_OF_GAME.exe. You can double click this program to play your game.

  7. To distribute your game you need to copy the entire mu_code folder. You could put it inside a zip file, and then put that on a website