2019-12-13 08:44:53 +00:00

778 lines
32 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from random import choice, randint, random, shuffle
from enum import Enum
import pygame, pgzero, pgzrun, sys
# 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.
if sys.version_info < (3,5):
print("This game requires at least version 3.5 of Python. Please download it from www.python.org")
sys.exit()
# 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
# a component of the version to contain letters as well as numbers (e.g. '2.0.dev0')
# We're using a Python feature called list comprehension - this is explained in the Bubble Bobble/Cavern chapter.
pgzero_version = [int(s) if s.isnumeric() else s for s in pgzero.__version__.split('.')]
if pgzero_version < [1,2]:
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__))
sys.exit()
# Set up constants
WIDTH = 800
HEIGHT = 480
TITLE = "Cavern"
NUM_ROWS = 18
NUM_COLUMNS = 28
LEVEL_X_OFFSET = 50
GRID_BLOCK_SIZE = 25
ANCHOR_CENTRE = ("center", "center")
ANCHOR_CENTRE_BOTTOM = ("center", "bottom")
LEVELS = [ ["XXXXX XXXXXXXX XXXXX",
"","","","",
" XXXXXXX XXXXXXX ",
"","","",
" XXXXXXXXXXXXXXXXXXXXXX ",
"","","",
"XXXXXXXXX XXXXXXXXX",
"","",""],
["XXXX XXXXXXXXXXXX XXXX",
"","","","",
" XXXXXXXXXXXXXXXXXXXX ",
"","","",
"XXXXXX XXXXXX",
" X X ",
" X X ",
" X X ",
" X X ",
"","",""],
["XXXX XXXX XXXX XXXX",
"","","","",
" XXXXXXXX XXXXXXXX ",
"","","",
"XXXX XXXXXXXX XXXX",
"","","",
" XXXXXX XXXXXX ",
"","",""]]
def block(x,y):
# Is there a level grid block at these coordinates?
grid_x = (x - LEVEL_X_OFFSET) // GRID_BLOCK_SIZE
grid_y = y // GRID_BLOCK_SIZE
if grid_y > 0 and grid_y < NUM_ROWS:
row = game.grid[grid_y]
return grid_x >= 0 and grid_x < NUM_COLUMNS and len(row) > 0 and row[grid_x] != " "
else:
return False
def sign(x):
# Returns -1 or 1 depending on whether number is positive or negative
return -1 if x < 0 else 1
class CollideActor(Actor):
def __init__(self, pos, anchor=ANCHOR_CENTRE):
super().__init__("blank", pos, anchor)
def move(self, dx, dy, speed):
new_x, new_y = int(self.x), int(self.y)
# Movement is done 1 pixel at a time, which ensures we don't get embedded into a wall we're moving towards
for i in range(speed):
new_x, new_y = new_x + dx, new_y + dy
if new_x < 70 or new_x > 730:
# Collided with edge of level
return True
# Normally you don't need brackets surrounding the condition for an if statement (unlike many other
# languages), but in the case where the condition is split into multiple lines, using brackets removes
# the need to use the \ symbol at the end of each line.
# The code below checks to see if we're position we're trying to move into overlaps with a block. We only
# need to check the direction we're actually moving in. So first, we check to see if we're moving down
# (dy > 0). If that's the case, we then check to see if the proposed new y coordinate is a multiple of
# GRID_BLOCK_SIZE. If it is, that means we're directly on top of a place where a block might be. If that's
# also true, we then check to see if there is actually a block at the given position. If there's a block
# there, we return True and don't update the object to the new position.
# For movement to the right, it's the same except we check to ensure that the new x coordinate is a multiple
# of GRID_BLOCK_SIZE. For moving left, we check to see if the new x coordinate is the last (right-most)
# pixel of a grid block.
# Note that we don't check for collisions when the player is moving up.
if ((dy > 0 and new_y % GRID_BLOCK_SIZE == 0 or
dx > 0 and new_x % GRID_BLOCK_SIZE == 0 or
dx < 0 and new_x % GRID_BLOCK_SIZE == GRID_BLOCK_SIZE-1)
and block(new_x, new_y)):
return True
# We only update the object's position if there wasn't a block there.
self.pos = new_x, new_y
# Didn't collide with block or edge of level
return False
class Orb(CollideActor):
MAX_TIMER = 250
def __init__(self, pos, dir_x):
super().__init__(pos)
# Orbs are initially blown horizontally, then start floating upwards
self.direction_x = dir_x
self.floating = False
self.trapped_enemy_type = None # Number representing which type of enemy is trapped in this bubble
self.timer = -1
self.blown_frames = 6 # Number of frames during which we will be pushed horizontally
def hit_test(self, bolt):
# Check for collision with a bolt
collided = self.collidepoint(bolt.pos)
if collided:
self.timer = Orb.MAX_TIMER - 1
return collided
def update(self):
self.timer += 1
if self.floating:
# Float upwards
self.move(0, -1, randint(1, 2))
else:
# Move horizontally
if self.move(self.direction_x, 0, 4):
# If we hit a block, start floating
self.floating = True
if self.timer == self.blown_frames:
self.floating = True
elif self.timer >= Orb.MAX_TIMER or self.y <= -40:
# Pop if our lifetime has run out or if we have gone off the top of the screen
game.pops.append(Pop(self.pos, 1))
if self.trapped_enemy_type != None:
# trapped_enemy_type is either zero or one. A value of one means there's a chance of creating a
# powerup such as an extra life or extra health
game.fruits.append(Fruit(self.pos, self.trapped_enemy_type))
game.play_sound("pop", 4)
if self.timer < 9:
# Orb grows to full size over the course of 9 frames - the animation frame updating every 3 frames
self.image = "orb" + str(self.timer // 3)
else:
if self.trapped_enemy_type != None:
self.image = "trap" + str(self.trapped_enemy_type) + str((self.timer // 4) % 8)
else:
self.image = "orb" + str(3 + (((self.timer - 9) // 8) % 4))
class Bolt(CollideActor):
SPEED = 7
def __init__(self, pos, dir_x):
super().__init__(pos)
self.direction_x = dir_x
self.active = True
def update(self):
# Move horizontally and check to see if we've collided with a block
if self.move(self.direction_x, 0, Bolt.SPEED):
# Collided
self.active = False
else:
# We didn't collide with a block - check to see if we collided with an orb or the player
for obj in game.orbs + [game.player]:
if obj and obj.hit_test(self):
self.active = False
break
direction_idx = "1" if self.direction_x > 0 else "0"
anim_frame = str((game.timer // 4) % 2)
self.image = "bolt" + direction_idx + anim_frame
class Pop(Actor):
def __init__(self, pos, type):
super().__init__("blank", pos)
self.type = type
self.timer = -1
def update(self):
self.timer += 1
self.image = "pop" + str(self.type) + str(self.timer // 2)
class GravityActor(CollideActor):
MAX_FALL_SPEED = 10
def __init__(self, pos):
super().__init__(pos, ANCHOR_CENTRE_BOTTOM)
self.vel_y = 0
self.landed = False
def update(self, detect=True):
# Apply gravity, without going over the maximum fall speed
self.vel_y = min(self.vel_y + 1, GravityActor.MAX_FALL_SPEED)
# The detect parameter indicates whether we should check for collisions with blocks as we fall. Normally we
# want this to be the case - hence why this parameter is optional, and is True by default. If the player is
# in the process of losing a life, however, we want them to just fall out of the level, so False is passed
# in this case.
if detect:
# Move vertically in the appropriate direction, at the appropriate speed
if self.move(0, sign(self.vel_y), abs(self.vel_y)):
# If move returned True, we must have landed on a block.
# Note that move doesn't apply any collision detection when the player is moving up - only down
self.vel_y = 0
self.landed = True
if self.top >= HEIGHT:
# Fallen off bottom - reappear at top
self.y = 1
else:
# Collision detection disabled - just update the Y coordinate without any further checks
self.y += self.vel_y
# Class for pickups including fruit, extra health and extra life
class Fruit(GravityActor):
APPLE = 0
RASPBERRY = 1
LEMON = 2
EXTRA_HEALTH = 3
EXTRA_LIFE = 4
def __init__(self, pos, trapped_enemy_type=0):
super().__init__(pos)
# Choose which type of fruit we're going to be.
if trapped_enemy_type == Robot.TYPE_NORMAL:
self.type = choice([Fruit.APPLE, Fruit.RASPBERRY, Fruit.LEMON])
else:
# If trapped_enemy_type is 1, it means this fruit came from bursting an orb containing the more dangerous type
# of enemy. In this case there is a chance of getting an extra help or extra life power up
# We create a list containing the possible types of fruit, in proportions based on the probability we want
# each type of fruit to be chosen
types = 10 * [Fruit.APPLE, Fruit.RASPBERRY, Fruit.LEMON] # Each of these appear in the list 10 times
types += 9 * [Fruit.EXTRA_HEALTH] # This appears 9 times
types += [Fruit.EXTRA_LIFE] # This only appears once
self.type = choice(types) # Randomly choose one from the list
self.time_to_live = 500 # Counts down to zero
def update(self):
super().update()
# Does the player exist, and are they colliding with us?
if game.player and game.player.collidepoint(self.center):
if self.type == Fruit.EXTRA_HEALTH:
game.player.health = min(3, game.player.health + 1)
game.play_sound("bonus")
elif self.type == Fruit.EXTRA_LIFE:
game.player.lives += 1
game.play_sound("bonus")
else:
game.player.score += (self.type + 1) * 100
game.play_sound("score")
self.time_to_live = 0 # Disappear
else:
self.time_to_live -= 1
if self.time_to_live <= 0:
# Create 'pop' animation
game.pops.append(Pop((self.x, self.y - 27), 0))
anim_frame = str([0, 1, 2, 1][(game.timer // 6) % 4])
self.image = "fruit" + str(self.type) + anim_frame
class Player(GravityActor):
def __init__(self):
# Call constructor of parent class. Initial pos is 0,0 but reset is always called straight afterwards which
# will set the actual starting position.
super().__init__((0, 0))
self.lives = 2
self.score = 0
def reset(self):
self.pos = (WIDTH / 2, 100)
self.vel_y = 0
self.direction_x = 1 # -1 = left, 1 = right
self.fire_timer = 0
self.hurt_timer = 100 # Invulnerable for this many frames
self.health = 3
self.blowing_orb = None
def hit_test(self, other):
# Check for collision between player and bolt - called from Bolt.update. Also check hurt_timer - after being hurt,
# there is a period during which the player cannot be hurt again
if self.collidepoint(other.pos) and self.hurt_timer < 0:
# Player loses 1 health, is knocked in the direction the bolt had been moving, and can't be hurt again
# for a while
self.hurt_timer = 200
self.health -= 1
self.vel_y = -12
self.landed = False
self.direction_x = other.direction_x
if self.health > 0:
game.play_sound("ouch", 4)
else:
game.play_sound("die")
return True
else:
return False
def update(self):
# Call GravityActor.update - parameter is whether we want to perform collision detection as we fall. If health
# is zero, we want the player to just fall out of the level
super().update(self.health > 0)
self.fire_timer -= 1
self.hurt_timer -= 1
if self.landed:
# Hurt timer starts at 200, but drops to 100 once the player has landed
self.hurt_timer = min(self.hurt_timer, 100)
if self.hurt_timer > 100:
# We've just been hurt. Either carry out the sideways motion from being knocked by a bolt, or if health is
# zero, we're dropping out of the level, so check for our sprite reaching a certain Y coordinate before
# reducing our lives count and responding the player. We check for the Y coordinate being the screen height
# plus 50%, rather than simply the screen height, because the former effectively gives us a short delay
# before the player respawns.
if self.health > 0:
self.move(self.direction_x, 0, 4)
else:
if self.top >= HEIGHT*1.5:
self.lives -= 1
self.reset()
else:
# We're not hurt
# Get keyboard input. dx represents the direction the player is facing
dx = 0
if keyboard.left:
dx = -1
elif keyboard.right:
dx = 1
if dx != 0:
self.direction_x = dx
# If we haven't just fired an orb, carry out horizontal movement
if self.fire_timer < 10:
self.move(dx, 0, 4)
# Do we need to create a new orb? Space must have been pressed and released, the minimum time between
# orbs must have passed, and there is a limit of 5 orbs.
if space_pressed() and self.fire_timer <= 0 and len(game.orbs) < 5:
# x position will be 38 pixels in front of the player position, while ensuring it is within the
# bounds of the level
x = min(730, max(70, self.x + self.direction_x * 38))
y = self.y - 35
self.blowing_orb = Orb((x,y), self.direction_x)
game.orbs.append(self.blowing_orb)
game.play_sound("blow", 4)
self.fire_timer = 20
if keyboard.up and self.vel_y == 0 and self.landed:
# Jump
self.vel_y = -16
self.landed = False
game.play_sound("jump")
# Holding down space causes the current orb (if there is one) to be blown further
if keyboard.space:
if self.blowing_orb:
# Increase blown distance up to a maximum of 120
self.blowing_orb.blown_frames += 4
if self.blowing_orb.blown_frames >= 120:
# Can't be blown any further
self.blowing_orb = None
else:
# If we let go of space, we relinquish control over the current orb - it can't be blown any further
self.blowing_orb = None
# Set sprite image. If we're currently hurt, the sprite will flash on and off on alternate frames.
self.image = "blank"
if self.hurt_timer <= 0 or self.hurt_timer % 2 == 1:
dir_index = "1" if self.direction_x > 0 else "0"
if self.hurt_timer > 100:
if self.health > 0:
self.image = "recoil" + dir_index
else:
self.image = "fall" + str((game.timer // 4) % 2)
elif self.fire_timer > 0:
self.image = "blow" + dir_index
elif dx == 0:
self.image = "still"
else:
self.image = "run" + dir_index + str((game.timer // 8) % 4)
class Robot(GravityActor):
TYPE_NORMAL = 0
TYPE_AGGRESSIVE = 1
def __init__(self, pos, type):
super().__init__(pos)
self.type = type
self.speed = randint(1, 3)
self.direction_x = 1
self.alive = True
self.change_dir_timer = 0
self.fire_timer = 100
def update(self):
super().update()
self.change_dir_timer -= 1
self.fire_timer += 1
# Move in current direction - turn around if we hit a wall
if self.move(self.direction_x, 0, self.speed):
self.change_dir_timer = 0
if self.change_dir_timer <= 0:
# Randomly choose a direction to move in
# If there's a player, there's a two thirds chance that we'll move towards them
directions = [-1, 1]
if game.player:
directions.append(sign(game.player.x - self.x))
self.direction_x = choice(directions)
self.change_dir_timer = randint(100, 250)
# The more powerful type of robot can deliberately shoot at orbs - turning to face them if necessary
if self.type == Robot.TYPE_AGGRESSIVE and self.fire_timer >= 24:
# Go through all orbs to see if any can be shot at
for orb in game.orbs:
# The orb must be at our height, and within 200 pixels on the x axis
if orb.y >= self.top and orb.y < self.bottom and abs(orb.x - self.x) < 200:
self.direction_x = sign(orb.x - self.x)
self.fire_timer = 0
break
# Check to see if we can fire at player
if self.fire_timer >= 12:
# Random chance of firing each frame. Likelihood increases 10 times if player is at the same height as us
fire_probability = game.fire_probability()
if game.player and self.top < game.player.bottom and self.bottom > game.player.top:
fire_probability *= 10
if random() < fire_probability:
self.fire_timer = 0
game.play_sound("laser", 4)
elif self.fire_timer == 8:
# Once the fire timer has been set to 0, it will count up - frame 8 of the animation is when the actual bolt is fired
game.bolts.append(Bolt((self.x + self.direction_x * 20, self.y - 38), self.direction_x))
# Am I colliding with an orb? If so, become trapped by it
for orb in game.orbs:
if orb.trapped_enemy_type == None and self.collidepoint(orb.center):
self.alive = False
orb.floating = True
orb.trapped_enemy_type = self.type
game.play_sound("trap", 4)
break
# Choose and set sprite image
direction_idx = "1" if self.direction_x > 0 else "0"
image = "robot" + str(self.type) + direction_idx
if self.fire_timer < 12:
image += str(5 + (self.fire_timer // 4))
else:
image += str(1 + ((game.timer // 4) % 4))
self.image = image
class Game:
def __init__(self, player=None):
self.player = player
self.level_colour = -1
self.level = -1
self.next_level()
def fire_probability(self):
# Likelihood per frame of each robot firing a bolt - they fire more often on higher levels
return 0.001 + (0.0001 * min(100, self.level))
def max_enemies(self):
# Maximum number of enemies on-screen at once increases as you progress through the levels
return min((self.level + 6) // 2, 8)
def next_level(self):
self.level_colour = (self.level_colour + 1) % 4
self.level += 1
# Set up grid
self.grid = LEVELS[self.level % len(LEVELS)]
# The last row is a copy of the first row
# Note that we don't do 'self.grid.append(self.grid[0])'. That would alter the original data in the LEVELS list
# Instead, what this line does is create a brand new list, which is distinct from the list in LEVELS, and
# consists of the level data plus the first row of the level. It's also interesting to note that you can't
# do 'self.grid += [self.grid[0]]', because that's equivalent to using append.
# As an alternative, we could have copied the list on the line below '# Set up grid', by writing
# 'self.grid = list(LEVELS...', then used append or += on the line below.
self.grid = self.grid + [self.grid[0]]
self.timer = -1
if self.player:
self.player.reset()
self.fruits = []
self.bolts = []
self.enemies = []
self.pops = []
self.orbs = []
# At the start of each level we create a list of pending enemies - enemies to be created as the level plays out.
# When this list is empty, we have no more enemies left to create, and the level will end once we have destroyed
# all enemies currently on-screen. Each element of the list will be either 0 or 1, where 0 corresponds to
# a standard enemy, and 1 is a more powerful enemy.
# First we work out how many total enemies and how many of each type to create
num_enemies = 10 + self.level
num_strong_enemies = 1 + int(self.level / 1.5)
num_weak_enemies = num_enemies - num_strong_enemies
# Then we create the list of pending enemies, using Python's ability to create a list by multiplying a list
# by a number, and by adding two lists together. The resulting list will consist of a series of copies of
# the number 1 (the number depending on the value of num_strong_enemies), followed by a series of copies of
# the number zero, based on num_weak_enemies.
self.pending_enemies = num_strong_enemies * [Robot.TYPE_AGGRESSIVE] + num_weak_enemies * [Robot.TYPE_NORMAL]
# Finally we shuffle the list so that the order is randomised (using Python's random.shuffle function)
shuffle(self.pending_enemies)
self.play_sound("level", 1)
def get_robot_spawn_x(self):
# Find a spawn location for a robot, by checking the top row of the grid for empty spots
# Start by choosing a random grid column
r = randint(0, NUM_COLUMNS-1)
for i in range(NUM_COLUMNS):
# Keep looking at successive columns (wrapping round if we go off the right-hand side) until
# we find one where the top grid column is unoccupied
grid_x = (r+i) % NUM_COLUMNS
if self.grid[0][grid_x] == ' ':
return GRID_BLOCK_SIZE * grid_x + LEVEL_X_OFFSET + 12
# If we failed to find an opening in the top grid row (shouldn't ever happen), just spawn the enemy
# in the centre of the screen
return WIDTH/2
def update(self):
self.timer += 1
# Update all objects
for obj in self.fruits + self.bolts + self.enemies + self.pops + [self.player] + self.orbs:
if obj:
obj.update()
# Use list comprehensions to remove objects which are no longer wanted from the lists. For example, we recreate
# self.fruits such that it contains all existing fruits except those whose time_to_live counter has reached zero
self.fruits = [f for f in self.fruits if f.time_to_live > 0]
self.bolts = [b for b in self.bolts if b.active]
self.enemies = [e for e in self.enemies if e.alive]
self.pops = [p for p in self.pops if p.timer < 12]
self.orbs = [o for o in self.orbs if o.timer < 250 and o.y > -40]
# Every 100 frames, create a random fruit (unless there are no remaining enemies on this level)
if self.timer % 100 == 0 and len(self.pending_enemies + self.enemies) > 0:
# Create fruit at random position
self.fruits.append(Fruit((randint(70, 730), randint(75, 400))))
# Every 81 frames, if there is at least 1 pending enemy, and the number of active enemies is below the current
# level's maximum enemies, create a robot
if self.timer % 81 == 0 and len(self.pending_enemies) > 0 and len(self.enemies) < self.max_enemies():
# Retrieve and remove the last element from the pending enemies list
robot_type = self.pending_enemies.pop()
pos = (self.get_robot_spawn_x(), -30)
self.enemies.append(Robot(pos, robot_type))
# End level if there are no enemies remaining to be created, no existing enemies, no fruit, no popping orbs,
# and no orbs containing trapped enemies. (We don't want to include orbs which don't contain trapped enemies,
# as the level would never end if the player kept firing new orbs)
if len(self.pending_enemies + self.fruits + self.enemies + self.pops) == 0:
if len([orb for orb in self.orbs if orb.trapped_enemy_type != None]) == 0:
self.next_level()
def draw(self):
# Draw appropriate background for this level
screen.blit("bg%d" % self.level_colour, (0, 0))
block_sprite = "block" + str(self.level % 4)
# Display blocks
for row_y in range(NUM_ROWS):
row = self.grid[row_y]
if len(row) > 0:
# Initial offset - large blocks at edge of level are 50 pixels wide
x = LEVEL_X_OFFSET
for block in row:
if block != ' ':
screen.blit(block_sprite, (x, row_y * GRID_BLOCK_SIZE))
x += GRID_BLOCK_SIZE
# Draw all objects
all_objs = self.fruits + self.bolts + self.enemies + self.pops + self.orbs
all_objs.append(self.player)
for obj in all_objs:
if obj:
obj.draw()
def play_sound(self, name, count=1):
# 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 self.player:
try:
# Pygame Zero allows you to write things like 'sounds.explosion.play()'
# This automatically loads and plays a file named 'explosion.wav' (or .ogg) from the sounds folder (if
# such a file exists)
# But what if you have files named 'explosion0.ogg' to 'explosion5.ogg' 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
sound = getattr(sounds, name + str(randint(0, count - 1)))
sound.play()
except Exception as e:
# If no such sound file exists, print the name
print(e)
# Widths of the letters A to Z in the font images
CHAR_WIDTH = [27, 26, 25, 26, 25, 25, 26, 25, 12, 26, 26, 25, 33, 25, 26,
25, 27, 26, 26, 25, 26, 26, 38, 25, 25, 25]
def char_width(char):
# Return width of given character. For characters other than the letters A to Z (i.e. space, and the digits 0 to 9),
# the width of the letter A is returned. ord gives the ASCII/Unicode code for the given character.
index = max(0, ord(char) - 65)
return CHAR_WIDTH[index]
def draw_text(text, y, x=None):
if x == None:
# If no X pos specified, draw text in centre of the screen - must first work out total width of text
x = (WIDTH - sum([char_width(c) for c in text])) // 2
for char in text:
screen.blit("font0"+str(ord(char)), (x, y))
x += char_width(char)
IMAGE_WIDTH = {"life":44, "plus":40, "health":40}
def draw_status():
# Display score, right-justified at edge of screen
number_width = CHAR_WIDTH[0]
s = str(game.player.score)
draw_text(s, 451, WIDTH - 2 - (number_width * len(s)))
# Display level number
draw_text("LEVEL " + str(game.level + 1), 451)
# Display lives and health
# We only display a maximum of two lives - if there are more than two, a plus symbol is displayed
lives_health = ["life"] * min(2, game.player.lives)
if game.player.lives > 2:
lives_health.append("plus")
if game.player.lives >= 0:
lives_health += ["health"] * game.player.health
x = 0
for image in lives_health:
screen.blit(image, (x, 450))
x += IMAGE_WIDTH[image]
# Is the space bar currently being pressed down?
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 keyboard.space:
if space_down:
# Space was down previous frame, and is still down
return False
else:
# Space wasn't down previous frame, but now is
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():
global state, game
if state == State.MENU:
if space_pressed():
# Switch to play state, and create a new Game object, passing it a new Player object to use
state = State.PLAY
game = Game(Player())
else:
game.update()
elif state == State.PLAY:
if game.player.lives < 0:
game.play_sound("over")
state = State.GAME_OVER
else:
game.update()
elif state == State.GAME_OVER:
if space_pressed():
# Switch to menu state, and create a new game object without a player
state = State.MENU
game = Game()
def draw():
game.draw()
if state == State.MENU:
# Draw title screen
screen.blit("title", (0, 0))
# Draw "Press SPACE" animation, which has 10 frames numbered 0 to 9
# The first part gives us a number between 0 and 159, based on the game timer
# Dividing by 4 means we go to a new animation frame every 4 frames
# We enclose this calculation in the min function, with the other argument being 9, which results in the
# animation staying on frame 9 for three quarters of the time. Adding 40 to the game timer is done to alter
# which stage the animation is at when the game first starts
anim_frame = min(((game.timer + 40) % 160) // 4, 9)
screen.blit("space" + str(anim_frame), (130, 280))
elif state == State.PLAY:
draw_status()
elif state == State.GAME_OVER:
draw_status()
# Display "Game Over" image
screen.blit("over", (0, 0))
# The mixer allows us to play sounds and music
pygame.mixer.quit()
pygame.mixer.init(44100, -16, 2, 1024)
music.play("theme")
music.set_volume(0.3)
# Set the initial game state
state = State.MENU
# Create a new Game object, without a Player object
game = Game()
pgzrun.go()