CS21 Lab 6: Graphics, Using Objects

Due Sunday, October 25, before midnight

Goals

The goals for this lab assignment are:

  • Practice with object-oriented programming, creating and using objects

  • Become more comfortable using dot notation to call methods of objects

  • Learn how to draw graphics in python

  • Write code to interact with the mouse

As you write programs, use good programming practices:

  • Use a comment at the top of the file to describe the purpose of the program (see example).

  • All programs should have a main() function (see example).

  • Use variable names that describe the contents of the variables.

  • Write your programs incrementally and test them as you go. This is really crucial to success: don’t write lots of code and then test it all at once! Write a little code, make sure it works, then add some more and test it again.

  • Don’t assume that if your program passes the sample tests we provide that it is completely correct. Come up with your own test cases and verify that the program is producing the right output on them.

  • Avoid writing any lines of code that exceed 80 columns.

    • Always work in a terminal window that is 80 characters wide (resize it to be this wide)

    • In vscode, at the bottom right in the window, there is an indication of both the line and the column of the cursor.

Function Comments

All functions should include a comment explaining their purpose, parameters, return value, and describe any side effects. Please see our function example page if you are confused about writing function comments.

1. Draw a Face

In face.py you will write a program that draws a face. Your face can be that of a human, an animal, a cartoon, a fantasy creature, or something similar.

This is an opportunity to be creative and have some fun with the graphics library.

1.1. Requirements

The code you submit for labs is expected to follow good style practices, and to meet one of the course standards, you’ll need to demonstrate good style on six or more of the lab assignments across the semester. To meet the good style expectations, you should:

  • Write a comment that describes the program as a whole using a comment at the top of the file.

  • All programs should have a main() function.

  • Use descriptive variable names.

  • Write code with readable line length, avoiding writing any lines of code that exceed 80 columns.

  • Add comments to explain complex code segments (as applicable).

  • Write a comment for each function (except main) that describes its purpose, parameters and return value.

In addition, you’ll need to demonstrate good top-down design practices on two or more lab assignments across the semester. To meet the top-down design expectations, you should:

  • Have a main() function that serves as an outline for your program.

  • Your main() function should call other functions that implement subtasks.

  • The functions that are called by main() should be well-named and have good comments explaining their purpose, parameters (including their types), and return value (including its type).

Your program should have the following features:

  • Draw a face with at least 3 different features in addition to the main head shape. Some features include: eyes, nose, mouth, ears, hair, beard, antenna, horns, or teeth.

  • Your face should use at least 2 different shapes, and 3 different colors (background color counts).

  • The first mouse click should have your character say something (in text). Please keep the message friendly.

  • The second mouse click should exit your program (i.e. make sure that your program does not exit before the user clicks the mouse).

1.2. Example Output

Here are some example faces drawn by some past CS21 students, CS21 ninjas, and CS21 staff:

$ python3 face.py
face of a cat saying meow
Figure 1. Jenna
face of a hello kitty saying hello
Figure 2. Anon
face of a felix the cat saying hello
Figure 3. Jocelyn
face of a pig saying oink
Figure 4. Tia
yet another face
Figure 5. Sophie
yet another face
Figure 6. Ernesto
yet another face
Figure 7. Bohou

1.3. Hints/Tips

  • You need to run graphics library programs from CS machines (not remotely connected).

  • Refer to the in-class programs for examples of how to use the graphics library and some different methods of some of the classes.

  • Make use of the Zelle Graphics Reference, and other references off the course Schedule, to find methods that might be useful for setting the color and position of different objects.

  • If you are reproducing two or more identical features, for example you have two eyes that are drawn from the same shapes of the same sizes, remember you can use the clone method to make a copy of a shape object and the move method to move a shape to a new location.

  • The getMouse method of a Window object waits for a mouse click.

  • Remember that the order in which you draw objects that overlap changes the final outcome of what is ultimately drawn.

2. Zombies

For this Halloween-themed program, we’ll create a small click-based game in which the player needs to click zombie faces as they appear in a grid on the screen (kind of like whack-a-mole). At a regular interval (every 0.1 seconds), each grid location has a random chance for a zombie to appear, with the probability going up over time as the game progresses. The goal for the player is to click as many faces as possible and survive as long as they can. The player loses the game when every grid location on the screen contains a zombie.

Because the rate of zombie appearance increases over time, there is no way to win the game — you simply survive as long as you can. The zombie horde is inevitable and cannot be sated:

2.1. One out of N Chance

As we’ve done with other programs, we’ll write this game one function at a time and test as we go along. To start with, let’s write a helper function, one_out_of_N_chance(N) , for generating a one out of N chance at random. This function should take one parameter, an integer, representing how unlikely the chance is to occur. The function should return a boolean value, with True indicating that the rare one out of N chance occurred, or False otherwise.

