Balloon Ninja: Adding game logic

Back to Tutorial Contents

Faster Balloons

In the last section we added a scoreboard to Balloon Ninja. Let’s make the game a little more interesting by making the balloons speed up a little every time a balloon is popped. If you recall, the balloon’s speed is controlled by a setting in Balloon.py:

import pygame
from pygame.sprite import Sprite
from random import randint

class Balloon(Sprite):

    def __init__(self, screen):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()
        self.speed = 0.1

        self.x_position = randint(self.image_w/2, self.screen.get_width()-self.image_w/2)
        self.y_position = self.screen.get_height() + self.image_h/2

        self.update_rect()

    def update(self, time_passed):
        self.y_position -= self.speed * time_passed
        self.update_rect()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

    def update_rect(self):
        self.rect = pygame.Rect(self.x_position-self.image_w/2, self.y_position-self.image_h/2,
                                self.image_w, self.image_h)

We need to be able to change this value as the game progresses. To do this, we need to move control of this setting to balloon_ninja.py, and let the Balloon’s init function set the speed for the current balloon. Modify Balloon.py, so that new balloons take their speed from whichever file creates them:

import pygame
from pygame.sprite import Sprite
from random import randint

class Balloon(Sprite):

    def __init__(self, screen, balloon_speed):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()
        self.speed = balloon_speed

        self.x_position = randint(self.image_w/2, self.screen.get_width()-self.image_w/2)
        self.y_position = self.screen.get_height() + self.image_h/2

        self.update_rect()

    def update(self, time_passed):
        self.y_position -= self.speed * time_passed
        self.update_rect()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

    def update_rect(self):
        self.rect = pygame.Rect(self.x_position-self.image_w/2, self.y_position-self.image_h/2,
                                self.image_w, self.image_h)

Now we need to modify balloon_ninja.py to define an overall balloon speed, and send this information to Balloon.py anytime a balloon is created:

import pygame, sys
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 200, 200, 200
    scoreboard_height = 50

    # game play parameters
    balloon_speed = 0.1
    points_per_balloon = 10

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, scoreboard_height)

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen, balloon_speed)]

    # Create our dagger
    sword = Sword(screen, scoreboard_height)

    # main event loop
    while True:
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]

        # Check for game events
        check_events(sword, mouse_x, mouse_y)

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update the sword's position, and draw the sword on the screen
        sword.x_position = mouse_x
        if sword.grabbed:
            sword.y_position = mouse_y
        else:
            sword.y_position = sword.image_h/2 + scoreboard_height
        sword.update_rect()
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)

            if balloon.rect.colliderect(sword.rect):
                scoreboard.balloons_popped += 1
                scoreboard.score += points_per_balloon
                balloons.remove(balloon)
                balloons.append(Balloon(screen, balloon_speed))
                continue

            if balloon.y_position < -balloon.image_h/2 + scoreboard_height:
                scoreboard.balloons_missed += 1
                balloons.remove(balloon)
                balloons.append(Balloon(screen, balloon_speed))
                continue

            balloon.blitme()

        # Display scoreboard
        scoreboard.blitme()

        pygame.display.flip()

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

These changes don’t make the game do anything different yet. You might want to run the game once at this point to verify that the game behaves as it has previously. We can add one line now, that will speed the game up as it progresses. This line goes in balloon_ninja.py:

import pygame, sys
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # screen and game parameters
    screen_width, screen_height = 800, 600
    bg_color = 200, 200, 200
    scoreboard_height = 50

    # game play parameters
    balloon_speed = 0.1
    points_per_balloon = 10

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (screen_width, screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, scoreboard_height)

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen, balloon_speed)]

    # Create our dagger
    sword = Sword(screen, scoreboard_height)

    # main event loop
    while True:
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]

        # Check for game events
        check_events(sword, mouse_x, mouse_y)

        # redraw the screen, every pass through the event loop
        screen.fill(bg_color)

        # Update the sword's position, and draw the sword on the screen
        sword.x_position = mouse_x
        if sword.grabbed:
            sword.y_position = mouse_y
        else:
            sword.y_position = sword.image_h/2 + scoreboard_height
        sword.update_rect()
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)

            if balloon.rect.colliderect(sword.rect):
                scoreboard.balloons_popped += 1
                scoreboard.score += points_per_balloon
                balloons.remove(balloon)
                # Increase game speed every time a balloon is popped:
                balloon_speed *= 1.05
                balloons.append(Balloon(screen, balloon_speed))
                continue

            if balloon.y_position < -balloon.image_h/2 + scoreboard_height:
                scoreboard.balloons_missed += 1
                balloons.remove(balloon)
                balloons.append(Balloon(screen, balloon_speed))
                continue

            balloon.blitme()

        # Display scoreboard
        scoreboard.blitme()

        pygame.display.flip()

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

