Game Python Postgres with Sprite Intelligence

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

Introduction

In this part, we continue building a game with Python and Postgres with Sprite intelligence as part 6 of a multi-part series of lessons where the final result will be a basic 2D graphical top-down view game similar to PacMan. We are using Python’s “Arcade” framework for the gaming-related features. We used PostgreSQL to read and write screen data. In this lesson we will play with increasing the “intelligence” of monsters/enemies. In future lessons in this series, we will add sound, various graphics, and even a screen builder/editor.

Prerequisites

See part 1 through 5, where we learned how to draw a screen, create tables in Postgres for storing and reading screen data and screen objects as Sprites, reading keys to control player movement, added random movements to the monsters, and then we added collision detection. To get the most from this lesson, please study those lessons starting at Build game with Python and draw a screen.

Sprites with Python Arcade

Assuming you followed the previous parts of this series, we’ll leave out creating the Postgres tables and re-doing the details of setting up a window with Arcade, except in the source code at the bottom of this part six.

1
2
3
4
5
6
7
8
9
10
11
12
13
# use Python's arcade framework
import arcade

# See past parts of this lesson for step-by-step directions for:
    # Set up screen with dimension variables.
    # Open the window. Set title and x/y dimensions.
    # Set the screen bkg color.

# Set up primary sprite; PlayerSprite.
arcade.PlayerSprite = arcade.Sprite("game-graphics/icon-player.png")

# Change the x/y coordinates for that Sprite.
arcade.PlayerSprite.set_position(player_x, player_y)

Before adding some “intelligence” to monster movement, we will look at the purely random method we used in previous parts of this overall lesson. THEN we will look at a method to ADD to that random influence. We will add our new method AFTER the random factor has been calculated. Previous to now we put “intelligent” in quotes because we want to distinguish between what we are doing, which is simplistic, and true artificial intelligence and machine learning. We’ll incorporate that level of intelligence into a future game tutorial.

Sprite movement

Random monster movement

The following code picks a random number between 1 and 8, inclusive of 1 and 8. These represent eight different directions. As you may have discovered if you ran that code in previous lessons, you would find the monsters do not tend to get very far because they are constantly changing directions. After this section of randomness, we’ll play with a couple bits of added code to give the monsters more smarts, as well as more “distance” they will travel before switching direction. Why leave in a random factor? Two reasons: (1) Keeping a certain amount of unpredictability; and (2) If a monster runs into a corner, it might get stuck if the player is in the direction opposite that corner. Randomness will increase the potential of the monster to back up for long enough to get around that impediment.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
direction = random.randint(1, 8)
if direction == 1:
    y += 2
if direction == 2:
    x += 2
    y += 2
if direction == 3:
    x += 2
if direction == 4:
    x += 2
    y -= 2
if direction == 5:
    y -= 2
if direction == 6:
    y -= 2
    x -= 2
if direction == 7:
    x -= 2
if direction == 8:
    x -= 2

One change you may notice between the lesson on monster movement and this one is that we increased the x and y influence of the random factor from 1 to 2, so the enemy Sprite goes double the random distance.

Intelligent Sprite movement

1
2
3
4
5
6
7
8
if player_y > y:
    y += 2
if player_x > x:
    x += 2
if player_y < y:
    y -= 2
if player_x < x:
    x -= 2

Analysis: The above code compares the position of the player (player_x and player_y) with the position of a given enemy. If the enemy is above the player, the monster’s y is incremented by 2, so the monster has a tendency to move up, consistently, while inconsistently zig-zagging based on the random factor above.

NOTE: Depending on the order you calculate these two factors, you will get different monster behavior. Think of it this way: If you do random first, then the “intelligent” factor will calculate based on different positioning than if the “intelligent” method is calculated BEFORE the random factor. You are invited to play with both methods.

Full source code

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
import arcade
import psycopg2
import random
from flask import Flask
from flask import render_template

# connect to database
t_host = "PostgreSQL database host address"
t_port = "5432"
t_dbname = "database name"
t_user = "database user name"
t_pw = "password"
db_conn = psycopg2.connect(host=t_host, port=t_port, dbname=t_dbname, user=t_user, password=t_pw)
db_cursor = db_conn.cursor()

# initialize globals
id_screen = 0
t_title = ""
i_width = 0
i_height = 0
t_color = ""
i_enemy_hit = 0
i_hit_points = 100
i_points = 0
i_lives = 3

@app.route("/main")

