CSCI 171: Lab exercise 2 sample solution


Part 1: functions

The two programs part1a.py and part1b.py are identical except for the use of a function to encapsulate one segment of code.

Your objective for this part of the exercise is to run the two programs, write the output each produces in the table below, and then explain why they differ the way they do based on the code and the output.
part1a.py part1b.py
#! /usr/bin/python

print "progress check A"

lives = 3
while (lives > 0):
   print "lives left: ", lives
   lives = lives - 1

print "progress check B"

#! /usr/bin/python

print "progress check A"

def mainGame(numLives):
   lives = numLives
   while (lives > 0):
      print "lives left: ", lives
      lives = lives - 1

print "progress check B"

mainGame(4)

print "progress check C"

mainGame(2)

print "progress check D"

Output from part1a.py
progress check A
lives left:  3
lives left:  2
lives left:  1
progress check B
Output from part1b.py
progress check A
progress check B
lives left:  4
lives left:  3
lives left:  2
lives left:  1
progress check C
lives left:  2
lives left:  1
progress check D
Explanation of the difference in output between 1a and 1b
part1a executes each of the statements in the order they appear,
   i.e. check A, 3 passes through the while loop, then check B

part1b executes check A,
       then the DEFINITION of the mainGame routine (containing the loop)
            but this doesn't actually execute the loop yet
       then check B
       then calls mainGame with a value of 4,
            getting it to make 4 passes through the while loop
       then check C
       then calls mainGame with a value of 2,
            getting it to make 2 passes through the while loop
       then check D

Part 2: Objects

The two programs part2a.py and part2b.py are similar except for the use of classes to group data and functions into objects.

Your objective for this part of the exercise is to run the two programs, write the output each produces in the table below, and then explain what advantages, from a coding perspective, part b might have over part a.

part2a.py part2b.py
#! /usr/bin/python

# create two critters, specify their name and age
bobsName = 'Bob'
bobsAge = 5
philsName = 'Phil'
philsAge = 1

# run the update routine on each once every 'year'
year = 1
while (year <= 3):
   print "It is year", year
   bobsAge = bobsAge + 1
   philsAge = philsAge + 1
   print bobsName, "is now", bobsAge
   print philsName, "is now", philsAge
   year = year + 1

#! /usr/bin/python

class Critter:

   # this routine gets run automatically
   #    when a Critter is first created
   def __init__(self, age, name):
      # store the critter's name and age
      self.myAge = age
      self.myName = name
      
   # this routine ages the critter by a year
   def update(self):
      self.myAge = self.myAge + 1
      print self.myName, "is now", self.myAge


# create a couple of Critters using the class
bob = Critter(5, 'Bob')   # bob starts off at age 5
phil = Critter(1, 'Phil') # phil starts off at age 1

# run the update routine on each once every 'year'
year = 1
while (year <= 3):
   print "It is year", year
   bob.update()
   phil.update()
   year = year + 1

Output from part2a.py
It is year 1
Bob is now 6
Phil is now 2
It is year 2
Bob is now 7
Phil is now 3
It is year 3
Bob is now 8
Phil is now 4
Output from part2b.py

...THE SAME AS part2a...

Possible coding advantages of 2b over 2a
In version 2b, all the code associated with a 'critter'
   is encapsulated in one place (tracking it's name/age,
   updating them, displaying them, etc)

This means that to add new functionality or information to
   all our critters we only need to update the definition
   of the class, and all the critter processing will automatically
   be refined.

This also means that creating a critter is done in a single step
   (the call to the Critter() routine) and calls to update a
   critter's information is done in a single step (by calling
   the .update() routine)

Together, this makes it far easier to use/manipulate/maintain larger
   collections of critters than would be practical if we took the
   method used in part2a, i.e. coding every facet of each critter
   individually.

(The downside is that every critter is restricted to the same core
 functionality, so there is some loss in flexibility if we want all
 the critters to behave substantially differently.)

Part 3: Objects (part 2)

The program part3.py is somewhat similar to the game example we were working on in lab 5, but the code has been substantially reorganized to

  1. Use functions to group different logical portions of the game control, and

  2. use classes to control the behaviour of the moving objects (i.e. the star and the ship) in the game

