Skip to content

Difficulty

In this lesson we will learn

  • how to let players choose a difficulty that suits their skill
  • how to add a new Room between two existing Rooms
  • how to store settings in Globals.py so every Room and object can use them
  • how to change an object's image to show the player's choice

In Game Design we found that our game's difficulty never changes, so it doesn't suit every player. Let's add a difficulty menu.

Select difficulty menu

Planning

What changes with difficulty?

The asteroids are the main danger in our game, so we'll change two things:

  • how often Zork throws asteroids (the spawn timer)
  • how fast the asteroids move
Difficulty Key Asteroid spawn time (ticks) Asteroid speed
Easy E 30 to 180 7
Medium M 15 to 150 (what we have now) 10
Hard H 8 to 90 14

Where do the settings live?

The player chooses the difficulty in one Room, but Zork and Asteroid use it in another Room. Values that need to be shared across Rooms go in Globals.py, just like SCORE and LIVES. We'll add three new variables:

  • asteroid_spawn_min and asteroid_spawn_max → the range for Zork's asteroid timer
  • asteroid_speed → how fast new asteroids move

A new Room

We'll add a DifficultySelect Room between the WelcomeScreen and GamePlay. It holds one RoomObject, the DifficultyMenu. Look in Images/Select_difficulty_frames: there are three images of the menu, each with a different choice highlighted.

Input Process Output
the player presses E, M or H save the matching settings in Globals, show the menu with that choice highlighted, then wait half a second the DifficultySelect Room ends and GamePlay starts

The short wait means the player sees their choice highlighted before the game starts. We'll use a timer for that.


Add the settings

Open GameFrame/Globals.py, change the highlighted code below and save it.

GameFrame/Globals.py
class Globals:

    running = True
    FRAMES_PER_SECOND = 30

    SCREEN_WIDTH = 1280
    SCREEN_HEIGHT = 800

    SCORE = 0

    # - Set the starting number of lives - #
    LIVES = 3

    # - Set the Window display name - #
    window_name = 'Space Rescue'

    # - Set the order of the rooms - #
    levels = ["WelcomeScreen", "DifficultySelect", "GamePlay"]

    # - Set the starting level - #
    start_level = 0

    # - Set this number to the level you want to jump to when the game ends - #
    end_game_level = 0

    # - This variable keeps track of the room that will follow the current room - #
    # - Change this value to move through rooms in a non-sequential manner - #
    next_level = 0

    # - Change variable to True to exit the program - #
    exiting = False


# ############################################################# #
# ###### User Defined Global Variables below this line ######## #
# ############################################################# #

    total_count = 0
    destroyed_count = 0

    # - Space Rescue variables - #
    music_playing = False

    # - difficulty settings (Medium until the player chooses) - #
    asteroid_spawn_min = 15
    asteroid_spawn_max = 150
    asteroid_speed = 10
Code explanation
  • line 19 → adds "DifficultySelect" to the list of Rooms, between the WelcomeScreen and GamePlay.
  • lines 46–47 → store the shortest and longest time between asteroids, starting with the Medium values.
  • line 48 → stores the asteroid speed, starting with the Medium value.

Create the DifficultyMenu

Create a new file in the Objects folder, add the code below and save it as DifficultyMenu.py.

Objects/DifficultyMenu.py
from GameFrame import RoomObject, Globals
import pygame

class DifficultyMenu(RoomObject):
    """
    A menu for choosing how hard the game is
    """
    def __init__(self, room, x, y):
        """
        Initialise the difficulty menu
        """
        RoomObject.__init__(self, room, x, y)

        # load the menu images (each one highlights a different choice)
        self.menu_images = []
        for index in range(3):
            self.menu_images.append(self.load_image(f"Select_difficulty_frames/Select_difficulty_{index}.png"))
        self.set_image(self.menu_images[1], 500, 78)

        # register for key events
        self.handle_key_events = True
        self.chosen = False

    def key_pressed(self, key):
        """
        Choose a difficulty with the E, M or H key
        """
        if self.chosen:
            return

        if key[pygame.K_e]:
            self.choose(0, 30, 180, 7)
        elif key[pygame.K_m]:
            self.choose(1, 15, 150, 10)
        elif key[pygame.K_h]:
            self.choose(2, 8, 90, 14)

    def choose(self, index, spawn_min, spawn_max, speed):
        """
        Saves the chosen difficulty, highlights it, then starts the game
        """
        Globals.asteroid_spawn_min = spawn_min
        Globals.asteroid_spawn_max = spawn_max
        Globals.asteroid_speed = speed
        self.set_image(self.menu_images[index], 500, 78)
        self.chosen = True
        self.set_timer(15, self.start_game)

    def start_game(self):
        """
        Ends this Room so the game moves on to GamePlay
        """
        self.room.running = False
