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()
|