You will then complete two exercises in revising the maps and the exploration algorithms as the 'marked' portion of the lab, worth 5% of your total course mark, and to be submitted by the start of the lecture session on Thursday April 8th.
A compact version of the exercise portion is available for printing here.
The initial setup involves copying python and image from the instructor's account to your own, and making each of the programs executable.
Turn in the (paper) copy of your work by the deadline noted above.
cd csci171 mkdir lab10 cd lab10Next, copy across some files for this week's lab (from Dave's account to your current directory):
cp ~wesselsd/labfiles/lab10/* .
Run the chmod command to make any python scripts executable:
chmod u+x *.py
This completes the setup, you can now proceed to the actual lab exercises below
As discussed in lectures, one way to simplify map handling is to divide the game map into a collection of rows and columns, and identify a terrain type for each square on the map.
When drawing the map we draw the squares, or tiles, individually - picking a small image appropriate for the terrain in that square.
In today's example, we will use a grid of text characters to store the terrain types for a map divided into 15 rows and 15 columns.
Each tile will be 32 pixels by 32 pixels, making the overall map 480 x 480.
We will have a handful of different terrain types, each represented by a different alphabetic character:
# define the size of the pygame display window scrSize = scrWidth, scrHeight = 480,480 mapRows, mapCols = 15, 15 # the size of the map in rows by columns tilePixels = 32 defaultMap = [ ['w','w','w','w','w','w','w','w','w','w','w','w','w','w','w'], ['w','g','g','g','g','g','g','g','g','g','w','g','g','g','w'], ['w','g','w','g','g','w','w','w','w','g','w','g','w','f','w'], ['w','g','w','g','g','w','g','g','g','g','w','g','w','w','w'], ['w','g','w','w','w','w','g','w','w','w','w','g','g','g','w'], ['w','g','g','w','g','g','g','g','g','g','w','g','g','g','w'], ['w','g','g','w','g','g','g','g','l','g','w','w','w','g','w'], ['w','g','g','w','g','g','l','l','l','g','g','g','g','g','w'], ['w','g','w','w','w','w','l','l','l','l','g','g','g','g','w'], ['w','g','w','g','g','g','g','l','w','g','g','w','w','w','w'], ['w','g','w','g','g','g','g','g','w','g','g','w','g','g','w'], ['w','g','w','w','w','w','w','g','w','g','w','w','g','g','w'], ['w','g','g','g','g','w','g','g','w','g','w','w','g','w','w'], ['w','g','w','g','g','g','g','g','w','g','g','g','g','g','w'], ['w','w','w','w','w','w','w','w','w','w','w','w','w','w','w'] ] |
To look up the kind of terrain in any map square we could use the row and column, e.g. something like:
print "looking up row 3, column 7" t = defaultMap[r][c] if (t == 'w'): print "it is a wall" elif (t == 'g'): print "it is grass" elif (t == 'l'): print "it is a lake" elif (t == 'f'): print "it is the finishing flag" else: print "oops, there is a bad character in the map data"Note: remember that indexing in Python starts at 0, so when there are 15 rows they are numbered 0..14, and that the 15 columns are similarly numbered 0..14.
Part 1 showed how we'll store information about the map internally, but we also need to create a set of terrain objects, the images to draw on the screen as the background (i.e. the pieces of wall, lake, grassland, etc).
We'll create an extension of the Sprite class to represent terrain objects, we'll make sure the setup routine reads through our map description from part 1 and creates the needed terrain objects for the display, and we'll make sure the main routine updates them the same way it updated other game objects in our previous labs.
The class for the terrain objects might look something like this:
class Terrain(pygame.sprite.Sprite): # the constructor (initialization routine) def __init__(self, terrainType, r, c): # initialize a pygame sprite for the object pygame.sprite.Sprite.__init__(self) # establish the terrain attributes global tilePixels self.terrainType = terrainType self.row = r self.col = c self.position = self.x, self.y = r*tilePixels, c*tilePixels if terrainType == 'g': self.image = pygame.image.load('grass.gif') elif terrainType == 'w': self.image = pygame.image.load('wall.gif') elif terrainType == 'f': self.image = pygame.image.load('finish.gif') else: self.image = pygame.image.load('lake.gif') self.rect = self.image.get_rect() # draw the terrain global gameScreen gameScreen.blit(self.image, (self.x, self.y)) def update(self): # redraws the terrain item self.rect.topleft = self.x,self.y |
Our gameSetup routine needs to read our defaultMap and create a seperate object for each map square, keeping them all in a global list, e.g.:
def gameSetup(): # specify the global variables the setup routine needs to access global gameScreen, scrSize, backImage, terrainList, mapRows, mapCols # initialize the randon number generator random.seed() # initialize the display screen pygame.init() gameScreen = pygame.display.set_mode(scrSize) backImage = pygame.image.load('backdrop.gif') gameScreen.blit(backImage, (0,0)) # now build the terrain objects from the text map, # a seperate object is created for each tile on the map r = 0 while r < mapRows: c = 0 while c < mapCols: terrainList.append(Terrain(defaultMap[r][c], r, c)) c += 1 r += 1 |
Finally, our main routine needs to create a group for the terrain objects and update them, just as we would do for character objects in our game, e.g.:
# create a group out of the list of character objects characterGroup = pygame.sprite.RenderUpdates(*characterList) # create a group out of the list of terrain objects (map tiles) terrainGroup = pygame.sprite.RenderUpdates(*terrainList) # run the main game loop keepPlaying = True while keepPlaying: # handle any pending events processEvents() # clear both the character group and the terrain group # update both groups # get a list of changed sections of the screen # as a result of changes to the characters and tiles # and redraw the display characterGroup.clear(gameScreen, backImage) terrainGroup.clear(gameScreen, backImage) characterGroup.update() terrainGroup.update() updateSections = terrainGroup.draw(gameScreen) updateSections += characterGroup.draw(gameScreen) pygame.display.update(updateSections) pygame.display.flip() |
Our AI characters will now keep track of which map square they are in, and will try to explore the map by looking at the grid we created and seeing if they can move into adjacent squares on the map. Here we'll assume the characters can only move around on grass squares.
When we create a character, we'll specify the image for it, the starting map row and column, the direction they face initially (north, south, east, or west), a movement style (random or clockwise, described a little later), and a unique integer id.
E.g. in the main routine the list of created AI might look like:
characterList = [ Character('ghost1.gif', 1, 1, 'e', 'random', 0), Character('ghost2.gif', mapCols - 2, 1, 'n', 'clockwise', 1), ] # create a group out of the list of character objects characterGroup = pygame.sprite.RenderUpdates(*characterList) |
The AI exploration code itself is a little more extensive, so it has been divided into a handful of smaller routines:
If the AI chooses/needs to switch directions then the plotDirection routine is called to choose the new direction.
If the AI is using random movement, then the AI has an equal probability of picking any of the four directions, regardless of whether there is a wall in the way or not.
If the AI is using clockwise movement (hugging the wall) then:
Code for the complete character class, with all the setup and movement routines, is given below.
class Character(pygame.sprite.Sprite): # the constructor (initialization routine) def __init__(self, image, r, c, mdir, moveAlg, cid): # initialize a pygame sprite for the character pygame.sprite.Sprite.__init__(self) # record the character's unique id self.characterID = cid # load an image for the character self.image = pygame.image.load(image) self.rect = self.image.get_rect() # set up the initial position for the character, # both as a map square (row,col) # and as a display (pixel) position (x,y) global tilePixels self.mapLocation = self.row, self.col = r, c self.position = self.x, self.y = r*tilePixels, c*tilePixels # set up the initial direction for the character self.movingDir = mdir # record the movement plotting algorithm the character should use # (e.g. 'random' movement, 'clockwise' movement, etc) self.plotting = moveAlg # check for a winner (adj to finish flag) def checkForWin(self): global keepPlaying win = False if (defaultMap[self.row+1][self.col] == 'f'): win = True elif (defaultMap[self.row-1][self.col] == 'f'): win = True elif (defaultMap[self.row][self.col-1] == 'f'): win = True elif (defaultMap[self.row][self.col+1] == 'f'): win = True if win: print "***************************" print "!!!!! AI number", self.characterID, "won !!!!!" print "***************************" pygame.time.delay(2000) keepPlaying = False return True else: return False # the update routine adjusts the character's current position # and image based on its direction and the local terrain def update(self): # calculate the object's new position based on its old position, # its current direction, and the local map terrain global defaultMap, tilePixels # end the game if a character has found the flag if self.checkForWin(): return # figure out where the character should move next, # based on their plotting algorithm if (self.plotting == 'clockwise'): # characters attempting to follow the walls around the maze # should always run their plotting algorithm self.plotDirection() elif (self.plotting == 'random'): # characters using random plotting have roughly a 1/3 chance # of replotting their direction (i.e. randomly changing # which direction they're going) if (random.randint(0,100) < 34): self.plotDirection() # otherwise, characters using random plotting will try to # keep going in the same direction if possible # if their way turns out to be blocked then they'll # randomly plot a new direction elif not self.checkAndMove(self.movingDir): self.plotDirection() # position the image correctly self.rect = self.image.get_rect() self.rect.topleft = self.x,self.y = self.row*tilePixels, self.col*tilePixels # check to see if you are able to move in the specified direction (n,s,e,w) # if it is possible, i.e. if the target tile is grass, # then move, set your direction movement, and return true # otherwise return false def checkAndMove(self, d): global defaultMap if (d == 'n'): if (defaultMap[self.row-1][self.col] == 'g'): self.row -= 1 self.movingDir = 'n' return True elif (d == 's'): if (defaultMap[self.row+1][self.col] == 'g'): self.row += 1 self.movingDir = 's' return True elif (d == 'e'): if (defaultMap[self.row][self.col+1] == 'g'): self.col += 1 self.movingDir = 'e' return True elif (d == 'w'): if (defaultMap[self.row][self.col-1] == 'g'): self.col -= 1 self.movingDir = 'w' return True return False # plotDirection calculates a new facing for an AI based on # both the surrounding terrain and the AI's destination def plotDirection(self): global defaultMap # in random movement, there is an equal chance of the AI # attempting to move in each of the four directions if (self.plotting == 'random'): choice = random.randint(0,100) if (choice < 25): self.checkAndMove('n') elif (choice < 50): self.checkAndMove('e') elif (choice < 75): self.checkAndMove('w') elif (defaultMap[self.row+1][self.col] == 'g'): self.checkAndMove('s') # in clockwise movement the AI basically tries to hug the wall: # the AI tries to turn clockwise from its current facing, # but if that is blocked the AI tries to keep moving # in its old direction # if that is also blocked, # then the AI tries to turn still further # and if that is also blocked then the AI goes in # the one direction left elif (self.plotting == 'clockwise'): if (self.movingDir == 'n'): # was moving north, see if we can turn east if self.checkAndMove('e'): return # otherwise see if we can go north elif self.checkAndMove('n'): return # otherwise see if we can go west elif self.checkAndMove('w'): return # otherwise go south else: self.checkAndMove('s') elif (self.movingDir == 'e'): # was moving east, see if we can turn south if self.checkAndMove('s'): return # otherwise see if we can go east elif self.checkAndMove('e'): return # otherwise see if we can go north elif self.checkAndMove('n'): return # otherwise go west else: self.checkAndMove('w') elif (self.movingDir == 's'): # was moving south, see if we can turn west if self.checkAndMove('w'): return # otherwise see if we can go south elif self.checkAndMove('s'): return # otherwise see if we can go east elif self.checkAndMove('e'): return # otherwise go north else: self.checkAndMove('n') else: # was moving west, see if we can turn north if self.checkAndMove('n'): return # otherwise see if we can go west elif self.checkAndMove('w'): return # otherwise see if we can go south elif self.checkAndMove('s'): return # otherwise go east else: self.checkAndMove('e') |
|
#! /usr/bin/python import sys, os, pygame, random # ===================================================================== # GLOBAL VARIABLES gameScreen = None # the main display screen scrRefreshRate = 250 # the pause (in milliseconds) between updates keepPlaying = True # flag to identify if the game should continue backImage = None terrainList = [] # define the size of the pygame display window scrSize = scrWidth, scrHeight = 480,480 mapRows, mapCols = 15, 15 # the size of the map in rows by columns tilePixels = 32 defaultMap = [ ['w','w','w','w','w','w','w','w','w','w','w','w','w','w','w'], ['w','g','g','g','g','g','g','g','g','g','w','g','g','g','w'], ['w','g','w','g','g','w','w','w','w','g','w','g','w','f','w'], ['w','g','w','g','g','w','g','g','g','g','w','g','w','w','w'], ['w','g','w','w','w','w','g','w','w','w','w','g','g','g','w'], ['w','g','g','w','g','g','g','g','g','g','w','g','g','g','w'], ['w','g','g','w','g','g','g','g','l','g','w','w','w','g','w'], ['w','g','g','w','g','g','l','l','l','g','g','g','g','g','w'], ['w','g','w','w','w','w','l','l','l','l','g','g','g','g','w'], ['w','g','w','g','g','g','g','l','w','g','g','w','w','w','w'], ['w','g','w','g','g','g','g','g','w','g','g','w','g','g','w'], ['w','g','w','w','w','w','w','g','w','g','w','w','g','g','w'], ['w','g','g','g','g','w','g','g','w','g','w','w','g','w','w'], ['w','g','w','g','g','g','g','g','w','g','g','g','g','g','w'], ['w','w','w','w','w','w','w','w','w','w','w','w','w','w','w'] ] # ===================================================================== # SETUP ROUTINE # - initializes the pygame display screen and background image def gameSetup(): # specify the global variables the setup routine needs to access global gameScreen, scrSize, backImage, terrainList, mapRows, mapCols # initialize the randon number generator random.seed() # initialize the display screen pygame.init() gameScreen = pygame.display.set_mode(scrSize) backImage = pygame.image.load('backdrop.gif') gameScreen.blit(backImage, (0,0)) # now build the terrain objects from the text map, # a seperate object is created for each tile on the map r = 0 while r < mapRows: c = 0 while c < mapCols: terrainList.append(Terrain(defaultMap[r][c], r, c)) c += 1 r += 1 # ==================================================================== # TERRAIN OBJECT # terrain constitutes the background of the game map, # e.g. grass, walls, water, etc # # each terrain object has several properties: # - the image loaded for that terrain object # - the terrain object's pixel position on the display # - the terrain object's map square (row, column) # - the terrain type (grass, lake, wall) class Terrain(pygame.sprite.Sprite): # the constructor (initialization routine) def __init__(self, terrainType, r, c): # initialize a pygame sprite for the object pygame.sprite.Sprite.__init__(self) # establish the terrain attributes global tilePixels self.terrainType = terrainType self.row = r self.col = c self.position = self.x, self.y = r*tilePixels, c*tilePixels if terrainType == 'g': self.image = pygame.image.load('grass.gif') elif terrainType == 'w': self.image = pygame.image.load('wall.gif') elif terrainType == 'f': self.image = pygame.image.load('finish.gif') else: self.image = pygame.image.load('lake.gif') self.rect = self.image.get_rect() # draw the terrain global gameScreen gameScreen.blit(self.image, (self.x, self.y)) def update(self): # redraws the terrain item self.rect.topleft = self.x,self.y # ===================================================================== # CHARACTER OBJECT class Character(pygame.sprite.Sprite): # the constructor (initialization routine) def __init__(self, image, r, c, mdir, moveAlg, cid): # initialize a pygame sprite for the character pygame.sprite.Sprite.__init__(self) # record the character's unique id self.characterID = cid # load an image for the character self.image = pygame.image.load(image) self.rect = self.image.get_rect() # set up the initial position for the character, # both as a map square (row,col) # and as a display (pixel) position (x,y) global tilePixels self.mapLocation = self.row, self.col = r, c self.position = self.x, self.y = r*tilePixels, c*tilePixels # set up the initial direction for the character self.movingDir = mdir # record the movement plotting algorithm the character should use # (e.g. 'random' movement, 'clockwise' movement, etc) self.plotting = moveAlg # check for a winner (adj to finish flag) def checkForWin(self): global keepPlaying win = False if (defaultMap[self.row+1][self.col] == 'f'): win = True elif (defaultMap[self.row-1][self.col] == 'f'): win = True elif (defaultMap[self.row][self.col-1] == 'f'): win = True elif (defaultMap[self.row][self.col+1] == 'f'): win = True if win: print "***************************" print "!!!!! AI number", self.characterID, "won !!!!!" print "***************************" pygame.time.delay(2000) keepPlaying = False return True else: return False # the update routine adjusts the character's current position # and image based on its direction and the local terrain def update(self): # calculate the object's new position based on its old position, # its current direction, and the local map terrain global defaultMap, tilePixels # end the game if a character has found the flag if self.checkForWin(): return # figure out where the character should move next, # based on their plotting algorithm if (self.plotting == 'clockwise'): # characters attempting to follow the walls around the maze # should always run their plotting algorithm self.plotDirection() elif (self.plotting == 'random'): # characters using random plotting have roughly a 1/3 chance # of replotting their direction (i.e. randomly changing # which direction they're going) if (random.randint(0,100) < 34): self.plotDirection() # otherwise, characters using random plotting will try to # keep going in the same direction if possible # if their way turns out to be blocked then they'll # randomly plot a new direction elif not self.checkAndMove(self.movingDir): self.plotDirection() # position the image correctly self.rect = self.image.get_rect() self.rect.topleft = self.x,self.y = self.row*tilePixels, self.col*tilePixels # check to see if you are able to move in the specified direction (n,s,e,w) # if it is possible, i.e. if the target tile is grass, # then move, set your direction movement, and return true # otherwise return false def checkAndMove(self, d): global defaultMap if (d == 'n'): if (defaultMap[self.row-1][self.col] == 'g'): self.row -= 1 self.movingDir = 'n' return True elif (d == 's'): if (defaultMap[self.row+1][self.col] == 'g'): self.row += 1 self.movingDir = 's' return True elif (d == 'e'): if (defaultMap[self.row][self.col+1] == 'g'): self.col += 1 self.movingDir = 'e' return True elif (d == 'w'): if (defaultMap[self.row][self.col-1] == 'g'): self.col -= 1 self.movingDir = 'w' return True return False # plotDirection calculates a new facing for an AI based on # both the surrounding terrain and the AI's destination def plotDirection(self): global defaultMap # in random movement, there is an equal chance of the AI # attempting to move in each of the four directions if (self.plotting == 'random'): choice = random.randint(0,100) if (choice < 25): self.checkAndMove('n') elif (choice < 50): self.checkAndMove('e') elif (choice < 75): self.checkAndMove('w') elif (defaultMap[self.row+1][self.col] == 'g'): self.checkAndMove('s') # in clockwise movement the AI basically tries to hug the wall: # the AI tries to turn clockwise from its current facing, # but if that is blocked the AI tries to keep moving # in its old direction # if that is also blocked, # then the AI tries to turn still further # and if that is also blocked then the AI goes in # the one direction left elif (self.plotting == 'clockwise'): if (self.movingDir == 'n'): # was moving north, see if we can turn east if self.checkAndMove('e'): return # otherwise see if we can go north elif self.checkAndMove('n'): return # otherwise see if we can go west elif self.checkAndMove('w'): return # otherwise go south else: self.checkAndMove('s') elif (self.movingDir == 'e'): # was moving east, see if we can turn south if self.checkAndMove('s'): return # otherwise see if we can go east elif self.checkAndMove('e'): return # otherwise see if we can go north elif self.checkAndMove('n'): return # otherwise go west else: self.checkAndMove('w') elif (self.movingDir == 's'): # was moving south, see if we can turn west if self.checkAndMove('w'): return # otherwise see if we can go south elif self.checkAndMove('s'): return # otherwise see if we can go east elif self.checkAndMove('e'): return # otherwise go north else: self.checkAndMove('n') else: # was moving west, see if we can turn north if self.checkAndMove('n'): return # otherwise see if we can go west elif self.checkAndMove('w'): return # otherwise see if we can go south elif self.checkAndMove('s'): return # otherwise go east else: self.checkAndMove('e') # ===================================================================== # EVENT HANDLING ROUTINE # - processes any pending in-game events def processEvents(): # specify which global variables the routine needs access to global keepPlaying # process each pending event for event in pygame.event.get(): # if the user closed the window set keepPlaying to False # to tell the game to quit playing if event.type == pygame.QUIT: keepPlaying = False # check if the user has pressed a key elif event.type == pygame.KEYDOWN: # the escape and q keys quit the game if event.key == pygame.K_ESCAPE: keepPlaying = False elif event.key == pygame.K_q: keepPlaying = False # ===================================================================== # MAIN GAME CONTROL ROUTINE # - sets up the game and runs the main game update loop # until instructed to quit def main(): # identify any global variables the main routine needs to access global gameScreen, backImage, scrRefreshRate, keepPlaying # initialize pygame and the game's display screen gameSetup() # create a list of characters to add to the display, # giving each of them an image, map row, map column, # facing direction, movement plotting style, and unique id characterList = [ Character('ghost1.gif', 1, 1, 'e', 'random', 0), Character('ghost2.gif', mapCols - 2, 1, 'n', 'clockwise', 1), ] # create a group out of the list of character objects characterGroup = pygame.sprite.RenderUpdates(*characterList) # create a group out of the list of terrain objects (map tiles) terrainGroup = pygame.sprite.RenderUpdates(*terrainList) # run the main game loop keepPlaying = True while keepPlaying: # handle any pending events processEvents() # clear both the character group and the terrain group # update both groups # get a list of changed sections of the screen # as a result of changes to the characters and tiles # and redraw the display characterGroup.clear(gameScreen, backImage) terrainGroup.clear(gameScreen, backImage) characterGroup.update() terrainGroup.update() updateSections = terrainGroup.draw(gameScreen) updateSections += characterGroup.draw(gameScreen) pygame.display.update(updateSections) pygame.display.flip() # pause before initiating the next loop cycle pygame.time.delay(scrRefreshRate) # shut down the game pygame.display.quit() sys.exit() # ===================================================================== # INITIATE THE GAME # - calls the main() routine if __name__ == "__main__": main() |
There are two parts to the actual exercise portion of the lab
(i) changing the code to use a new, smaller, map
(ii) changing the code to use a new wall-hugging algorithm
The code that determines the map size and content in this lab is this section:
scrSize = scrWidth, scrHeight = 480,480 mapRows, mapCols = 15, 15 # the size of the map in rows by columns tilePixels = 32 defaultMap = [ ['w','w','w','w','w','w','w','w','w','w','w','w','w','w','w'], ['w','g','g','g','g','g','g','g','g','g','w','g','g','g','w'], ['w','g','w','g','g','w','w','w','w','g','w','g','w','f','w'], ['w','g','w','g','g','w','g','g','g','g','w','g','w','w','w'], ['w','g','w','w','w','w','g','w','w','w','w','g','g','g','w'], ['w','g','g','w','g','g','g','g','g','g','w','g','g','g','w'], ['w','g','g','w','g','g','g','g','l','g','w','w','w','g','w'], ['w','g','g','w','g','g','l','l','l','g','g','g','g','g','w'], ['w','g','w','w','w','w','l','l','l','l','g','g','g','g','w'], ['w','g','w','g','g','g','g','l','w','g','g','w','w','w','w'], ['w','g','w','g','g','g','g','g','w','g','g','w','g','g','w'], ['w','g','w','w','w','w','w','g','w','g','w','w','g','g','w'], ['w','g','g','g','g','w','g','g','w','g','w','w','g','w','w'], ['w','g','w','g','g','g','g','g','w','g','g','g','g','g','w'], ['w','w','w','w','w','w','w','w','w','w','w','w','w','w','w'] ] |
The first exercise is to replace the map with a map that is 5 rows by 5 columns, with walls around the outside, the finish flag in the center, and grass everywhere else. (This should result in 10 lines of code.)
The code that controls the 'wall-hugging' algorithm for the AI contained in a portion of the plotDirection routine (shown below with the comments removed).
The second portion of the lab exercise is to provide an alternate version of the code that causes the AI to hug the wall in the opposite direction - i.e. counter clockwise instead of clockwise.
elif (self.plotting == 'clockwise'): if (self.movingDir == 'n'): if self.checkAndMove('e'): return elif self.checkAndMove('n'): return elif self.checkAndMove('w'): return else: self.checkAndMove('s') elif (self.movingDir == 'e'): if self.checkAndMove('s'): return elif self.checkAndMove('e'): return elif self.checkAndMove('n'): return else: self.checkAndMove('w') elif (self.movingDir == 's'): if self.checkAndMove('w'): return elif self.checkAndMove('s'): return elif self.checkAndMove('e'): return else: self.checkAndMove('n') else: if self.checkAndMove('n'): return elif self.checkAndMove('w'): return elif self.checkAndMove('s'): return else: self.checkAndMove('e') |