Build a complete game with Python and PyGame

Lead Image © ragakawaw, 123RF.com

Robots, Rubble, and Python

We show how to use PyGame to implement a fully playable version of the classic "robots" game.

Imagine yourself on a game grid populated by robots. The robots will chase any non-robot, but if they collide with each other, they disintegrate into a pile of rubble. Other robots that collide with the rubble also become a part of the pile. If you are the last one standing, then you've won! However, if a robot enters the same square you're in, then you've been killed. That's the basic gameplay scenario for the game described in this article.

In the game, you and the robots both move orthogonally and diagonally (Figure 1). For every move you make, all of the robots also make a move – directly toward you. You can navigate with the arrow keys or with Q, W, E, A, D, Z, X, and C.

Figure 1: The game board immediately after launching.

As a player, you have one special ability: teleporting, which can be activated with the T key. Teleporting can be hazardous, though (Figure 2); it could put you on top of a robot or in a square without a safe move. Pressing the space bar (or any other unused key) leaves you in place and allows the robots to make a move.

Figure 2: The player is in a tight spot! Maybe teleporting will help.

In this article, I detail the coding steps involved in creating the game (see Listing 1 for the complete code).

Listing 1

The Code

001 import pygame
002 import random
003
004 class robotsGame:
005    def __init__ ( self , screen , startingRobots ):
006       self.screen = screen
007
008       self.grid = dict()
009       for y in range ( 25 ):
010          for x in range ( 50 ):
011             self.grid [ ( x , y ) ] = None
012
013       self.robots = list()
014       for i in range ( startingRobots ):
015          while 1:
016             x = random.randrange ( 0 , 50 )
017             y = random.randrange ( 0 , 25 )
018
019             if ( x , y ) not in self.robots:
020                self.robots.append ( ( x , y ) )
021                self.grid [ ( x , y ) ] = "ROBOT"
022                break
023
024       while 1:
025          x = random.randrange ( 0 , 50 )
026          y = random.randrange ( 0 , 25 )
027
028          if self.checkGrid ( ( x , y ) ) == False:
029             self.grid [ ( x , y ) ] = "PLAYER"
030             self.playerX = x
031             self.playerY = y
032             break
033
034       self.legend()
035
036    def legend ( self ):
037       pygame.draw.rect ( self.screen , ( 255 , 0 , 0 ) , ( 50 , 550 , 20 , 20 ) , 0 )   # Robot
038       pygame.draw.rect ( self.screen , ( 0 , 255 , 0 ) , ( 50 , 580 , 20 , 20 ) , 0 )   # You
039       pygame.draw.rect ( self.screen , ( 255 , 255 , 0 ) , ( 50 , 610 , 20 , 20 ) , 0 ) # Rubble
040
041       pygame.font.init()
042       font = pygame.font.SysFont ( "" , 20 )
043
044       robotLabel = font.render ( "Robots" , True , ( 0 , 255 , 0 ) )
045       playerLabel = font.render ( "Player" , True , ( 0 , 255 , 0 ) )
046       rubbleLabel = font.render ( "Rubble" , True , ( 0 , 255 , 0 ) )
047       moveLabel = font.render ( "Move with Q, W, E, A, D, Z, X, C or the arrow keys" , True , ( 0 , 255 , 0 ) )
048       teleportLabel = font.render ( "Teleport with T" , True , ( 0 , 255 , 0 ) )
049
050       self.screen.blit ( robotLabel , ( 75 , 550 ) )
051       self.screen.blit ( playerLabel , ( 75 , 580 ) )
052       self.screen.blit ( rubbleLabel , ( 75 , 610 ) )
053       self.screen.blit ( moveLabel , ( 550 , 550 ) )
054       self.screen.blit ( teleportLabel , ( 550 , 580 ) )
055
056    def drawGrid ( self ):
057       for y in range ( 25 ):
058          for x in range ( 50 ):
059             pygame.draw.rect ( self.screen , ( 0 , 0 , 255 ) , ( ( x * 20 ) , ( y * 20 ) , 20 , 20 ) , 1)
060             if self.grid [ ( x , y ) ] == "ROBOT":
061                pygame.draw.rect ( self.screen , ( 255 , 0 , 0 ) , ( ( x * 20 ) + 1 , ( y * 20 ) + 1 , 18 , 18 ) , 0 )
062             elif self.grid [ ( x , y ) ] == "PLAYER":
063                pygame.draw.rect ( self.screen , ( 0 , 255 , 0 ) , ( ( x * 20 ) + 1 , ( y * 20 ) + 1 , 18 , 18 ) , 0 )
064             elif self.grid [ ( x , y ) ] == "RUBBLE":
065                pygame.draw.rect ( self.screen , ( 255 , 255 , 0 ) , ( ( x * 20 ) + 1 , ( y * 20 ) + 1 , 18 , 18 ) , 0 )
066             else:
067                pygame.draw.rect ( self.screen , ( 0 , 0 , 0 ) , ( ( x * 20 ) + 1 , ( y * 20 ) + 1 , 18 , 18 ) , 0 )
068
069       pygame.display.flip()
070
071    def checkWinLose ( self ):
072       mycount = 0
073       for index , bot in enumerate ( self.robots ):
074 #DBG         print "checkWinLose", index, bot, self.grid [ bot ], "mycount =", mycount
075          if self.grid [ bot ] == "ROBOT":
076             mycount += 1
077
078 #DBG      print "nRobots =", mycount
079       if mycount == 0:
080          return "WIN"
081       elif ( self.playerX , self.playerY ) in self.robots:
082          return "LOSE"
083       else:
084          return None
085
086    def moveBots ( self ):
087       for index , bot in enumerate ( self.robots ):
088 #DBG         print "move bot", index, bot,
089
090          if self.grid [ bot ] == "RUBBLE":
091 #DBG            print " rubble"
092             continue
093
094          self.grid [ bot ] = ""
095
096          botx , boty = bot
097 #         botx = bot [ 0 ]
098 #         boty = bot [ 1 ]
099          if botx  > self.playerX: botx -= 1
100          elif botx < self.playerX: botx += 1
101
102          if boty > self.playerY: boty -= 1
103          elif boty < self.playerY: boty += 1
104
105          bot = ( botx , boty )
106
107          self.robots [ index ] = bot
108 #DBG         print " to", bot
109
110          if self.grid [ bot ] == "PLAYER":
111             return
112
113          if self.grid [ bot ] == "ROBOT":
114             self.grid [ bot ] = "RUBBLE"
115 #DBG            print "Collision"
116 #DBG            for j , jbot in enumerate ( self.robots ):
117 #DBG               if j == index:
118 #DBG                  continue
119 #DBG               if bot == jbot:
120 #DBG                  print "Collision with", j, jbot
121
122          if self.grid [ bot ] == "RUBBLE":
123             continue
124
125          self.grid [ bot ] = "ROBOT"
126
127    def checkGrid ( self , position ):
128       result = False
129       # Check left
130       check = ( position [ 0 ] - 1 , position [ 1 ] )
131       if check in self.robots: result = True
132       # Check right
133       check = ( position [ 0 ] + 1 , position [ 1 ] )
134       if check in self.robots: result = True
135       # Check up
136       check = ( position [ 0 ] , position [ 1 ] - 1 )
137       if check in self.robots: result = True
138       # Check down
139       check = ( position [ 0 ] , position [ 1 ] + 1 )
140       if check in self.robots: result = True
141
142       return result
143
144    def run ( self ):
145       running = True
146       while running:
147          for event in pygame.event.get():
148             if event.type == pygame.KEYDOWN:
149                if event.key == pygame.K_DOWN or event.key == ord ( "x" ):
150                   self.grid [ ( self.playerX , self.playerY ) ] = ""
151                   self.playerY += 1
152                   self.grid [ ( self.playerX , self.playerY ) ] = "PLAYER"
153                elif event.key == pygame.K_UP or event.key == ord ( "w" ):
154                   self.grid [ ( self.playerX , self.playerY ) ] = ""
155                   self.playerY -= 1
156                   self.grid [ ( self.playerX , self.playerY ) ] = "PLAYER"
157                elif event.key == pygame.K_RIGHT or event.key == ord ( "d" ):
158                   self.grid [ ( self.playerX , self.playerY ) ] = ""
159                   self.playerX += 1
160                   self.grid [ ( self.playerX , self.playerY ) ] = "PLAYER"
161                elif event.key == pygame.K_LEFT or event.key == ord ( "a" ):
162                   self.grid [ ( self.playerX , self.playerY ) ] = ""
163                   self.playerX -= 1
164                   self.grid [ ( self.playerX , self.playerY ) ] = "PLAYER"
165                elif event.key == ord ( "e" ):
166                   self.grid [ ( self.playerX , self.playerY ) ] = ""
167                   self.playerX += 1
168                   self.playerY -= 1
169                   self.grid [ ( self.playerX , self.playerY ) ] = "PLAYER"
170                elif event.key == ord ( "q" ):
171                   self.grid [ ( self.playerX , self.playerY ) ] = ""
172                   self.playerX -= 1
173                   self.playerY -= 1
174                   self.grid [ ( self.playerX , self.playerY ) ] = "PLAYER"
175                elif event.key == ord ( "z" ):
176                   self.grid [ ( self.playerX , self.playerY ) ] = ""
177                   self.playerX -= 1
178                   self.playerY += 1
179                   self.grid [ ( self.playerX , self.playerY ) ] = "PLAYER"
180                elif event.key == ord ( "c" ):
181                   self.grid [ ( self.playerX , self.playerY ) ] = ""
182                   self.playerX += 1
183                   self.playerY += 1
184                   self.grid [ ( self.playerX , self.playerY ) ] = "PLAYER"
185                elif event.key == ord ( "t" ):
186                   self.grid [ ( self.playerX , self.playerY ) ] = ""
187                   self.playerX = random.randrange ( 1 , 50 )
188                   self.playerY = random.randrange ( 1 , 25 )
189                   self.grid [ ( self.playerX , self.playerY ) ] = "PLAYER"
190                elif event.key == ord ( "p" ):
191                   running = False
192                self.moveBots()
193                over = self.checkWinLose()
194                if over != None:
195                   if over == "WIN": print "You survived!"
196                   elif over == "LOSE": print "Looks like the robots got you this time!"
197                   running = False
198
199 #DBG                  print "Player", self.playerX, self.playerY
200 #DBG                  for index , bot in enumerate ( self.robots ):
201 #DBG                     print "bot", index, bot, self.grid [ bot ]
202
203                self.drawGrid()
204
205 pygame.display.init()
206 screen = pygame.display.set_mode ( ( 1024 , 768 ) )
207
208 game = robotsGame ( screen , 25 )
209 game.drawGrid()
210 game.run()

