Programming Assignment 7: Piper Game
Initial Due Date: 2024-11-07 8:00AM
Final Due Date: 2024-11-21 4:15PM
Note: On the this assignment 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 present 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 the submitter adds their partner to your Gradescope submission.
Background
In this assignment 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!
Getting Started
- We will be using the PyGame module to implement some of the mechanics of game play. Install the PyGame 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 installingpygame
, please visit office hours or consult with the ASIs. - Download the program starter file
- Download the two images needed for the game: the piper and the clam. These images must be saved to the same directory as your program file.
Specifications
For this assignment 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 thecollide
method.Player
, which is derived (inherits from) fromEntity
and implements an__init__
method and arender
method to draw itself on the screenClam
, which is derived (inherits from) fromEntity
and implements an__init__
method and arender
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 screen
Your 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.
Guide
PyGame Window
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”.
Entity
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
Player
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!
Clam
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!).
Wave
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.
Creativity points
Here are some possible creativity additions, although you are encouraged to include your own ideas. Make sure to document your additions in the docstring comment at the top of the file.
- [1 point] Increase difficulty by reducing the number of clams that are generated as a time elapses.
- [2 points] The
Clam
and thePlayer
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 forClam
andPlayer
that derives fromEntity
. 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 modificationClam
andPlayer
will not need their ownrender
methods and can instead use the inherited method! - [1.5 points] Increase the difficulty of the game by having the wave move higher and higher up the beach. That is, the wave should start with its original amplitude, but when time has elapsed, the left edge of the wave should oscillate between 50 and
SCREEN_WIDTH
). Don’t change where the clams are generated.
When you’re done
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:
- You should have a docstring at the very beginning of the file briefly describing your program and stating your name, section and creativity additions.
- Each class, method and function should have an appropriate docstring (including arguments and return value if applicable).
- Other miscellaneous inline/block comments if the code might otherwise be unclear.
In addition, make sure that you’ve used good code design and style (including helper functions where useful, meaningful variable names, constants where relevant, vertical white space, removing “dead code” that doesn’t do anything, removing testing code, etc.).
Submit your program via Gradescope. Your program program file must be named pa7_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 especially limited for this assignment. 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.
Grading
Assessment | Requirements |
---|---|
Revision needed | Some but not all tests are passing. |
Meets Expectations | All tests pass, the required functions are implemented correctly and your implementation uses satisfactory style. |
Exemplary | All requirements for Meets Expectations, 2 creativity points, and your implementation is clear, concise, readily understood, and maintainable. |
This assignment was adapted from Philip Caplan.