View on GitHub

Making a Game of IT

Michigan State University - Summer 2019

Making a Game of IT - Day 3 - Pygame

Logistics and announcements

Pygame - Text, Buttons, and Menus

Text

We skipped over rendering text yesterday, so let’s revisit that…

Drawing text in pygame is a lot like drawing images.

'''
Rendering text

Code copied and adapted from https://inventwithpython.com/pygame/chapter2.html
'''

import sys
import pygame

pygame.init()

FPS = 60
clock = pygame.time.Clock()

screen_width = 400
screen_height = 300
display_bg_color = [255,255,255]

display_surface = pygame.display.set_mode([screen_width, screen_height])
pygame.display.set_caption('This window will have some text in it. Woo?')

# Font documentation: https://www.pygame.org/docs/ref/font.html

# use the pygame.font.get_fonts() function to get a list of available fonts on your
# system
print('Available fonts:', pygame.font.get_fonts())

# To render text with pygame, first we need to create a font
# - when we create a font, we tell it which font-style to use and what font size
#   we want.
# - CHALLENGE: try changing the font size and the font type (use get_fonts() to
#              figure out what is possible)
font = pygame.font.SysFont('impact', 32)

# Next, we can render the font onto a surface (which we'll eventually draw on the
# display_display surface)
# - To render text, we need to specify: (1) the string we want to write, (2) use
#   anti-aliasing? (smoothing technique), (3) what color we want the text to be?,
#   and (4) what background color we want? (if blank, no background color)
hello_color = [0,0,0]
hello_bg = [0,255,0]
hello_surface = font.render('Hello World!', True, hello_color, hello_bg)

# Where do we want to draw the text?
hello_x = screen_width / 2
hello_y = screen_height / 2

# CHALLENGE: add some more text to the screen, try rendering a few different fonts
#            at once
goodbye_color = [255,255,255]
goodbye_bg = [0,0,0]
goodbye_surface = font.render('Goodbye forever!', True, goodbye_color, goodbye_bg)
goodbye_x = 0
goodbye_y = 0

# CHALLENGE: just like the cat animation, can you make some text move around?
moving_text_color = [0,0,0]
moving_text_surface = font.render('MOVING', True, moving_text_color)
moving_text_bounding_rect = moving_text_surface.get_rect()
moving_text_bounding_rect.x = screen_width - moving_text_surface.get_rect().width
moving_text_bounding_rect.y = 0
moving_text_dir = 'southwest'

# rot_degrees = 1
while True:
    # fill the background
    display_surface.fill(display_bg_color)

    # blit the text onto the screen
    # - The top left corner of the text should be at hello_x, hello_y
    display_surface.blit(hello_surface, [hello_x, hello_y])

    # blit goodbye to the screen
    display_surface.blit(goodbye_surface, [goodbye_x, goodbye_y])

    # Animate the moving text
    if moving_text_dir == 'southwest':
        moving_text_bounding_rect.x -= 1 # Move text to the left by 1
        moving_text_bounding_rect.y += 1 # Move text down by 1
        if (moving_text_bounding_rect.bottomleft[1] >= screen_height) or (moving_text_bounding_rect.x <= 0):
            moving_text_dir = 'northeast'
    elif moving_text_dir == 'northeast':
        moving_text_bounding_rect.x += 1 # Move text to the right by 1
        moving_text_bounding_rect.y -= 1 # Move text up by 1
        if (moving_text_bounding_rect.topright[1] <= 0) or (moving_text_bounding_rect.x >= screen_width):
            moving_text_dir = 'southwest'

    # blit the moving text
    display_surface.blit(moving_text_surface, moving_text_bounding_rect)

    # Events!
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    # Update the display
    pygame.display.update()
    clock.tick(FPS)

|>> download text_finished.py

CHALLENGE: Make each of the three texts a different font type and font size.

Buttons

Buttons in pygame are very simple: they’re rectangles that we draw on the screen, and we detect button presses by detecting collisions between the mouse pointer and the button rectangle when the player clicks. We can also use an image as a button. You’ll need to download red-button.png into the folder containing your Python file for the code below to work.