When a balloon is popped, we increase the overall balloon speed slightly before creating the next balloon. I did a little experimentation before settling on a speed increase factor of 1.05. This makes a noticeable speedup after about 20 balloons have been popped. If you are experimenting, try a larger number like 1.1 or 1.5. If you use a factor of 2, the balloons will fly past your sword in no time.

Code Cleanup

There are a number of other changes we’d like to make, but we really need to clean up our code before we add some new features. Let’s start by creating a Settings class, which will store some of our overall game settings. This will remove some clutter from our main file. It will also allow us to move some of our game logic to helper functions, without having long lists of arguments to pass back and forth. Here is our new Settings.py file:

class Settings():

    def __init__(self):
        # screen parameters
        self.screen_width, self.screen_height = 800, 600
        self.bg_color = 200, 200, 200
        self.scoreboard_height = 50

        self.initialize_game_parameters()

    def initialize_game_parameters(self):
        # game play parameters
        self.balloon_speed = 0.1
        self.points_per_balloon = 10

There is nothing new here. These are the same parameters we have been using. Some parameters, such as balloon_speed, can change over the course of a game. These parameters are initialized in a separate function, so that we can easily reset these parameters when we implement a “play again” feature. Let’s modify balloon_ninja.py to use these settings:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)

    # Create a list to hold our balloons, and include our first balloon in the list
    balloons = [Balloon(screen, settings.balloon_speed)]

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]

        # Check for game events
        check_events(sword, mouse_x, mouse_y)

        # redraw the screen, every pass through the event loop
        screen.fill(settings.bg_color)

        # Update the sword's position, and draw the sword on the screen
        sword.x_position = mouse_x
        if sword.grabbed:
            sword.y_position = mouse_y
        else:
            sword.y_position = sword.image_h/2 + settings.scoreboard_height
        sword.update_rect()
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)

            if balloon.rect.colliderect(sword.rect):
                scoreboard.balloons_popped += 1
                scoreboard.score += settings.points_per_balloon
                balloons.remove(balloon)
                # Increase game speed every time a balloon is popped:
                settings.balloon_speed *= 1.05
                balloons.append(Balloon(screen, settings.balloon_speed))
                continue

            if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
                scoreboard.balloons_missed += 1
                balloons.remove(balloon)
                balloons.append(Balloon(screen, settings.balloon_speed))
                continue

            balloon.blitme()

        # Display scoreboard
        scoreboard.blitme()

        pygame.display.flip()

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

On line 2 we import our Settings class, and on line 9 we create a settings object which we will use to access our game parameters. The rest of the changes just tell balloon_ninja to look in the settings object to access parameters as needed.

Now let’s take care of some repetitive code. We create a balloon at three different places in our file, so let’s move the creation of a balloon to its own function:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]

        # Check for game events
        check_events(sword, mouse_x, mouse_y)

        # redraw the screen, every pass through the event loop
        screen.fill(settings.bg_color)

        # Update the sword's position, and draw the sword on the screen
        sword.x_position = mouse_x
        if sword.grabbed:
            sword.y_position = mouse_y
        else:
            sword.y_position = sword.image_h/2 + settings.scoreboard_height
        sword.update_rect()
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)

            if balloon.rect.colliderect(sword.rect):
                scoreboard.balloons_popped += 1
                scoreboard.score += settings.points_per_balloon
                balloons.remove(balloon)
                # Increase game speed every time a balloon is popped:
                settings.balloon_speed *= 1.05
                spawn_balloon(screen, settings, balloons)
                continue

            if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
                scoreboard.balloons_missed += 1
                balloons.remove(balloon)
                spawn_balloon(screen, settings, balloons)
                continue

            balloon.blitme()

        # Display scoreboard
        scoreboard.blitme()

        pygame.display.flip()

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings.balloon_speed))

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

