Balloon Ninja: Kittens!

Back to Tutorial Contents

Kittens!

In the last section we improved the scoring system, which made the game a little more interesting. But so far the game strategy for playing Balloon Ninja is pretty simple. You just grab the sword, and swipe away at balloons. The only challenging aspects of the game are the steadily increasing number of balloons, and the steadily increasing speed of the balloons. Let’s add a new challenge: kittens! Once implemented, the player will have to swipe as many balloons as possible while trying not to kill any kittens.

Our ultimate goal is to release about one kitten for every ten balloons. For now, let’s just try to make a kitten appear with every balloon. Once that is working, we will refine the logic for how often a kitten should be released. First, we need a Kitten.py file, which is similar to our Balloon.py file. A kitten is really just a balloon with a different image, so the Kitten.py file is very simple:

import pygame
from Balloon import Balloon

class Kitten(Balloon):

    def __init__(self, screen, settings):

        Balloon.__init__(self, screen, settings)
        self.image = pygame.image.load('kitten_50px.png').convert_alpha()

We need to import Balloon because the Kitten class inherits from the Balloon class, and we need to import pygame because we use the pygame.image.load function in the Kitten class. On line 4, we tell Python that the Kitten class inherits all of Balloon’s attributes and behavior. On line 6, we need to receive all of the information needed to create a balloon. On line 8, we give our kitten object all of the functionality of a balloon. Finally, on line 9, we override the balloon image with our kitten image.

I found an image of a kitten that was licensed for reuse, and modified it by cropping, scaling, and making the background transparent. Here is the new image, which you should be able to right-click and save:

A nice kitten, with a transparent background.

A nice kitten, with a transparent background.

Now, we need to add some functions to Engine.py that will let us spawn a kitten:

import pygame, sys
from Balloon import Balloon
from Button import Button
from Kitten import Kitten