The robotsGame Class

The import tool is Python's way of loading external libraries [1]. On line 1, I load PyGame [2], and line 2 loads the random module, which will be used to generate the player and the robot starting locations.

The robotsGame class occupies lines 4 through 202. A class is a collection of variables and functions. To use a class, it has to be instantiated (which happens on line 208). Instantiating creates a copy of the class that you can manipulate. In the case of the robotsGame, there's only one copy; but, classes can be instantiated more than once. Each copy (stored in its own variable, or even a list or dictionary) is independent of any others. Values will not cross over between class instances, and functions within a class will only operate on their own internal variables.

When a class is initialized, the __init__ function is called automatically (lines 5-34). The arguments for __init__ must be provided when instantiating the class or else the initialization will fail. Here, the function is looking for a reference to the screen variable (created when you initialize PyGame) and the number of robots to place on the grid.

On line 6, the screen variable is stored into the class itself by using the self variable; self.<anything> is available to any function within the class.

Line 8 initializes self.grid as a dictionary; self.grid is where the board will be stored. Lines 9 and 10 initialize the entire grid to None. Note that the tuple (x,y) is the dictionary key. Line 13 initializes self.robots as a list, which is the list of robot positions.

Line 14 is the start of a loop that runs once for every robot that should populate the initial grid. startingRobots is passed to __init__ as a parameter – the number of robots to place on the grid.

