Note: On the this lab you will again be able to work in pairs if you want to do so. If you do work with a teammate, you must both be there whenever you’re working on the lab. Only one of you should submit the assignment, but make sure both your names are in the comment at the top of the file and you add your partner to your Gradescope submission.
In this lab you will be implementing the “Piper Game” using Object-Oriented Programming (OOP) techniques. The sand piper must race the clock to collect as many clams on the beach without getting wet. A screencast of a run of the game is shown below.
Recall that two benefits of OOP are encapsulation/abstraction and reuse. We will explore both by specializing a provided Entity
class to implement the different entities in the game, e.g. the piper, and implementing methods to abstract the operations (like moving or rendering) on those entities. As an example of successful abstraction you should be able to change how you store the position and size of the game entities (i.e., change the instance variables of the Entity
class) without changing any code within your game loop!
matplotlib
module (as described in class). After installation you should be able to execute import pygame
in the shell without any error. If you have trouble installing pygame
, please visit office hours or consult with the ASIs.For this lab you will be extending the starter file into a fully fledged game. Make sure to read the entire assignment thoroughly and follow the instructions exactly.
At a minimum your program must include the following classes:
Entity
, which is provided in the skeleton, will serve as the parent/base class for the other classes. Some of the methods are implemented for you. You will need to implement the collide
method.Player
, which is derived (inherits from) from Entity
and implements an __init__
method and a render
method to draw itself on the screenClam
, which is derived (inherits from) from Entity
and implements an __init__
method and a render
method to draw itself on the screenWave
, which is derived (inherits from) Entity
and implements an __init__
method andrender
method to draw itself on the screenYour program must also contain a play_game
function, which has one parameter, max_time
, the time in seconds for the game. play_game
should only be invoked when your program is run, i.e., when __name__
is "__main__"
, not when it is imported. An incomplete implementation of play_game
is included in the skeleton.
The above methods represent the minimum. You are encouraged to implement additional methods if needed.
The PyGame screen and a few other aspects of the PyGame engine are initialized for you. The size of the screen is set by a pair of constants at the top of the file. Any computations that involve the screen size should use those constants. PyGame specifies the upper left corner as 0,0. Thus increasing the “y” coordinate for any element, i.e., a positive shift, moves that element “down”.
The Entity
class serves as the base class for all of the other game elements. All entities have a PyGame Rect
instance variable that is used to track their position and size. You will need to extend the Entity
class with a method collide
that has one parameter (in addition to self
), another Entity
, and returns True
if the two entities overlap. By implementing this method in Entity
, it will be inherited by all of the other games entities that derive from Entity
. For example:
>>> e1 = Entity(0, 0, 50, 50)
>>> e2 = Entity(25, 25, 50, 50)
>>> e1.collide(e2)
True
>>> e3 = Entity(75, 75, 25, 25)
>>> e1.collide(e3)
False
The Player
class should derive from Entity
. Its __init__
method should take no parameters other than self
and should initialize the player in the top-left corner of the screen with a size of 50×50. Recall that a derived class should invoke its base class’s initializer with super().__init__
.
The player will appear on screen as the piper image you previously downloaded. To do so create an image
instance variable in the Player
class assigned the value returned by the pygame.image.load
function. Why an instance variable? Since the image is created in one method (__init__
) and used in another render
, we need to store that image as an instance variable that persists between those method calls. Use the pygame.transform.scale
function to resize the image instance variable to match the size of its rectangle. Note that like functions on strings, pygame.transform.scale
does not modify its argument, it returns a new image. Since Player
inherits from Entity
it can access the rect
instance variable via self.rect
.
Player
should implement a render
method that has two parameters, self
and the PyGame display created in the play_game
function. It should use the blit
method on that display to draw its image
instance variable at the current location of the player’s rectangle.
Once you have implemented the above, create a single Player
object prior to the main game loop (in the section with the “Initialize Player, Wave and Clams” comment). Invoke its render
method with screen
as the argument in the section with the comment “Draw all of the game elements”. Note that the order matters, we want to draw the background first, then the clams, then the piper, then the wave (so everything is properly layered), so make sure to render the Player
after the screen.fill
method. You should now be able to see the piper on the screen!
Next modify the event handling conditional to shift the piper based on the player’s key presses. Each key press should shift the piper by the amount specified in the STEP
constant. With that modification you should now be able to move the piper around the screen!
The Clam
class should derive from Entity
. Its __init__
method should take no parameters other than self
and should initialize the clam randomly in the right-half of the screen (the part of the screen touched by the wave) with a size of 30×30. Thus each clam should have a random x-coordinate between 0.5*SCREEN_WIDTH
and SCREEN_WIDTH-30
, and a random y-coordinate between 0 and SCREEN_HEIGHT-30
. Similar to Player
, the clam should appear as the image you downloaded earlier, loaded into an image
instance variable and scaled to match the size of its rectangle.
Create a list of Clam
objects before entering the game loop containing NUM_CLAMS
clams (NUM_CLAMS
is a constant pre-defined at the top of the starter file). Inside the game loop render those clams after the background but before the wave (so they are “covered” by the wave). Similar to Player
implement a render
method to draw the clam image on the display at the current location of the clam’s rectangle.
The objective of the player is to gather clams. If the piper overlaps (collides) with a clam, that clam is collected. Inside your game loop implement another loop to check if the piper overlaps any of the clams (your collide
method in Entity
will be helpful here!). If so, increment the score by 1.
When the piper collects a clam, that clam should disappear. One way to do so is to add a boolean instance variable to the Clam
class that specifies whether that clam is visible
, and thus should be drawn (and is eligible to be collected). Modify the Clam.render
method to only draw the clam if visible and modify your “collection” loop to only collect visible clams. When a clam is collected it should be made invisible.
You should now be able to move the piper around the screen collecting all the clams (and increase your score accordingly!).
We will model the wave as a blue rectangle the same size as the screen that periodically moves back and forth over the right half of the screen - like a wave (that is, some or all of the rectangle will “hang off” the right side of the screen at any moment in time and not be displayed). The Wave
class should inherit from Entity
. Its __init__
method should take no parameters other than self
and should initialize the wave at 0.75*SCREEN_WIDTH
, 0 (the middle of its movement) with a size of SCREEN_WIDTH
×SCREEN_HEIGHT
.
Wave
should implement a render
method that has two parameters, self
and the PyGame display created in the play_game
function. It can use the pygame.draw.rect
function to draw its rectangle on the display. The first argument to draw
will be the display, the second the color ((0, 0, 255)
for blue) and the third the rectangle to draw.
Create a single Wave
object prior to the main game loop (in the section with the “Initialize Player, Wave and Clams” comment). Invoke its render
method with screen
as the argument in the section with the comment “Draw all of the game elements”. Make sure to render the wave last so it is “on top” of all the other elements.
The wave will move back and forth in time (like a real wave!). You should model the x-coordinate of the left-side of the wave as
\(x(t)=0.75\cdot w - 0.25\cdot w\cdot\sin(t)\)
where t is the time
variable in the game loop and w is the SCREEN_WIDTH
. With this expression the left edge of the wave should oscillate between 0.5*SCREEN_WIDTH
and SCREEN_WIDTH
, i.e., the right half of the screen. Implement the above expression in the game loop to set the x-coordinate of the wave object. With this implemented, you should now be able to watch the wave oscillate back and forth!
The piper does not like to get hit by a wave and so it is game over if the piper touches the water. Add a conditional to check if the piper has collided with the wave, and if so, terminate the game loop early.
Every time the wave washes into shore it brings a new group of randomly distributed clams (i.e., the clams regenerate). Implement a conditional that when the wave is near its left-most terminus, i.e. the x-coordinate is less than 0.51*SCREEN_WIDTH
(recall our discussion of the challenges comparing floating point values as to why we don’t check if the x-coordinate is equal to 0.5*SCREEN_WIDTH
), you replace your previous clams (some of which may have been collected, and some not) with a new group of clams (i.e., NUM_CLAMS
new clams). You could do so by overwriting the current list of clams.
You may earn up to 2 creativity points on this assignment. Below are some ideas, but you may incorporate your own if you’d like. Make sure to document your extra point additions in the docstring at the top of the file.
%Run lab10_piper.py
should play for the default time, %Run lab10_piper.py 20
would play for 20 seconds.[2 points] The Clam
and the Player
share common functionality (both render an image to the screen with the same size as their rectangle). To DRY it up, implement a common base class for Clam
and Player
that derives from Entity
. This new class would implement the functionality in common between the two classes. To do so you will likely need to add an argument to its __init__
function to specify the image that should be displayed. With this modification Clam
and Player
will not need their own render
methods and can instead use the inherited method!
SCREEN_WIDTH
). Don’t change where the clams are generated.You should have a fully operational game! Make sure that the game only starts when the program is run (i.e., with the green arrow). Nothing should happen when your program is imported.
Make sure that your program is properly documented:
In addition, make sure that you’ve used good code design and style (including helper functions for repeated computations, meaningful variable names, constants where relevant, vertical white space, etc.).
Submit your program via Gradescope. Your program program file must be named lab10_piper.py. You do not need to upload the image files, they are already available on Gradescope. You can submit multiple times, with only the most recent submission (before the due date) graded. Note that the tests performed by Gradescope are limited. Passing all of the visible tests does not guarantee that your submission correctly satisfies all of the requirements of the assignment.
Gradescope will import your file for testing so make sure that no code executes on import. That is when imported your program should not try to play the game.
Features | Points |
---|---|
Entity |
2 |
Player (including movement) |
4 |
Clam |
4 |
Wave (including movement) |
5 |
Game mechanics (picking up and regenerating clams, rendering order, game over) | 5 |
Code design and style | 5 |
Creativity points | 2 |
Total | 27 |
Credits: This assignment was adapted from Philip Caplan.