Python Arena
Step 2 of 2

A complete small game

Here's the coin chase finished: a monster, a time limit and an end. Read it through first. The sections after it explain what's new.

The image robot.png
robot.png, 40 × 40 pixels
The image coin.png
coin.png, 24 × 24 pixels
The image monster.png
monster.png, 40 × 40 pixels
import random

import pygame

WIDTH = 480
HEIGHT = 360
GAME_TIME = 15


class CoinChase:
    def __init__(self):
        pygame.init()
        self.window = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption("Coin Chase")
        self.robot_picture = pygame.image.load("robot.png")
        self.coin_picture = pygame.image.load("coin.png")
        self.monster_picture = pygame.image.load("monster.png")
        self.font = pygame.font.SysFont("Arial", 24)
        self.clock = pygame.time.Clock()
        self.reset()

    def reset(self):
        self.robot_x = 40
        self.robot_y = 160
        self.monster_x = 420
        self.monster_y = 40
        self.coins = 0
        self.frames = 0
        self.over = False
        self.place_coin()

    def place_coin(self):
        self.coin_x = random.randint(0, WIDTH - 24)
        self.coin_y = random.randint(40, HEIGHT - 24)

    def seconds_left(self):
        return GAME_TIME - self.frames // 60

    def run(self):
        while True:
            self.handle_events()
            if not self.over:
                self.update()
            self.draw()
            self.clock.tick(60)

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                exit()
            if event.type == pygame.KEYDOWN and event.key == pygame.K_r:
                self.reset()

    def update(self):
        self.move_robot()
        self.move_monster()
        robot = pygame.Rect(self.robot_x, self.robot_y, 40, 40)
        coin = pygame.Rect(self.coin_x, self.coin_y, 24, 24)
        monster = pygame.Rect(self.monster_x, self.monster_y, 40, 40)
        if robot.colliderect(coin):
            self.coins += 1
            self.place_coin()
        if robot.colliderect(monster):
            self.over = True
        self.frames += 1
        if self.seconds_left() == 0:
            self.over = True

    def move_robot(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            self.robot_x -= 3
        if keys[pygame.K_RIGHT]:
            self.robot_x += 3
        if keys[pygame.K_UP]:
            self.robot_y -= 3
        if keys[pygame.K_DOWN]:
            self.robot_y += 3
        self.robot_x = max(0, min(self.robot_x, WIDTH - 40))
        self.robot_y = max(0, min(self.robot_y, HEIGHT - 40))

    def move_monster(self):
        if self.monster_x < self.robot_x:
            self.monster_x += 1
        if self.monster_x > self.robot_x:
            self.monster_x -= 1
        if self.monster_y < self.robot_y:
            self.monster_y += 1
        if self.monster_y > self.robot_y:
            self.monster_y -= 1

    def draw(self):
        self.window.fill((30, 90, 60))
        self.window.blit(self.coin_picture, (self.coin_x, self.coin_y))
        self.window.blit(self.robot_picture, (self.robot_x, self.robot_y))
        self.window.blit(self.monster_picture, (self.monster_x, self.monster_y))
        coins_text = self.font.render(f"Coins: {self.coins}", True, (255, 255, 255))
        self.window.blit(coins_text, (10, 10))
        time_text = self.font.render(f"Time: {self.seconds_left()}", True, (255, 255, 255))
        self.window.blit(time_text, (390, 10))
        if self.over:
            message = self.font.render("Game over! Press R to play again.", True, (255, 230, 120))
            self.window.blit(message, (106, 172))
        pygame.display.flip()


if __name__ == "__main__":
    game = CoinChase()
    game.run()
Coin Chase480 × 360
The program's window after 640 frames
The window as text
fill (30, 90, 60)
blit coin.png at (253, 270)
blit robot.png at (35, 85)
blit monster.png at (74, 85)
blit text "Coins: 3" at (10, 10)
blit text "Time: 5" at (390, 10)
blit text "Game over! Press R to play again." at (106, 172)
The window after 640 frames (After 5 frames: the down arrow is pressed. After 55 frames: the down arrow is let go and the right arrow is pressed. After 190 frames: the right arrow is let go and the up arrow is pressed. After 265 frames: the up arrow is let go and the left arrow is pressed. After 400 frames: the left arrow is let go.).

The monster

move_monster moves the monster one pixel closer to the robot on each axis, on every frame:

def move_monster(self):
    if self.monster_x < self.robot_x:
        self.monster_x += 1
    if self.monster_x > self.robot_x:
        self.monster_x -= 1
    if self.monster_y < self.robot_y:
        self.monster_y += 1
    if self.monster_y > self.robot_y:
        self.monster_y -= 1

The robot moves 3 pixels a frame and the monster only 1, so a player who keeps moving can stay out of its reach. A game like this is all about such numbers. Try other speeds and see how the game changes.

Time

self.frames counts the frames that the game has run. At 60 frames a second, self.frames // 60 is the number of whole seconds played, and seconds_left subtracts it from the game's length, GAME_TIME. A constant at the top of the program makes it easy to try a shorter or a longer game.

When the game is over

The attribute self.over tells whether the game has ended. update sets it to True when the monster catches the robot or the time runs out. From then on the main loop skips update, so nothing moves, but draw keeps showing the last picture with a message on top. R calls reset, which sets everything back, self.over included.

An attribute that tells which phase a game is in, playing or over, is common in games. A bigger game could have more phases: a title screen, a pause, the next level.

Make it yours

A game grows one small change at a time. The challenges of this step make three changes to the coin chase: a faster monster, coins that give more time, and lives, so that being caught once doesn't end the game. Some more ideas, for this game or your own:

  • more monsters, kept in a list, each moved by the same code
  • a monster that gets faster with every five coins
  • a best score that stays while the program runs, in an attribute that reset doesn't touch

Testing your game

Test the part you're working on without playing the whole game: start the monster right next to the robot to see the ending, or set GAME_TIME to 3. Print values while you look for a bug. Then put the real values back.

Then hand the game to someone else, and watch them play without helping. Did they know what to do without being told? Where did they get stuck, or bored? Did they want another round when it ended? That shows you what to work on next. Someone who can read code can also tell you which part of yours was hard to follow.

You can write and play a game of your own in the Playground of this site, where all its pictures are available.

Your task

Challenge 1 of 3

In the challenges of this step the monster is faster. It moves MONSTER_SPEED pixels a frame, 2 to begin with. That needs care: a monster 1 pixel to the left of the robot would jump 1 pixel past it, then back again on the next frame, and so on. It would shake.

Write the method move_monster. On every frame, the monster's x moves MONSTER_SPEED pixels toward the robot's x, but never past it: if the robot's x is closer than that, the monster's x becomes the robot's x. If the two are already the same, the monster's x stays as it is. The monster's y moves toward the robot's y in the same way.

Challenge 2

The time in the corner stays at 15: it never runs out. Add a time limit, in which every coin gives the robot more time:

  • in update, add one to self.frames on every frame
  • write the method seconds_left(). It returns GAME_TIME, plus SECONDS_PER_COIN seconds for each coin collected (self.coins), minus the number of whole seconds played (self.frames // 60)
  • at the end of update, set self.over to True when no seconds are left

Challenge 3

The monster walks right through the robot, and nothing happens. Give the robot three lives: reset already sets self.lives to 3, and draw shows them. In update, when the monster's rectangle and the robot's overlap (both are 40 by 40 pixels):

  • the robot loses a life: self.lives gets one smaller
  • the monster goes back to where it started, (420, 40)
  • if no lives are left, the game is over: set self.over to True

When the time runs out, the game must still end, as before.