Tutorial: Maze Game#

In this chapter we will build a maze game together, step by step.

Maze Game - First step

The technique of creating a tilemap is common in games and after seeing it here you should be able to incorporate it into your own projects.

  • Based on: https://github.com/electronstudio/pygame-zero-book

  • License: Attribution-NonCommercial-ShareAlike 4.0 International

Step 1: Reading actors from Tilemap#

A tilemap uses a small number of images (the tiles) and draws them many times to build a much larger game level (the map). This saves you from creating a lot of artwork and makes it very easy to change the design of the level on a whim. Here we create a maze level.

We must create three image files for the tiles: player.png , wall.png and save them in the mu_code/images folder.

my_code
|
|--images
|----images/player.png
|----images/wall.png

Now we can code a framework:

Create a world:#

You can use this framework for your game:

In line 2 a TiledWorld is created, which contains the logic for tiled worlds. In the last line you must call world.run() to start the game.

from miniworlds import * 
world = TiledWorld(8, 8)
world.tile_size = 64
world.add_background((0,0,0,255))

# Your code here

world.run() 

Create Actor-Subclasses#

Create Actor-Subclasses for every type of Actor:

class Player(Actor):
    def on_setup(self):
        self.add_costume("player")
        self.layer = 1
        
class Wall(Actor):
    def on_setup(self):
        self.add_costume("wall")

self.add_costume adds a costume to the actor. A costume can be based on an image (“player”, “wall” - You can ommit file endings like .png or .jpeg) or on a color.

A color can be a 3-tuple (r,g,b) or an 4-tuple (r,g,b,a).

Create a Tile-Map#

A Tile-Map is a 2D-List which contains information, where actors should be positioned.

The value in the maze-list defines the position in the tiles list. 0: None 1: Wall 2: Player

tiles = [None, Wall, Player]

maze = [
    [1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 1, 2, 0, 1],
    [1, 0, 1, 0, 1, 1, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1]
]

Create Objects for each cell in Tile-Map#

For each cell in Tile-Map, a Actor can/must be generated. You can get the class from tilemap.

@world.register
def on_setup(self):
    for row in range(len(maze)):
        for column in range(len(maze[row])):
            x = column
            y = row
            actor_cls = tiles[maze[row][column]]
            if actor_cls:
                t = actor_cls(x, y)

Step 2: Movement#

Move Player#

Add this code to the class Player:

class Player(Actor):
    def on_setup(self):
        self.add_costume("player")
        self.layer = 1

    def on_key_down(self, keys):
        if "UP" in keys:
            self.y -= 1
        elif "DOWN" in keys:
            self.y += 1
        elif "LEFT" in keys:
            self.x -= 1
        elif "RIGHT" in keys:
            self.x += 1

The on_key_down-Method reacts to key-down events. The argument keys stores the keys pressed as list.

Block Movement#

You can restrict Movement with the move_back()-Method - It moves a actor back to its last position.

You can modify the methon on_key_down in the Player-class

def on_key_down(self, keys):
        if "UP" in keys:
            self.y -= 1
        elif "DOWN" in keys:
            self.y += 1
        elif "LEFT" in keys:
            self.x -= 1
        elif "RIGHT" in keys:
            self.x += 1
        if self.detect_actor(Wall):
            self.move_back()

Create an Enemy#

Create an Enemy-Class (put it in your code near to your other classes):

Create enemy class#

Create the enemy class:

class Enemy(Actor):
    def on_setup(self):
        self.add_costume("enemy")
        self.layer = 1

The layer must be 1, so that the actor is placed before the Nothing-Actor.

Enemy Movement#

Add a velocity-attribute to Player-setup and add a method act() to the Player.

act() is called every frame. The Enemy actor should move velocity.

The opponent actor should move “velocity” many steps in y-direction. If it detects a wall, the direction should be inverted. For this the variable “velocity” is inverted, so that the actor either goes 1 step in y-direction or -1 step in y-direction.

def on_setup(self):
        self.add_costume("enemy") # add enemy.png to your images-folder 
        self.velocity = 1
        self.layer = 1

    def act(self):
        self.move(self.velocity)
        if self.detect_actor(Wall):
            self.move_back()
            self.velocity = - self.velocity
            self.move(self.velocity)
        if self.detect_actor(Player):
            print("You died")
            exit()

