Skip to content

Goals and Rewards

In this lesson we will learn

  • how to give the player a clear goal and show their progress
  • how to reward the player for reaching the goal
  • how to make the game harder as the player gets closer to the goal
  • how to use an f-string to build text from variables

Terminology

  • f-string – a Python string with an f before the quotes that puts the values of variables inside { } into the text.
  • robust – describes code that still works correctly when something unexpected happens.

In Game Design we found that Space Rescue has no goal: the player just collects astronauts forever. That means there's no what-if effect and no reason to keep trying. Let's fix that.

Planning

Here's what we decided in Game Design:

The problem The solution
The player doesn't have a goal set a goal for the number of astronauts rescued
The player doesn't know what the goal is show the number of astronauts to rescue
The player doesn't know how close they are show the number of astronauts rescued
The player might reach the goal too quickly spawn astronauts less often as more are rescued
No reward for reaching the goal give big bonus points for reaching the goal

So we need:

  1. two new variables in Globals.py: the goal (rescue_goal) and the number rescued so far (rescued)
  2. a new HUD item that shows Rescued: 3 / 10
  3. code in the Astronaut's ship collision that adds to rescued, updates the HUD, and checks if the goal is reached
  4. a reward when the goal is reached: 500 bonus points and a sound, then the game ends
  5. Zork's astronaut timer gets longer as more astronauts are rescued
Input Process Output
the ship rescues an astronaut add 1 to rescued and update the HUD; if rescued has reached rescue_goal, add 500 points, play the bonus sound and end the game the HUD shows the new count; the player wins when they reach the goal

Add the goal

Open GameFrame/Globals.py, add the highlighted code below at the bottom and save it.

GameFrame/Globals.py
    # - difficulty settings (Medium until the player chooses) - #
    asteroid_spawn_min = 15
    asteroid_spawn_max = 150
    asteroid_speed = 10

    # - rescue goal - #
    rescue_goal = 10
    rescued = 0
Code explanation
  • line 51 → sets the goal: rescue 10 astronauts.
  • line 52 → counts the astronauts rescued so far, starting at 0.

Show the progress

We'll add a Rescued class to the HUD. It's a TextObject, like the Score. Open Objects/Hud.py, add the highlighted code below to the bottom and save it.

Objects/Hud.py
class Rescued(TextObject):
    """
    A class for showing how many astronauts have been rescued
    """
    def __init__(self, room, x: int, y: int):
        """
        Initialises the rescued counter
        """
        TextObject.__init__(self, room, x, y)

        # set values
        self.size = 40
        self.font = 'Arial Black'
        self.colour = (255,255,255)
        self.update_rescued()

    def update_rescued(self):
        """
        Shows the number of astronauts rescued out of the goal
        """
        self.text = f"Rescued: {Globals.rescued} / {Globals.rescue_goal}"
        self.update_text()
Code explanation
  • line 53 → defines the Rescued class as a subclass of TextObject.
  • lines 54–56 → a docstring that explains what the class is for.
  • line 57 → defines the __init__ method. Unlike Score, it doesn't take any text, because it builds its own.
  • lines 58–60 → a docstring that explains what the method does.
  • line 61 → runs TextObject's __init__ method.
  • lines 64–66 → set the font size, font and colour.
  • line 67 → calls update_rescued to draw the text for the first time.
  • line 69 → defines the update_rescued method.
  • lines 70–72 → a docstring that explains what the method does.
  • line 73 → builds the text with an f-string, which puts the values of Globals.rescued and Globals.rescue_goal inside the { }, for example Rescued: 3 / 10.
  • line 74 → redraws the text on the screen.

Open Objects/__init__.py, change the highlighted code below and save it.

Objects/__init__.py
1
2
3
4
5
6
7
8
from Objects.Title import Title
from Objects.Ship import Ship
from Objects.Zork import Zork
from Objects.Asteroid import Asteroid
from Objects.Laser import Laser
from Objects.Astronaut import Astronaut
from Objects.Hud import Score, Lives, Rescued
from Objects.DifficultyMenu import DifficultyMenu
Code explanation
  • line 7 → imports the Rescued class as well as Score and Lives.