'''
Buttons example
'''

import sys
import pygame

# Constants
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 800
FPS = 60

# colors
RED = [255, 0, 0]
BLUE = [0, 0, 255]
DARK_RED = [200, 10, 10]
BLACK = [0, 0, 0]
GREY = [230, 230, 230]

# initialize pygame
pygame.init()

screen = pygame.display.set_mode([SCREEN_WIDTH, SCREEN_HEIGHT]) # Setup the game screen surface
clock = pygame.time.Clock()                                     # make a clock to manage FPS

font = pygame.font.SysFont('impact', 32) # Here's the font we'll use to render text

# Rect button
rect_button = pygame.Rect([10, 10], [100, 30])
rect_button_color = RED
rect_clicks = 0 # click tracker

# Image button
button_img = pygame.image.load('media/red-button.png') # Make an image surface
button_img_rect = button_img.get_rect()          # Get a bounding+drawing rectangle for the surface
                                                 # (this is what we'll use to detect collisions w/mouse)
button_img_rect.x = 10    # Position the img rectangle
button_img_rect.y = 100
button_img_clicks = 0 # click tracker

# Our game loop
while True:
    screen.fill(GREY)

    # CHALLENGE: if mouse if hovering over the button, make the color darker/outline it
    # We'll need to know the mouse position

    ############################################################################
    # Draw the rectangle button
    pygame.draw.rect(screen, rect_button_color, rect_button)
    rect_clicks_surf = font.render(str(rect_clicks), True, BLACK)
    screen.blit(rect_clicks_surf, [rect_button.topright[0]+10, rect_button.topright[1]-5])
    ############################################################################

    ############################################################################
    # Draw the image button
    screen.blit(button_img, button_img_rect)
    img_button_clicks_surf = font.render(str(button_img_clicks), True, BLACK)
    screen.blit(img_button_clicks_surf, [button_img_rect.topright[0]+10, button_img_rect.topright[1]-5])
    ############################################################################

    # Event loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            # when the mouse is clicked, did the player press any buttons?
            mouse_pos = pygame.mouse.get_pos()
            if rect_button.collidepoint(mouse_pos):
                # rectangle button was clicked
                print("RECTANGLE BUTTON CLICK")
                rect_clicks += 1
            if button_img_rect.collidepoint(mouse_pos):
                print("IMG BUTTON CLICK")
                button_img_clicks += 1

    pygame.display.update()
    clock.tick(FPS)

|>> download buttons.py

CHALLENGE: Add another button that resets the click counts for the other two buttons.

Menus are just several buttons strung together. We can make a menu screen by having multiple game modes. In the example below, we have two game modes:

  1. a title screen mode and
  2. a menu screen mode. In the menu screen, the player uses the arrow keys to navigate (we could have used the mouse here instead), and presses enter to make a selection.
'''
Menu example
'''

import sys
import random
import pygame

# Constants
FPS = 60
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 800
BTN_PADDING = 10    # How much padding are we going to put around a button?
BTN_MARGIN = 10     # How much space do we want around button text?

# Colors
WHITE = [255, 255, 255]
GREY = [175, 175, 175]
BLACK = [0, 0, 5]
YELLOW = [255, 229, 153]
DARKER_YELLOW = [255, 217, 102]

pygame.init() # As always, initialize pygame

screen = pygame.display.set_mode([SCREEN_WIDTH, SCREEN_HEIGHT])
clock = pygame.time.Clock()
menu_font = pygame.font.SysFont('impact', 32)   # Here's our button font
print("Loaded the font!")
screen_mode = 'title'   # Modes: title, menu

menu_btn_color = YELLOW
menu_btn_hover_color = DARKER_YELLOW

################################################################################
# Title screen components
################################################################################
title_screen_bg_color = BLACK
# === Title text ===
title_font = pygame.font.SysFont('impact', 128)
title_surface = title_font.render('THE MENU GAME', True, WHITE)
title_rect = title_surface.get_rect()
title_rect.x = (SCREEN_WIDTH / 2) - (title_rect.width / 2) # Put rect in middle of screen (perfectly in middle x)
title_rect.y = (SCREEN_HEIGHT / 2) - (title_rect.height)   # Put rect in middle of the screen (sitting on top of horizontal midline)

