Assignment 3. Homework 3 - Breakout


Due Sunday, July 23 at 11:59 pm Pacific

  • Submissions received by due date receive a small on-time bonus.
  • All students are granted a pre-approved extension or "grace period" of 24 hours after the due date. Late submissions are accepted during the grace period with no penalty.
  • The grace period expires Mon, Jul 24 at 11:59 pm Pacific, after which we cannot accept further late submissions.
  • In this course, we express all date/times in Pacific time GMT -7. Our Paperless submission system also displays/records due dates and submission times in Pacific time.

Homework 3 - Breakout

Based on an assignment created by Eric Roberts.


This assignment starts with a sandcastle that gives you some practice with lists, parameters, and returns. Next, you'll use your new graphics skills to create an animated, interactive game! Here's the Assignment 3 starter code, where you'll implement both the sandcastle and the Breakout game. Download and unzip the file, then open the folder in Pycharm.

Sandcastle

In this sandcastle, you'll write a function first_num_not_equal(num, lst) which takes in an integer num and a list of integers lst and returns the first number in lst that's not equal to num. If there aren't any numbers in lst that aren't equal to num, you should return (not print) -1. Here are a few sample calls of first_num_not_equal(num, lst) and what should be returned.

>>> first_num_not_equal(4, [1, 2, 3, 4])
1
>>> first_num_not_equal(10, [10, 10, 5, 10, 20])
5
>>> first_num_not_equal(10, [10, 10, 10])
-1
>>> first_num_not_equal(10, [])
-1

Implement this function in not_equal.py. We've included the above function calls as doctests in the starter code. To run these doctests and confirm that your helper function behaves as expected, enter the following into your terminal (using python or py instead of python3 on Windows computers): python3 -m doctest not_equal.py -v

Check out the main function in the starter code. Look at how the values from main 1, [1, 2, 3, 4] get sent in as parameters to be stored in num, list in the helper function. Then, see how the variable result in main stores whatever gets returned from the helper function. As you move on to code Breakout, keep in mind the way that information gets passed between functions - you'll certainly need parameters and returns in this program!

Breakout

Your job in this program is to write the classic arcade game of Breakout, which was invented by Steve Wozniak before he founded Apple with Steve Jobs. It is a longer program, but entirely manageable as long as you break the problem up into pieces (hint, hint: decomposition). Check out our Graphics Reference for a rundown of common graphics functions.

alt image of the breakout game with several rows of multicolored bricks, a paddle and a ball

A complete game consists of three turns. On each turn, the user clicks the canvas, and then the ball is launched from the center of the window toward the bottom of the screen at a random angle. The ball bounces off the paddle and the walls of the world in accordance with the physical principle generally expressed as "the angle of incidence equals the angle of reflection" (which is something we can implement as discussed later in this handout). The turn ends when the bottom of the ball hits the bottom of the canvas, after which the ball is re-centered. Here's a video of a sample run - the game ends and the ball does not reset after the user loses the third round.

Each round, the ball bounces off the paddle, bricks, and walls until one of following two conditions occurs:

  1. The bottom of the ball hits the bottom wall, which means that the player must have missed it with the paddle. In this case, the turn ends and the ball is reset if the player has any turns left. If not, the game ends in a loss for the player.
  2. The last brick is eliminated. In this case, the player wins, and the game ends immediately.

Success in this assignment will depend on breaking up the problem into manageable pieces and getting each one working before you move on to the next. The next few sections describe a reasonable staged approach to the problem.

We've provided lots of constants at the top of your breakout.py file. Rather than typing numbers directly in your code, you should refer to these constants whenever you need these values. This makes your code easier to read - basing your code on constants is good style!

The following is a suggested ordering of important milestones to complete Breakout. We highly recommed you think of each step as an opportunity to decompose as well ;)

Step 1: Create the bricks

Before starting the game, it's crucial to set up the various pieces. From a stylistic point of view, the brick setup should be implemented as a helper function, called from main. As you break down this entire program into helper functions, consider: what is the information required by this function and what information should it return back to the calling function? This information will form your parameters and returns. Note that sometimes functions might not have any parameters or returns.

The number, dimensions, and spacing of the bricks are defined using constants that can be found under "Step 1" at the top of the starter file, as is the BRICK_Y_OFFSET, which is the distance from the top of the window to the first line of bricks. You'll need to compute the x value of the left-most bricks, which should be chosen so that the bricks are centered in the window, with the leftover space divided equally on the left and right sides.

The bricks follow a rainbow-like color sequence: "red", "orange", "yellow", "green", "blue", with each color accounting for two rows of bricks. These colors are stored in a constant COLORS.

alt image of 10 rows of 10 bricks top 2 rows are red, next 2 are orange, next 2 yellow, next 2 green last 2 blue