Lines 49-53 all respond to a balloon being popped, and lines 58-59 handle a balloon disappearing off the top of the screen. Let’s move this work to separate functions as well:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]

        # Check for game events
        check_events(sword, mouse_x, mouse_y)

        # redraw the screen, every pass through the event loop
        screen.fill(settings.bg_color)

        # Update the sword's position, and draw the sword on the screen
        sword.x_position = mouse_x
        if sword.grabbed:
            sword.y_position = mouse_y
        else:
            sword.y_position = sword.image_h/2 + settings.scoreboard_height
        sword.update_rect()
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)

            if balloon.rect.colliderect(sword.rect):
                pop_balloon(scoreboard, settings, balloon, balloons)
                spawn_balloon(screen, settings, balloons)
                continue

            if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
                miss_balloon(scoreboard, balloon, balloons)
                spawn_balloon(screen, settings, balloons)
                continue

            balloon.blitme()

        # Display scoreboard
        scoreboard.blitme()

        pygame.display.flip()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # Increase game speed every time a balloon is popped:
    settings.balloon_speed *= 1.05

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings.balloon_speed))

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

We can clean up the main event loop a bit more. Let’s consolidate a bit, and clarify some of our comments:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(sword, mouse_x, mouse_y)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        # Update the sword's position, and draw the sword on the screen
        sword.x_position = mouse_x
        if sword.grabbed:
            sword.y_position = mouse_y
        else:
            sword.y_position = sword.image_h/2 + settings.scoreboard_height
        sword.update_rect()
        sword.blitme()

        # Update our balloons, and draw them on the screen
        for balloon in balloons:
            balloon.update(time_passed)

            if balloon.rect.colliderect(sword.rect):
                pop_balloon(scoreboard, settings, balloon, balloons)
                spawn_balloon(screen, settings, balloons)
                continue

            if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
                miss_balloon(scoreboard, balloon, balloons)
                spawn_balloon(screen, settings, balloons)
                continue

            balloon.blitme()

        # Display updated scoreboard, and show the redrawn screen
        scoreboard.blitme()
        pygame.display.flip()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # Increase game speed every time a balloon is popped:
    settings.balloon_speed *= 1.05

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings.balloon_speed))

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

Lines 26-29 are all about getting the new state of the game each time we pass through the event loop. The comment on line 31 clarifies why we are redrawing the screen before dealing with any changes to our game objects. We must draw a blank screen first, then process changes to individual game elements, and finally flip to our new screen.

For our last cleaning effort, let’s move the sword and balloon processing to their own functions. Lines 35-41 are all about the sword, and lines 44-57 are all about the balloons. Moving all of this work to separate functions really cleans up our main event loop:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(sword, mouse_x, mouse_y)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        # Update the sword's position and check for popped or disappeared balloons
        update_sword(sword, mouse_x, mouse_y, settings)
        check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

        # Display updated scoreboard
        scoreboard.blitme()
        # Show the redrawn screen
        pygame.display.flip()

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        balloon.blitme()

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # Increase game speed every time a balloon is popped:
    settings.balloon_speed *= 1.05

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings.balloon_speed))

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

We could go on refactoring our code, but that is enough cleaning to let us focus on developing game play for a bit. Let’s start making the game more challenging.

Multiple Balloons

We have made the game a bit more challenging by speeding things up a bit every time a balloon is popped. Let’s add a different kind of challenge. We will start to release more than one balloon at a time as the game progresses.

We probably want to let about 10 balloons go by before starting to increase the number of balloons in a batch, but we don’t want to have to pop 10 balloons every time we test our game. So let’s make a couple settings to manage this behavior. We will keep our parameters low during testing, and set them higher for the actual game.

Implementing this feature is actually somewhat complicated. Let’s try a simple approach, and see why it gets complicated. Let’s add a game parameter called batch_size, which specifies how many balloons to release at once. The next parameter we need is harder to name: it is the number of balloons that must be popped before we increase the batch size. Let’s call this pops_needed. Here is our modified Settings.py file:

class Settings():

    def __init__(self):
        # screen parameters
        self.screen_width, self.screen_height = 800, 600
        self.bg_color = 200, 200, 200
        self.scoreboard_height = 50

        self.initialize_game_parameters()

    def initialize_game_parameters(self):
        # game play parameters
        self.balloon_speed = 0.1
        self.points_per_balloon = 10

        # Number of balloons to release in a spawning:
        self.batch_size = 1
        # Number of balloons that need to be popped before increasing batch_size
        #  For actual play, probably want ~10; for testing, ~3
        self.pops_needed = 3

For variables that are difficult to name well such as this one, it is especially important to write a good comment like we see in lines 18-19. Now let’s update balloon_ninja.py to use batch_size:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(sword, mouse_x, mouse_y)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        # Update the sword's position and check for popped or disappeared balloons
        update_sword(sword, mouse_x, mouse_y, settings)
        check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

        # Display updated scoreboard
        scoreboard.blitme()
        # Show the redrawn screen
        pygame.display.flip()

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            # Release a number of balloons, corresponding to current batch size:
            for x in range(0, settings.batch_size):
                spawn_balloon(screen, settings, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        balloon.blitme()

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # Increase game speed every time a balloon is popped:
    settings.balloon_speed *= 1.05
    # If we have popped enough balloons, increase batch_size:
    if scoreboard.balloons_popped % settings.pops_needed == 0:
        settings.batch_size += 1

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings.balloon_speed))

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

When we play the game now, we can see how this doesn’t quite work:

That's way too many balloons!

That’s way too many balloons!

There are way too many balloons showing up! This happens because once we start having multiple balloons appear, every single popped balloon spawns multiple new balloons. We pretty quickly get into an exponential increase in the number of balloons. To fix this, we need to figure out exactly when to spawn a new batch of balloons.

Let’s think it through. The player pops a single balloon. Then a new balloon appears, and this keeps happening until they pop enough balloons (pops_needed) to start seeing multiple balloons appear. The first time this happens, batch_size is increased to two. So now two balloons appear. Currently, we will release two more balloons every time a balloon is popped. What we want to do is release a new batch once this current batch has disappeared, either through popping or disappearing off the top of the screen. A simple way to do this is to release a new set of balloons as soon as our list of balloons is empty. This approach leads to a few changes in balloon_ninja.py:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(sword, mouse_x, mouse_y)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        # Update the sword's position and check for popped or disappeared balloons
        update_sword(sword, mouse_x, mouse_y, settings)
        check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

        # If all balloons have disappeared, either through popping or rising,
        #  release a new batch of balloons.
        if len(balloons) == 0:
            release_batch(screen, settings, balloons)

        # Display updated scoreboard
        scoreboard.blitme()
        # Show the redrawn screen
        pygame.display.flip()

def release_batch(screen, settings, balloons):
    for x in range(0, settings.batch_size):
        spawn_balloon(screen, settings, balloons)

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        balloon.blitme()

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # Increase game speed every time a balloon is popped:
    settings.balloon_speed *= 1.05
    # If we have popped enough balloons, increase batch_size:
    if scoreboard.balloons_popped % settings.pops_needed == 0:
        settings.batch_size += 1

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings.balloon_speed))

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

In lines 38-41 of our main event loop, we check to see if all of the balloons have either been popped or disappeared off the top of the screen. If they have, we call a function that will release a new set of balloons. This function is defined in lines 48-50. This is the same code that was in check_balloons earlier. Lines 58-60 are highlighted to show that we have removed the code that releases a batch of balloons from check_balloons.

These changes take care of the exponential growth problem. If we play the game at this point, it is significantly improved. However, the code that releases a new batch of balloons is a little too simple. Take a look at what happens when the batch size grows a bit:

All balloons in a batch are released at exactly the same level, which is pretty unappealing.

All balloons in a batch are released at exactly the same level, which is pretty unappealing.

When we release a batch of balloons, all of the balloons are released at exactly the same level. We can address this pretty easily, by randomly varying the y-position of each balloon as it is spawned. This is an example of why it is nice to use classes. Our game logic works, so we won’t touch balloon_ninja at this point. We are simply changing the behavior of a Balloon object, so we got back to our Balloon.py file. We can see that the y-position of a new balloon is defined in the __init__ function. So we add a random value to the initial y-position of each balloon. We need to add, so that the offset moves the balloon lower initially. If we subtracted an offset, some balloons would appear above the bottom of the screen, rather than rising up from below the screen. Here is our new Balloon.py file:

import pygame
from pygame.sprite import Sprite
from random import randint

class Balloon(Sprite):

    def __init__(self, screen, balloon_speed):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()
        self.speed = balloon_speed

        self.x_position = randint(self.image_w/2, self.screen.get_width()-self.image_w/2)
        self.y_position = self.screen.get_height() + self.image_h/2 + randint(0,100)

        self.update_rect()

    def update(self, time_passed):
        self.y_position -= self.speed * time_passed
        self.update_rect()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

    def update_rect(self):
        self.rect = pygame.Rect(self.x_position-self.image_w/2, self.y_position-self.image_h/2,
                                self.image_w, self.image_h)

A bit of experimentation showed that a random offset between 0 and 100 seems to work reasonably well:

A random offset in the vertical position of each balloon makes things look a little more interesting.

A random offset in the vertical position of each balloon makes things look a little more interesting.

This is better, but it is actually quite easy to make things a lot more interesting. If we allow the random vertical offset to be as big as the screen height, we get much more interesting game play. We get totally away from a line of balloons appearing across a band of the screen, and we start to use the entire playing area:

import pygame
from pygame.sprite import Sprite
from random import randint

class Balloon(Sprite):

    def __init__(self, screen, balloon_speed):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()
        self.speed = balloon_speed

        self.x_position = randint(self.image_w/2, self.screen.get_width()-self.image_w/2)
        self.y_position = self.screen.get_height() + self.image_h/2 + randint(0, self.screen.get_height())

        self.update_rect()

    def update(self, time_passed):
        self.y_position -= self.speed * time_passed
        self.update_rect()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

    def update_rect(self):
        self.rect = pygame.Rect(self.x_position-self.image_w/2, self.y_position-self.image_h/2,
                                self.image_w, self.image_h)

Here is what we see, once batch size has increased a little bit:

If the offset is increased to match the screen height, we start to use the entire playing area.

If the offset is increased to match the screen height, we start to use the entire playing area.

This is much more interesting, because we start to use the whole screen. But there is a slight problem with the code as it is. The beginning of the game is confusing now, because a balloon that starts out with a large offset takes a few seconds to appear on the screen. We can address this by using a smaller maximum offset when the batch size is small, and expanding the maximum offset as the batch size grows. Here is what that code looks like in Balloon.py:

import pygame
from pygame.sprite import Sprite
from random import randint

class Balloon(Sprite):

    def __init__(self, screen, settings):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()
        self.speed = settings.balloon_speed

        self.x_position = randint(self.image_w/2, self.screen.get_width()-self.image_w/2)
        max_offset = min(settings.batch_size*self.image_h, self.screen.get_height())
        print max_offset
        self.y_position = self.screen.get_height() + self.image_h/2 + randint(0, max_offset)

        self.update_rect()

    def update(self, time_passed):
        self.y_position -= self.speed * time_passed
        self.update_rect()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

    def update_rect(self):
        self.rect = pygame.Rect(self.x_position-self.image_w/2, self.y_position-self.image_h/2,
                                self.image_w, self.image_h)

The Balloon class now need access to the settings object, so it can know what the current batch size is. On line 7, we change the parameter of Balloon to accept the settings object, rather than just the balloon’s speed. We modify line 13 to get the speed from the settings object. On line 16, we set the maximum offset to be whatever is smaller: twice a balloon’s height, or the size of the screen.

We also need to change balloon_ninja.py, to send the settings object to Balloon every time a new balloon is created. Changes such as this remind us why we cleaned up our code earlier. We create new balloons on one line in the main file, so we only have to modify that single line when we change the definition of a Balloon:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(sword, mouse_x, mouse_y)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        # Update the sword's position and check for popped or disappeared balloons
        update_sword(sword, mouse_x, mouse_y, settings)
        check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

        # If all balloons have disappeared, either through popping or rising,
        #  release a new batch of balloons.
        if len(balloons) == 0:
            release_batch(screen, settings, balloons)

        # Display updated scoreboard
        scoreboard.blitme()
        # Show the redrawn screen
        pygame.display.flip()

def release_batch(screen, settings, balloons):
    for x in range(0, settings.batch_size):
        spawn_balloon(screen, settings, balloons)

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        balloon.blitme()

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # Increase game speed every time a balloon is popped:
    settings.balloon_speed *= 1.05
    # If we have popped enough balloons, increase batch_size:
    if scoreboard.balloons_popped % settings.pops_needed == 0:
        settings.batch_size += 1

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings))

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