For example, if the user passes in 2 for N, this function would have a 1/2 (or 50%) chance of returning True. If the user passes in 10, this function would have a 1/10 (or 10%) chance of returning True.

The randint function you used in lab 4 will be helpful again here, as you can check whether randint returned a specific value you might be looking for.

2.1.1. Testing

To test this function, we’ll take advantage of the law of large numbers and run a large number of trials.

For example, you might write a main function that looks like:

def main():
    successes = 0
    trials = 100000
    for i in range(trials):
        if one_out_of_N_chance(10):
            successes = successes + 1
    print("In %d trials with a 10%% chance of success..." % (trials))
    print("We succeeded %d times: %.2f%%" % (successes, 100*successes/trials))

    successes = 0
    trials = 100000
    for i in range(trials):
        if one_out_of_N_chance(2):
            successes = successes + 1
    print("In %d trials with a 50%% chance of success..." % (trials))
    print("We succeeded %d times: %.2f%%" % (successes, 100*successes/trials))

With 100,000 trials, you should see close to 50,000 successes when N is 2 and around 10,000 successes when N is 10.

2.2. Creating the Grid

Next, we’ll write a draw_grid function to divide up a graphics window into a 5x5 grid with some space at the bottom for a timer and score. This function will take one parameter, a Zelle graphics window object, and it doesn’t return anything. Instead, it has the side effect of drawing a grid of boxes on the window.

The code provided to you in main already contains:

    # Create a graphics window and set the coordinates to make it easier
    # to draw a 5x5 grid.
    win = GraphWin("Zombie invasion!", 800, 800)
    win.setCoords(0,5.5,5,0)

    # TODO: Initialize some game variables here

    # Wait for the user to click the mouse before beginning the game.
    win.getMouse()


    # TODO: Add game logic here


    # After the game is over, pause for two seconds and then wait for a
    # mouse click so that the user can see the final score.
    sleep(2)
    win.getMouse()
    win.close()

The call to setCoords on the window object is setting the top left corner to be coordinate 0, 0 and the lower right corner to be 5, 5.5. The extra 0.5 in the vertical direction gives us a little room below an otherwise five by five canvas. To draw the grid, you can create and draw Line objects on the graphics window.

A 5x5 grid, annotated with 0,0 at the top left corner and 5,5 at the lower right corner.

There are multiple ways to create the grid of lines. Don’t copy and paste multiple calls to the Line constructor. Instead, you can be strategic and create lines in a loop or loops where the loop variable is advancing which row or column number you’re drawing a line along.

2.2.1. Testing

To test draw_grid, call it in main on the win graphics window. You should see an image that looks like the one above (without the coordinate grid annotations).

2.3. Determining Grid Clicks

Now that we have a grid, we need a function to help us determine where a user is clicking. Instead of treating the grid as pairs of coordinates, we’ll be modeling the grid as one long list of 25 entries in row-major order. This ordering means that position 0 is the top left box, with 1, 2, 3, and 4 as the rest of the first row. The second row would contain (from left to right), 5, 6, 7, 8, and 9, and the pattern would continue from there, looking like:

A 5x5 grid, annotated with 0 in the top left box, counting up in each box from left to right and then top to bottom.

We’ll write a function named get_click_index(click) that takes a Zelle Point object and returns the integer for the corresponding index number (numbered box in the image above).

You can query the Point parameter object’s x and y values to learn where the user clicked. You may find it helpful to convert those values to integers to remove any fractional component (e.g., 1.5 would become 1) to determine the row and column of the click. From there, you can do some arithmetic to figure out which index each grid location should land on. For example:

  • If the user clicks at coordinates 0.3, 0.4, converting these values to integers gives us column 0, row 0, which corresponds to index 0.

  • If the user clicks at coordinates 1.5, 4.7, converting these values to integers gives us column 1, row 4, which corresponds to index 21.

  • If the user clicks at coordinates 3.1, 1.6, converting these values to integers gives us column 3, row 1, which corresponds to index 8.

The index can be computed using a short formula based on the row and column values as determined above.

When working on this function, consider what the X and Y coordinates imply about where on the grid a click will land. You may find it helpful to ask yourself the following questions:

  1. How many boxes are in each row?

  2. What does the Y coordinate imply about which row a click lands on?

  3. If you know which row you’re on, how can you get to the correct column from there?

2.3.1. Testing

To test get_click_index, you can expand your existing main function with a loop that looks something like:

    for i in range(10):
        p = win.getMouse()
        index = get_click_index(p)
        print("%s corresponds to index %d" % (str(p), index))

This loop will allow you to test clicking on the grid 10 times, printing the point coordinates and the index you’re mapping it to each time. Compare the index values with the image above to ensure that get_click_index is returning the correct index.