# === Open menu button ===
menu_btn_txt_surface = menu_font.render('open menu', True, BLACK)

# setup open menu button background
menu_btn_bg_rect = menu_btn_txt_surface.get_rect()
menu_btn_bg_rect.width += 2 * BTN_MARGIN  # Add some margins to the button
menu_btn_bg_rect.height += 2 * BTN_MARGIN # Add margin to the button
menu_btn_bg_rect.x = title_rect.midbottom[0] - (menu_btn_bg_rect.width / 2)
menu_btn_bg_rect.y = title_rect.midbottom[1] + BTN_PADDING

# setup text rectangle (used to determine where we'll draw text)
menu_btn_txt_rect = menu_btn_txt_surface.get_rect()
menu_btn_txt_rect.x = title_rect.midbottom[0] - (menu_btn_txt_rect.width / 2)
menu_btn_txt_rect.y = title_rect.midbottom[1] + BTN_PADDING + BTN_MARGIN

################################################################################
# Menu screen components
################################################################################
menu_screen_bg_color = GREY

menu_screen_buttons = ['resume', 'random', 'quit'] # Available buttons
cur_menu_btn_id = 0                                # What button are we currently on?
btn_color = YELLOW

# === Resume button ===
# Render resume btn text onto surface
resume_btn_txt_surface = menu_font.render('Resume', True, BLACK)
# Setup resume button background
resume_btn_bg_rect = resume_btn_txt_surface.get_rect()
resume_btn_bg_rect.width += 2 * BTN_MARGIN
resume_btn_bg_rect.height += 2 * BTN_MARGIN
resume_btn_bg_rect.x = (SCREEN_WIDTH / 2) - (0.5 * resume_btn_bg_rect.width)
resume_btn_bg_rect.y = 50
# Setup the resume button text
resume_btn_txt_rect = resume_btn_txt_surface.get_rect()
resume_btn_txt_rect.x = resume_btn_bg_rect.x + BTN_MARGIN
resume_btn_txt_rect.y = resume_btn_bg_rect.y + BTN_MARGIN

# === Random button ===
# Render random btn text onto surface
random_btn_txt_surface = menu_font.render('???', True, BLACK)
# Setup random button background
random_btn_bg_rect = random_btn_txt_surface.get_rect()
random_btn_bg_rect.width += 2 * BTN_MARGIN
random_btn_bg_rect.height += 2 * BTN_MARGIN
random_btn_bg_rect.x = (SCREEN_WIDTH / 2) - (0.5 * random_btn_bg_rect.width)
random_btn_bg_rect.y = resume_btn_bg_rect.y + (resume_btn_bg_rect.height + BTN_PADDING)
# Setup the random button text
random_btn_txt_rect = random_btn_txt_surface.get_rect()
random_btn_txt_rect.x = random_btn_bg_rect.x + BTN_MARGIN
random_btn_txt_rect.y = random_btn_bg_rect.y + BTN_MARGIN

# === Quit button ===
# Render quit btn text onto surface
quit_btn_txt_surface = menu_font.render('Quit', True, BLACK)
# Setup quit button background
quit_btn_bg_rect = quit_btn_txt_surface.get_rect()
quit_btn_bg_rect.width += 2 * BTN_MARGIN
quit_btn_bg_rect.height += 2 * BTN_MARGIN
quit_btn_bg_rect.x = (SCREEN_WIDTH / 2) - (0.5 * quit_btn_bg_rect.width)
quit_btn_bg_rect.y = random_btn_bg_rect.y + (resume_btn_bg_rect.height + BTN_PADDING)
# Setup the quit button text
quit_btn_txt_rect = quit_btn_txt_surface.get_rect()
quit_btn_txt_rect.x = quit_btn_bg_rect.x + BTN_MARGIN
quit_btn_txt_rect.y = quit_btn_bg_rect.y + BTN_MARGIN
################################################################################