Step 2: Add a bouncing ball

The ball is an oval with a radius specified by the BALL_RADIUS constant. After creating the ball and placing it in the middle of the screen, you'll want to get it to move and bounce appropriately. You are now past the "setup" phase and into the "play" phase of the game. To start, create a ball and put it in the center of the window.

The program needs to keep track of the velocity of the ball, which consists of two separate components that you should declare as variables change_x and change_y. The "change" variables represent the change in position that occurs at each time step. You'll select the starting change_x of the ball uniformly from the range VELOCITY_X_MIN and VELOCITY_X_MAX. You should also make change_x positive or negative with equal probability, to start the ball moving right or left. The change_y will always start as VELOCITY_Y, another constant we've provided.

Recall that there are two ways to move an object in the Python graphics library:

# Move the object to a specific new_x, new_y
canvas.moveto(object_name, new_x, new_y)

# Increase the x coordinate by change_x and the y coordinate by change_y
canvas.move(object_name, change_x, change_y)

Once you've created your ball and the change variables, your next challenge is to get the ball to bounce around the world, ignoring the paddle and the bricks. This will require that you program an "animation loop" where you move the ball, update the canvas and then pause. You should pause for DELAY seconds.

After you've got the ball moving, it's time to make the ball bounce. To do so, you need to check to see if the coordinates of the ball have gone beyond the canvas' boundary, taking into account the ball's radius. Thus, to see if the ball has bounced off the right wall, you need to see whether the coordinate of the right edge of the ball has become greater than the width of the window; the other three directions are treated similarly. For now, have the ball bounce off the bottom wall as well so that you can watch it make its path around the canvas. You might find the following functions helpful for getting the current coordinates of the ball:

 obj_x = canvas.get_left_x(object_name)
obj_y = canvas.get_top_y(object_name)

Once you've determined the ball should bounce, how do we encode the physics of a realistic bounce? If a ball bounces off the top or bottom wall, you need to reverse the sign of change_y. Symmetrically, bounces off the side walls reverse the sign of change_x.

Step 3: Add the paddle

The next step is to create the paddle. There is only one paddle, which is a rectangle. You should start the paddle horizontally in the center of the screen, with the top of the paddle PADDLE_Y_OFFSET pixels up from the bottom of the canvas.

Once you create the paddle, you need to make it follow the mouse. Here, however, you only have to pay attention to the x-coordinate of the mouse because the y position of the paddle is fixed. Each time through the animation loop, get the x location of the mouse and move the rectangle representing the paddle to be centered at that location. To get the location of the mouse, you can do:

mouse_x = canvas.get_mouse_x()

4. Check for collisions

In order to make Breakout into a real game, you have to be able to tell whether the ball is colliding with another object in the window. One simplification we can make is to imagine a square "bounding box" around our ball, and find all of the objects that overlap with this box. There is a canvas function that returns a list of all objects that are overlapping the imaginary rectangle with upper-left corner at (x_1, y_1) and bottom-right corner at (x_2, y_2):

collider_list = canvas.find_overlapping(x_1, y_1, x_2, y_2)

Here's a visualization of the bounding box around the ball, which you'll use to identify colliding objects. In video game programming, this approach is often the easiest thing to do, rather than looking at the complicated geometry of the object.

alt circle with a square around it

You can get the coordinates of an object's bounding box using the function canvas.coords(object_name) and then unpacking each coordinate from the returned list. We can put these functions together to get a list of colliding objects collider_list as follows:

# this graphics function gets bounding box coordinates as a list
ball_coords = canvas.coords(ball)

# the list has the (x, y) coordinates for two points
x_1 = ball_coords[0]
y_1 = ball_coords[1]
x_2 = ball_coords[2]
y_2 = ball_coords[3]

# we can then get a list of all objects in that area
collider_list = canvas.find_overlapping(x_1, y_1, x_2, y_2)

When you get your list of colliding objects, the ball itself will be in that list, as well as any objects that the ball is currently colliding with. To test if a colliding object is the ball, you can check if the object is == to your ball object.

If the ball collides with the paddle, you need to bounce the ball so that it starts traveling up. If the collider isn't the paddle, the only other thing it might be is a brick, since those are the only other objects in the world. You need to cause a bounce in the vertical direction (flip the sign of the change_y variable), but you also need to take the brick away. To do so, remove it from the screen by calling the delete function:

canvas.delete(object_name)  # deletes the object

The ball should collide with just one object (e.g. paddle, brick) per animation cycle. In other words, as soon as you find out that the ball collided with the paddle or with a brick, you should respond to that collision and ignore the rest of the colliders if, for example, the ball collided with multiple bricks simultaneously.

5. Finishing touches