Open Rooms/GamePlay.py, add the highlighted code below and save it.

Rooms/GamePlay.py
from GameFrame import Level, Globals
from Objects.Ship import Ship
from Objects.Zork import Zork
from Objects.Hud import Score, Lives, Rescued

class GamePlay(Level):
    def __init__(self, screen, joysticks):
        Level.__init__(self, screen, joysticks)

        # set background image
        self.set_background_image("background.png")

        # add objects
        self.add_room_object(Ship(self, 25, 50))
        self.add_room_object(Zork(self,1120, 50))

        # add HUD items
        self.score = Score(self, 
                           Globals.SCREEN_WIDTH/2 - 20, 20, 
                           str(Globals.SCORE))
        self.add_room_object(self.score)
        self.lives = Lives(self, Globals.SCREEN_WIDTH - 150, 20)
        self.add_room_object(self.lives)
        self.rescued = Rescued(self, 20, Globals.SCREEN_HEIGHT - 60)
        self.add_room_object(self.rescued)

        # load sound files
        self.shoot_laser = self.load_sound("Laser_shot.ogg")
        self.asteroid_shot = self.load_sound("Asteroid_shot.wav")
        self.astronaut_saved = self.load_sound("Astronaut_saved.ogg")
        self.asteroid_collision = self.load_sound("Ship_damage.ogg")
        self.astronaut_shot = self.load_sound("Astronaut_hit.ogg")
        self.goal_reached = self.load_sound("Bonus_score.mp3")
Code explanation
  • line 4 → imports the Rescued class.
  • line 24 → creates a Rescued counter in the bottom-left corner of the screen, and stores it in self.rescued so the astronauts can update it.
  • line 25 → adds the counter to the Room.
  • line 33 → loads the bonus sound for reaching the goal.

Count rescues and reward the goal

Open Objects/Astronaut.py, add the highlighted code below and save it.

Objects/Astronaut.py
from GameFrame import RoomObject, Globals

class Astronaut(RoomObject):
    """
    Class for the astronauts escaping from Zork
    """

    def __init__(self, room, x, y):
        """
        Initialise the astronaut instance
        """
        # include attributes and methods from RoomObject
        RoomObject.__init__(self, room, x, y)

        # set image
        image = self.load_image("Astronaut.png")
        self.set_image(image,50,49)

        # set travel direction
        self.set_direction(180, 5)

        # handle events
        self.register_collision_object("Ship")

    def step(self):
        """
        Determines what happens to the astronaut on each tick of the game clock
        """
        self.outside_of_room()

    # --- Event Handlers
    def handle_collision(self, other, other_type):
        """
        Handles the collision event for Astronaut objects
        """
        # ship collision
        if other_type == "Ship":
            self.room.astronaut_saved.play()
            self.room.delete_object(self)
            self.room.score.update_score(50)
            Globals.rescued += 1
            self.room.rescued.update_rescued()
            if Globals.rescued >= Globals.rescue_goal:
                self.room.goal_reached.play()
                self.room.score.update_score(500)
                self.room.running = False
Code explanation
  • line 1 → imports Globals, so the astronaut can update the rescued count.
  • line 41 → adds 1 to the number of astronauts rescued.
  • line 42 → updates the counter on the screen.
  • line 43 → checks if the player has reached the goal…
  • line 44 → …plays the bonus sound…
  • line 45 → …adds 500 bonus points…
  • line 46 → …and ends the GamePlay Room, so the game goes back to the welcome screen.

Why >= and not ==?

Globals.rescued should never jump past the goal, so == would work too. But using >= means the game still ends if something unexpected pushes the count past the goal. It's a small habit that makes code more robust.