Exercises#

Exercise#

Verify that the enemy moves up and down and kills the player.

Advanced#

Make another enemy that moves horizontally (left and right).

A door and a key#

Add three images: key.png door_open.png and door_closed to your images-Directory

Add classes#

Add a Door-Class:

class Door(Actor):
    def on_setup(self):
        self.add_costume("door_closed")
        self.add_costume("door_open")
        self.switch_costume(0)
        self.layer = 1
        self.open = False

    def open_door(self):
        self.open = True
        self.switch_costume(1)

Add a global variable has_key to store whether the player has picked up the key. (Alternatively, you can add an attribute self.has_key to the player class. )

from miniworlds import * 

has_key = False
world = TiledWorld(8, 8)
world.tile_size = 64

...

Add a Key-Class

class Key(Actor):
    def on_setup(self):
        self.add_costume("key")
        self.layer = 1
        
    def get_key(self):
        global has_key
        has_key = True
        self.remove()

Modify the tilemap#

Modify the Tilemap:

tiles = [None, Wall, Player, Enemy, Key, Door]

maze = [
    [1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 1, 2, 0, 1],
    [1, 0, 1, 0, 1, 1, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 0, 1],
    [1, 5, 1, 3, 4, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1]
]

Add sensor to Player-Class#

Add the folowing sensor to player class. It detects other actors, if other actors have the class key:

def on_detecting_key(self, other):
    other.get_key()

Exercise#

Exercise#

Check that the game ends when the player reaches the door and the door is open.

Full Code#

from miniworlds import * 

has_key = False
world = TiledWorld(8, 8)
world.tile_size = 64
world.add_background((0,0,0,255))

class Player(Actor):
    def on_setup(self):
        self.add_costume("player")
        self.layer = 1

    def on_key_down(self, keys):
        if "UP" in keys:
            self.y -= 1
        elif "DOWN" in keys:
            self.y += 1
        elif "LEFT" in keys:
            self.x -= 1
        elif "RIGHT" in keys:
            self.x += 1
        if self.detect_actor(Wall):
            self.move_back()
            
    def on_detecting_key(self, other):
        other.get_key()
        
class Wall(Actor):
    def on_setup(self):
        self.add_costume("wall")
        
class Enemy(Actor):
    def on_setup(self):
        self.add_costume("enemy") # add enemy.png to your images-folder 
        self.velocity = 1
        self.layer = 1
        
    def act(self):
        self.move(self.velocity)
        if self.detect_actor(Wall):
            self.move_back()
            self.velocity = - self.velocity
            self.move(self.velocity)
        if self.detect_actor(Player):
            print("You died")
            exit()

class Door(Actor):
    def on_setup(self):
        self.add_costume("door_closed")
        self.add_costume("door_open")
        self.switch_costume(0)
        self.layer = 1
        self.open = False

    def open_door(self):
        self.open = True
        self.switch_costume(1)

class Key(Actor):
    def on_setup(self):
        self.add_costume("key")
        self.layer = 1
        
    def get_key(self):
        global has_key
        has_key = True
        self.remove()
        
            
tiles = [None, Wall, Player, Enemy, Key, Door]

maze = [
    [1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 1, 2, 0, 1],
    [1, 0, 1, 0, 1, 1, 0, 1],
    [1, 0, 1, 0, 0, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 0, 1],
    [1, 0, 1, 0, 1, 0, 0, 1],
    [1, 5, 1, 3, 4, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1]
]

@world.register
def on_setup(self):
    for row in range(len(maze)):
        for column in range(len(maze[row])):
            x = column
            y = row
            actor_cls = tiles[maze[row][column]]
            if actor_cls:
                t = actor_cls(x, y)


world.run()

Maze Game - First step

Ideas for extension#

However that is not the end! There are many things you could add to this game.

  • Show the player score.

  • Coins that the player collects to increase score.

  • Trap tiles that are difficult to see and kill the player.

  • Treasure chest that is unlocked with the key and increases score.

  • Instead of ending the game, give the player 3 lives.

  • Add more types of tile to the map: water, rock, brick, etc.

  • Change the player image depending on the direction they are moving.