After you've got the ball moving around the canvas as expected, we've got a few more details to take into account:

  1. Start each turn with a click. Before launching the ball each turn, wait for the user to click the canvas using the function canvas.wait_for_click().

  2. Properly handle the case in which the ball hits the bottom wall. In the prototype you've been building, the ball just bounces off this wall like all the others, but that makes the game pretty hard to lose. Make it so that the turn ends when you hit the bottom wall, and the ball resets in the middle of the screen. After three turns, the game ends and the ball doesn't reset.

  3. Check for the other terminating condition, which is hitting the last brick. How do you know when you've done so? Although there are other ways to do it, a good idea is to have your program keep track of the number of bricks remaining. Every time you hit a brick, subtract one from that counter. When the count reaches zero, you must be done. It would be nice to give the player a little feedback that indicates whether the game was won or lost - this might help you with debugging as well. You can do this by printing a message in the terminal.

  4. Test your program to see that it works. If you think everything is working, here is something to try: Just before the ball is going to pass the paddle, move the paddle quickly so that the side of the paddle collides with the ball. You may notice that your ball gets "glued" to the paddle. This error occurs because the ball collides with the paddle, changes direction, and then collides with the paddle again before escaping. How can you fix this bug?

Extensions

Once you've completed all the required parts of the assignment, you might want to consider adding an optional extension. Remember not to modify your completed assignment files, but instead put the extension in a new file like extension.py.

Extend Breakout

Add sounds

Do this in extension.py. It might be fun to play a sound whenever the ball bounces, a brick is removed, the user wins, and so forth. To do this, first add the sound file you want to use to your assignment folder. You can do this by dragging the file into your assignment folder on your computer.

Then, you'll need to install the library that can play sound files. First open the terminal in PyCharm. Type the following 1 or 2 commands below into the Terminal. On Windows, type py or python instead of python3:

> python3 -m pip install playsound
        ...prints stuff...
> python3 -m pip install pyobjc  # only Macs need this command
        ...prints stuff...

Next, add the following import at the top of your program - make sure to use the correct one for Mac or Windows:

# Use this for Mac
from playsound import playsound

# Use this for Windows
from winsound import PlaySound,SND_FILENAME,SND_ASYNC

Finally, whenever you want to play a sound in your program, call the following function - make sure to use the correct one for Mac or Windows:

# Use this for Mac
playsound(mysoundfile, block=False)

# Use this for Windows
PlaySound(mysoundfile, SND_FILENAME | SND_ASYNC)

You should replace 'mysoundfile' with the name of the sound file in your project, e.g. 'bounce.au'.

We include bounce.au for Mac users and bounce.wav for Windows users, and these are declared as constants BOUNCE_SOUND_MAC and BOUNCE_SOUND_WINDOWS respectively in the extension.py file.

Improve the bounces.

Do this in extension.py. The program gets more interesting if the player can control the ball by hitting it at different parts of the paddle. The way the old arcade game worked was that the ball would bounce in both the x and y directions if you hit it on the edge of the paddle from which the ball was coming.

Create a something of your own!

Now that you've got the skills to make awesome graphics and animations, what else might you want to create? Check out the Graphics Reference for a list of functions you can use. Implement this in extension.py (we still want a standard copy of Breakout to grade).

Submitting your code

To submit assignments electronically, please follow the instructions below:

  1. All assignments should be submitted through the Paperless website. Please click on the provided link to access the website.

  2. Select the assignment you wish to submit. In this case, it is "Assignment 3: Breakout."

  3. Upload all the necessary files for the chosen assignment. Do not rename the files when downloading them. For this assignment, you need to submit the following files:
    • not_equal.py
    • breakout.py
  4. If you completed an optional extension for the assignment, you should also submit:
    • extension.py
    • Any additional files required for the extension (e.g., audio files)
  5. Important: If you are adding functionality to Breakout for extra credit, please use a file other than breakout.py. This will ensure that you receive full credit for the main parts of the assignment in case your extended program introduces any bugs. If you have image files, audio clips, or extra Python files as part of your extra credit submission, make sure to submit those as well.

  6. Once you have uploaded the files and verified that you included all the required files mentioned in the assignment handout, click the "Submit Assignment" button.

  7. Always double-check the contents of your submission to ensure that the code files are up-to-date and correct. You can inspect your submission by clicking on the specific assignment and using the dropdown to view each file's contents.

  8. Congratulations on completing Assignment 3! You can submit your assignment as many times as you like by following these instructions. However, please note that your section leader will only grade the most recent submission.

Feedback

This part is completely optional, but if you have feedback on this or any other assignment, we would love to hear it! Let us know what you liked and disliked at the link below! (We will update it as the quarter continues to be open for other assignments as well) Give us feedback!

Thanks to Anushree Aggarwal for assembling assignment materials for this format