There is no new screenshot to show, because the game doesn’t look any different. But if you play it now, you will see that new balloons always appear in a reasonable amount of time. The game is much more interesting at this point than it used to be. By the way, if you are playing along, you might have noticed that the speed of the balloons increases really quickly. The game gets really difficult really quickly. This is due to the increase in balloon speed defined on line 88 in balloon_ninja. I have been using a speed increase factor of 1.05 to facilitate quicker testing during development. If you want to make better actual gameplay at this point, just change this number to something more like 1.01. We should probably move this value to the Settings file:

class Settings():

    def __init__(self):
        # screen parameters
        self.screen_width, self.screen_height = 800, 600
        self.bg_color = 200, 200, 200
        self.scoreboard_height = 50

        self.initialize_game_parameters()

    def initialize_game_parameters(self):
        # game play parameters
        self.balloon_speed = 0.1
        # How quickly the speed of balloons rises
        #  ~1.05 during testing and ~1.01 for actual play
        self.speed_increase_factor = 1.05
        self.points_per_balloon = 10

        # Number of balloons to release in a spawning:
        self.batch_size = 1
        # Number of balloons that need to be popped before increasing batch_size
        #  For actual play, probably want ~10; for testing, ~3
        self.pops_needed = 3

Now balloon_ninja.py needs a one-line change to use this setting:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(sword, mouse_x, mouse_y)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        # Update the sword's position and check for popped or disappeared balloons
        update_sword(sword, mouse_x, mouse_y, settings)
        check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

        # If all balloons have disappeared, either through popping or rising,
        #  release a new batch of balloons.
        if len(balloons) == 0:
            release_batch(screen, settings, balloons)

        # Display updated scoreboard
        scoreboard.blitme()
        # Show the redrawn screen
        pygame.display.flip()

def release_batch(screen, settings, balloons):
    for x in range(0, settings.batch_size):
        spawn_balloon(screen, settings, balloons)

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        balloon.blitme()

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # Increase game speed every time a balloon is popped:
    settings.balloon_speed *= settings.speed_increase_factor
    # If we have popped enough balloons, increase batch_size:
    if scoreboard.balloons_popped % settings.pops_needed == 0:
        settings.batch_size += 1

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings))

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

Let’s do one more cleanup in balloon_ninja. Right now we are increasing the speed of the game every time a balloon is popped. This makes for rapidly changing game play within a batch of balloons. It is actually much better if we keep the speed constant during each batch. This gives the player the chance to “clear the screen” before seeing a speed increase. To do this, we will move the code for increasing the speed from pop_balloon to the section where we release a new batch of balloons. We will increase the speed just before we release a new batch of balloons:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard

def run_game():
    # Get access to our game settings
    settings = Settings()

    # initialize game
    pygame.init()
    screen = pygame.display.set_mode( (settings.screen_width, settings.screen_height), 0, 32)
    clock = pygame.time.Clock()
    scoreboard = Scoreboard(screen, settings.scoreboard_height)

    # Create a list to hold our balloons, and create our first balloon
    balloons = []
    spawn_balloon(screen, settings, balloons)

    # Create our dagger
    sword = Sword(screen, settings.scoreboard_height)

    # main event loop
    while True:
        # Advance our game clock, get the current mouse position, and check for new events
        time_passed = clock.tick(50)
        mouse_x, mouse_y = pygame.mouse.get_pos()[0], pygame.mouse.get_pos()[1]
        check_events(sword, mouse_x, mouse_y)

        # Redraw the empty screen before redrawing any game objects
        screen.fill(settings.bg_color)

        # Update the sword's position and check for popped or disappeared balloons
        update_sword(sword, mouse_x, mouse_y, settings)
        check_balloons(balloons, sword, scoreboard, screen, settings, time_passed)

        # If all balloons have disappeared, either through popping or rising,
        #  release a new batch of balloons.
        #  Increase the balloon speed for each new batch of balloons.
        if len(balloons) == 0:
            settings.balloon_speed *= settings.speed_increase_factor
            release_batch(screen, settings, balloons)

        # Display updated scoreboard
        scoreboard.blitme()
        # Show the redrawn screen
        pygame.display.flip()

