Pathfinding in Python and Cockroach

Have a Database Problem? Speak with an Expert for Free
Get Started >>

Introduction

Welcome to the fifth part of a multi-article tutorials on building a retro video game. In this part we learn how to calculate pathfinding in Python and Cockroach, which we use to store and retrieve various kinds of game-related data.

In this part, we focus on initializing enemy Sprites with PNG images and using Python’s “random” library and “random seeds” to move opponents around the screen in two potential ways, which are (1) random direction; and (2) with “smarts”, where they move toward the Player. We will also work on setting up a sort of timer so the enemy Sprite movement keeps going in the chosen direction for a short period of time. Otherwise, we find they “jitter” and really get nowhere.

In the articles remaining in this series, we’ll learn to create and direct bullets, song and sounds, collisions, score tracking, and a difficulty and speed change system.

Prerequisites

  • Be sure to study part one through part four where we learned to create and populate a game GUI using Cockroach for storing and retrieving that data, how to create Sprites and Sprite Lists, how to listen for key strokes, and moving the Player Sprite around the GUI.

  • The working source code, images, and sounds are here for download.

  • Documentation for Arcade. We utilized the Arcade framework library – based on Pyglet – for much of the game-related functionality of this 2D retro-style game.

  • You can use Python’s PIP to install the arcade, datetime, flask, math, os, psycopg2, pyautogui, and random frameworks. As we move through each lesson, we’ll make use of more functions that need these libraries.

Python game libraries

1
2
3
4
5
6
7
import arcade # Primary Game-making framework for Python.
import math # Library needed for certain math functions.
import random # Need this for random number generation.
import os # To access the file system to get sound and image files into the game.
import pyautogui # For finding out the monitor's resolution.
import psycopg2 # For interaction with the Cockroach database.
from datetime import datetime, timedelta # For making a more truly random seed.

Game constants

Here, as usual for these articles, we are including a bit from previous articles and parts of the full application source code that are most relevant to the current tutorial, while leaving out the remainder that is inapplicable. This means if you want to see the full list of constants we set up near the beginning, pull up the source code we linked to above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# Set up constants
SCREEN_TITLE = "Zombie Feeder"
# Beginning difficulty level, which can be
# changed via the "-" and "+(=)" buttons.
# This affects game speed, enemy
# intelligence, and score awards.
DIFFICULTY = 4
# How many zombies?
ENEMY_COUNT_INITIAL = 8
ENEMY_SPEED = 2 + (DIFFICULTY/12)
# Set the following to true for more
# difficult game play where enemies,
# when enraged, are immune to obstacles.
ENEMY_ENRAGED_IMMUNE_TO_BLOCKS = False
# Allow enemies to "blink" from
# the edge of the GUI to the opposite
# edge of the GUI.
# Set to False for less challenging
# play, where the enemies "bounce" from
# the screen edge.
ENEMY_SCREEN_EDGE_TRAVERSE = True
# Beginning size of all
# Sprites on the screen.
# Later we will learn how to swap
# New images when a zombie has
# Been hit a certain number of
# times.
SCALE = 0.25
PLAYER_STARTING_LIVES = 4
PLAYER_IMMUNE_TO_BLOCKS = True
MONITOR_RES_WIDTH, MONITOR_RES_HEIGHT = pyautogui.size()
SCREEN_WIDTH = 1920
SCREEN_HEIGHT = 1080
# Make sure GUI width is not more than monitor width.
if SCREEN_WIDTH > MONITOR_RES_WIDTH:
    SCREEN_WIDTH = MONITOR_RES_WIDTH
# Make sure GUI HEIGHT is not more than monitor width.
if SCREEN_HEIGHT > MONITOR_RES_HEIGHT:
    SCREEN_HEIGHT = MONITOR_RES_HEIGHT
# We base number of obstacles on screen width.
BLOCKS_NUMBER = int(SCREEN_WIDTH/20)
# Set up GUI edges.
SPACE_OFFSCREEN = 1
LIMIT_LEFT = -SPACE_OFFSCREEN
LIMIT_RIGHT = SCREEN_WIDTH + SPACE_OFFSCREEN
LIMIT_BOTTOM = -SPACE_OFFSCREEN
LIMIT_TOP = SCREEN_HEIGHT + SPACE_OFFSCREEN
# Draw screens from database.
# If set to False, screens will
# be drawn using the random function
# to place obstacles.
SCREEN_FROM_DATABASE = False