Your objectives for this part of the exercise are to:

  1. Run the program and describe how the ship and star are controlled (i.e. the effect of the various keypresses and mouseclicks).

  2. Describe in detail the sequence of routines that get called during a typical pass through the "while keepPlaying" loop of the mainGame() function. (For simplicity, you can assume that no collisions or wrap-arounds take place during this pass.)

part3.py code
#! /usr/bin/python

# ---------------------------------------------------------------------
#     **** LIBRARY SELECTION AND GLOBAL VARIABLE SETUP ****
# ---------------------------------------------------------------------
# import and initialize the pygame module
import pygame

# set up default values for the screen sizes
# (these can be overriden by the setup_pygame function)
screenSize = screenWidth,screenHeight = 480,320

# set up a default display and background image for the game
gameDisplay = None
background = None


# ---------------------------------------------------------------------
#     **** PYGAME/DISPLAY SETUP ROUTINE ****
# ---------------------------------------------------------------------
def setup_pygame(width, height):

   # make sure we use the global (shared) versions
   #    of the various screen size variables
   global screenSize, screenWidth, screenHeight
   global gameDisplay, background

   # initialize pygame and the display values
   pygame.init()
   screenSize = screenWidth,screenHeight = width,height
   gameDisplay = pygame.display.set_mode(screenSize)

   # load the background image
   background = pygame.image.load("backdrop.gif")


# ---------------------------------------------------------------------
#  **** CLASS TO DEFINE AND CONTROL OBJECTS THAT MOVE IN THE GAME ****
# ---------------------------------------------------------------------
class movingItem:

   def __init__(self, imageFile, (x,y)):
       # load the original image from a file
       self.Original = pygame.image.load(imageFile)

       # set up the initial facing of the item and rotate 
       #     its image to face that direction
       self.Facing = 0
       self.Image = pygame.transform.rotate(self.Original, self.Facing)

       # set up a box (rectangle) around the item
       self.Box = self.Image.get_rect()

       # set up the initial coordinates for the item
       self.Box.x = x
       self.Box.y = y

       # set the speed so the item is initially stationary
       self.Speed = [0,0]

   def update(self):

      # adjust the object's position based on its current speed
      self.Box = self.Box.move(self.Speed)

      # have the object 'wrap around' if it goes off the screen
      if self.Box.left > screenWidth:
         self.Box.right = 1
      elif self.Box.right < 0:
         self.Box.left = screenWidth - 1
      if self.Box.bottom < 0:
         self.Box.top = screenHeight - 1
      elif self.Box.top > screenHeight:
         self.Box.bottom = 1

      # rotate the object appropriately for its current facing
      self.Image = pygame.transform.rotate(self.Original, self.Facing)


# ---------------------------------------------------------------------
#     **** COLLISION HANDLING ROUTINE FOR THE GAME ****
# ---------------------------------------------------------------------
# this routine handles collisions between two movingItem objects
def handleCollision(item1, item2):
   red = 255,0,0
   if (item1.Box.colliderect(item2.Box)):
      pygame.draw.line(gameDisplay, red, (0, 0), (screenWidth, screenHeight), 5)
      pygame.draw.line(gameDisplay, red, (0, screenHeight), (screenWidth, 0), 5)
      pygame.display.flip()
      pygame.time.delay(1000)
   

# ---------------------------------------------------------------------
#     **** DISPLAY UPDATING ROUTINE FOR THE GAME ****
# ---------------------------------------------------------------------
# this routine handles updating and redrawing the display,
#    assuming it is given the background image and a list
#    of movingItems to update
def updateDisplay(bck, itemList):

   # remember to look up the global display variable
   global gameDisplay

   # redraw the background picture to the screen buffer
   gameDisplay.blit(background, (0,0))

   # redraw the items in the order they appear in the list
   for i in itemList:
      gameDisplay.blit(i.Image, i.Box)

   # update the visible display from the screen buffer
   pygame.display.flip()
   

