Lab 10: Piper Game Due: 07:00:00AM on 2021-12-09

FAQ

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 attributes of the Entity class) without changing any code within your game loop!

Piper game

Getting Started

  1. We will be using the PyGame module to implement some of the mechanics of game play. Install the PyGame module much as you did for the 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.
  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 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:

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 attribute 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 attribute in the Player class assigned the value returned by the pygame.image.load function. Why an attribute? Since the image is created in one method (__init__) and used in another render, we need to store that image as an attribute that persists between those method calls. Use the pygame.transform.scale function to resize the image attribute 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 attribute 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 attribute 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 attribute 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 attribute 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

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.

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:

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.

Grading

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

FAQ Excerpts Click entry title for more information

Credits: This assignment was adapted from Philip Caplan.