With our constants set up, we can more easily understand the code below that will rely on some of those functions.

Enemy Sprite initialization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# MAKE THE ENEMIES
# Create a Python list of strings for storing four image URLs:
image_list = ("./resources/images/char-z-fem-01-blue-front.png",
                "./resources/images/char-z-fem-01-purple-front.png",
                "./resources/images/char-z-fem-01-yellow-front.png",
                "./resources/images/char-z-guy-01-green-front.png",
                "./resources/images/char-z-guy-01-orange-front.png",
                "./resources/images/char-z-guy-02-front.png",
                "./resources/images/char-z-guy-03-front.png",
                "./resources/images/char-z-guy-04-front.png")
# If no enraged sound, place "z" in front of name.
name_list = ("char-z-fem-01-blue",
                "char-z-fem-01-purple",
                "char-z-fem-01-yellow",
                "char-z-guy-01-green",
                "char-z-guy-01-orange",
                "char-z-guy-02",
                "char-z-guy-03",
                "char-z-guy-04")
# Cycle through a number of enemies based on the constant below.
for i in range(ENEMY_COUNT_INITIAL):
    image_no = i
    # Create zombie sprite and give it an image using image_list Python List object.
    enemy_sprite = EnemySprite(image_list[image_no], SCALE)
    # Assign a name into the guid property, which we use in a later article when
    # we do image swapping and sound for showing the enemy is enraged.
    enemy_sprite.guid = name_list[image_no]
    # Set the enemy movement speed based on difficulty.
    enemy_sprite.speed = 2 + (DIFFICULTY/12)
    # Create all enemies to have no immunity to obstacles.
    enemy_sprite.immunity = False
    # Create all enemies to begin NOT enraged.
    enemy_sprite.enraged = False
    # Frustration we'll use later.
    enemy_sprite.frustration = 0
    # Set up a sound for when the zombie is enraged.
    enemy_sprite.which = "self.sound_char_" + str(i).zfill(2) + "_enraged"
    enemy_sprite.scale = SCALE
    # Initialize the random timer.
    enemy_sprite.timer_rand = 0
    # Initialize the smart timer.
    enemy_sprite.timer_smart = 0
    # Assign an image name to the url_image parameter we added to
    # the enemy Sprite class.
    enemy_sprite.url_image = image_list[image_no]
    # Set enemy position to be near the top of the screen and spaced out equally
    # based on GUI resolution.
    enemy_sprite.center_y = 800
    enemy_sprite.center_x = int((SCREEN_WIDTH/8 * (i+1))-(SCREEN_WIDTH/13.9))
    # Start enemies out not moving:
    enemy_sprite.change_x = 0
    enemy_sprite.change_y = 0
    # Add this sprite to both of the applicable sprite lists.
    self.list_all_sprites.append(enemy_sprite)
    self.list_enemies.append(enemy_sprite)

Here’s the class we created to represent an enemy Sprite:

1
2
3
4
5
6
7
8
9
10
# Class for the Sprite that represents an enemy.
class EnemySprite(arcade.Sprite):
    # Initialize enemy size, movement-type timers, and speed
    def __init__(self, image_file_name, scale):
        super().__init__(image_file_name, scale=scale)
        self.timer_rand = 0
        self.timer_smart = 0
        self.speed = 0
        self.url_image = image_file_name
        self.immunity = False

Be aware how in the above Python script, we started our timers and set the enemy Sprites’ speed to be 2 plus difficulty divided by 12.

Handle enemy movement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Move the enemy and check for screen boundary.
def update(self):
    super().update()
    # If zombie hits screen boundary,
    # cause a "bounce" toward opposite direction.
    if ENEMY_SCREEN_EDGE_TRAVERSE == False:
        if self.center_x < LIMIT_LEFT:
            self.center_x = LIMIT_LEFT
            self.change_x *= -1
        if self.center_x > LIMIT_RIGHT:
            self.center_x = LIMIT_RIGHT
            self.change_x *= -1
        if self.center_y > LIMIT_TOP:
            self.center_y = LIMIT_TOP
            self.change_y *= -1
        if self.center_y < LIMIT_BOTTOM:
            self.center_y = LIMIT_BOTTOM
            self.change_y *= -1
    else:
        # If the zombie goes off-screen,
        # teleport to the other side of the GUI.
        if self.right < 0:
            self.left = SCREEN_WIDTH
        if self.left > SCREEN_WIDTH:
            self.right = 0
        if self.bottom < 0:
            self.top = SCREEN_HEIGHT
        if self.top > SCREEN_HEIGHT:
            self.bottom = 0