# Game loop
while True:
    # We need to show different things depending on whether or not we're in 'title'
    # or 'menu' mode
    if screen_mode == 'title':
        # ==== TITLE SCREEN MODE ====
        screen.fill(title_screen_bg_color)
        # Draw the title screen
        # Render the title text in the middle of the screen
        screen.blit(title_surface, title_rect)
        # Draw the menu button, first: the text
        pygame.draw.rect(screen, menu_btn_color, menu_btn_bg_rect)
        screen.blit(menu_btn_txt_surface, menu_btn_txt_rect)

    elif screen_mode == 'menu':
        # === MENU SCREEN MODE ===
        screen.fill(menu_screen_bg_color)
        # Draw button rectangles!
        # - If button is active, color background with hover color and an outline
        # - otherwise, draw with normal color and no outline
        if menu_screen_buttons[cur_menu_btn_id] == 'resume':
            pygame.draw.rect(screen, menu_btn_hover_color, resume_btn_bg_rect)
            pygame.draw.rect(screen, BLACK, resume_btn_bg_rect, 5)
        else:
            pygame.draw.rect(screen, menu_btn_color, resume_btn_bg_rect)

        if menu_screen_buttons[cur_menu_btn_id] == 'random':
            pygame.draw.rect(screen, menu_btn_hover_color, random_btn_bg_rect)
            pygame.draw.rect(screen, BLACK, random_btn_bg_rect, 5)
        else:
            pygame.draw.rect(screen, menu_btn_color, random_btn_bg_rect)

        if menu_screen_buttons[cur_menu_btn_id] == 'quit':
            pygame.draw.rect(screen, menu_btn_hover_color, quit_btn_bg_rect)
            pygame.draw.rect(screen, BLACK, quit_btn_bg_rect, 5)
        else:
            pygame.draw.rect(screen, menu_btn_color, quit_btn_bg_rect)

        # Layer button text over button backgrounds
        screen.blit(resume_btn_txt_surface, resume_btn_txt_rect)
        screen.blit(random_btn_txt_surface, random_btn_txt_rect)
        screen.blit(quit_btn_txt_surface, quit_btn_txt_rect)

    else:
        # ==== ???? MODE ====
        print("AAAH UNRECOGNIZED SCREEN MODE! Exiting")
        pygame.quit()
        sys.exit()

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

        # ===== TITLE MODE EVENTS =====
        if screen_mode == 'title' and event.type == pygame.MOUSEBUTTONDOWN:
            mouse_pos = pygame.mouse.get_pos()
            if menu_btn_bg_rect.collidepoint(mouse_pos):
                screen_mode = 'menu'
                cur_menu_btn_id = 0

        # ===== MENU MODE EVENTS =====
        if screen_mode == 'menu' and event.type == pygame.KEYDOWN:
            if event.key == pygame.K_DOWN:
                # player presses down arrow
                cur_menu_btn_id = (cur_menu_btn_id + 1) % len(menu_screen_buttons)
            if event.key == pygame.K_UP:
                # player presses up arrow
                cur_menu_btn_id = (cur_menu_btn_id - 1) % len(menu_screen_buttons)
            if event.key == pygame.K_RETURN:
                # Player presses return (selects current option)
                if menu_screen_buttons[cur_menu_btn_id] == 'resume':
                    # if on resume button, go back to title screen
                    screen_mode = 'title'
                elif menu_screen_buttons[cur_menu_btn_id] == 'random':
                    # if on random ('???') button, randomize background color
                    menu_screen_bg_color = [random.randint(0, 255),
                                            random.randint(0, 255),
                                            random.randint(0, 255)]
                elif menu_screen_buttons[cur_menu_btn_id] == 'quit':
                    # if on quit button, quit the game
                    pygame.quit()
                    sys.exit()
    clock.tick(FPS)
    pygame.display.update()

|>> download menu.py

CHALLENGE: Add another option to the menu screen that changes the title screen’s background

LUNCH SPECIAL TASK

Brainstorm the type of game you’re interested in making.

For example…

Pygame - Crank It Up to 11