2.4. Checking for Game Over

Now that we can determine which box a user clicked on, we can create a list to keep track of what’s in each box. For the purposes of this game, each box can only be one of two things:

  1. An empty box, represented by the None object.

  2. A Zombie object (an instance of the Zombie class, which we provided to you).

To start with, we want all the boxes to be considered empty, which we can express to python in main as a list containing None repeated 25 times:

    status = [None] * 25

Our next function, continue_game(status), will take in a status list like the one above and check the contents of each box. It should return True if the game should continue (if there are any boxes without zombies) or False if the game should end (all boxes contain a zombie). You can check each box to see if it’s equal to none (e.g., == None) to determine whether it contains a zombie or not.

2.4.1. Testing

To test this function, you can adjust the loop we added to main previously to instead look something like:

    status = [None] * 25

    while continue_game(status):
        p = win.getMouse()
        index = get_click_index(p)
        status[index] = Zombie(index)
        status[index].draw(win)

    print("Game over")

This loop will cause zombies to appear in each box the user clicks on. If continue_game is working correctly, the program will keep going until every box is filled, at which point it should print the "Game over" message.

The provided Zombie class behaves like other graphics objects. The constructor takes one parameter: the index (box number) for where on the grid the zombie will be located. After constructing a Zombie, you need to call its .draw method and pass in a graphics window just like Circles, Rectangles, and other graphics objects.

2.5. Random Zombie Appearance

Now that we can draw zombies, lets add them randomly rather than as the user clicks. To help, write a function called update_game_state(timer, status, win) that takes three parameters:

  • timer: an indication of how much time has passed (in seconds) since the game started. We’ll use this to ramp up how quickly zombies appear as the game progresses.

  • status: the same list we used in the previous function to keep track of what’s in each box (either None or a Zombie).

  • win: the graphics window to draw zombies on, when we decide to add them.

This function doesn’t return anything. Instead, it should loop over every box, and for any box that doesn’t already have a zombie, randomly decide whether to add one. The process for adding a zombie looks identical to what we used above to assign it to a list index and then draw it.

When choosing whether or not to randomly add a zombie, you can call your one_out_of_N_chance function with a parameter of int( 100 / timer ). Note that the one_out_of_N_chance function expects to receive an integer, so that call to int is important to convert any fractional result of the division to an integer first. Using something like 100 / timer yields a low chance of zombies appearing at first, but it quickly ramps up as timer gets bigger. You’re welcome to experiment with other values besides 100 here, but your game ultimately needs to be playable during grading, so we suggest using 100 for the version that you turn in.

2.5.1. Testing

To test update_game_state, you’ll need to adjust your loop to keep track of time, periodically pause, and then call update_game_state to draw zombies:

    status = [None] * 25
    timer = 0.0

    while continue_game(status):
        sleep(0.1)
        timer = timer + 0.1

        update_game_state(timer, status, win)

    print("Game over")

You should start to see zombies randomly appearing on the grid, increasing in frequency as time passes. Eventually every box will fill and the game will end.

2.6. Removing Zombies by Clicking

The final function, handle_click(p, status) will help our game process the user’s mouse clicks and update the game state accordingly. This function takes two parameters: p, the Point object where the user clicked, and status, the list of what’s contained in each of the grid boxes (None or a Zombie object). The function is expected to return an integer whose value is either 0 or 1 depending on whether or not the user successfully clicked on a zombie. 1 indicates that the user clicked a zombie (and therefore will increase their score by 1 later in main), and 0 indicates that they didn’t click a zombie and their score won’t change.

