#! /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()