In order for the following code to work, you’ll need to download beep1.ogg, Jeopardy.ogg, and Tetris.ogg into the folder containing your Python file.

Sound Effects

First, let’s talk about sound effects: recordings you want to play in response to an event on screen. In the following example, we ring a bell in response to key presses.

import sys
import pygame

# Initialize and set up screen.
pygame.init()
screen = pygame.display.set_mode([100, 100])

clock = pygame.time.Clock()

beep_effect = pygame.mixer.Sound('beep1.ogg')

# main game loop
while True:

  # event-handling loop
  for event in pygame.event.get():
    if event.type == pygame.QUIT:
      pygame.quit()
      sys.exit()
    elif event.type == pygame.KEYDOWN:
      print("bing!")
      beep_effect.play()

  clock.tick(60)

If you want to add your own sound effects, note that pygame has limited support for different formats: only WAV and OGG files work.

Background Music

Next, let’s cover background music — something you’d want to be playing for the entire game or throughout a level of the game. Unlike the sound effects, this sound is streamed instead of loaded all at once — that means that pygame can handle big and long sound files efficiently here. However, you can only have a single piece of background music playing at once.

If we want to just have one piece of background music playing the whole time, it would look like this.

import sys
import pygame

# Initialize and set up screen.
pygame.init()
screen = pygame.display.set_mode([100, 100])

clock = pygame.time.Clock()


pygame.mixer.music.load('Jeopardy.ogg')
pygame.mixer.music.play(-1) # how many loops to play? -1 means infinity

# main game loop
while True:

  # event-handling loop
  for event in pygame.event.get():
    if event.type == pygame.QUIT:
      pygame.quit()
      sys.exit()

  clock.tick(60)

If we want to switch between two tracks (maybe on different levels, in different game modes, or between the title and game play screens) we use itertools.cycle to change out the currently playing track for an alternate on a keypress.

import sys
import pygame
import itertools

# Initialize and set up screen.
pygame.init()
screen = pygame.display.set_mode([100, 100])

clock = pygame.time.Clock()

filenames = itertools.cycle(['Tetris.ogg', 'Jeopardy.ogg'])

# main game loop
while True:

  # event-handling loop
  for event in pygame.event.get():
    if event.type == pygame.QUIT:
      pygame.quit()
      sys.exit()
    elif event.type == pygame.KEYDOWN:
      print("switch tracks")
      pygame.mixer.music.stop()
      pygame.mixer.music.load(next(filenames))
      pygame.mixer.music.play(-1)

  clock.tick(60)

If you want to add your own background music, note that PyGame has limited support for different formats: only MP3 (some), OGG, XM, and MOD files work.

Pygame - High-level structure

Let’s take a crack at piecing together a complete pygame with a

  1. start screen,
  2. game play mode with scorekeeping and a lose condition, and
  3. an end screen (with the option for replay).
'''
Block catchy game with all the bells and whistles(?), maybe
'''

import sys
import random
import pygame

# CONSTANTS
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 800
FPS = 60
BTN_MARGIN = 5

CATCHER_WIDTH = 100
CATCHER_HEIGHT = 25
CATCHER_SPEED = 10
CATCHER_START_HEALTH = 2

OBJ_SPAWN_CHANCE = 0.2
CHANCE_SPAWN_IS_GOOD = 0.25
OBJ_SPEED = 5

BAD_OBJECT_WIDTH = 10
BAD_OBJECT_HEIGHT = 10
BAD_OBJECT_DMG = 1

GOOD_OBJECT_WIDTH = 10
GOOD_OBJECT_HEIGHT = 10
BAD_OBJECT_WIDTH = 10
BAD_OBJECT_HEIGHT = 10

RED = [255, 0, 0]
LIGHT_GREY = [230, 230, 230]
BLACK = [0, 0, 0]
BLUE = [0,0,230]
WHITE = [255, 255, 255]
YELLOW = [255, 229, 153]

# Initial setup
pygame.init()
screen = pygame.display.set_mode([SCREEN_WIDTH, SCREEN_HEIGHT])
pygame.display.set_caption("EXTREME BLOCK CATCHING")
clock = pygame.time.Clock()

