Python Arena
Step 1 of 4

The game map

In this part you put everything from part 13 together into one bigger program: a puzzle game. A robot works in a warehouse. Boxes stand on the floor, some squares of the floor are marked as targets, and the robot's job is to push every box onto a target. It can push a box one square at a time, but it can't pull one, so a box pushed into a corner stays there for good. Puzzles like this are known as Sokoban, Japanese for "warehouse keeper".

Each step of this module adds a piece to the game: the map in this step, then the robot, then the boxes, and last the move counter, restarting and winning. Every step ends with the whole game so far, and you can play it right in the page.

Tiles

The map is made of tiles: small square pictures that fit together side by side, like the tiles of a floor. These three are 40 by 40 pixels each:

The image floor.png
floor.png, 40 × 40 pixels
The image wall.png
wall.png, 40 × 40 pixels
The image target.png
target.png, 40 × 40 pixels

The map as data

Before it can draw anything, the program needs to know what the room looks like. That's data, and a list of strings describes it well: one string for each row of squares, one character for each square. Here # is a wall, . is floor and X is a target:

LEVEL = [
    "#########",
    "#...#...#",
    "#.....X.#",
    "#...#...#",
    "#..X#...#",
    "#.......#",
    "#########",
]

print(len(LEVEL), "rows")
print(len(LEVEL[0]), "squares in a row")
print(LEVEL[2])
print(LEVEL[2][6])
Output
7 rows
9 squares in a row
#.....X.#
X

A list of strings works like a list of lists here: LEVEL[2] is the row at index 2, and LEVEL[2][6] is the square at index 6 of that row, a target. Notice the order: the row comes first. In the map you write LEVEL[y][x], where y counts the rows from the top and x the squares from the left. pygame does it the other way round: a point in the window is (x, y), x first. Both are normal, so keep an eye on which one you are writing.

Keeping the map as data pays off right away. To change the room, you edit the strings, and the code that draws it stays the same.

Drawing the map

Square (x, y) of the map goes x * 40 pixels from the left edge of the window and y * 40 pixels from the top. Two loops, one inside the other, go through the rows from top to bottom and the squares of each row from left to right, and a dictionary gives the tile for each character:

import pygame

SIZE = 40
ROOM = [
    "######",
    "#..X.#",
    "#....#",
    "######",
]

pygame.init()
window = pygame.display.set_mode((len(ROOM[0]) * SIZE, len(ROOM) * SIZE))
tiles = {
    "#": pygame.image.load("wall.png"),
    ".": pygame.image.load("floor.png"),
    "X": pygame.image.load("target.png"),
}

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            exit()
    window.fill((0, 0, 0))
    for y in range(len(ROOM)):
        for x in range(len(ROOM[y])):
            square = ROOM[y][x]
            window.blit(tiles[square], (x * SIZE, y * SIZE))
    pygame.display.flip()
pygame window240 × 160
The program's window after 2 frames
The window as text
fill (0, 0, 0)
blit wall.png at (0, 0)
blit wall.png at (40, 0)
blit wall.png at (80, 0)
blit wall.png at (120, 0)
blit wall.png at (160, 0)
blit wall.png at (200, 0)
blit wall.png at (0, 40)
blit floor.png at (40, 40)
blit floor.png at (80, 40)
blit target.png at (120, 40)
blit floor.png at (160, 40)
blit wall.png at (200, 40)
blit wall.png at (0, 80)
blit floor.png at (40, 80)
blit floor.png at (80, 80)
blit floor.png at (120, 80)
blit floor.png at (160, 80)
blit wall.png at (200, 80)
blit wall.png at (0, 120)
blit wall.png at (40, 120)
blit wall.png at (80, 120)
blit wall.png at (120, 120)
blit wall.png at (160, 120)
blit wall.png at (200, 120)
The window after 2 frames.

The window's size comes from the map too. A map 6 squares wide and 4 high gets a window of 240 by 160 pixels, and a bigger map would get a bigger window without any other change.

The game as a class

The game will grow to about a hundred lines, with many parts that use the same data: the map, the pictures, the window, and soon the robot. A class keeps all of it together in one object, and its methods split the work into parts with names. Here is the start of the game, with the real map:

import pygame

SIZE = 40
LEVEL = [
    "#########",
    "#...#...#",
    "#.....X.#",
    "#...#...#",
    "#..X#...#",
    "#.......#",
    "#########",
]