class Engine():

    def __init__(self):
        pass

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

    def check_balloons(self, balloons, kittens, 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):
                self.pop_balloon(scoreboard, settings, balloon, balloons)
                continue

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

            balloon.blitme()

        if scoreboard.balloons_popped > 0 or scoreboard.balloons_missed > 0:
            scoreboard.popped_ratio = float(scoreboard.balloons_popped)/(scoreboard.balloons_popped + scoreboard.balloons_missed)
            if scoreboard.popped_ratio < settings.min_popped_ratio:
                # Set game_active to false, empty the list of balloons, and increment games_played
                settings.game_active = False
                settings.games_played += 1

    def check_kittens(self, kittens, sword, scoreboard, screen, settings, time_passed):
        for kitten in kittens:
            kitten.update(time_passed)
            kitten.blitme()

    def update_sword(self, 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(self, scoreboard, balloon, balloons):
        scoreboard.balloons_missed += 1
        balloons.remove(balloon)

    def pop_balloon(self, scoreboard, settings, balloon, balloons):
        scoreboard.balloons_popped += 1
        scoreboard.score += settings.points_per_balloon
        balloons.remove(balloon)

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

    def spawn_kitten(self, screen, settings, kittens):
        kittens.append(Kitten(screen, settings))

    def check_events(self, settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons):
        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 play_button.rect.collidepoint(mouse_x, mouse_y):
                    # Play button has been pressed.  Empty list of balloons,
                    #  initialize scoreboard and game parameters, and make game active.
                    del balloons[:]
                    scoreboard.initialize_stats()
                    settings.initialize_game_parameters()
                    settings.game_active = True
            if event.type == pygame.MOUSEBUTTONUP:
                sword.grabbed = False
    

There are a lot of little changes in here, so I will explain them in the order we implemented them. The first change is to import our new class Kitten, on line 4. Then we want to make a simple function to release a kitten, which is almost identical to the spawn_balloon function. This is a two-line function called spawn_kitten, on lines 67 and 68. This function uses a list called “kittens” that we will create in balloon_ninja, which is just like the list we use to hold our balloons.

Where do we call this function from? We are going to want to call it from spawn_balloon, so for now we will just add a simple line in spawn_balloon. This is line 65. We need access to the kittens list, so line 63 modifies spawn_balloon to receive the list of kittens. Working backwards, this means lines 13 and 11, and lines 27 and 15 also need access to the kittens list. (These argument lists are getting a bit messy, so we will clean them up shortly.)

Finally, we need a function similar to check_balloons for our kittens. So lines 39-42 define check_kittens, which simply updates the position of each kitten before blitting it to the screen.

To see our kittens in action, we need to make a few changes to balloon_ninja.py, mostly driven by our changes to Engine.py:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard
from Button import Button
from Engine import Engine
from Instructions import Instructions

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

    # 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)
    play_button = Button(screen, settings.screen_width/2-settings.button_width/2,
                            settings.screen_height/2-settings.button_height/2, settings, "Play Balloon Ninja")
    game_over_button = Button(screen, play_button.x_position, play_button.y_position-2*settings.button_height,
                            settings, "Game Over")
    instructions = Instructions(screen, settings)

    # Create a list to hold our balloons, and our kittens
    balloons = []
    kittens = []

    # 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]
        engine.check_events(settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons)

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

        if settings.game_active:
            # Update the sword's position and check for popped or disappeared balloons
            engine.update_sword(sword, mouse_x, mouse_y, settings)
            engine.check_balloons(balloons, kittens, sword, scoreboard, screen, settings, time_passed)
            engine.check_kittens(kittens, 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:
                # If we are not just starting a game, increase the balloon speed and points per balloon,
                #  and increment batches_finished
                if scoreboard.balloons_popped > 0:
                    #  Increase the balloon speed for each new batch of balloons.
                    settings.balloon_speed *= settings.speed_increase_factor
                    settings.points_per_balloon = int(round(settings.points_per_balloon * settings.speed_increase_factor))
                    scoreboard.batches_finished += 1
                # If player has completed required batches, increase batch_size
                if scoreboard.batches_finished % settings.batches_needed == 0 and scoreboard.batches_finished > 0:
                    settings.batch_size += 1
                engine.release_batch(screen, settings, balloons, kittens)
        else:
            # Game is not active, so...
            #  Show play button
            play_button.blitme()
            #  Show instructions for first few games.
            if settings.games_played < 3:
                instructions.blitme()
            #  If a game has just ended, show Game Over button
            if settings.games_played > 0:
                game_over_button.blitme()

        # Display updated scoreboard
        scoreboard.blitme()

        # Show the redrawn screen
        pygame.display.flip()

run_game()

We create an empty list of kittens on line 28. After this, there are only three lines that need to send the list of kittens to Engine.py. These are the calls to check_balloons on line 46, to check_kittens on line 47, and to release_batch on line 62.

Now if we play the game we see a whole bunch of kittens, which don’t affect game play at all yet:

One kitten is released for every balloon, but they don't affect game play at this point.

One kitten is released for every balloon, but they don’t affect game play at this point.

Taming the Kittens

Right now we are releasing one kitten for every balloon that is released. We need to make this a smaller ratio. We will do this by creating a setting called kitten_ratio, and using that setting to control how often a kitten is released. Let’s modify Settings.py next:

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.button_width, self.button_height = 250, 50
        self.button_bg = (0,163,0)
        self.button_text_color = (235,235,235)
        self.button_font, self.button_font_size = 'Arial', 24

        # game status
        self.game_active = False

        # game over conditions
        self.min_popped_ratio = 0.9
        self.games_played = 0

        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 batches that must be completed to increment batch_size
        self.batches_needed = 3

        # Ratio of kittens released per balloon released:
        self.kitten_ratio = 0.10

This setting will result in a 10% probability that a kitten is released every time a balloon is released. That is, one kitten will be released for every 10 balloons, but the player will not be able to predict when a kitten is about to be released. With this setting, we can also increase the ratio throughout the game to make it progressively more challenging.

Now let’s modify Engine.py, to use this ratio:

import pygame, sys
from Balloon import Balloon
from Button import Button
from Kitten import Kitten
from random import random

class Engine():

    def __init__(self):
        pass

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

    def check_balloons(self, balloons, kittens, 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):
                self.pop_balloon(scoreboard, settings, balloon, balloons)
                continue

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

            balloon.blitme()

        if scoreboard.balloons_popped > 0 or scoreboard.balloons_missed > 0:
            scoreboard.popped_ratio = float(scoreboard.balloons_popped)/(scoreboard.balloons_popped + scoreboard.balloons_missed)
            if scoreboard.popped_ratio < settings.min_popped_ratio:
                # Set game_active to false, empty the list of balloons, and increment games_played
                settings.game_active = False
                settings.games_played += 1

    def check_kittens(self, kittens, sword, scoreboard, screen, settings, time_passed):
        for kitten in kittens:
            kitten.update(time_passed)
            kitten.blitme()

    def update_sword(self, 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(self, scoreboard, balloon, balloons):
        scoreboard.balloons_missed += 1
        balloons.remove(balloon)

    def pop_balloon(self, scoreboard, settings, balloon, balloons):
        scoreboard.balloons_popped += 1
        scoreboard.score += settings.points_per_balloon
        balloons.remove(balloon)

    def spawn_balloon(self, screen, settings, balloons, kittens):
        balloons.append(Balloon(screen, settings))
        # Periodically release a kitten:
        if random() < settings.kitten_ratio:
            self.spawn_kitten(screen, settings, kittens)

    def spawn_kitten(self, screen, settings, kittens):
        kittens.append(Kitten(screen, settings))

    def check_events(self, settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons):
        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 play_button.rect.collidepoint(mouse_x, mouse_y):
                    # Play button has been pressed.  Empty list of balloons,
                    #  initialize scoreboard and game parameters, and make game active.
                    del balloons[:]
                    scoreboard.initialize_stats()
                    settings.initialize_game_parameters()
                    settings.game_active = True
            if event.type == pygame.MOUSEBUTTONUP:
                sword.grabbed = False
    

On line 6 we need to import the random function. On line 67 we generate a random number between 0.0 and 1.0, and if that number is less than our kitten_ratio we release a balloon.

Now let’s scale this ratio every time batch_size is increased, to release more kittens as the game goes on. We do this in balloon_ninja.py:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard
from Button import Button
from Engine import Engine
from Instructions import Instructions

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

    # 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)
    play_button = Button(screen, settings.screen_width/2-settings.button_width/2,
                            settings.screen_height/2-settings.button_height/2, settings, "Play Balloon Ninja")
    game_over_button = Button(screen, play_button.x_position, play_button.y_position-2*settings.button_height,
                            settings, "Game Over")
    instructions = Instructions(screen, settings)

    # Create a list to hold our balloons, and our kittens
    balloons = []
    kittens = []

    # 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]
        engine.check_events(settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons)

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

        if settings.game_active:
            # Update the sword's position and check for popped or disappeared balloons
            engine.update_sword(sword, mouse_x, mouse_y, settings)
            engine.check_balloons(balloons, kittens, sword, scoreboard, screen, settings, time_passed)
            engine.check_kittens(kittens, 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:
                # If we are not just starting a game, increase the balloon speed and points per balloon,
                #  and increment batches_finished
                if scoreboard.balloons_popped > 0:
                    #  Increase the balloon speed, and other factors, for each new batch of balloons.
                    settings.balloon_speed *= settings.speed_increase_factor
                    settings.kitten_ratio *= settings.speed_increase_factor
                    settings.points_per_balloon = int(round(settings.points_per_balloon * settings.speed_increase_factor))
                    scoreboard.batches_finished += 1
                # If player has completed required batches, increase batch_size
                if scoreboard.batches_finished % settings.batches_needed == 0 and scoreboard.batches_finished > 0:
                    settings.batch_size += 1
                engine.release_batch(screen, settings, balloons, kittens)
        else:
            # Game is not active, so...
            #  Show play button
            play_button.blitme()
            #  Show instructions for first few games.
            if settings.games_played < 3:
                instructions.blitme()
            #  If a game has just ended, show Game Over button
            if settings.games_played > 0:
                game_over_button.blitme()

        # Display updated scoreboard
        scoreboard.blitme()

        # Show the redrawn screen
        pygame.display.flip()

run_game()

This is really a one-line change. On line 57, just after we scale the balloon speed, we scale the kitten_ratio by the same amount. It doesn’t come through in a screenshot, but when you play through the game now, you can clearly see a steady increase in the number of kittens being released.

Tracking kittens that survive

The kittens are really just decoration at this point. Let’s detect when they are hit and when they make it to the top of the screen. When they make it to the top of the screen unhurt we will award points, but when they are hit we will deduct points. Kittens are more valuable than balloons, though, so let’s consider every kitten to be worth three balloons. We will start by making another setting:

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.button_width, self.button_height = 250, 50
        self.button_bg = (0,163,0)
        self.button_text_color = (235,235,235)
        self.button_font, self.button_font_size = 'Arial', 24

        # game status
        self.game_active = False

        # game over conditions
        self.min_popped_ratio = 0.9
        self.games_played = 0

        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 batches that must be completed to increment batch_size
        self.batches_needed = 3

        # Ratio of kittens released per balloon released:
        self.kitten_ratio = 0.10
        # Relative value of kittens, in terms of balloons:
        self.kitten_score_factor = 3

This is a simple value that we will multiply by the point value of a balloon, to determine the point value of a kitten. Now, we will make a similar set of changes in Scoreboard.py:

import pygame, pygame.font
from pygame.sprite import Sprite

class Scoreboard(Sprite):

    def __init__(self, screen, settings):

        Sprite.__init__(self)
        self.screen = screen
        self.settings = settings

        self.initialize_stats()

        # Set dimensions and properties of scoreboard
        self.sb_height, self.sb_width = settings.scoreboard_height, self.screen.get_width()
        self.rect = pygame.Rect(0,0, self.sb_width, self.sb_height)
        self.bg_color=(100,100,100)
        self.text_color = (225,225,225)
        self.font = pygame.font.SysFont('Arial', 18)

        # Set positions of individual scoring elements on the scoreboard
        self.x_popped_position, self.y_popped_position = 20.0, 10.0
        self.x_ratio_position, self.y_ratio_position = 150, 10.0
        self.x_points_position, self.y_points_position = 350, 10.0
        self.x_score_position, self.y_score_position = self.screen.get_width() - 200, 10.0

    def initialize_stats(self):
        # Game attributes to track for scoring
        self.balloons_popped = 0
        self.balloons_missed = 0
        self.score = 0
        self.popped_ratio = 1.0
        self.batches_finished = 0
        self.kittens_spared = 0
        self.kittens_killed = 0

    def prep_scores(self):
        self.popped_string = "Popped: " + str(self.balloons_popped)
        self.popped_image = self.font.render(self.popped_string, True, self.text_color)

        self.score_string = "Score: " + format(self.score, ',d')
        self.score_image = self.font.render(self.score_string, True, self.text_color)

        self.set_ratio_string()
        self.popped_ratio_image = self.font.render(self.popped_ratio_string, True, self.ratio_text_color)

        self.points_string = "Points per Balloon: " + str(self.settings.points_per_balloon)
        self.points_image = self.font.render(self.points_string, True, self.text_color)

    def set_ratio_string(self):
        if self.popped_ratio == 1.0:
            self.popped_ratio_string = "Pop Rate: 100%"
        else:
            self.popped_ratio_string = "Pop Rate: " + "{0:.3}%".format(self.popped_ratio*100.0)
        if self.popped_ratio < 0.95:
            self.ratio_text_color = (255, 51, 51)
        else:
            self.ratio_text_color = self.text_color

    def blitme(self):
        # Turn individual scoring elements into images that can be drawn
        self.prep_scores()
        # Draw blank scoreboard
        self.screen.fill(self.bg_color, self.rect)
        # Draw individual scoring elements
        self.screen.blit(self.popped_image, (self.x_popped_position, self.y_popped_position))
        self.screen.blit(self.points_image, (self.x_points_position, self.y_points_position))
        self.screen.blit(self.score_image, (self.x_score_position, self.y_score_position))
        self.screen.blit(self.popped_ratio_image, (self.x_ratio_position, self.y_ratio_position))

We define two new variables that we will track, kittens_spared and kittens_killed. We are not going to display stats about kittens at this point, but we will easily be able to do so at a later time if we start tracking them now.

Finally, we need to make some more significant changes to Engine.py. These are not complicated changes, however, because all of the new code is similar to code we have already written for handling balloons:

import pygame, sys
from Balloon import Balloon
from Button import Button
from Kitten import Kitten
from random import random

class Engine():

    def __init__(self):
        pass

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

    def check_balloons(self, balloons, kittens, 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):
                self.pop_balloon(scoreboard, settings, balloon, balloons)
                continue

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

            balloon.blitme()

        if scoreboard.balloons_popped > 0 or scoreboard.balloons_missed > 0:
            scoreboard.popped_ratio = float(scoreboard.balloons_popped)/(scoreboard.balloons_popped + scoreboard.balloons_missed)
            if scoreboard.popped_ratio < settings.min_popped_ratio:
                # Set game_active to false, empty the list of balloons, and increment games_played
                settings.game_active = False
                settings.games_played += 1

    def check_kittens(self, kittens, sword, scoreboard, screen, settings, time_passed):
        # Find any kittens that have been killed, or have survived to top of screen
        for kitten in kittens:
            kitten.update(time_passed)

            if kitten.rect.colliderect(sword.rect):
                self.kill_kitten(scoreboard, settings, kitten, kittens)
                continue

            if kitten.y_position < -kitten.image_h/2 + settings.scoreboard_height:
                self.spare_kitten(scoreboard, settings, kitten, kittens)
                continue

            kitten.blitme()

    def update_sword(self, 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(self, scoreboard, balloon, balloons):
        scoreboard.balloons_missed += 1
        balloons.remove(balloon)

    def spare_kitten(self, scoreboard, settings, kitten, kittens):
        scoreboard.kittens_spared += 1
        scoreboard.score += settings.kitten_score_factor * settings.points_per_balloon
        kittens.remove(kitten)

    def pop_balloon(self, scoreboard, settings, balloon, balloons):
        scoreboard.balloons_popped += 1
        scoreboard.score += settings.points_per_balloon
        balloons.remove(balloon)

    def kill_kitten(self, scoreboard, settings, kitten, kittens):
        scoreboard.kittens_killed += 1
        scoreboard.score -= settings.kitten_score_factor * settings.points_per_balloon
        kittens.remove(kitten)

    def spawn_balloon(self, screen, settings, balloons, kittens):
        balloons.append(Balloon(screen, settings))
        # Periodically release a kitten:
        if random() < settings.kitten_ratio:
            self.spawn_kitten(screen, settings, kittens)

    def spawn_kitten(self, screen, settings, kittens):
        kittens.append(Kitten(screen, settings))

    def check_events(self, settings, scoreboard, sword, play_button, mouse_x, mouse_y, balloons):
        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 play_button.rect.collidepoint(mouse_x, mouse_y):
                    # Play button has been pressed.  Empty list of balloons,
                    #  initialize scoreboard and game parameters, and make game active.
                    del balloons[:]
                    scoreboard.initialize_stats()
                    settings.initialize_game_parameters()
                    settings.game_active = True
            if event.type == pygame.MOUSEBUTTONUP:
                sword.grabbed = False
    

Lines 45-47 handle the situation in which a sword has slashed a kitten. If so, we kill the kitten, and then continue dealing with the next kitten in our list. We don’t bother blitting this kitten to the screen, because it no longer exists. In a similar manner, lines 49-51 handle kittens that have been spared long enough to reach the top of the screen.

Lines 69-72 handle a kitten that has been spared. We increment the scoreboard’s tally of kittens_spared. Then we add to the score, by multiplying the current point value of a balloon by the kitten_score_factor. Finally, we remove this kitten from our list of kittens.

Lines 79-82 handle the sad situation of a kitten being slashed. In this unfortunate case, we tally the untimely demise using scoreboard’s kittens_killed variable. Then we take points off, equivalent to the current value of a kitten. Finally, we remove this kitten from the list of kittens.

If we play the game now, it takes a lot more skill to pop balloons consistently without killing too many kittens. If you can do so, however, you are rewarded with an appropriately higher score:

Spare a kitten, and your score rises significantly.  Slash a kitten, and you lose points.

Spare a kitten, and your score rises significantly. Slash a kitten, and you lose points.

Revising our instructions

We want to let people know how much we value kittens in this game. Let’s modify our Instructions.py file to share our love of kittens:

import pygame.font
from pygame.sprite import Sprite

class Instructions(Sprite):

    def __init__(self, screen, settings):

        Sprite.__init__(self)
        self.screen = screen
        self.settings = settings

        self.text_color = (30,30,30)
        self.font = pygame.font.SysFont('Arial', 24)

        # Store the set of instructions
        self.instr_lines = ["Move your mouse to swipe the sword back and forth."]
        self.instr_lines.append("Or, click on the sword to grab it.")
        self.instr_lines.append("Keep your pop rate above 90%.")
        self.instr_lines.append("--- Please spare the kittens! ---")
        self.instr_lines.append("Letting a kitten live earns several balloons' worth of points;")
        self.instr_lines.append("Killing a kitten loses several balloons' worth of points.")

        # The instruction message only needs to be prepped once, not on every blit
        self.prep_msg()

    def prep_msg(self):
        y_position = self.settings.screen_height/2 + self.settings.button_height
        self.msg_images, self.msg_x, self.msg_y = [], [], []
        for index, line in enumerate(self.instr_lines):
            self.msg_images.append(self.font.render(line, True, self.text_color))
            self.msg_x.append(self.settings.screen_width/2-self.font.size(line)[0]/2)
            self.msg_y.append(y_position + index*self.font.size(line)[1])

    def blitme(self):
        for msg_x, msg_y, msg_image in zip(self.msg_x, self.msg_y, self.msg_images):
            self.screen.blit(msg_image, (msg_x, msg_y))

These are just a few lines to let people know the value of kittens if you let them survive, and the penalty if you kill a kitten:

A few lines to tell people how much we value kittens.

A few lines to tell people how much we value kittens.

Making Engine.py a real class

Our Engine class has made things easier, but it is really just a function library right now. The function definitions are getting messy, with many of the same arguments being passed around. Let’s try to clean this up a bit by giving the engine object access to most of the values that are being passed around.

import pygame, sys
from Balloon import Balloon
from Button import Button
from Kitten import Kitten
from random import random

class Engine():

    def __init__(self, screen, settings, scoreboard, balloons, kittens, sword):
        self.screen = screen
        self.settings = settings
        self.scoreboard = scoreboard
        self.balloons = balloons
        self.kittens = kittens
        self.sword = sword

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

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

            if balloon.rect.colliderect(self.sword.rect):
                self.pop_balloon(balloon)
                continue

            if balloon.y_position < -balloon.image_h/2 + self.settings.scoreboard_height:
                self.miss_balloon(balloon)
                self.spawn_balloon()
                continue

            balloon.blitme()

        if self.scoreboard.balloons_popped > 0 or self.scoreboard.balloons_missed > 0:
            self.scoreboard.popped_ratio = float(self.scoreboard.balloons_popped)/(self.scoreboard.balloons_popped + self.scoreboard.balloons_missed)
            if self.scoreboard.popped_ratio < self.settings.min_popped_ratio:
                # Set game_active to false, empty the list of balloons, and increment games_played
                self.settings.game_active = False
                self.settings.games_played += 1

    def check_kittens(self, time_passed):
        # Find any kittens that have been killed, or have survived to top of screen
        for kitten in self.kittens:
            kitten.update(time_passed)

            if kitten.rect.colliderect(self.sword.rect):
                self.kill_kitten(kitten)
                continue

            if kitten.y_position < -kitten.image_h/2 + self.settings.scoreboard_height:
                self.spare_kitten(kitten)
                continue

            kitten.blitme()

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

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

    def spare_kitten(self, kitten):
        self.scoreboard.kittens_spared += 1
        self.scoreboard.score += self.settings.kitten_score_factor * self.settings.points_per_balloon
        self.kittens.remove(kitten)

    def pop_balloon(self, balloon):
        self.scoreboard.balloons_popped += 1
        self.scoreboard.score += self.settings.points_per_balloon
        self.balloons.remove(balloon)

    def kill_kitten(self, kitten):
        self.scoreboard.kittens_killed += 1
        self.scoreboard.score -= self.settings.kitten_score_factor * self.settings.points_per_balloon
        self.kittens.remove(kitten)

    def spawn_balloon(self):
        self.balloons.append(Balloon(self.screen, self.settings))
        # Periodically release a kitten:
        if random() < self.settings.kitten_ratio:
            self.spawn_kitten()

    def spawn_kitten(self):
        self.kittens.append(Kitten(self.screen, self.settings))

    def check_events(self, play_button, mouse_x, mouse_y):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                if self.sword.rect.collidepoint(mouse_x, mouse_y):
                    self.sword.grabbed = True
                if play_button.rect.collidepoint(mouse_x, mouse_y):
                    # Play button has been pressed.  Empty list of balloons,
                    #  initialize scoreboard and game parameters, and make game active.
                    del self.balloons[:]
                    self.scoreboard.initialize_stats()
                    self.settings.initialize_game_parameters()
                    self.settings.game_active = True
            if event.type == pygame.MOUSEBUTTONUP:
                self.sword.grabbed = False

Most of this file has changed, so we will explain the kinds of changes made rather than explaining every single line that has changed. Lines 9-15 now define all of the main game parameters that engine functions need access to. These include the screen, settings, and scoreboard objects, and the collections of balloons and kittens, and the sword itself. There are a few items that will still be passed directly to individual functions.

Line 17 shows us how much simpler the function definitions are now. The function release_batch used to take 4 arguments in addition to the self object. Now the self object is the only argument needed! This leads to cleaner calls to other engine functions, so line 19 is significantly simpler as well. Many variables now need a “self” prefix, such as the settings object in line 18.

The rest of the changes in Engine.py follow this same line of thinking. We now need to make a few changes in balloon_ninja.py to use our new version of Engine.py:

import pygame, sys
from Settings import Settings
from Balloon import Balloon
from Sword import Sword
from Scoreboard import Scoreboard
from Button import Button
from Engine import Engine
from Instructions import Instructions

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)
    play_button = Button(screen, settings.screen_width/2-settings.button_width/2,
                            settings.screen_height/2-settings.button_height/2, settings, "Play Balloon Ninja")
    game_over_button = Button(screen, play_button.x_position, play_button.y_position-2*settings.button_height,
                            settings, "Game Over")
    instructions = Instructions(screen, settings)

    # Create a list to hold our balloons, and our kittens
    balloons = []
    kittens = []

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

    # Create our game engine, with access to appropriate game parameters:
    engine = Engine(screen, settings, scoreboard, balloons, kittens, sword)

    # 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]
        engine.check_events(play_button, mouse_x, mouse_y)

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

        if settings.game_active:
            # Update the sword's position and check for popped or disappeared balloons
            engine.update_sword(mouse_x, mouse_y)
            engine.check_balloons(time_passed)
            engine.check_kittens(time_passed)

            # If all balloons have disappeared, either through popping or rising,
            #  release a new batch of balloons.
            if len(balloons) == 0:
                # If we are not just starting a game, increase the balloon speed and points per balloon,
                #  and increment batches_finished
                if scoreboard.balloons_popped > 0:
                    #  Increase the balloon speed, and other factors, for each new batch of balloons.
                    settings.balloon_speed *= settings.speed_increase_factor
                    settings.kitten_ratio *= settings.speed_increase_factor
                    settings.points_per_balloon = int(round(settings.points_per_balloon * settings.speed_increase_factor))
                    scoreboard.batches_finished += 1
                # If player has completed required batches, increase batch_size
                if scoreboard.batches_finished % settings.batches_needed == 0 and scoreboard.batches_finished > 0:
                    settings.batch_size += 1
                engine.release_batch()
        else:
            # Game is not active, so...
            #  Show play button
            play_button.blitme()
            #  Show instructions for first few games.
            if settings.games_played < 3:
                instructions.blitme()
            #  If a game has just ended, show Game Over button
            if settings.games_played > 0:
                game_over_button.blitme()

        # Display updated scoreboard
        scoreboard.blitme()

        # Show the redrawn screen
        pygame.display.flip()

run_game()

We move the creation of the engine object from the beginning of the file to just before the main loop, on line 33. We do this because we need to send the Engine some of the variables we have just defined. The rest of the changes involve simplifying the argument lists of each engine method we call. We no longer have to send screen, settings, balloons, etc. to each function; the only variables we explicitly send are the constantly updated values such as mouse position information and time information.

That is all for now. In the next section, we will wrap things up with a trip to GitHub and a list of future features that could be implemented.

Next: Wrapping Up

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