game_screens = ['title', 'play', 'end']
cur_game_screen = 'title'

title_bg_color = BLACK
play_bg_color = LIGHT_GREY
end_bg_color = BLACK
button_color = YELLOW

title_font = pygame.font.SysFont('impact', 64)
button_font = pygame.font.SysFont('impact', 18)
game_info_font = pygame.font.SysFont('impact', 24)
################################################################################
# Title screen components
################################################################################
title_screen_bg_color = BLACK
# Title text
title_surface = title_font.render('2 FAST 2 CATCH BLOCKS', True, WHITE)
title_rect = title_surface.get_rect()
title_rect.x = (SCREEN_WIDTH / 2) - (title_rect.width / 2) # Put rect in middle of screen (perfectly in middle x)
title_rect.y = (SCREEN_HEIGHT / 2) - (title_rect.height)   # Put rect in middle of the screen (sitting on top of horizontal midline)

# Play button
play_btn_txt_surface = button_font.render('>>>> CATCH BLOCKS <<<<', True, BLACK)
# setup play button background
play_btn_bg_rect = play_btn_txt_surface.get_rect()
play_btn_bg_rect.width += 2 * BTN_MARGIN  # Add some margins to the button
play_btn_bg_rect.height += 2 * BTN_MARGIN # Add margin to the button
play_btn_bg_rect.x = title_rect.midbottom[0] - (play_btn_bg_rect.width / 2)
play_btn_bg_rect.y = title_rect.midbottom[1]
# setup play button text
play_btn_txt_rect = play_btn_txt_surface.get_rect()
play_btn_txt_rect.x = play_btn_bg_rect.x + BTN_MARGIN
play_btn_txt_rect.y = play_btn_bg_rect.y + BTN_MARGIN

################################################################################
# Play screen components
################################################################################

catcher_start_x = (SCREEN_WIDTH / 2) - (CATCHER_WIDTH / 2)
catcher_start_y = SCREEN_HEIGHT - CATCHER_HEIGHT - 1
catcher = pygame.Rect([catcher_start_x, catcher_start_y], [CATCHER_WIDTH, CATCHER_HEIGHT])
falling_objects = []

# bomb_img = pygame.image.load('bomb-small.png') # Challenge: use bomb img instead of red squares

health = CATCHER_START_HEALTH
score = 0

################################################################################
# End screen components
################################################################################

# game over
game_over_txt = title_font.render('GAME OVER', True, BLACK)
game_over_rect = game_over_txt.get_rect()
game_over_rect.x = (SCREEN_WIDTH / 2) - (game_over_rect.width / 2) # Put rect in middle of screen (perfectly in middle x)
game_over_rect.y = (SCREEN_HEIGHT / 2) - (game_over_rect.height)   # Put rect in middle of the screen (sitting on top of horizontal midline)

# Replay button
replay_btn_txt = game_info_font.render('PLAY AGAIN?', True, BLACK)
# setup play button background
replay_btn_bg_rect = replay_btn_txt.get_rect()
replay_btn_bg_rect.width += 2 * BTN_MARGIN  # Add some margins to the button
replay_btn_bg_rect.height += 2 * BTN_MARGIN # Add margin to the button
replay_btn_bg_rect.x = game_over_rect.midbottom[0] - (replay_btn_bg_rect.width / 2)
replay_btn_bg_rect.y = game_over_rect.midbottom[1]
# setup play button text
replay_btn_txt_rect = replay_btn_txt.get_rect()
replay_btn_txt_rect.x = replay_btn_bg_rect.x + BTN_MARGIN
replay_btn_txt_rect.y = replay_btn_bg_rect.y + BTN_MARGIN

