Programming Assignment 7: Piper Game

Initial Due Date: 2025-11-06 8:00AM
Final Due Date: 2025-11-20 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 assignment. 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. This assignment is motivated from the Pixar Short in which a hungry sandpiper (called “Piper”) learns how to gather clams. The sandpiper 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 sandpiper, 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!

Piper game in action!

Getting Started

  1. We will be using the PyGame module to implement some of the mechanics of the game, which should already be installed and available from within your cs146 folder. If you have issues with the pygame installation, please visit office hours or consult with the ASIs.
  2. Download the program starter file to your cs146 folder.
  3. Download the two images needed for the game: the sandpiper (originally from here) and the clam into your cs146 folder. 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 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 screen.
  • Clam, which is derived (inherits from) from Entity and implements an __init__ method and a render method to draw itself on the screen.
  • Wave, 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 called 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 sandpiper 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 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 Piper on the screen!

Next modify the event handling conditional to shift Piper based on the player’s key presses. Each key press should shift Piper by the amount specified in the STEP constant. With that modification you should now be able to move 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 Piper overlaps (collides) with a clam, that clam is collected. Inside your game loop implement another loop to check if Piper overlaps any of the clams (your collide method in Entity will be helpful here!). If so, increment the score by 1.

When 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 it is 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 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 is 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!

Piper does not like to get hit by a wave (at least not at the beginning of the Pixar short) and so it is game over if Piper touches the water. Add a conditional to check if 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.

  • [2 points] Make the story of the game similar to the Pixar short by having Piper “duck” when a wave comes in. Define an additional boolean ducking attribute for Player which is initially False and toggled when the pygame.K_SPACE (the space bar) is held. When this ducking attribute is True, Piper is unbothered by the wave, so the game is not over when the player collides with the wave. However, for complete creativity points, there are some additional things to be careful of:
    • The player should not collect clams when Piper is ducking (i.e. score should not increase when the ducking attribute is True).
    • The player should not be able to moving when Piper is ducking (since Piper is burrowed in the sand…). In other words, disable the arrow key motion when Piper is ducking.
    • The ducking attribute is only True when the space bar is held. As soon as it is released (this happens with event.type == pygame.KEYUP and event.key == pygame.K_SPACE, then ducking should be reset to False.
    • In the Player render method, change the image to blit this ducking image when the ducking attribute is True. In the end, the game should look like this (the main game play should still work as expected and pass the Gradescope tests):

    Extended Piper game in action! Piper ducks when the space bar is held.
  • [1 point] Increase the difficulty by reducing the number of clams that are generated as time elapses.
  • [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!
  • [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 button). Nothing should happen when your program is imported.

Make sure that your program is properly documented:

  • Include your name (and partner’s name, if applicable) and list any 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 since 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.