# ---------------------------------------------------------------------
#     **** EVENT PROCESSING ROUTINE FOR THE GAME ****
# ---------------------------------------------------------------------
# this routine handles all event processing for the game
#    it assumes it is given two movingItems to manipulate,
#    the first one jumps when the player clicks on the display,
#    the second one's speed is controlled by the arrow keys
def handleEvents(jumpingItem, travelingItem):

   # check for all events that have taken place during
   #    this turn (keypresses, mouseclicks, etc)
   #    and process them one at a time
   for event in pygame.event.get():

      # check to see if the player clicked the window close box
      if event.type == pygame.QUIT:
         keepPlaying = False
         print "Player command: close window"

      # if the player clicked on the screen
      #    have the star "jump" to the new location
      if event.type == pygame.MOUSEBUTTONDOWN:
         jumpingItem.Box.center = event.pos

      # check to see if the player pressed any keys
      if event.type == pygame.KEYDOWN:

         # treat the Q key as a quit command
         if event.key == pygame.K_q:
            print "Player command: quit (q)"
            return False

	 elif event.key == pygame.K_UP:
	    travelingItem.Speed = [0, -4]
	    travelingItem.Facing = 45

	 elif event.key == pygame.K_DOWN:
	    travelingItem.Speed = [0, 4]
	    travelingItem.Facing = 225

	 elif event.key == pygame.K_LEFT:
	    travelingItem.Speed = [-4, 0]
	    travelingItem.Facing = 135

	 elif event.key == pygame.K_RIGHT:
	    travelingItem.Speed = [4, 0]
	    travelingItem.Facing = 315

      # end of the events for loop

   # none of the events said to quit, so return True
   return True


# ---------------------------------------------------------------------
#     **** MAIN CONTROL/PROCESSING ROUTINE FOR THE GAME ****
# ---------------------------------------------------------------------
# this routine controls the operational portion of the game,
#    assuming the pygame setup routine has already been run
def mainGame():

   # create the moving star and ship
   star = movingItem('star.gif', (100,100))
   ship = movingItem('ship.gif', (200,200))

   # run the game loop
   keepPlaying = True
   numCollisions = 0
   while keepPlaying:
   
      # update the facing, speed, and position of 
      #    the star and the ship
      ship.update()
      star.update()

      # check for collisions between the star and the ship
      handleCollision(star, ship)

      # redraw the display
      updateDisplay(background, [star, ship])

      # pause for 20 milliseconds before continuing
      pygame.time.delay(30)

      # handle any pending events
      keepPlaying = handleEvents(star, ship)

      # end of the keepPlaying while loop


# ---------------------------------------------------------------------
#     **** RUN THE SETUP AND MAIN GAME ROUTINES ***
# ---------------------------------------------------------------------
setup_pygame(640,480)
mainGame()
print "shutting down the game"
# ---------------------------------------------------------------------
#     **** END OF PROGRAM ****
# ---------------------------------------------------------------------

Description of ship/star control and behaviour
 - the arrow keys control the direction of the ship, the speed is fixed
 - q ends the game (clicking the close box does not)
 - clicking the mouse makes the star jump to the designated spot
 - the ships 'wrap around' when they go off a map edge
 - when the ship and star collide the game paints a red x across
   the screen and pauses for one second
Explanation of the event sequence
on each pass through 'while keepplaying'
   ship update is called
      - the ship is moved
      - wrap around is checked and handled
      - the image is rotated to the current facing
   star update is called
      - the star is moved
      - wrap around is checked and handled
      - the image is rotated to the current facing
   handle collision is called
      - if the ship and star rectangles overlap
           a red x is drawn on the screen buffer
	   the display is flipped to match the buffer
	   the game is paused for one second
   update display is called
      - the background is repainted on the buffer
      - the ship and star are redrawn on the buffer
      - the display is flipped to match the buffer
   the game is paused for 30 milliseconds
   handle events is called
      - each pending event in the queue is processed in turn,
            checking for keypresses and mouse clicks
	    and handling them appropriately
      - true is returned if the game should keep playing,
        false is returned otherwise
   the keepplaying flag is updated to whatever
      value was returned by the handleevents routine