The initial setup involves copying python and image from the instructor's account to your own, and making each of the programs executable.
cd games mkdir lab9 cd lab9Next, copy across a whack of files for this week's lab: backdrop.gif, finish.gif, game9.py, ghost1.gif, ghost2.gif, grass.gif, lake.gif, map0.txt, map1.txt, map2.txt, map3.txt, map4.txt, map5.txt, map6.txt, music.mid, wall.gif, win.wav.
Run the chmod command to make any python scripts executable:
chmod u+x *.py
Open the program with your editor:
gedit game9.py &
And run the program to test it out:
./game9.py
This completes the setup, you can now proceed to the actual lab exercises below
The objective here is to have a bunch of text files that each contain a description of a different map, and when we want to play the game we get it to load one of the maps and create the appropriate display.
To read the map in, our program will need to know the format we decide on for the text file.
In this example, I'll have the file start with three numbers: the size of the tiles in pixels, the number of map rows, and the number of map columns.
After that, we'll have the actual ASCII map. Here using ~ to represent water, * to represent walls, f to represent the finish flag, and blank spaces to represent grass:
32 15 15 *************** * * * * * **** * *f* * * * * *** * **** **** * * * * * * * ~ *** * * * ~~~ * * ****~~~~ * * * ~* **** * * * * * * ***** * ** * * * * ** ** * * * * *************** |
To read data from a file we go through several steps:
The syntax for doing this in any given programming language is sometimes a trifle odd, but here's an example of the Python approach:
# ===================================================================== # READMAP ROUTINE # read a text version of the map from the specified file # terrain text file in-game # walls '*' 'w' # grass ' ' 'g' # finish 'f' 'f' # lake water '~' 'l' def readMap(fname): # global map variables needed global tilePixels, mapRows, mapCols, defaultMap, scrSize, scrWidth, scrHeight # check the file actually exists if not os.path.isfile(fname): return False # open the file in read mode fileID = open(fname, 'r') # read the 1st line of the file, containing the pixels per tile tilePixels = int(fileID.readline()) # read the 2nd line of the file, containing the number of map rows mapRows = int(fileID.readline()) # read the 3rd line of the file, containing the number of map columns mapCols = int(fileID.readline()) # calculate the needed size of display scrSize = scrWidth, scrHeight = mapRows * tilePixels, mapCols * tilePixels # echo the information print "map read", fname, "rows:", mapRows, "cols:", mapCols, "tilesize:", tilePixels print " screen size:", scrWidth, "x", scrHeight # start the map off as a blank list defaultMap = [] # read each row of the map from the file # and append it to the map r = 0 while (r < mapRows): defaultMap.append([]) # read each column of the row # and append it to the row c = 0 while (c < mapCols): defaultMap[r].append([]) tile = fileID.read(1) if (tile == '*'): defaultMap[r][c] = 'w' # wall tile elif (tile == '~'): defaultMap[r][c] = 'l' # lake tile elif (tile == 'f'): defaultMap[r][c] = 'f' # flag tile else: defaultMap[r][c] = 'g' # default is grass tile c = c + 1 fileID.read(1) # get rid of the newline r = r + 1 # close the file and return fileID.close() return True |
Now, suppose something goes wrong - the file contains bad data, or the file is missing, or is too big, or a variety of other things that could go wrong. Maybe we should introduce a fallback routine, that can build a default map if the attempt to read a map goes awry.
This looks an awful lot like our original definition of the various global map variables, just stuck in a function this time.
# ===================================================================== # DEFAULT MAP SETUP ROUTINE # - relies on a hardcoded map def useDefaults(): global defaultMap, tilePixels, mapRows, mapCols, scrSize, scrWidth, scrHeight # 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'] ] |
Of course, somewhere we need to try to read in the map, see if it worked, and use the default routine otherwise.
The logical place for this is in the setup routine, before we start our display (since we're calculating the size of the display based on the size of the tiles and the number of map rows and columns).
# try reading the custom map if not readMap(mapName): print "map reading failed, using default map" useDefaults() |
Let's throw in one more test, checking to see if the map we've created will actually fit on the screen the player is playing the game on.
This can be done using pygame's display.list_modes() routine:
# check the size of the user's screen against the # size of the map/display we just created, and for # now just print a message if there isn't enough room [(userScrWidth, userScrHeight)] = pygame.display.list_modes() if (userScrWidth < scrWidth) or (userScrHeight < scrHeight): print "***********************************************" print "*** WARNING: DISPLAY WILL NOT FIT ON SCREEN ***" print "***********************************************" |
If we know we have a variety of map files to choose from, we can add a bit of unpredictability by having the game randomly choose which map to load at startup.
This would happen in the setup routine, just before calling readMap
# randomly pick one of the 7 custom maps mapName = 'map0.txt' choice = random.randint(0,100) if (choice < 14): mapName = 'map1.txt' elif (choice < 29): mapName = 'map2.txt' elif (choice < 44): mapName = 'map3.txt' elif (choice < 58): mapName = 'map4.txt' elif (choice < 72): mapName = 'map5.txt' elif (choice < 86): mapName = 'map6.txt' |
We'll add some music and sound effects, but we'll also add a global flag that lets us turn them off. This is important, since they must be turned off when running them in the lab.
useSound = False # set to true when not used in lab 102 |
In the main routine, sometime before the beginning of the keepPlaying loop, we'll start the background music playing:
# start the background music global useSound if useSound: pygame.mixer.music.load('music.mid') pygame.mixer.music.play(-1) |
In the routine that checks for a win, if someone has won we'll stop the music and play a victory sound:
if win: global useSound if useSound: # stop the music pygame.mixer.music.stop() # play the victory jingle victory = pygame.mixer.Sound('win.wav') victory.play() |
Here's the full beast, with all the parts in the right places:
#! /usr/bin/python import sys, os, pygame, random # ===================================================================== # GLOBAL VARIABLES useSound = False # set to true when not used in lab 102 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() # randomly pick one of the 7 custom maps mapName = 'map0.txt' choice = random.randint(0,100) if (choice < 14): mapName = 'map1.txt' elif (choice < 29): mapName = 'map2.txt' elif (choice < 44): mapName = 'map3.txt' elif (choice < 58): mapName = 'map4.txt' elif (choice < 72): mapName = 'map5.txt' elif (choice < 86): mapName = 'map6.txt' # try reading the custom map if not readMap(mapName): print "map reading failed, using default map" useDefaults() # initialize the display screen pygame.init() gameScreen = pygame.display.set_mode(scrSize) backImage = pygame.image.load('backdrop.gif') gameScreen.blit(backImage, (0,0)) # check the size of the user's screen against the # size of the map/display we just created, and for # now just print a message if there isn't enough room [(userScrWidth, userScrHeight)] = pygame.display.list_modes() if (userScrWidth < scrWidth) or (userScrHeight < scrHeight): print "***********************************************" print "*** WARNING: DISPLAY WILL NOT FIT ON SCREEN ***" print "***********************************************" # 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 # ===================================================================== # DEFAULT MAP SETUP ROUTINE # - relies on a hardcoded map def useDefaults(): global defaultMap, tilePixels, mapRows, mapCols, scrSize, scrWidth, scrHeight # 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'] ] # ===================================================================== # READMAP ROUTINE # read a text version of the map from the specified file # terrain text file in-game # walls '*' 'w' # grass ' ' 'g' # finish 'f' 'f' # lake water '~' 'l' def readMap(fname): # global map variables needed global tilePixels, mapRows, mapCols, defaultMap, scrSize, scrWidth, scrHeight # check the file actually exists if not os.path.isfile(fname): return False # open the file in read mode fileID = open(fname, 'r') # read the 1st line of the file, containing the pixels per tile tilePixels = int(fileID.readline()) # read the 2nd line of the file, containing the number of map rows mapRows = int(fileID.readline()) # read the 3rd line of the file, containing the number of map columns mapCols = int(fileID.readline()) # calculate the needed size of display scrSize = scrWidth, scrHeight = mapRows * tilePixels, mapCols * tilePixels # echo the information print "map read", fname, "rows:", mapRows, "cols:", mapCols, "tilesize:", tilePixels print " screen size:", scrWidth, "x", scrHeight # start the map off as a blank list defaultMap = [] # read each row of the map from the file # and append it to the map r = 0 while (r < mapRows): defaultMap.append([]) # read each column of the row # and append it to the row c = 0 while (c < mapCols): defaultMap[r].append([]) tile = fileID.read(1) if (tile == '*'): defaultMap[r][c] = 'w' # wall tile elif (tile == '~'): defaultMap[r][c] = 'l' # lake tile elif (tile == 'f'): defaultMap[r][c] = 'f' # flag tile else: defaultMap[r][c] = 'g' # default is grass tile c = c + 1 fileID.read(1) # get rid of the newline r = r + 1 # close the file and return fileID.close() return True # ==================================================================== # 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: global useSound if useSound: # stop the music pygame.mixer.music.stop() # play the victory jingle victory = pygame.mixer.Sound('win.wav') victory.play() 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() # start the background music global useSound if useSound: pygame.mixer.music.load('music.mid') pygame.mixer.music.play(-1) # 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, 'w', 'clockwise', 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 display pygame.display.quit() # ===================================================================== # INITIATE THE GAME # - calls the main() routine if __name__ == "__main__": main() # shutdown the script sys.exit() |