The rescued count needs to start at 0 in every new game, just like the score and lives. Open Objects/Title.py, add the highlighted code below and save it.

Objects/Title.py
    def key_pressed(self, key):
        """
        If the key pressed is space the game will start
        """

        if key[pygame.K_SPACE]:
            # reset the score and lives for a new game
            Globals.SCORE = 0
            Globals.LIVES = 3
            Globals.rescued = 0
            self.room.running = False
Code explanation
  • line 27 → resets the rescued count to 0 when a new game starts.

PRIMM

  1. Predict what will happen when you rescue the tenth astronaut.
  2. Run MainController.py and rescue ten astronauts.
  3. Investigate: change rescue_goal to 3 to test it faster. Change it back when you're done.

Make the goal harder to reach

To make the what-if effect stronger, astronauts should appear less often as the player gets closer to the goal. We'll add 20 ticks to Zork's astronaut timer for every astronaut rescued. Open Objects/Zork.py, change the highlighted code below and save it.

Objects/Zork.py
from GameFrame import RoomObject, Globals
from Objects.Asteroid import Asteroid
from Objects.Astronaut import Astronaut
import random

class Zork(RoomObject):
    """
    A class for the game's antagonist
    """
    def __init__(self, room, x, y):
        """
        Initialise the Zork object
        """
        # include attributes and methods from RoomObject
        RoomObject.__init__(self, room, x, y)

        # set image
        image = self.load_image("Zork.png")
        self.set_image(image,135,165)

        # set initial movement
        self.y_speed = random.choice([-10, 10])

        # start asteroid timer
        asteroid_spawn_time = random.randint(Globals.asteroid_spawn_min, Globals.asteroid_spawn_max)
        self.set_timer(asteroid_spawn_time, self.spawn_asteroid)

        # start astronaut timer (slower as more astronauts are rescued)
        astronaut_spawn_time = random.randint(30, 200) + Globals.rescued * 20
        self.set_timer(astronaut_spawn_time, self.spawn_astronaut)

    def keep_in_room(self):
        """
        Keeps the Zork inside the top and bottom room limits
        """
        if self.y < 0 or self.y > Globals.SCREEN_HEIGHT - self.height:
            self.y_speed *= -1

    def step(self):
        """
        Determine what happens to Zork on each tick of the game clock
        """
        self.keep_in_room()

    def spawn_asteroid(self):
        """
        Randomly spawns a new Asteroid
        """
        # spawn Asteroid and add to room
        new_asteroid = Asteroid(self.room, self.x, self.y + self.height/2)
        self.room.add_room_object(new_asteroid)

        # reset time for next Asteroid spawn
        asteroid_spawn_time = random.randint(Globals.asteroid_spawn_min, Globals.asteroid_spawn_max)
        self.set_timer(asteroid_spawn_time, self.spawn_asteroid)

    def spawn_astronaut(self):
        """
        Randomly spawns a new astronaut
        """
        # spawn astronaut and add to room
        new_astronaut = Astronaut(self.room, self.x, self.y + self.height/2)
        self.room.add_room_object(new_astronaut)

        # reset timer for next astronaut spawn (slower as more are rescued)
        astronaut_spawn_time = random.randint(30, 200) + Globals.rescued * 20
        self.set_timer(astronaut_spawn_time, self.spawn_astronaut)
Code explanation
  • line 29 → adds Globals.rescued * 20 ticks to the first astronaut's timer. At the start of a game rescued is 0, so nothing changes.
  • line 66 → adds the same extra time when choosing when the next astronaut appears. After 9 rescues, astronauts take up to 180 ticks (6 seconds) longer to appear.

PRIMM

  1. Predict how the game will feel near the end now.
  2. Run MainController.py and play a whole game.
  3. Modify: is 20 ticks too much or too little? Change it until the end of the game feels tense but fair.

Commit and push

  1. In GitHub Desktop, type Added rescue goal in the Summary box.
  2. Click Commit to main.
  3. Click Push origin.