def release_batch(screen, settings, balloons):
    for x in range(0, settings.batch_size):
        spawn_balloon(screen, settings, balloons)

def check_balloons(balloons, sword, scoreboard, screen, settings, time_passed):
    # Find any balloons that have been popped,
    #  or have disappeared off the top of the screen
    for balloon in balloons:
        balloon.update(time_passed)

        if balloon.rect.colliderect(sword.rect):
            pop_balloon(scoreboard, settings, balloon, balloons)
            continue

        if balloon.y_position < -balloon.image_h/2 + settings.scoreboard_height:
            miss_balloon(scoreboard, balloon, balloons)
            spawn_balloon(screen, settings, balloons)
            continue

        balloon.blitme()

def update_sword(sword, mouse_x, mouse_y, settings):
    # Update the sword's position, and draw the sword on the screen
    sword.x_position = mouse_x
    if sword.grabbed:
        sword.y_position = mouse_y
    else:
        sword.y_position = sword.image_h/2 + settings.scoreboard_height
    sword.update_rect()
    sword.blitme()

def miss_balloon(scoreboard, balloon, balloons):
    scoreboard.balloons_missed += 1
    balloons.remove(balloon)

def pop_balloon(scoreboard, settings, balloon, balloons):
    scoreboard.balloons_popped += 1
    scoreboard.score += settings.points_per_balloon
    balloons.remove(balloon)
    # If we have popped enough balloons, increase batch_size:
    if scoreboard.balloons_popped % settings.pops_needed == 0:
        settings.batch_size += 1

def spawn_balloon(screen, settings, balloons):
    balloons.append(Balloon(screen, settings))

def check_events(sword, mouse_x, mouse_y):
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.MOUSEBUTTONDOWN:
            if sword.rect.collidepoint(mouse_x, mouse_y):
                sword.grabbed = True
        if event.type == pygame.MOUSEBUTTONUP:
            sword.grabbed = False

run_game()

Again, there is no screenshot to show because the game doesn’t look any different. But if you play the game at this point, there is a more consistent feel to the game. It also makes the speed increase factor of 1.05 more appropriate.

We can make a slight change to balloon behavior right now that will make the game much more dynamic.  Right now every balloon in a batch has the same speed.  So once a batch of balloons is created, that entire group rises up the screen at the same rate.  If we include a random speed scale factor when each balloon is created, all of the balloons will rise at a slightly different rate.  We only need to change Balloon.py:

import pygame
from pygame.sprite import Sprite
from random import randint, uniform

class Balloon(Sprite):

    def __init__(self, screen, settings):

        Sprite.__init__(self)
        self.screen = screen
        self.image = pygame.image.load('green_balloon_50px.png').convert_alpha()
        self.image_w, self.image_h = self.image.get_size()
        self.speed = uniform(0.75,1.25)*settings.balloon_speed

        self.x_position = randint(self.image_w/2, self.screen.get_width()-self.image_w/2)
        max_offset = min(settings.batch_size*self.image_h, self.screen.get_height())
        self.y_position = self.screen.get_height() + self.image_h/2 + randint(0, max_offset)

        self.update_rect()

    def update(self, time_passed):
        self.y_position -= self.speed * time_passed
        self.update_rect()

    def blitme(self):
        draw_pos = self.image.get_rect().move(self.x_position-self.image_w/2, self.y_position-self.image_h/2)
        self.screen.blit(self.image, draw_pos)

    def update_rect(self):
        self.rect = pygame.Rect(self.x_position-self.image_w/2, self.y_position-self.image_h/2,
                                self.image_w, self.image_h)

On line 3 we import the random.uniform function, which returns a random float between two numbers. On line 13 we use a random number between 0.75 and 1.25 to generate the speed of each balloon. When you play the game now, batches of balloons have much more interesting behavior, and you find yourself scrambling to pop the fastest balloons first.

We have a few things left to do before we finish this tutorial. The next thing we will do is add a start screen so that the game does not start as soon as we open it. We will also add some logic to make the game end when we miss too many balloons.

Next: Start Screen and Game Over

Back to Tutorial Contents

Advertisements

About ehmatthes

Teacher, hacker, new dad, outdoor guy
This entry was posted in programming and tagged , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s