while True:

    if cur_game_screen == 'title':
        screen.fill(title_bg_color)
        # Draw the title
        screen.blit(title_surface, title_rect)
        # Draw the play button
        pygame.draw.rect(screen, button_color, play_btn_bg_rect)
        screen.blit(play_btn_txt_surface, play_btn_txt_rect)

    elif cur_game_screen == 'play':
        screen.fill(play_bg_color)

        # Use arrow keys to control the catcher
        pressed_keys = pygame.key.get_pressed()
        if pressed_keys[pygame.K_RIGHT] and not pressed_keys[pygame.K_LEFT]:
            catcher.x = (catcher.x + CATCHER_SPEED) % SCREEN_WIDTH
        elif pressed_keys[pygame.K_LEFT] and not pressed_keys[pygame.K_RIGHT]:
            catcher.x = (catcher.x - CATCHER_SPEED) % SCREEN_WIDTH

        # Draw falling rocks
        spawn_new_falling_object = len(falling_objects) == 0 or (random.random() < OBJ_SPAWN_CHANCE)
        if spawn_new_falling_object:
            # Good or bad object?
            obj_good = random.random() < CHANCE_SPAWN_IS_GOOD
            if obj_good:
                # obj is friendly
                new_obj_rect = pygame.Rect([random.randint(GOOD_OBJECT_WIDTH, SCREEN_WIDTH - 2*GOOD_OBJECT_WIDTH), 0], [GOOD_OBJECT_WIDTH, GOOD_OBJECT_HEIGHT])
                falling_objects.append({"rect": new_obj_rect, "good": True})
            else:
                # obj is EVIL
                new_obj_rect = pygame.Rect([random.randint(BAD_OBJECT_WIDTH, SCREEN_WIDTH - 2*BAD_OBJECT_WIDTH), 0], [BAD_OBJECT_WIDTH, BAD_OBJECT_HEIGHT])
                falling_objects.append({"rect": new_obj_rect, "good": False})

        # Draw objects
        on_screen_objs = []
        for obj in falling_objects:
            obj["rect"].y += OBJ_SPEED
            color = BLUE if obj["good"] else RED

            pygame.draw.rect(screen, color, obj["rect"])

            if catcher.colliderect(obj["rect"]):
                # Collision!
                if obj["good"]:
                    score += 1
                else:
                    health -= BAD_OBJECT_DMG
                print("Health =", health, "Score =", score)
            elif obj["rect"].y < SCREEN_HEIGHT:
                on_screen_objs.append(obj)

        falling_objects = on_screen_objs


        # Draw the catcher rectangle
        pygame.draw.rect(screen, BLACK, catcher)

        # Draw game info
        health_surface = game_info_font.render('HEALTH: ' + str(health), True, BLACK)
        health_rect = health_surface.get_rect()
        health_rect.x = SCREEN_WIDTH - health_rect.width - 50
        health_rect.y = 50
        screen.blit(health_surface, health_rect)

        score_surface = game_info_font.render('SCORE: ' + str(score), True, BLACK)
        score_rect = score_surface.get_rect()
        score_rect.x = SCREEN_WIDTH - score_rect.width - 50
        score_rect.y = health_rect.bottom + 50
        screen.blit(score_surface, score_rect)

        # End game state
        if health == 0:
            cur_game_screen = 'end'

    elif cur_game_screen == 'end':
        # screen.fill(end_bg_color)
        screen.blit(game_over_txt, game_over_rect)

        pygame.draw.rect(screen, button_color, replay_btn_bg_rect)
        screen.blit(replay_btn_txt, replay_btn_txt_rect)


    # setup the event loop
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        if cur_game_screen == 'title':
            # Title screen events
            if event.type == pygame.MOUSEBUTTONDOWN:
                mouse_pos = pygame.mouse.get_pos()
                if play_btn_bg_rect.collidepoint(mouse_pos):
                    cur_game_screen = 'play'
        if cur_game_screen == 'end':
            # End game screen events
            if event.type == pygame.MOUSEBUTTONDOWN:
                mouse_pos = pygame.mouse.get_pos()
                if replay_btn_bg_rect.collidepoint(mouse_pos):
                    cur_game_screen = 'title'
                    score = 0
                    health = CATCHER_START_HEALTH
                    falling_objects = []
                    catcher.x = catcher_start_x
                    catcher.y = catcher_start_y

    pygame.display.update()
    clock.tick(FPS)

|>> download block_catchy.py