class BoxGame:
    def __init__(self):
        pygame.init()
        self.load_pictures()
        self.map = LEVEL
        self.height = len(self.map)
        self.width = len(self.map[0])
        self.window = pygame.display.set_mode((self.width * SIZE, self.height * SIZE))
        pygame.display.set_caption("Box Robot")
        self.clock = pygame.time.Clock()

    def load_pictures(self):
        self.tiles = {
            "#": pygame.image.load("wall.png"),
            ".": pygame.image.load("floor.png"),
            "X": pygame.image.load("target.png"),
        }

    def run(self):
        while True:
            self.handle_events()
            self.draw()
            self.clock.tick(60)

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                exit()

    def draw(self):
        self.window.fill((0, 0, 0))
        for y in range(self.height):
            for x in range(self.width):
                square = self.map[y][x]
                self.window.blit(self.tiles[square], (x * SIZE, y * SIZE))
        pygame.display.flip()


if __name__ == "__main__":
    game = BoxGame()
    game.run()
Box Robot360 × 280
The program's window after 2 frames
The window as text
fill (0, 0, 0)
blit wall.png at (0, 0)
blit wall.png at (40, 0)
blit wall.png at (80, 0)
blit wall.png at (120, 0)
blit wall.png at (160, 0)
blit wall.png at (200, 0)
blit wall.png at (240, 0)
blit wall.png at (280, 0)
blit wall.png at (320, 0)
blit wall.png at (0, 40)
blit floor.png at (40, 40)
blit floor.png at (80, 40)
blit floor.png at (120, 40)
blit wall.png at (160, 40)
blit floor.png at (200, 40)
blit floor.png at (240, 40)
blit floor.png at (280, 40)
blit wall.png at (320, 40)
blit wall.png at (0, 80)
blit floor.png at (40, 80)
blit floor.png at (80, 80)
blit floor.png at (120, 80)
blit floor.png at (160, 80)
blit floor.png at (200, 80)
blit target.png at (240, 80)
blit floor.png at (280, 80)
blit wall.png at (320, 80)
blit wall.png at (0, 120)
blit floor.png at (40, 120)
blit floor.png at (80, 120)
blit floor.png at (120, 120)
blit wall.png at (160, 120)
blit floor.png at (200, 120)
blit floor.png at (240, 120)
blit floor.png at (280, 120)
blit wall.png at (320, 120)
blit wall.png at (0, 160)
blit floor.png at (40, 160)
blit floor.png at (80, 160)
blit target.png at (120, 160)
blit wall.png at (160, 160)
blit floor.png at (200, 160)
blit floor.png at (240, 160)
blit floor.png at (280, 160)
blit wall.png at (320, 160)
blit wall.png at (0, 200)
blit floor.png at (40, 200)
blit floor.png at (80, 200)
blit floor.png at (120, 200)
blit floor.png at (160, 200)
blit floor.png at (200, 200)
blit floor.png at (240, 200)
blit floor.png at (280, 200)
blit wall.png at (320, 200)
blit wall.png at (0, 240)
blit wall.png at (40, 240)
blit wall.png at (80, 240)
blit wall.png at (120, 240)
blit wall.png at (160, 240)
blit wall.png at (200, 240)
blit wall.png at (240, 240)
blit wall.png at (280, 240)
blit wall.png at (320, 240)
The window after 2 frames.
  • __init__ prepares everything: pygame, the pictures, the map, a window that fits the map, its title and a clock.
  • load_pictures puts the tiles in a dictionary, so the picture of a square is self.tiles[square].
  • run is the main loop. It handles the events, draws the window and ticks the clock, over and over. Nothing in this puzzle moves by itself, but self.clock.tick(60) still keeps the loop from drawing thousands of frames a second for nothing.
  • handle_events only closes the window for now. The next step teaches it the arrow keys.
  • draw fills the window and draws every square again, on every frame.

Creating the game object doesn't start the game: game.run() in the main block does. That keeps the two apart, so a game can be created and its data looked at without starting the loop.

Your task

Challenge 1 of 3

The draw method has lost the place of each tile. Fill in the blanks so that every square is drawn at its own place in the window.

Challenge 2

What does this program print? Type each line it prints.

Challenge 3

This level isn't a rectangle. The squares outside its walls are spaces in the map, and nothing should be drawn there, so that the window's black shows through.

Write the two loops in draw that draw the map. Go through the rows from top to bottom and the squares of each row from left to right (the check compares the drawing in this order), and draw the tile of every square that isn't a space.