Pygame Zero
Pygame Zero
# Check Python version number. sys.version_info gives version as a tuple, e.g. if (3,7,2,'final',0)
for version 3.7.2.
# Unlike many languages, Python can compare two tuples in the same way that you can
compare numbers.
print("This game requires at least version 3.5 of Python. Please download it from
[Link]")
[Link]()
# Check Pygame Zero version. This is a bit trickier because Pygame Zero only lets us get its
version number as a string.
# So we have to split the string into a list, using '.' as the character to split on. We convert each
element of the
# version number into an integer - but only if the string contains numbers and nothing else,
because it's possible for
# We're using a Python feature called list comprehension - this is explained in the Bubble
Bobble/Cavern chapter.
print("This game requires at least version 1.2 of Pygame Zero. You have version {0}. Please
upgrade using the command 'pip3 install --upgrade pgzero'".format(pgzero.__version__))
[Link]()
WIDTH = 480
HEIGHT = 800
TITLE = "Myriapod"
DEBUG_TEST_RANDOM_POSITIONS = False
# Pygame Zero allows you to access and change sprite positions based on various
# anchor points
num_grid_rows = 25
num_grid_cols = 14
# Convert a position in pixel units to a position in grid units. In this game, a grid square is 32
pixels.
# If the requested offset is zero, returns the centre of the requested cell, hence the +16. In
the case of the
# X axis, there's a 16 pixel border at the left and right of the screen, hence +16 becomes +32.
class Explosion(Actor):
[Link] = type
[Link] = 0
def update(self):
[Link] += 1
# Set sprite based on explosion type and timer - update to a new image
class Player(Actor):
INVULNERABILITY_TIME = 100
RESPAWN_TIME = 100
RELOAD_TIME = 10
super().__init__("blank", pos)
# These determine which frame of animation the player sprite will use
[Link] = 0
[Link] = 0
[Link] = 3
[Link] = True
# timer is used for animation, respawning and for ensuring the player is
[Link] = 0
self.fire_timer = 0
for i in range(speed):
# For each pixel we want to move, we must first check if it's a valid place to move to
self.x += dx
self.y += dy
def update(self):
[Link] += 1
if [Link]:
# Get keyboard input. dx and dy represent the direction the player is facing on each
axis
dx = 0
if [Link]:
dx = -1
elif [Link]:
dx = 1
dy = 0
if [Link]:
dy = -1
elif [Link]:
dy = 1
# Move in the relevant directions by the specified number of pixels. The purpose of 3 -
abs(dy) is to
# generate vectors which look either like (3,0) (which is 3 units long) or (2, 2) (which is
sqrt(8) long)
# so we move roughly the same distance regardless of whether we're travelling straight
along the x or y axis.
[Link](dx, 0, 3 - abs(dy))
# When the player presses a key to start handing in a new direction, we don't want the
sprite to just
# instantly change to facing in that new direction. That would look wrong, since in the
real world vehicles
# Instead, we want the vehicle to turn to face the new direction over several frames. If
the vehicle is
# currently facing down, and the player presses the left arrow key, the vehicle should
first turn to face
# diagonally down and to the left, and then turn to face left.
#7 0 1
# 6 -1 2
#5 4 3
directions = [7,0,1,6,-1,2,5,4,3]
# But! If you look at the values that [Link] actually takes on during the game,
you only see
# numbers from 0 to 3. This is because although there are eight possible directions of
travel, there are
# only four orientations of the player vehicle. The same sprite, for example, is used if
the player is
# travelling either left or right. This is why the direction is ultimately clamped to a
range of 0 to 4.
# 0 = facing up or down
# # It can be useful to think of the vehicle as being able to drive both forwards and
backwards.
# Choose the relevant direction from the above list, based on dx and dy
dir = directions[dx+3*dy+4]
# Every other frame, if the player is pressing a key to move in a particular direction,
update the current
# We first calculate the difference between the desired direction and the current
direction.
# We use the following list to decide how much to rotate by each frame, based on
difference.
# It's easiest to think about this by just considering the first four direction values - 0
to 3,
# corresponding to facing up, to fit into the bottom right. However, because of the
symmetry of the
# player sprites as described above, these calculations work for all possible directions.
# If the difference is 2, then the target direction is at right angles to the current
direction,
# so we have a free choice as to whether to turn clockwise or anti-clockwise to align
with the
# If the difference is three, the symmetry of the player sprites means that we can
reach the desired
rotation = rotation_table[difference % 4]
self.fire_timer -= 1
if [Link] == 0:
# Create a bullet
game.play_sound("laser")
[Link] = ([Link] + 1) % 3
self.fire_timer = Player.RELOAD_TIME
# Check to see if any enemy segments collide with the player, as well as the flying enemy.
# We create a list consisting of all enemy segments, and append another list containing
only the
# flying enemy.
all_enemies = [Link] + [game.flying_enemy]
# The flying enemy might not exist, in which case its value
game.play_sound("player_explode")
[Link](Explosion([Link], 1))
[Link] = False
[Link] = 0
[Link] -= 1
else:
# Not alive
# Respawn
[Link] = True
[Link] = 0
# Display the player sprite if alive - BUT, if player is currently invulnerable, due to having
just respawned,
# switch between showing and not showing the player sprite on alternate frames
else:
[Link] = "blank"
class FlyingEnemy(Actor):
# Choose which side of the screen we start from. Don't start right next to the player as
that would be
side = 1 if player_x < 160 else 0 if player_x > 320 else randint(0, 1)
# Always moves in the same X direction, but randomly pauses to just fly straight up or
down
[Link] = 1 - 2 * side # Move left or right depending on which side of the screen we're on
[Link] = 1
[Link] = 0
def update(self):
[Link] += 1
# Move
self.moving_x = randint(0, 1)
[Link] = -[Link]
class Rock(Actor):
# Use a custom anchor point for totem rocks, which are taller than other rocks
[Link] = randint(0, 3)
if totem:
game.play_sound("totem_create")
[Link] = 5
self.show_health = 5
else:
# Non-totem rocks are initially displayed as if they have one health, and animate until
they
# show the actualy sprite for their health level - resulting in a 'growing' animation.
[Link] = randint(3, 4)
self.show_health = 1
[Link] = 1
# Damage can occur by being hit by bullets, or by being destroyed by a segment, or by being
cleared from the
# player's respawn location. Points can be earned by hitting special "totem" rocks, which
have 5 health, but
game.play_sound("totem_destroy")
[Link] += 100
else:
game.play_sound("rock_destroy")
else:
game.play_sound("hit", 4)
[Link] -= amount
self.show_health = [Link]
def update(self):
[Link] += 1
self.show_health += 1
# Totem rocks turn into normal rocks if not shot within 200 frames
[Link](1)
colour = str(max([Link], 0) % 3)
class Bullet(Actor):
super().__init__("bullet", pos)
[Link] = False
def update(self):
self.y -= 24
# [Link] checks to see if there is a rock at the given position - if so, it damages
# An asterisk before a list or tuple will unpack the contents into separate values
grid_cell = pos2cell(*[Link])
if [Link](*grid_cell, 1, True):
[Link] = True
else:
# Check each myriapod segment, and the flying enemy, to see if this bullet collides with
them
# Is this a valid object reference, and if so, does this bullet's location overlap with
the
# Create explosion
[Link](Explosion([Link], 2))
[Link] -= 1
if isinstance(obj, Segment):
# Should we create a new rock in the segment's place? Health must be zero, there
must be no
# rock there already, and the player sprite must not overlap with the location
game.play_sound("segment_explode")
[Link] += 10
else:
game.play_sound("meanie_explode")
[Link] += 20
return
# SEGMENT MOVEMENT
# The code below creates several constants used in the Segment class in relation to movement
and directions
# A segment enters a cell from a particular edge (stored in 'in_edge' in the Segment class)
# After five frames it decides which edge it's going leave that cell through (stored in
out_edge).
# For example, it might carry straight on and leave through the opposite edge from the one it
started at.
# Or it might turn 90 degrees and leave through an edge to its left or right.
# In this case it initially turn 45 degrees and continues along that path for 8 frames. It then
turns another
# 45 degrees, at which point they are heading directly towards the next grid cell.
# A segment spends a total of 16 frames in each cell. Within the update method, the variable
'phase' refers to
# where it is in that cycle - 0 meaning it's just entered a grid cell, and 15 meaning it's about
to leave it.
# Let's imagine the case where a segment enters from the left edge of a cell and then turns to
leave from the
# bottom edge. The segment will initially move along the horizontal (X) axis, and will end up
moving along the
# vertical (Y) axis. In this case we'll call the X axis the primary axis, and the Y axis the
secondary axis.
# are repeated the specified number of times. So the code below is equivalent to:
# SECONDARY_AXIS_SPEED = [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1 , 1, 2, 2, 2, 2]
# This list represents how much the segment moves along the secondary axis, in situations
where it makes two 45° turns
# as described above. For the first four frames it doesn't move at all along the secondary axis.
For the next eight
# frames it moves at one pixel per frame, then for the last four frames it moves at two pixels
per frame.
# The code below creates a list of 16 elements, where each element is the sum of all the
equivalent elements in the
# It is equivalent to writing:
# This list stores the total secondary axis movement that will have occurred at each phase in
the segment's movement
DIRECTION_UP = 0
DIRECTION_RIGHT = 1
DIRECTION_DOWN = 2
DIRECTION_LEFT = 3
# The indices correspond to the direction numbers above, i.e. 0 = up, 1 = right, 2 = down, 3 =
left
DX = [0,1,0,-1]
DY = [-1,0,1,0]
def inverse_direction(dir):
if dir == DIRECTION_UP:
return DIRECTION_DOWN
return DIRECTION_LEFT
return DIRECTION_UP
return DIRECTION_RIGHT
def is_horizontal(dir):
super().__init__("blank")
self.cell_x = cx
self.cell_y = cy
[Link] = health
# Determines whether the 'fast' version of the sprite is used. Note that the actual speed
of the myriapod is
[Link] = fast
# Each myriapod segment moves in a defined pattern within its current cell, before
moving to the next one.
# Several frames after entering a cell, it chooses which edge to leave through - stored in
out_edge
self.out_edge = DIRECTION_RIGHT
def rank(self):
# The rank method creates and returns a function. Don't worry if this seems a strange
concept - it is
# fairly advanced stuff. The returned function is passed to Python's 'min' function in
the update method,
# as the 'key' optional parameter. min then calls this function with the numbers 0 to 3,
representing the four
# directions
def inner(proposed_out_edge):
# This function returns a tuple consisting of a series of factors determining which grid
cell the segment should try to move into next.
# These are not absolute rules - rather they are used to rank the four directions in
order of preference,
# i.e. which direction is the best (or at least, least bad) to move in. The factors are
boolean (True or False)
# The order of the factors in the returned tuple determines their importance in
deciding which way to go,
# Note: when the segments start, they are all outside the grid so this would be True,
except for the case of
# walking onto the top-left cell of the grid. But the end result of this and the following
factors is that
# rock will either be the Rock object at the new grid cell, or None.
# It will be set to None if there is no Rock object is at the new location, or if the new
location is
# outside the grid. We also have to account for the special case where the segment is off
the left-hand
# side of the screen on the first row, where it is initially created. We mustn't try to
access that grid
# cell (unlike most languages, in Python trying to access a list index with negative value
won't necessarily
# result in a crash, but it's still not a good idea)
rock = None
else:
rock = [Link][new_cell_y][new_cell_x]
# If there are rocks both horizontally and vertically, prefer to move vertically
if rock_present:
horizontal_blocked = is_horizontal(proposed_out_edge)
else:
# Finally we create and return a tuple of factors determining which cell segment should
try to move into next.
# Most important first - e.g. we shouldn't enter a new cell if if's outside the grid
return (out, turning_back_on_self, direction_disallowed, occupied_by_segment,
rock_present, horizontal_blocked, same_as_previous_x_direction)
return inner
def update(self):
# Segments take either 16 or 8 frames to pass through each grid cell, depending on the
amount by which
# [Link] is updated each frame. phase will be a number between 0 and 15 indicating
where we're at
# in that cycle.
phase = [Link] % 16
if phase == 0:
# At this point, the segment is entering a new grid cell. We first update our current grid
cell coordinates.
self.cell_x += DX[self.out_edge]
self.cell_y += DY[self.out_edge]
# We then need to update in_edge. If, for example, we left the previous cell via its right
edge, that means
self.in_edge = inverse_direction(self.out_edge)
# During normal gameplay, once a segment reaches the bottom of the screen, it starts
moving up again.
# Once it reaches row 18, it starts moving down again, so that it remains a threat to the
player.
# During the title screen, we allow segments to go all the way back up to the top of the
screen.
self.disallow_direction = DIRECTION_UP
if self.cell_y == num_grid_rows-1:
self.disallow_direction = DIRECTION_DOWN
elif phase == 4:
# At this point we decide which new cell we're going to go into (and therefore, which
edge of the current
# Python's built-in 'min' function usually chooses the lowest number, so would usually
return 0 as the result.
# But if the optional 'key' argument is specified, this changes how the function
determines the result.
# The rank function (see above) returns a function (named 'inner' in rank), which min
calls to decide
# how the items should be ordered. The argument to inner represents a possible
direction to move in.
# When Python compares two such tuples, it considers values of False to be less than
values of True,
# and values that come earlier in the sequence are more significant than later values.
So (False,True)
self.previous_x_direction = self.out_edge
[Link](new_cell_x, new_cell_y, 5)
# Set new cell as occupied. It's a case of whichever segment is processed first, gets first
dibs on a cell
# The second line deals with the case where two segments are moving towards each other
and are in
# neighbouring cells. It allows a segment to tell if another segment trying to enter its
cell from
[Link]((new_cell_x, new_cell_y))
# turn_idx tells us whether the segment is going to be making a 90 degree turn in the
current cell, or moving
# We start off assuming that the segment is starting from the top of the cell - i.e.
self.in_edge being DIRECTION_UP,
# corresponding to zero. The primary and secondary axes, as described under "SEGMENT
MOVEMENT" above, are Y and X.
# We then apply a calculation to rotate these X and Y offsets, based on the actual
direction the segment is coming from.
# Let's take as an example the case where the segment is moving in a straight line from
top to bottom.
# will be 2. So 2 - turn_idx will be zero. Multiplying anything by zero gives zero, so we end
up with no
# The starting point for the offset_y calculation is that the segment starts at an offset
of -16 and must cover
# 32 pixels over the 16 phases - therefore we must multiply phase by 2. We then subtract
the result of the
# turn_idx % 2. mod 2 gives either zero (if turn_idx is 0 or 2), or 1 if it's 1 or 3. In the
case we're looking
# The end result of all this is that in the case where the segment is moving in a straight
line through a cell,
# it just moves at 2 pixels per frame along the primary axis. If it's turning, it starts out
moving at 2px
# per frame on the primary axis, but then starts moving along the secondary axis based
on the values in
# is stealing movement from the primary axis - hence the name 'stolen_y_movement'
# coordinates being rotated. Recall that the code above makes the assumption that
segment is starting from the
# top edge of the cell and moving down. The code below chooses the appropriate rotation
matrix based on the
# actual edge the segment started from, and then modifies offset_x and offset_y based
on this rotation matrix.
rotation_matrix = [[1,0,0,1],[0,-1,1,0],[-1,0,0,-1],[0,1,-1,0]][self.in_edge]
# Finally, we can calculate the segment's position on the screen. See cell2pos function
above.
# We now need to decide which image the segment should use as its sprite.
# Images for segment sprites follow the format 'segABCDE' where A is 0 or 1 depending on
whether this is a
# is the head segment of a myriapod, D represents the direction we're facing (0 = up, 1 =
top right,
# up to 7 = top left) and E is how far we are through the walking animation (0 to 3)
# Three variables go into the calculation of the direction. turn_idx tells us if we're making
a turn in this
# cell - and if so, whether we're turning clockwise or anti-clockwise. self.in_edge tells us
which side of the
# grid cell we entered from. And we can use SECONDARY_AXIS_SPEED[phase] to find out
whether we should be facing
# we are going to be turning but have not yet begun to turn. If we are doing a turn in this
cell, and we're
# at a phase where we should be showing a sprite with a new rotation, the result will be -1
or 1 if we're
# The next part of the calculation multiplies in_edge by 2 and then adds the result to the
result of the previous
# part. in_edge will be a number from 0 to 3, representing all possible directions in 90°
increments.
# It must be multiplied by two because the direction value we're calculating will be a
number between 0 and 7,
# In the sprite filenames, the penultimate number represents the direction the sprite is
facing, where a value
# of zero means it's facing up. But in this code, if, for example, in_edge were zero, this
means the segment is
# coming from the top edge of its cell, and therefore should be facing down. So we add 4
to account for this.
# After all this, we may have ended up with a number outside the desired range of 0 to 7.
So the final step
# is to MOD by 8.
# Converting a boolean value to an integer gives 0 for False and 1 for True. We then need
to convert the
class Game:
[Link] = -1
[Link] = 0
[Link] = player
# Create empty grid of 14 columns, 25 rows, each element intially just containing the
value 'None'
[Link] = []
[Link] = []
self.flying_enemy = None
[Link] = 0
# Find the rock at this grid cell (or None if no rock here)
rock = [Link][cell_y][cell_x]
if rock != None:
# [Link] returns False if the rock has lost all its health - in this case, the grid cell
will be set
if [Link](amount, from_bullet):
[Link][cell_y][cell_x] = None
# ax/ay are only supplied when a segment is being destroyed, and we check to see if we
should create a new
# rock in the segment's place. They indicate a grid cell location where we're planning to
create the new rock,
# we need to ensure the new rock would not overlap with the player sprite
return False
return False
return True
# Destroy any rocks that might be overlapping with the player when they respawn
[Link](xi, yi, 5)
def update(self):
# Increment time - used by segments. Time moves twice as fast every fourth wave.
# At the start of each frame, we reset occupied to be an empty set. As each individual
myriapod segment is
# updated, it will create entries in the occupied set to indicate that other segments should
not attempt to
# enter its current grid cell. There are two types of entries that are created in the
occupied set. One is a
# tuple consisting of a pair of numbers, representing grid cell coordinates. The other is a
tuple consisting of
# three numbers - the first two being grid cell coordinates, the third representing an edge
through which a
# It is only used for myriapod segments - not rocks. Those are stored in [Link].
[Link] = set()
# Call update method on all objects. grid is a list of lists, equivalent to a 2-dimensional
array,
# so sum can be used to produce a single list containing all grid objects plus the contents
of the other
# Actor lists. The player and flying enemy, which are object references rather than lists,
are appended as single-item lists.
if obj:
[Link]()
# Recreate the bullets list, which will contain all existing bullets except those which have
gone off the screen or have hit something
# Recreate the explosions list, which will contain all existing explosions except those which
have completed their animations
# Recreate the segments list, which will contain all existing segments except those whose
health is zero
if self.flying_enemy:
# Destroy flying enemy if it goes off the left or right sides of the screen, or health is
zero
self.flying_enemy = None
elif random() < .01: # If there is no flying enemy, small chance of creating one each
frame
if [Link] == []:
# create one per frame. Initially there should be 30 rocks - each wave, this goes up by
one.
num_rocks = 0
if element != None:
num_rocks += 1
while True:
if [Link][y][x] == None:
[Link][y][x] = Rock(x, y)
break
else:
game.play_sound("wave")
[Link] += 1
[Link] = 0
[Link] = []
for i in range(num_segments):
if DEBUG_TEST_RANDOM_POSITIONS:
# Determines whether segments take one or two hits to kill, based on the wave
number.
# e.g. on wave 0 all segments take one hit; on wave 1 they alternate between one
and two hits
return self
def draw(self):
# Create a list of all grid locations and other objects which need to be drawn
# (Most grid locations will be set to None as they are unoccupied, hence the check "if obj:"
further down)
# We want to draw objects in order based on their Y position. Objects further down the
screen should be drawn
# after (and therefore in front of) objects higher up the screen. We can use Python's
built-in sort function
# to put the items in the desired order, before we draw them. The following function
specifies the criteria
# Returns a tuple consisting of two elements. The first is whether the object is an
instance of the
# Explosion class (True or False). A value of true means it will be displayed in front of
other objects.
# The second element is a number - either the objects why position, or zero if obj is
'None'
all_objs.sort(key=sort_key)
all_objs.append(self.flying_enemy)
if obj:
[Link]()
# Some sounds have multiple varieties. If count > 1, we'll randomly choose one from those
# We don't play any sounds if there is no player (e.g. if we're on the menu)
if [Link]:
try:
# This automatically loads and plays a file named '[Link]' (or .ogg) from the
sounds folder (if
# But what if you have files named '[Link]' to '[Link]' and want to
randomly choose
# one of them to play? You can generate a string such as 'explosion3', but to use such
a string
# to access an attribute of Pygame Zero's sounds object, we must use Python's built-
in function getattr
[Link]()
except Exception as e:
print(e)
space_down = False
# Has the space bar just been pressed? i.e. gone from not being pressed, to being pressed
def space_pressed():
global space_down
if [Link]:
if space_down:
return False
else:
space_down = True
return True
else:
space_down = False
return False
# Pygame Zero calls the update and draw functions each frame
class State(Enum):
MENU = 1
PLAY = 2
GAME_OVER = 3
def update():
if state == [Link]:
if space_pressed():
state = [Link]
game = Game(Player((240, 768))) # Create new Game object, with a Player object
[Link]()
state = State.GAME_OVER
else:
[Link]()
if space_pressed():
# Switch to menu state, and create a new game object without a player
state = [Link]
game = Game()
def draw():
# Draw the game, which covers both the game during gameplay but also the game displaying
in the background
[Link]()
if state == [Link]:
# Display logo
# Display score
score = str([Link])
# In Python, a negative index into a list (or in this case, into a string) gives you items in
reverse order,
digit = score[-i]
try:
[Link]()
[Link]("theme")
music.set_volume(0.4)
except:
pass
# Set the initial game state
state = [Link]
game = Game()
[Link]()