The while 1: on line 15 starts an infinite loop. Lines 16 and 17 generate random values for x and y; random.randrange takes two parameters, the lowest and highest possible values (inclusive). It will return a random number within those bounds.

Line 19 checks whether the generated coordinates are already in the robots list. If the value exists, then I've generated a set of coordinates that's already full. Otherwise, I add it to the list, (line 20) and mark the grid as having a robot at the listed coordinates (line  21). Because I've successfully added a robot, break (line 22) exits the infinite loop. This process will repeat until all of the robot coordinates have been generated.

Lines 24 through 32 work almost exactly the same way. The infinite loop repeats until a set of coordinates can be found that place the player in a safe starting position (not next to a robot). The self.checkGrid function will return True if a neighboring space holds a robot or False if it's clear. Once a safe starting location is found, the loop exits.

Line 34 calls self.legend, which draws the legend and player instructions below the grid.

The legend Function

The legend function doesn't take any arguments and draws the player instructions. Lines 37 through 39 draw one square of each color that is used on the grid.

Line 41 initializes the PyGame font module (all PyGame modules must be initialized before use), and line 42 gets the default system font; pygame.font.SysFont gets a font that matches the provided name. If no font with the provided name can be found, then it provides a default font. The second parameter is the size of the font in pixels.

Lines 44 through 48 create surfaces with instructions and labels; font.render returns a PyGame surface with the provided text rendered in the specified color. The first parameter is the text to render, the second parameter is whether or not to antialias the font, and the third is the color in which to render (as a tuple).

Lines 50 through 54 blit (copy) the provided surface at the designated location; self.screen is the PyGame surface that is the actual display. Its blit function accepts a source surface (first argument) and where to put it (second argument).

Note that the display isn't flipped after drawing the legend. The drawGrid function flips (redraws) the display after every move. I'll also call the drawGrid function as the game starts. This saves a re-draw, which is generally a slow operation.

Buy this article as PDF

Express-Checkout as PDF

Pages: 8

Price $2.95
(incl. VAT)

Buy Raspberry Pi Geek

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content