Robots, Rubble, and Python
We show how to use PyGame to implement a fully playable version of the classic "robots" game.
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.
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.
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 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 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.
Pages: 8
Price $15.99
(incl. VAT)