The overall logic for this function is:

  • Check that the user clicked on a grid box (i.e., the x, y coordinates of the point are both between 0 and 5. If not, the user didn’t click on a zombie, so no further processing is needed (return 0).

  • Determine the index of the grid box that the user clicked on.

  • If there’s no zombie in that box, no further processing is needed (return 0).

  • If there is a zombie in that box, call the .undraw() method on it, set the box to contain None, and return 1 to indicate that a zombie was successfully removed.

Once this function is complete, you have all the pieces you need to build and test the complete game.

2.7. Putting it All Together

By this point, your previous testing should have a while loop in main that is sleeping and updating a timer. Within that loop, you’ll also need to call win.checkMouse(), which returns either None (meaning that the user didn’t click anything) or a Point object (meaning that they did click) that contains the location of the click. If checkMouse() returns None, you can ignore it. Otherwise, you can pass the Point object it returns to your handle_click function.

Otherwise, all you’re missing is two text labels to keep track of the user’s score and time survived. To add those, you can create Zelle graphics Text objects and periodically update them with the setText method as the timer and score changes. We suggest that you create and draw the Text objects once, outside of your main game loop. From there, you only need to call the setText method to update their contents — there’s no need to call draw again. If you want your text to visually resemble the demo video, you can draw your timer text at coordinates 1.5, 5.2 and your score text at 3.5, 5.2.

To help make your game more playable for the user, we recommend adding a call in main to win.getMouse() prior to starting game’s main loop. This way, the user has to click once to indicate that they’re ready for zombies to start appearing. You can even make your timer Text object say something like "Click to begin" as shown in the demo above.

Similarly, after the game ends, users are likely to be frantically clicking, so it’s easy to accidentally click through and close the window before getting a chance to see the final timer and score. We recommend adding something like sleep(2) prior to the final win.getMouse() and win.close(). Adding a sleep like this one will force a two-second pause, which should be enough time for someone who is clicking like a maniac to realize that the game has ended!

2.8. Requirements

The code you submit for labs is expected to follow good style practices, and to meet one of the course standards, you’ll need to demonstrate good style on six or more of the lab assignments across the semester. To meet the good style expectations, you should:

  • Write a comment that describes the program as a whole using a comment at the top of the file.

  • All programs should have a main() function.

  • Use descriptive variable names.

  • Write code with readable line length, avoiding writing any lines of code that exceed 80 columns.

  • Add comments to explain complex code segments (as applicable).

  • Write a comment for each function (except main) that describes its purpose, parameters and return value.

In addition, you’ll need to demonstrate good top-down design practices on two or more lab assignments across the semester. To meet the top-down design expectations, you should:

  • Have a main() function that serves as an outline for your program.

  • Your main() function should call other functions that implement subtasks.

  • The functions that are called by main() should be well-named and have good comments explaining their purpose, parameters (including their types), and return value (including its type).

Your program should meet the following requirements:

  1. Your program should implement the one_out_of_N_chance function with the parameters, return value, and behavior as described above.

  2. Your program should draw a 5x5 grid of boxes that resembles the images in the draw_grid section above.

  3. Your program should implement the get_click_index function with the parameters, return value, and behavior as described above.

  4. Your program periodically draws zombies on the grid with a random chance.

  5. The chance of zombies appearing at random increases as time progresses.

  6. Clicking zombies causes them to disappear from the grid.

  7. Your program uses text labels to keep track of the time and score earned by the player and the score increases as the player clicks zombies.

  8. Your program’s game is playable by the grader and it ends when every grid box contains a zombie.

3. (Optional) Changing Zombie Appearance

If you’d like to change the appearance of the zombie faces, you can override the provided face by reassigning the Zombie.build_zombie_parts function. The code below shows how the provided faces are being created. You’re welcome to copy this code into your zombie.py file and edit it to your liking. If you use it, you’ll need to put it outside of any functions and before the call to main().

# If you want to override zombie appearance, edit this (NOT required).
def build_zombie_parts(center):
    face = Circle(center, 0.45)
    face.setFill("chartreuse3")

    eyel = Circle(Point(center.getX() - 0.15, center.getY() - 0.12), 0.09)
    eyer = Circle(Point(center.getX() + 0.15, center.getY() - 0.16), 0.09)
    eyel.setFill("white")
    eyer.setFill("white")

    pupl = Circle(Point(center.getX() - 0.15, center.getY() - 0.12), 0.03)
    pupr = Circle(Point(center.getX() + 0.15, center.getY() - 0.16), 0.03)
    pupl.setFill("black")
    pupr.setFill("black")

    mouthpoints = [
        Point(center.getX() - 0.15, center.getY() + 0.18),
        Point(center.getX() + 0.15, center.getY() + 0.13),
        Point(center.getX() + 0.25, center.getY() + 0.29),
        Point(center.getX() - 0.25, center.getY() + 0.26)
    ]
    mouth = Polygon(mouthpoints)
    mouth.setFill("red")

    return [face, eyel, eyer, pupl, pupr, mouth]

Zombie.build_zombie_parts = build_zombie_parts

Answer the Questionnaire

After each lab, please complete the short Google Forms questionnaire. Please select the right lab number (Lab 06) from the dropdown menu on the first question.

Once you’re done with that, you should run handin21 again.

Submitting lab assignments

Remember to run handin21 to turn in your lab files! You may run handin21 as many times as you want. Each time it will turn in any new work. We recommend running handin21 after you complete each program or after you complete significant work on any one program.

Logging out

When you’re done working in the lab, you should log out of the computer you’re using.

First quit any applications you are running, including your vscode editor, the browser and the terminal. Then click on the logout icon (logout icon) and choose "log out".

If you plan to leave the lab for just a few minutes, you do not need to log out. It is, however, a good idea to lock your machine while you are gone. You can lock your screen by clicking on the lock xlock icon. PLEASE do not leave a session locked for a long period of time. Power may go out, someone might reboot the machine, etc. You don’t want to lose any work!