T-Th 9:05
or
T-Th 11:15
in Olin 155

CS 1110: Introduction to Computing Using Python

Spring 2014

Assignment 5:
Breakout

Due on CMS by Wednesday, May 7th at 5:00 pm.

Substantive updates (marked in orange in the text):
  • Sun Apr 27 10:05am: “code.py” in section 4.1 should be “constants.py”
  • Sun Apr 27 10:13pm: Section 5.2, second-to-last paragraph: remove reference to non-existent comment
  • Mon Apr 28 10:25pm: Section 5.3, 3rd paragraph: minor clarification that you create a new Model, not call a Model's initializer directly.

This assignment, including some of the wording of this document, is adapted from an assignment by Professor Eric Roberts at Stanford University. It is being used with permission.

Your task in this assignment is to write the classic arcade game Breakout.

If you have not played Breakout before, there are lots of versions online, particularly as flash games. The old school versions give you an example of the basic gameplay, though they are considered boring by modern standards. Most of the modern versions of Breakout were inspired by the Arkanoid series and have power-ups, lasers, and all sorts of increasingly complex features. The flash game Star Ball is an example of a quite elaborate Breakout variant.

One of the main challenges with this assignment is that the scope of it is completely up to you. There is a bare minimum of functionality that you must implement; you must have all the features of an old-school Breakout game. But after that point, you are free (and encouraged) to add more interesting features to your game. The video to the right shows our solution, which has several extra features such as sound and a countdown timer. You should not feel compelled to add these features to your game. You are permitted to do anything that you want, provided that the basic functionality is there.

While this assignment is more complex than earlier assignments, we know that it is easily within your grasp. You just have to start early, break the problem up into manageable pieces, and program/test incrementally. Below, we discuss stages of implementation and give suggestions for staying on top of the project. If you follow our advice and test each piece thoroughly before proceeding to the next, you should be successful. Our solution in the video above is ~500 lines (~200 in controller.py and ~300 in model.py). It has ~15 methods beyond those in the original code skeleton.


Learning Objectives

This final assignment has several important objectives.

  • It gives you practice with reading official class documentation and APIs.
  • It gives you experience using stateful controllers to handle a complex, interactive application.
  • It gives you experience with designing helper functions to structure your code properly.
  • It gives you experience with using constants to make your code more readable.
  • It gives you experience with programming animation and simple physics.
  • It gives you experience with an open-ended project that is not fully specified.
  • It brings together must of what you have learned in this class in one final assignment.
  • The end result is fun to play with!

Table of Contents

Authors: W. White, D. Gries, E. Roberts, L. Lee, S. Marschner, D. Rong


Academic Integrity and Collaboration

Academic Integrity

This is a classic assignment that we are giving again because students really like it. However, it is also one of the most complex assignments of the class and sometimes in desperation to finish it students are tempted to plagiarize. Every year, we end up catching and prosecuting academic integrity violations for this assignment. Do not add yourself to this ignoble list.

The program Moss checks for similarity in any part of the code, not just the whole thing in general. If you copy code from another student, we will know. The code is too complex for accidental similarities to happen.

Please review the CS1110 academic integrity page. The most important point is to not forget to cite any relevant sources of ideas in your file headers. This includes both documents (offline or online) and people, regardless of whether they are in your group or not. This includes course staff, if the course staff give you an idea of how to do something (this does not count debugging help).

Unless you make extremely drastic changes, Moss will catch any code copying. Do it right; cite the source.


Collaboration Policy

You may do this assignment with one other person. Regardless of whether you have grouped with that person for previous assignments or not, if you are going to work together with someone on this assignment, then form your group on CMS for this assignment before submitting. Both parties must perform a CMS action to form the group: The first person proposes, and then the other accepts. Once you've grouped on CMS, only one person submits the files.


Organization and Scope

This is a fairly long assignment, like the last one. Once again, the trick is to pace yourself. While there are no automatic unit tests this time, you should be able to figure out if everything is working simply by playing the game.

Assignment Source Code

The first thing to do in this assignment is to download the zip file A5.zip from this link. Unzip it and put the contents in a new directory. This time, you are going to get a directory with a lot more than usual. In particular, this zip file contains the following:

controller.py
This file contains the controller class (Breakout) for this application. This is one of the two modules that you will modify for this assignment. However, note that it has no application code. For that, you will use the module __main__.py below.
model.py
This file contains the model classes (e.g. Model, Ball and whatever other classes you want to add) for this application. This is the other of two files you should modify for this assignment.
game2d.py
This file contains the view class (e.g. GView) for this application. It also contains the parent classes for your model and controller. Do not modify this file.
constants.py
This is a module filled with constants (global variables that should not ever change). It is used by both controller.py and model.py to ensure that these modules agree on certain important values. It also contains code for adjusting your brick size. You should only modify this file if you are adding additional constants as part of your extended features.
__main__.py
This module contains the application code for this assignment; it is the one you run from the command line to start the game. However, the way that you run it is a tad unusual; see below. Like game2d.py, do not modify this file.
Sounds
This folder is a list of sound effects that you may wish to use as part of your extensions. You are also free to add more if you wish; just put them in this folder. All sounds must be WAV or OGG files, recorded at 16 bits and sampled at 44.1 kHz (i.e., CD quality).
Fonts
This folder is a collection of True Type Fonts, should you get tired of the default Kivy font. You can put whatever font you want in this folder, provided it is a .ttf file. Other Font formats (such as .ttc, .otf, or .dfont) are not supported.
Images
This folder is a collection of images that you may wish to use as part of your extensions. The class GImage allows you to animate images in this game, should you wish. You can also use it to provide a background.