Code explanation
  • lines 1–2 → import RoomObject, Globals and Pygame.
  • line 4 → defines the DifficultyMenu class as a subclass of RoomObject.
  • lines 5–7 → a docstring that explains what the class is for.
  • line 8 → defines the __init__ method.
  • lines 9–11 → a docstring that explains what the method does.
  • line 12 → runs RoomObject's __init__ method.
  • lines 15–17 → load the three menu images into a list, just like we did for the lives in Lesson 14.
  • line 18 → shows the menu with Medium highlighted (index 1), because Medium is the starting setting.
  • line 21 → registers the menu for key events.
  • line 22 → creates a chosen flag, so the player can only choose once.
  • line 24 → defines the key_pressed event handler.
  • lines 25–27 → a docstring that explains what the method does.
  • line 28 → checks if a difficulty has already been chosen…
  • line 29 → …and if so, leaves the method straight away with return, so nothing else happens.
  • lines 31–32 → if E is pressed, chooses Easy: menu image 0, spawn time 30 to 180, speed 7.
  • lines 33–34 → if M is pressed, chooses Medium.
  • lines 35–36 → if H is pressed, chooses Hard.
  • line 38 → defines the choose method, which takes the menu image to show and the three settings.
  • lines 39–41 → a docstring that explains what the method does.
  • lines 42–44 → save the settings in Globals, so Zork and the asteroids can use them.
  • line 45 → shows the menu image with the chosen difficulty highlighted.
  • line 46 → sets the chosen flag to True.
  • line 47 → starts a 15-tick (half a second) timer that calls start_game.
  • line 49 → defines the start_game method.
  • lines 50–52 → a docstring that explains what the method does.
  • line 53 → ends the DifficultySelect Room, so the game moves on to GamePlay.

Why a choose method?

Each key does the same four things with different values. Instead of writing those lines three times, we wrote them once in choose and passed in the values. If we want to change how choosing works later, we only change it in one place.

Open Objects/__init__.py, add 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
from Objects.DifficultyMenu import DifficultyMenu
Code explanation
  • line 8 → imports the DifficultyMenu class, so GameFrame can find it.

Create the DifficultySelect Room

Create a new file in the Rooms folder, add the code below and save it as DifficultySelect.py.

Rooms/DifficultySelect.py
from GameFrame import Level
from Objects.DifficultyMenu import DifficultyMenu

class DifficultySelect(Level):
    """
    Screen for choosing the difficulty
    """
    def __init__(self, screen, joysticks):
        Level.__init__(self, screen, joysticks)

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

        # add difficulty menu
        self.add_room_object(DifficultyMenu(self, 390, 361))
Code explanation
  • lines 1–2 → import Level and the DifficultyMenu class.
  • line 4 → defines the DifficultySelect class as a subclass of Level.
  • lines 5–7 → a docstring that explains what the class is for.
  • lines 8–9 → define __init__ and run Level's __init__ method.
  • line 12 → sets the background image.
  • line 15 → adds a DifficultyMenu in the middle of the screen. The menu is 500 × 78 pixels, so x = (1280 - 500) / 2 = 390 and y = (800 - 78) / 2 = 361.

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

Rooms/__init__.py
1
2
3
from Rooms.WelcomeScreen import WelcomeScreen
from Rooms.GamePlay import GamePlay
from Rooms.DifficultySelect import DifficultySelect
Code explanation
  • line 3 → imports the DifficultySelect class, so GameFrame can find it.

PRIMM

  1. Predict what will happen after you press space on the welcome screen.
  2. Run MainController.py and choose each difficulty in turn (you'll need to lose a game to get back to the menu).
  3. Investigate: does the game feel any different? Why not?

Use the settings

The menu saves the settings, but nothing uses them yet. 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
        astronaut_spawn_time = random.randint(30, 200)
        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
        astronaut_spawn_time = random.randint(30, 200)
        self.set_timer(astronaut_spawn_time, self.spawn_astronaut)
Code explanation
  • line 25 → chooses the first asteroid's spawn time from the difficulty's range instead of 15 to 150.
  • line 54 → does the same when choosing the time for the next asteroid.

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

Objects/Asteroid.py
        # set travel direction
        angle = random.randint(135,225)
        self.set_direction(angle, Globals.asteroid_speed)
Code explanation
  • line 22 → starts each asteroid moving at the difficulty's speed instead of 10.

PRIMM

  1. Predict how Easy and Hard will feel different.
  2. Run MainController.py and try all three difficulties.
  3. Modify: are the settings right for you? Get someone else to play too, then adjust the numbers in key_pressed until each level feels right.

Commit and push

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