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!

Piper game in action!

Getting Started

  1. 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 installing pygame, please visit office hours or consult with the ASIs.
  2. Download the program starter file
  3. 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 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 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 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 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.