Enemy pathfinding

Now we will “coin toss” to decide whether to use random movement vs. “intelligent” movement, then we’ll go into each of those movement types and set them up.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def on_update(self, x_delta):
    self.frame_count += 1
    fps = x_delta * 3600
    # If you want to see your FPS,
    # uncomment the print statement below.
    # print("fps: " + str(fps))
    # Set up enemy_speedy variable for local-to-this-function use.
    # enemy_speedy = 0
    if not self.game_over:
        self.list_all_sprites.update()
        # Cycle through all bullets currently on screen.
        for bullet in self.list_bullets:
            # Check for bullet collision with enemy
            enemies = arcade.check_for_collision_with_list(bullet, self.list_enemies)
            # Cycle through all (if any) enemies hit by current bullet.
            i = -1
            for enemy in enemies:
                i += 1
                # Give Player points for hitting enemy based on difficulty level.
                self.score += int(100 * self.player_sprite.difficulty)
                # Stop enemy that was hit
                enemy.change_x = 0
                enemy.change_y = 0
                url_image = enemy.url_image
                # Grow the enemy size.
                new_scale = enemy.scale + 0.07
                # When the enemy gets to a certain size,
                # they become "enraged".
                if new_scale > 0.59:
                    new_scale = 0.6
                    enemy.enraged = True
                    # enraged sound enabled for THIS enemy?
                    if url_image.find(enemy.guid) > -1:
                        # following builds this: "self.sound_char_02_enraged"
                        build_sound_name = enemy.which
                        arcade.play_sound(eval(build_sound_name))
                    else:
                        arcade.play_sound(self.sound_powerup)
                    # Change image to "enraged" version with red eyes.
                    url_image = url_image.replace("front","enraged")
                else:
                    arcade.play_sound(self.sound_powerup)
                    # arcade.play_sound(self.sound_munch)
                    # arcade.play_sound(self.sound_)
                # Prepare NEW Sprite to take exact place
                # and properties of the enraged Sprite
                # but with the "enraged" image.
                new_enemy_sprite = EnemySprite(url_image, new_scale)
                if new_scale > 0.4:
                    new_enemy_sprite.immunity = ENEMY_ENRAGED_IMMUNE_TO_BLOCKS
                new_enemy_sprite.guid = enemy.guid
                new_enemy_sprite.which = enemy.which
                new_enemy_sprite.center_x = enemy.center_x
                new_enemy_sprite.center_y = enemy.center_y
                new_enemy_sprite.speed = enemy.speed
                new_enemy_sprite.frustration = enemy.frustration
                new_enemy_sprite.enraged = enemy.enraged
                # If you want the zombie's speed to increase when they
                # are hit, uncommment the following line
                # new_enemy_sprite.speed = enemy.speed + 0.1
                new_enemy_sprite.change_x = 0
                new_enemy_sprite.change_y = 0
                new_enemy_sprite.timer_rand = int(fps * (12 - self.player_sprite.difficulty))
                new_enemy_sprite.timer_smart = 0
                # print(enemy.url_image)
                # print("enemy size: " + str(enemy.size))
                enemy.update()
                new_enemy_sprite.update()
                #enemies[0].remove_from_sprite_lists()
                enemy.remove_from_sprite_lists()
                self.list_all_sprites.append(new_enemy_sprite)
                self.list_enemies.append(new_enemy_sprite)
                bullet.remove_from_sprite_lists()

Conclusion

In this fifth part of a multi-article series of tutorials for building a 2D game, we learned how to use Arcade to move the zombies around the screen in random and “intelligent” ways, depending on a “coin toss”. We also looked at how to initialize the enemy Sprites; give them PNGs, coordinates on the GUI, and speeds; and add those enemy Sprites to Sprite Lists.

In the next part six, we will look at bullets – which are brains in this game – followed by collision detection, sound FX and music, dynamic game difficulty and speed controlled by the player, and tracking/displaying score.

Pilot the ObjectRocket Platform Free!

Try Fully-Managed CockroachDB, Elasticsearch, MongoDB, PostgreSQL (Beta) or Redis.

Get Started

Keep in the know!

Subscribe to our emails and we’ll let you know what’s going on at ObjectRocket. We hate spam and make it easy to unsubscribe.