Basic Classes

The class Breakout is a subclass of the class Game. With the exception of that and the Model class, any other classes you use should all be subclasses of GObject. As part of this assignment, you are expected to read the online documentation which describes how to use the basic classes.


Running the Application

Because there are so many files involved, this application is handled a little differently from previous assignments. Instead of running a module directly (as in python blah.py), put all of the files you unzipped in a folder, and give the folder a name like breakout. The file __main__.py turns the entire folder, not just one module, into an application. Navigate to the directory just outside of your breakout folder and type

python breakout

In this case, Python will "run the folder" by executing the application code in __main__.py. (This trick only works when you run the folder breakout as a application; you cannot import the folder. If you wanted to "import a folder" (which, for this assignment, you don't), you would create a file called __init__.py.)


Code Organization

The modules in this assignment are organized so as to closely follow the model-view-controller pattern discussed in class. This is shown in the illustration below. The arrows in this diagram mean "imports". So the controller imports the view and model (as should always be the case). The model imports the view because it needs the parent class GObject to perform any drawing. The view does not import anything (and should not be modified). Note that there are no cycles in this architecture; cyclical imports are very dangerous.

In addition to the three main modules, there is another module, constants.py, with no class or function definitions. The only thing it has are global variables that do not change (i.e., constants). This module is shared by the model and controller, and is a way to keep them synchronized.

When approaching this assignment, you should always be thinking about "what code goes where?" To break things down:

  • The controller handles input (e.g., the mouse) as well as the drawing code in the view. It also handles any changes to the program state.
  • Anything that changes the game models (Ball, Paddle, Bricks, etc.) belongs in the model class, to be called by the controller. This includes complex changes like game physics. (Think about the step() method in Database from the previous assignment. This did all the work in k-means clustering; all the controller did was control when or how many times the step() method was called.)

Assignment Scope

As you can see from the online documentation, the class Breakout needs to implement three main methods. They are as follows:

Method Description
init(self, ...) Initializes the game state and attributes. Because of how Kivy works, initialization code should go here and not in the initializer (since that __init__ method is called before the window is sized properly).
update(self, dt, ...) Update the models for the next animation frame. The speed at which this is called is determined by the (immutable) attribute fps, which is set by the initializer. The parameter dt is the amount of time since the last call to update.
draw(self, ...) Called when update is complete, and the application is ready to redraw the models. Implementing this method should be as simple as calling the method draw inherited from GObject.

Of course, if you put every single line of your code into these three methods alone, you would get a huge unreadable mess. An important part of this assignment is developing new helper methods when you need them, so that each method is small and manageable. Your grade will depend partly on the design of your program. As one guideline, points will be deducted for methods that are more than 40 lines long. This includes the specification. (For init, update, and draw, the specfication won't count since we gave it to you.)

You will also need to add methods and attributes to the classes Model and Ball in model.py. These classes are currently empty. Whenever you add an attribute to these classes, or to the controller class Breakout, you must fully state the corresponding class invariant(s) in your class specification (although there is no need to enforce these invariants).

If you go to a staff member for help and they see a method that has no specification or an attribute that is not mentioned in the class specification, they will ask you to fix it and come back at another time. As we have been stressing all semester, specifications are critical for both you and others to understand what your code is doing; lack of a specification indicates that you are trying to write code without first understanding what the code should be doing, which is a bad combination.

Should you desire to create any additional classes (e.g., for the bricks), they should go in the correct module. Controllers go in controller.py and models go in models.py. If you are unclear about where your class goes, first read the "Assignment Organization" section above; if you are still lost, then post a description of the class (do not post code) on Piazza, and we'll provide some guidance.


Pacing Yourself

You should start as soon as possible. If you wait until a few days before this assignment is due, you will have a very hard time completing it. If you do one part of it every day or so, you will enjoy it and get it done on time.

Implement the program in stages, as described in this handout. Do not try to get everything working all at once. Make sure that each stage is working before moving on to the next stage.

Set up a schedule. We have suggested some milestones, but make sure you leave time for learning things and asking questions. Above all, do not try to extend the program until you get the basic functionality working. If you add extensions too early, debugging may get very difficult.


Getting Help

We have tried to give you as much guidance in this document as we can. However, if you are still lost, please see someone immediately. Like the last assignment, this is a fairly involved project, and you should get started early. To get help, you may talk to the course instructor, a TA, or a consultant. See the staff page for more information.

In addition, you should always check Piazza for student questions as the assignment progresses. We may also periodically post announcements regarding this assignment on Piazza.


Breakout

The initial configuration of the game Breakout is shown in the left-most picture below. The colored rectangles in the top part of the screen are bricks, and the slightly larger rectangle at the bottom is the paddle. The paddle's vertical position is fixed; it moves back and forth horizontally across the screen along with the mouse (or finger, on a touch-screen device) unless the mouse goes past the edge of the window.


Starting Position
       
Hitting a Brick

A complete game consists of three lives. At the start of each life, a 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 screen, in accordance with the physical principle generally expressed as "the angle of incidence equals the angle of reflection" (it is easy to implement). The start of a possible trajectory, bouncing off the paddle and then off the right wall, is shown to the right. The dotted line is there only to show the ball's path and will not actually appear on the screen.

In the second diagram above, the ball is about to collide with a brick on the bottom row. When that happens, the ball bounces just as it does on any other collision, but the brick disappears. The left-most diagram below shows the game after that collision and after the player has moved the paddle to put it in line with the oncoming ball.


Intercepting the Ball
       
Breaking Out

The play during a turn continues in this way until one of two conditions occurs:

  • The ball hits the bottom wall, which means that the player missed it with the paddle.
  • The last brick is eliminated.

In the first case, the player loses a life; if any lives are remaining, the next ball is served; otherwise, the game ends in a loss. In the second case, the player wins and the game ends.

Clearing all the bricks in a particular column opens a path to the top wall. When this delightful situation occurs, the ball may bounce back and forth several times between the top wall and the upper line of bricks without the user having to worry about hitting the ball with the paddle. This condition, a reward for "breaking out", gives meaning to the name of the game. The last diagram above shows the situation shortly after the first ball has broken through the wall. The ball goes on to clear several more bricks before it comes back down the open channel.

Breaking out is an exciting part of the game, but you do not have to do anything special in your program to make it happen. The game is operating by the same rules it always applies: bouncing off walls, clearing bricks, and obeying the "laws of physics".


Game State

One of the challenges with making an application like this is keeping track of the game state. In the description above, we can identity four distinct phases of Breakout.

  • Before the game starts, and the bricks are not yet set up
  • While the game is ongoing, and the ball is in play
  • While the game is ongoing, but the player is waiting for a new ball.
  • While the game is paused (e.g., to show a message), and is waiting for a mouse click
  • After the game is over

Keeping these phases straight is an important part of implementing the game. You need this information to implement update correctly. For example, whenever the game is ongoing, the method update is used to move the paddle. However, if the game has not started yet, the method update should instead set up the bricks and start a new game.

For your convenience, we have provided you with constants for five states.

  • STATE_INACTIVE, before a new game has started
  • STATE_ACTIVE, when the game is ongoing and ball is in play
  • STATE_COUNTDOWN, when the player is waiting for a new ball
  • STATE_PAUSED, when the game is paused to display a message
  • STATE_COMPLETE, when the game is over

All of these constants are available in constants.py. The current application state should be stored in the attribute _state in your controller. You are free to add more states when you work on your game extensions. However, your basic game should stick to these five states.


The Basic Game

We have divided these instructions into two parts. The first part covers the basic things that you must implement to get the game started and running. Once you do that, the assignment gets more interesting. You should try to finish everything in this first part of the assignment by Monday, May 5. If you do that, you will be in good shape to add extensions (though you will not lose any points if you do not have extensions; they are a form of extra credit).


Before You Code Anything

The first thing that you should do is read the file constants.py. If you ever need a value like the size of the paddle, the size of the game window, or so on, this is where you go. When writing code, you should always use the constants, not raw numbers (or "magic numbers", as we call them). Magic numbers make your code very hard to debug, and if you want to make a change (e.g. to make the ball bigger), you have no idea about all of the locations in your code that use the ball size and will have to spend ages scouring your methods.

With that said, you are welcome to change any of these numbers if you wish. You are also encouraged to add more constants if you think of other numeric values that you need. Anytime that you find yourself putting a number in your code, ask yourself whether or not it would make sense as a constant.


Create a Welcome Screen

We start with a simple warm-up to get used to defining state and drawing graphics elements. When the user starts up the application, they should be greeted by a welcome screen. When you work on your extensions, you can embellish your welcome screen to be as fancy as you wish. But for now, we are going to keep it simple. Your initial welcome screen is going to consist of a single text message.

The text message will look something like the one shown to the right. It does not need to say "Press to Play". It could say something else, as long as it is clear that the user needs to click the mouse (or press the screen) to continue. You also do not have to use the dreaded Comic Sans font like we have.

To create a text message, you need to create a GLabel and store in it an attribute of Breakout. But if you read the specification of Breakout, you will not see any attributes for the text message. That means that you must add one. And as with any attributes you add to a class, you must describe it in the class specification. For now, we will assume that you named the attribute _message, but you could name it anything you like.

Since the welcome message should appear as soon as you start the game, it should be created in the method init, which is called at the beginning of the game.. When creating your message, you will want to set things like the font size and position of the text. As you can see from the documentation for GLabel and GObject, graphics objects have a lot of attributes to specify things such as position, size, color, font style, and so on. Feel free to experiment with these attributes to get the welcome screen that you want. Note that in Kivy, screen coordinates start from the bottom-left corner of the window.

Simply adding this message-creating code to init is not enough. If you were to run the application right now, all you would see is a blank white window, because while Python knows that the message exists, it doesn't know whether to draw it or not. To do this, go to the draw method, and add the line

    self._message.draw(self.view)

The attribute view is a reference to the window; this line instructs Python to draw your message in that window. Now run the application and check if you see your welcome message.

Initializing Game State

The other thing that you have to do in the beginning is initialize the game state. The attribute _state (included in the class specification) should start out as STATE_INACTIVE. That way we know that the game is not ongoing, and that the program should (not yet) be attempting to animate anything on the screen. In addition, the other attributes listed (particularly _model) should be None, as they (and the Ball, Paddle, etc.) should not yet exist until the game starts.

The _state attribute is an important part of many of the invariants in this game. In particular, we want your new attribute for the welcome message to have the following invariant:

  • If the state is STATE_INACTIVE, then there should be a welcome message.
  • If the state is not STATE_INACTIVE, then the welcome message should be None.

Does your definition of init satisfy this invariant?

Dismissing the Welcome Screen

The welcome screen should not show up forever. The player should be able to dismiss the welcome screen (and start a new game) when he or she clicks the mouse or touches the screen. You can track this with the attribute touch which is part of GView. This attribute is a GPoint if the mouse button is currently down, and is None if it isn't.

If you detect a press, then you should change the state STATE_INACTIVE to STATE_COUNTDOWN. The game has now started (but there is no ball or bricks yet). You are not ready to actually write the code for the game, but switching states is an important first activity.

Invariants must be statisfied at the end of every method. As stated before, you just changed the state to something other than STATE_INACTIVE, so now welcome message must be assigned None. This will require a simple change to draw to keep it from crashing (you cannot draw a None). Once you have done that, run the application. Does the message disappear when you click the mouse?

Important Considerations

This first part of the assignment looks relatively straightforward, but it gets you used to having to deal with controller state. In this part, you already immediately had to add attributes beyond the ones that we have provided. Whenever you need a new attribute, you must add it and its corresponding invariant to the class specification. Add it just after the comment starting "ADD MORE ATTRIBUTES", to make it easier for the graders (and you) to find them. You will lose points for instance attributes that lack a specification.

Try to finish this part by Monday, April 28. You may spend a lot of time reading the online documentation. But this will give you a solid understanding of how the application works.


Populate the Model with Bricks

Technically, you broke an invariant at the very end of the last step above; according to the specification for the _model attribute of Breakout, it must no longer be None once the state is no longer inactive. To fix this, you will need to construct a Model object and assign it to this attribute.

Right now, Model instances do not have any data in them, because the skeleton code doesn't provide an initializer. Eventually we want a Model to contain bricks, a ball, and a paddle. Right now, we are just going to focus on the bricks.

If you read the specification for class Model, you will see that it has an attribute bricks that is designed to store a list of bricks. These bricks must be created in the initializer for Model (the real one, __init__) so that once you create a Model in method update in controller.py, you will obtain a model with a full complement of bricks.

Eventually, we are going to want to do a lot of other things in the initializer for Model. Hence, even now you might want to create a helper method for the initializer that does nothing but set up the bricks. Whether you do or do not is up to you, but remember our 40-line limit on methods.

Set up the bricks as shown to the right. The number, size, and spacing of the bricks, as well as the gap between the top of the window and the first line of bricks, are specified using global constants given in module constants. The only value you need to calculate yourself is the x coordinate of the first column, which should be chosen so that the bricks are centered in the window, with the leftover space divided equally on the left and right extremes (Hint: the leftmost brick should be placed at x-coordinate BRICK_SEP_H/2). The colors of the bricks remain constant for two rows and run in the following sequence: RED, ORANGE, YELLOW, GREEN, CYAN. If there are more than 10 rows, start over with RED and loop the sequence again. We suggest that you add a constant BRICK_COLORS to constants.py that lists these colors in an appropriate way to help with this.

Creating Bricks

All of the attributes of Model should be subclasses of the class GObject. For example, bricks and the paddle are objects of subclass GRectangle. To define a rectangle, use the attributes x, y, width, height, linecolor, and fillcolor to specify how it looks on screen. You can either assign the attributes after the object is created, or assign them in the initializer using keywords; see the online documentation for more.

For color, we have provided a handy module for you here named colormodel; to access colors, all you need to do is use the notation colormodel.RED and so on. When you color a brick, make sure you set both its outline color and its interior color to the same color.

To begin, you might want to create a single Brick object of some position and size and add it to the playing board, just to see what happens. Then think about how you can place the BRICK_ROWS (in this assignment, 10) rows of bricks. You will probably need a loop of some kind. We do not care if it is a for-loop, a while-loop, or something else; just get the job done.

Drawing Bricks

Once again, adding bricks to the model is not going to draw them on the screen. You are going to have to modify the method draw in Breakout. Two things to note here: First, bricks contains a list of bricks, and you cannot draw a list to the window, only a GObject. Thus you will need to run a loop over said list. Second, pay attention to the class structure. Note that Breakout contains an attribute named _model which holds a Model object, while bricks is an attribute of a Model object. If you are in class Breakout and attempt to type in self.bricks, you will get an error (Why?).

If you do not want to bother with that second part, you are welcome to add your own draw method to class Model. It should take the view as a parameter, just like the draw method in GObject. Either way, you should now be able to start the application and press the button to see several rows of bricks.

Testing Your Code

When you are testing the later parts of this assignment, try playing with just 3-4 bricks per row and 1-2 rows of bricks. This will save time and let you quickly see whether the program works correctly when the ball breaks out (gets to the top of the window). It will also allow you to test when someone wins or loses. If you play with the default number of bricks (10 rows and 10 bricks per row), then each game will take a long time to test.

You might assume that testing in this manner would require you to go into constants.py and change the values of the global variables that hold the number of rows and number of bricks in a row. This is undesirable, as you might forget to change them back. Instead, we would like you to use command-line arguments to affect values in constants.py.

When you run your application (again, assuming that it is in a folder called breakout) try the command

python breakout 3 2
When you do this, Python changes the value of BRICKS_IN_ROW to 3 and the value of BRICK_ROWS to 2.

You should make sure that your creation of the rows of bricks works with any (reasonable) number of rows and any number of bricks in each row. This is one of the things we will be testing when we run your program.

Try to finish this part by Tuesday, April 29. All you need to do is to produce the brick diagram shown above (after the welcome screen). Once you have done this, you should be an expert with graphics objects. This will give you considerable confidence that you can get the rest done.


Create (and Animate) the Paddle

The next step is to create the black paddle. Again, this is to be stored in an attribute of class _model. That means that you must create it in the initializer for Model and modify your drawing code so that it appears. As with the bricks, the paddle should be an object of type GRectangle.

Animating the Paddle

The real challenge to this part is making the paddle move. Animation is handled by the controller, Breakout, so that means you will need to figure out how to access the paddle from there. Once you have done that, just change the paddle's x-position to make it move. (Do not assign a whole new paddle every time it moves, as that would be very slow and wasteful. Only assign the horizontal position of the paddle, via one of its attributes like x or center_x.)

The paddle should only start moving once the user presses the mouse; that is, when the touch attribute of the GView is no longer None.

However, you might notice (or anticipate) that if this was all there was to moving the paddle, then a player could simply click anywhere on the screen and the paddle will jump instantly to that location. This behavior, known as teleporting, should not happen. If you can warp the paddle anywhere on the screen, then you are not doing it right (and the gameplay is a bit unbalanced).

The way to prevent teleportation is to move the paddle in such a way that the distance between the paddle and the mouse remains fixed at all times. This requires that you know the location of both the current touch location and the previous touch location. The attribute touch only stores the current touch, so you will once again need a new attribute to store the previous touch (which we did not provide). Add an appropriate specification and invariant to Breakout and remember to initialize it in the init method in a way that satisfies the invariant.

Once you have both the previous touch position and the current touch position, all you need to do is move the paddle by the same amount that the mouse moved. That is, move the paddle by the difference between the current and previous touch position

Important Considerations

You must ensure that the entirety of the paddle stays completely on the board even if the touch moves off the board. Our code for this feature is 3 lines long; it uses the functions min and max.

Your animation of the paddle has added a lot of new lines of code to the method update. Ensure that your implementation only allows the paddle to be moved when the game is ongoing. That is, the state should either be STATE_COUNTDOWN or STATE_ACTIVE.

Complete this part by Thursday, May 1.


Create a Ball and Make it Bounce

You are now past the "setup" phase and into the "play" phase of the game. In this phase, a ball is created and moves and bounces appropriately. For the most part, a ball is just an instance of GEllipse. However, since the ball moves, it does not just have a position. It also must have a velocity (vx,vy). Since velocity is a property of the ball, these must be attributes of the Ball object. There are no such attributes in GEllipse, so we have to subclass GEllipse to add them. This is what we have done for you in the class Ball which is included at the end of your skeleton code.

Note that the class just includes specifications for the velocity attributes. You must create these attributes in the class initializer.

Initialize the Ball

Once again, you must write a proper initializer, overriding GEllipse's original __init__ method. Remember to call the initializer of the parent class very first thing. When you initialize the ball, it should be in the center of the window. (Note that the attributes x and y of a GEllipse refer to the bottom-left corner and not the center. To make this easy, use the attributes center_x and center_y instead.)

When you initialize the attribute vy, the ball should head downward. That means velocity should be negative. We suggest you start with a value of -5.0 for vy; you can adjust this later as you see fit. The game would be boring if every ball took the same course, so choose component vx randomly, with the module random.

To get you started, we suggest that you initialize vx as follows:

self.vx = random.uniform(1.0,5.0)
self.vx = self.vx * random.choice([-1, 1])
The first line sets vx to be a random float in the range 1.0 to 5.0 (inclusive). The second line multiplies it by -1 half the time (e.g. making it negative).

Serve the Ball

Serving the ball is as simple as adding it to the model (in the provided ball attribute) and drawing it. Once again, for the drawing part, you can either use the draw method in Breakout (but then you will need to figure out how to access the ball from Breakout rather than Model), or you can add a draw method to Model. When you serve the ball, you should also set the state to STATE_ACTIVE, indicating that the game is ongoing and there is a ball in play.

The tricky part is figuring out when to do this. One possible time to serve the ball is in the initializer for Model, immediately after you create the bricks and the paddle. But if you do this, then the ball will move before the player can orient him- or herself to the game (and hence the player will probably miss the ball). Ideally, we would like to delay the ball by 3 seconds, giving the player time to get ready.

That is why the state when you first start the game (setting up the bricks and paddle) is called STATE_COUNTDOWN. Your controller is supposed to delay for 3 seconds. After those 3 seconds are up, it should call a method in Model to create the ball and change the state to STATE_ACTIVE.

How do you delay something happening for 3 seconds? You are going to need yet another attribute (which you should specify) for keeping track of the time.

Breakout is designed to run at 60 frames a second. So once the timer passes 60, you can safely assume that 1 second has passed. (If you really want to be exact with your calculation, you can make use of the dt parameter in update. This parameter stores a float that is the time, in seconds, since the start of the last animation frame.)

Move the Ball

To move the ball, you are going to need to add another method to model. You can name this method whatever you want, but we have called ours moveBall. The purpose of this method is to move the ball and handle any physics. It is perhaps the most complex method in the entire assignment.

Each time this method is called, it should move the ball one step and change the ball direction if it hits a wall. Do not worry about collisions with the bricks or paddle just yet. For now, the ball will travel through them like a ghost. You will deal with collisions in the next task.

To move the ball one step, simply add the ball's velocity components to the ball's corresponding position coordinates. You might even want to add a method to the Ball class that does this for you. Once you have moved the ball one step, you should check for a collision with a wall. If the ball is going up, check if any part of the ball has a y-coordinate greater than or equal to GAME_HEIGHT. In that case the ball has reached the top and its direction has to be changed so that it goes down. You do this by setting vy to -vy. Check the other three sides of the game board in the same fashion. When you have finished this, the ball will bounce around the playing board forever until you stop it.

Keep in mind that it is not enough to simply look at the x or y position of the ball; remember that these refer to only the left and bottom side respectively. You want to know when any part of the ball has reached (or gone over) one of the sides. For example, to see whether the ball has gone over the right edge, you need to test whether the right side of the ball is over that edge. See the attributes in GObject for clues on how to take care of this problem.

Complete this part by Friday, May 2.


Check for Collisions

Now comes the interesting part. In order to make Breakout into a real game, you have to be able to tell when the ball collides with another object in the window. As scientists often do, we make a simplifying assumption and then relax the assumption later. Suppose the ball were a single point (x,y) rather than a circle. Then, for any GObject named blah_gobj, the method call

blah_gobj.contains(x,y)
returns True if the point is inside of the object and False if it is not.

However, the ball is not a single point. It occupies physical area, so it may collide with something on the screen even though its center does not. The easiest thing to do — which is typical of the simplifying assumptions made in real computer games — is to check a few carefully chosen points on the outside part of the ball and see whether any of those points has collided with anything. As soon as you find something at one of those points (other than the ball, of course) you can declare that the ball has collided with that object.

One of the easiest ways to come up with these "carefully chosen points" is to treat everything in the game as rectangles. A GEllipse is defined in terms of its bounding rectangle (i.e., the rectangle in which it is inscribed). Therefore the lower left corner of the ball is at the point (x,y) and the other corners are at the locations shown in the diagram to the right (d is the diameter of the ball). These points are slightly outside of the ball, but they are close enough to make it appear that a collision has occurred.

Using the above contains method, write a helper method for Model called _getCollidingObject with the following specification:

def _getCollidingObject(self):
    """Returns: GObject that has collided with the ball

    This method checks the four corners of the ball, one at a
    time. If one of these points collides with either the paddle
    or a brick, it stops the checking immediately and returns the
    object involved in the collision. It returns None if no
    collision occurred."""

You now need to modify the moveBall method of Model, discussed above. After moving the ball, call _getCollidingObject to check for a collision.

  • If the ball going up collides with the paddle, do not do anything (this prevents paddle "stickiness").
  • If the ball going down collides with the paddle, negate the vertical direction of the ball.
  • If the ball collides with a brick in either vertical direction, remove the brick and negate the vertical direction.

Once you have completed this, you should be able to start playing a game.

Try to finish this part by Sunday, May 4.


Finish the Game

You now have a (mostly) working game. However, there are two minor details left for you to take care before you can say that the game is truly finished.

Player Lives

You need to take care of the case that the ball hits the bottom wall. Right now, the ball just bounces off this wall like all the others, which makes the game really easy. In reality, hitting the bottom means that the ball is gone, and the player has lost a life. (For debugging purposes, we recommend that you simply comment out your code that handles the bottom wall bouncing, rather than deleting it entirely.)

In a single game, the player should get three balls before losing. Keeping track of this requires a new attribute, either in Breakout or Model. We will let you decide where it best fits.

If the player can have another ball, the update method should change the state to STATE_PAUSED and display a message (as you did on the welcome screen) that the player should click the screen to get a new ball. As soon as the player clicks the screen, switch the state to STATE_COUNTDOWN and prepare to serve a new ball.

Winning or Losing

Eventually the game will end. Each time the ball drops off the bottom of the screen, you need to check if there are any lives left. If not, the game is over. Additionally, as part of the update method, you need to check whether there are no more bricks. As you have been storing the active bricks in the attribute bricks, an easy way to do this is to check the length of this list. When the list is empty, the game ends and the player has won.

When the game ends, and the player has either won or lost, you should put up one last message. Use a GLabel to put up a congratulating (or admonishing) message. Finally, you should change the state one last time to indicate that the game is over. This is the purpose of the state STATE_COMPLETE.

Try to finish this part by Monday, May 5.


Extending the Game

If you have followed our suggested timeline, you now have one or two extra day that you can use to extend the game and try to make it more fun. In doing this, you might find yourself reorganizing some (or a lot) of the code above. You may add new methods, or change any of the three main methods above. You may add new classes. For example, you may decide to make Brick a subclass of GRectangle (as you did with Ball and GEllipse). That way the various bricks can hold extra information (e.g. power-ups). You can also change any of the constants or add new ones.

All of this is acceptable. Now that you have proved that you can get the code working, you are free to change it as you see fit. However, we highly suggest that you save a copy of the basic game in a separate folder before you start to make major changes. That way you have something to revert to if things go seriously awry when implementing your extensions. Also, please be sure to comment your code well in order to keep track of where you are in the coding process.

Extensions are not mandatory, and will be considered extra credit when it comes to grading. However, do make sure that any new methods, attributes, and invariants that you add are properly specified.


Possible Extensions

Here are some possible ways to extend the game, though do not feel constrained by any of them. Make the game you want to make. We will reward originality more than we will reward quantity. While this is a fairly simple game, the design space is wide open with possibilities.

Improve user control over bounces

The game gets rather boring if the only thing the player has to do is hit the ball. Let the player control the ball by hitting it with different parts of the paddle. For example, suppose the ball is coming down toward the right. If it hits the left 1/4 of the paddle, the ball goes back the way it came (both vx and vy are negated, instead of just vy). The same goes for if the ball is coming down toward the left, and hits the right 1/4 of the paddle.

Implement "Try Again"

Let the player play as many games as they want. After a game ends, allow the player to click the mouse button to start a new game. You will need to change how you handle your states to implement this.

Implement sound effects

A really easy extension is to add appropriate sounds for game events. We have provided several audio files in A5.zip. You are not restricted by those; you can easily find lots more (but it is a violation of the Academic Integrity Policy to use copyrighted material in your assignment).

To load an audio file, simply create a Sound object as follows:

bounceSound = Sound('bounce.wav')

Once it is loaded, you can play it whenever you want (such as when the ball hits something) by calling bounceSound.play(). The sound might get monotonous after a while, so you might want to make the sounds vary, and figure out a way to let the user turn sound off (and on).

Read the online specification to see how to use Sound objects. In particular, if you want to play the same sound multiple times simultaneously (such as when you hit two bricks simultaneously), you will need two different Sound objects for the same sound file.

Add a kicker

The arcade version of Breakout lures you in by starting off slowly. But as soon as you think you are getting the hang of things, the ball speeds up, making the game more exciting. Implement this in some fashion, perhaps by doubling the horizontal velocity of the ball on the seventh time it hits the paddle.

Keep score

Design some way to score the player's performance. This could be as simple as the number of bricks destroyed. You may also want to make the bricks in the higher rows more valuable.

If you keep score, you should display it at all times using a GLabel object. Where you display it is up to you (except do not block the player's view of the balls, paddle, or bricks). Do not make a new GLabel object each time the score changes. Simply change the text attribute in your GLabel object.

Use your imagination

What else have you always wanted a game like this to do? At some point your game might become less like Breakout and be more like Arkanoid.

You can make any modifications to the gameplay you want, but the core gameplay of bricks, paddle and balls should be there. Please do not submit an implementation of Asteroids.


Completing the Assignment

Before submitting anything, test your program to see that it works. Play for a while and make sure that as many parts of it as you can check are working. If you think everything is working, try this: With the ball moving downward, just when it is about to pass the paddle level, move the paddle quickly so that it hits the ball (from the side) rather than the ball hitting the paddle. Does the ball bounce upward as it should, or does it seem to get "glued" to the paddle? If you get this error, try to understand why it occurs and how you might fix it.

When you are done, re-read the specifications of all your methods and functions (including those we stubbed in for you), and be sure that your specifications are clear and that your functions follow their specifications. If you implemented extensions, make sure your documentation makes it clear what your extensions are.

As part of this assignment, please follow these style guidelines:

  • There are no tabs in the file, only spaces.
  • Classes are separated from each other by two blank lines.
  • Methods are separated from each other by one blank line.
  • Lines are short enough that horizontal scrolling is not necessary (about 80 chars max).
  • The specifications for all of the methods and properties are complete.
  • Specifications are immediately after the method header and indented.

Turning it In

You are potentially modifying a lot of files in this assignment. At a bare minimum, you should have added to controller.py and model.py. You might be modifying constants.py. You might have extra art and sound files.

In addition, you should create a text file called extensions.txt. In this file, you should write a brief description of your extensions. Tell us what you were trying to do and how you did it. If you did not include any extensions, say so in this file.

To simplify the submission process, we are not asking you upload each individual file. Instead, put all your files in a zip file called a5.zip and submit this instead. We need to be able to play your game, and if anything is missing, we cannot play it.

Make sure you have comments crediting every source (document or person) that contributed to your submission.


Frequently Asked Questions

Over the semesters that we have given out this assignment, there are a few questions that get asked during office hours, Piazza, etc. rather frequently. For your convenience, here is a list of some of them. (More may be added later on.) These are generally aimed toward errors that don't necessarily produce explicit error messages, which make them hard to debug.

  1. I'm getting an infinite loop somewhere in update, etc.; why?
    A: Questions like these usually arise from a misunderstanding about how update and draw work. These two functions are automatically called by game2d.py on every frame of the game. Don't call them yourself at any time. Anything that you put into one of these functions means that you intend it to happen every frame (barring if-statements, etc.)

  2. Why is the first method for Breakout named init? Isn't it supposed to be __init__?
    A: This relates to the above; init is not, strictly speaking, Breakout's initializer (as in it creates a new instance of Game). That is handled by game2d.py again, and is the reason why you already have a view attribute once the method starts, as well as a game window. After game2d.py creates a Game instance (with its own initializer), it calls Breakout's init once before moving on to update and draw.

  3. What's with all the single underscores before names like _state and _getCollidingObject?
    A: Good question! Single underscores before names in Python indicate that they are hidden methods/attributes. This means that it is not allowed to access them outside of the class they are in, so for example, Model is not allowed to access Breakout's _state, and so on. You had a little experience with these in A3, where _legacy was a hidden attribute that could only be accessed through the non-hidden method get_legacy (which handily calculated it right when it was needed).

  4. My paddle is mysteriously warping back to the middle every time I try to move it! What is happening?
    A: Try printing out the id of the paddle during update. Is it different every time? If so, then that means that you are somehow throwing away the old paddle and creating a brand new one on every frame of the game, which you probably don't want to do. The paddle jumps back to the middle since that is presumably the initial location you set it to.

  5. After the ball hits the right/left wall of the screen, it gets 'glued' there. Why?
    A: This is probably caused by a logic error in your wall collison code. As a hint toward how to fix it, make sure the ball only bounces off the right wall when it is moving to the right to begin with.

  6. You said that our program needs to work with any number of rows of bricks. But if we put 50+ rows of bricks then they overflow past the bottom of the screen and the game becomes impossible to beat. Now what?
    A: You do not have to worry about this corner case. If your program works with 20+ rows and the colors display properly, then that is fine.

  7. I want to create a background as one of my extensions. How do I do that?
    A: Just draw a large GRectangle object that covers the screen. Make sure that it is the very first thing you draw, as objects are drawn back-to-front (later objects appear on top of earlier objects).

  8. It's annoying to have to constantly press down the mouse to move the paddle. Is there a way to move the paddle just by moving the mouse without having to press down?
    A: Unfortunately, there is not. This is a limitation of Kivy, as it is designed for touch screen devices where there is no such concept as moving without pressing down.

  9. I tried right-clicking on the screen and some strange red dots appeared! What are they?
    A: As above, this is a functionality of Kivy. Right-clicking in any Kivy app is designed to simulate multi-touch on a touchscreen device (notice you can right-click repeatedly to get more red dots, and drag those dots around after creating them). Don't worry about it.

  10. How do I open Komodo Edit/How do I run Python/What is an object/I started 10 minutes before the deadline and now I can't finish, what do I do?
    A: .........

Course Material Authors: D. Gries, L. Lee, S. Marschner, & W. White (over the years)