def getScreenFromDB():
    # Get columns for first row/
    #   screen using "select top 1"
    s = ""
    s += "SELECT TOP 1"
    s += " id"
    s += ", t_color"
    s += ", t_title"
    s += ", i_height"
    s += ", i_width"
    s += " FROM tbl_screens"
    s += " ORDER BY i_order ASCENDING"
    try:
        db_cursor.execute(s)
        id_screen = db_cursor.fetch("id")
        t_color_screen = db_cursor.fetch("t_color")
        t_title = db_cursor.fetch("t_title")
        i_height = db_cursor.fetch("i_height")
        i_width = db_cursor.fetch("i_width")
    except psycopg2.Error as e:
        t_msg = "SQL error: " + e + "/n SQL: " + s
        return render_template("error.html", t_msg = t_msg)
    db_cursor.close

def getObjectsFromDB():
    # Get objects data from tbl_screens_objects in Postgres
    s = ""
    s += "SELECT"
    s += " id"
    s += ", t_obj_name"
    s += ", t_obj_type"
    s += ", t_obj_img_URL"
    s += ", i_y"
    s += ", i_x"
    s += ", t_color_hit"
    s += ", t_properties"
    s += ", t_hit_action"
    s += ", b_hit_possible"
    s += ", b_hit_destroys_it"
    s += ", b_hit_awards"
    s += ", n_hit_awards"
    s += ", n_hit_damage"
    s += " FROM tbl_screens_objects"
    s += " WHERE ("
    s += " id_screen = " + id_screen
    s += ")"
    s += " ORDER BY i_order"
    try:
        db_cursor.execute(s)
        return db_cursor
    except psycopg2.Error as e:
        t_msg = "SQL error: " + e + "/n SQL: " + s
        return render_template("error.html", t_msg = t_msg)

class gameWindow(arcade.Window):
    def__init__(self, i_width, i_height, t_title):
    super().__init__(width, height, title, resizable=false)
    self.set_location(100, 100)

    # Give our window a background color.
    arcade.set_background_color(arcade.color.t_color_screen)

    self.player_x = 100
    self.player_y = 200
    self.player_speed = 250

    self.right = false
    self.left = false
    self.up = false
    self.down = false

    # Initialize a spritelist to track objects like walls.
    # This type of object only stops player motion.
    self.ListSpriteObjects = arcade.SpriteList()

    # Initialize a spritelist to track monsters/enemies.
    # These objects cause damage to i_hit_points.
    self.ListSpriteEnemies = arcade.SpriteList()

    # Initialize a spritelist to track powerups.
    # These objects give points to the player when a collision occurs.
    self.ListSpritePowerUps = arcade.SpriteList()

    # Get data from the database into the 3 lists above.
    i = 0
    for each db_row in db_cursor:
        id_screen = db_row.fetch("id")
        t_obj_name[i] = db_row.fetch("t_obj_name")
        t_obj_type[i] = db_row.fetch("t_obj_type")
        t_obj_img_URL = db_row.fetch("t_obj_img_URL")
        i_y = db_row.fetch("i_y")
        i_x = db_row.fetch("i_x")
        t_properties[i] = db_row.fetch("t_properties")
        t_color_hit[i] = db_row.fetch("t_color_hit")
        t_hit_action[i] = db_row.fetch("t_hit_action")
        b_hit_possible[i] = db_row.fetch("b_hit_possible")
        b_hit_destroys_it[i] = db_row.fetch("b_hit_destroys_it")
        b_hit_awards[i] = db_row.fetch("b_hit_awards")
        n_hit_awards[i] = db_row.fetch("n_hit_awards")
        n_hit_damage[i] = db_row.fetch("n_hit_damage")
        # draw the current sprite in db_row
        if t_object_type == "obstacle":
            self.SpriteObject = arcade.Sprite(t_object_image_URL, i_x, i_y)
            # add current obstacle to growing list of Sprites (SpriteList)
            self.ListSpriteObjects.append(self.SpriteObject)
        elif t_object_type == "enemy":
            self.SpriteObstacle = arcade.Sprite(t_object_image_URL, i_x, i_y)
            # add current enemy (sprite) to growing list of Sprites (SpriteList)
            self.SpriteEnemies.append(self.SpriteObject)
        elif t_object_type == "powerup":
            self.SpriteObstacle = arcade.Sprite(t_object_image_URL, i_x, i_y)
            # add current PowerUp to growing list of Sprites (SpriteList)
            self.SpritePowerUps.append(self.SpriteObject)
        elif t_object_type == "Player":
            self.SpritePlayer = arcade.Sprite(t_object_image_URL, i_x, i_y)
        # increment the counter
        i += 1

def enemyMovement():
    # Iterate through enemy sprites.
    # In this case, there are only 2 enemies but we designed this so
    # you can add as many as you want to add to the database.
    # THEN we give them a random direction out of eight
    #   possible directions.
    # FINALLY we add our new-for-this-part "intelligent" direction to
    #   the random one.
    for currentSprite in self.ListSpriteEnemies:
        x = currentSprite.position[0]
        y = currentSprite.position[1]

        # Random movement
        direction = random.randint(1, 8)
        if direction == 1:
            y += 2
        if direction == 2:
            x += 2
            y += 2
        if direction == 3:
            x += 2
        if direction == 4:
            x += 2
            y -= 2
        if direction == 5:
            y -= 2
        if direction == 6:
            y -= 2
            x -= 2
        if direction == 7:
            x -= 2
        if direction == 8:
            x -= 2
            y += 2

        # NEW "intelligent" movement.
        if player_y > y:
            y += 2
        if player_x > x:
            x += 2
        if player_y < y:
            y -= 2
        if player_x < x:
            x -= 2

        # Set the current monster's position.
        currentSprite.set_position(x, y)

def on_draw(self):
    self.SpritePlayer.draw()
    self.ListSpriteObjects.draw()
    self.ListSpriteEnemies.draw()
    self.ListSpritePowerUps.draw()

def on_update(self, delta_time):
    # Delta_time is last time same
    #   function was run; used to scale
    #   movement to processing speed.
    if self.right = true:
        self.player_x += self.player_speed * delta_time
    if self.left = true:
        self.player_x -= self.player_speed * delta_time
    if self.up = true:
        self.player_y += self.player_speed * delta_time
    if self.down = true:
        self.player_y -= self.player_speed * delta_time

    # Set new position based on the "player_x" and "player_y" variables.
    self.SpritePlayer.set_position(self.player_x, self.player_y)
    # Call function to move all monster Sprites.
    enemyMovement()
    self.ListSpriteObjects.update()
    self.ListSpriteEnemies.update()
    self.ListSpritePowerUps.update()

    # Did the user collide with an enemy?
    i = 0
    for SpriteEnemy in self.ListSpriteEnemies:
        b_collision = arcade.check_for_collision(SpriteEnemy, self.SpritePlayer)
        if b_collision:
            i_enemy_hit = i
            i_hit_points -= n_hit_damage[i_enemy_hit]
            # Graphical changes (like explosion or something) and
            #   sound code to show crash will go here
            if i_hit_points < 1:
                i_lives -= 1
                if i_lives < 1:
                    # Dead - Next lesson we will write code to handle this event
                    # For now, the player lives forever.
                i_hit_points = 100
        i +=1

    # Did the user collide with a power-up?
    i = 0
    for SpritePowerUp in self.ListSpritePowerUps:
        b_collision = arcade.check_for_collision(SpritePowerUp, self.SpritePlayer)
        if b_collision:
            i_pu_hit = i
            i_points += n_hit_awards[i_pu_hit]
            # Graphics and sound code to show
            #   powerup hit will go here.
        i +=1

    # Did the user collide with an obstacle?
    i = 0
    for SpritePowerUp in self.ListSpritePowerUps:
        b_collision = arcade.check_for_collision(SpritePowerUp, self.SpritePlayer)
        if b_collision:
            i_pu_hit = i
            # half player movement
            self.right = false
            self.left = false
            self.up = false
            self.down = false
            # Sound and graphics code to show object collision
            #   here in a future part of this lesson.
        # increment our "index", which will be used in a future part.
        i += 1

def on_key_press(self, symbol, modifiers):
    # When a key is pressed, this function is called
    if symbol == arcade.key.right:
        self.right = true
    if symbol == arcade.key.left:
        self.left = true
    if symbol == arcade.key.up:
        self.up = true
    if symbol == arcade.key.down:
        self.down = true

def on_key_release(self, symbol, modifiers):
    # When a key is released, this function is called
    if symbol == arcade.key.right:
        self.right = false
    if symbol == arcade.key.left:
        self.left = false
    if symbol == arcade.key.up:
        self.up = false
    if symbol == arcade.key.down:
        self.down = false

Def main():
    getScreenFromDB()
    db_cursor = getObjectsFromDB()
    gameWindow(i_width, i_height, t_title)
    enemyMovement()
    arcade.run()

Conclusion

In this part 6, we continued building a game with Python and Postgres with better Sprite intelligence as part of a multi-part series of lessons where the final result will be a basic 2D graphical top-view game. We are using Python’s Arcade library for gaming-type features. We used PostgreSQL to read and write screen data. In this lesson we added “intelligence” to the player’s enemies. In future lessons in this series, we will add sound, various graphics, and even a screen builder/editor.

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.