T-Th 9:05 |
CS 1110: Introduction to Computing Using Python Fall 2013 |
||||||||||||||||
Main
About: Announcements Staff Consultants Times & Places Calendar Materials: Texts Python Command Shell Terminology VideoNote Handouts: Lectures Assignments Labs Assessment: Grading Exams Resources: CMS Piazza AEWs FAQ Python Tutor Python API Style Guide Academic Integrity |
Assignment 6:
|
This assignment, including some of the wording of this document, is adapted from an assignment by Professor Eric Roberts at Stanford University. Your task is to write the classic arcade game Breakout. If you have never 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 weird features. The flash game Star Ball is an example of a fairly 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. |
Despite the difficulties you might have had with the previous assignment, we know that this assignment 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 breakout.py and ~300 in model.py). It has ~15 methods (not including getters and setters) beyond those in the original code skeleton.
This final assignment has several important objectives.
Authors: W. White, D. Gries, E. Roberts
This is a classic assignment that we are giving again because students really like it. There are some major modifications made in the assignment this year, however. We are aware that a solution was leaked last year, and that it is floating around out there. So we made major changes in the code architecture this year, to minimize the effect of this solution.
With that said, please do not look up the old solution. That is still a violation of academic integrity, and copying code (without attribution) from that solution is an instance of plagiarism. The program Moss checks for similarity in parts of the code, not just all of it. If you copy code from the old solution (even though it is different) we will know. The code is too complex for accidental similarities to happen.
The other warnings about academic integrity also stand. Do not share your code with others, in this semester or with students in future semesters. We prohibit this behavior because engaging in it so would help no one in the long run.
There is a lot of code available for you to use in in the Lecture on OO Design. The animation and etch-a-sketch examples are particularly useful. You are permitted to copy whatever code you want from these (or other samples).
However, if you copy code you must cite the source. This is no different than quoting a book or article in an essay or research paper. In the function specification, you should add a paragraph citing the source module and the original authors (likely the CS 1110 instructors). You must cite your source even if you make changes; that is no different than paraphrasing a quote in an essay or research paper.
Unless you make drastic changes, Moss will catch any copying of the demo code from lecture. Do it right; cite the source.
You may do this assignment with one other person. If you are going to work together, then form your group on CMS as soon as possible. If you do this assignment with another person, you must work together. It is against the rules for one person to do some programming on this assignment without the other person sitting nearby and helping.
With the exception of your CMS-registered partner, you may not look at anyone else's code or show your code to anyone else, in any form what-so-ever.
This is another long assignment, like the last one. However, both I and the consultants have a lot more experience with this assignment, and so it should go much smoother. Once gain, the trick is to pace yourself. While there are no unit tests this time, you should be able to figure out if everything is working simply by playing the game.
The first thing to do in this assignment is to download the zip file A6.zip from this link. You should 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
Breakout
)
for this application. This is one of the two modules that you will modify
for this assignment. However, note that it has no script code. For that,
you will use the module __main__.py
below.
model.py
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
GView
)
for this application. It also contains the parent classes for your
model and controller. Under no circumstances should you ever
modify this file.
constants.py
breakout.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
Sounds
Fonts
Images
GImage
allows you to
animate images in this game, should you wish. You can also use it
to provide a background; just remember to draw the background image
first.
For the basic game, you will only modify the first
two files listed above. The class Breakout
is a subclass
of the class Game
. With the
exception of Model
, your model classes 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.
Because there are so many files involved, this application is handled a little
differently from previous assignments. To run the application, put all of the
files in a folder, and give the folder a
name like breakout
. The file __main__.py
turns the
entire folder, not just one module, into a script. Change the directory in
your command shell to just outside of the folder breakout
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 script; 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
.
This assignment closely follows the model-view-controller pattern
discussed in class. The modules are clearly organized in as model
view and controller, as 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 importants
the view because it needs the parent class GObject
to perform any drawing. The view does not import anything
(and should not be modified). Not that there are no cycles
in this architecture; cyclical imports are very dangerous.
In addition to the three main modules, there is another module with no class or function definitions. The only thing it has is constants (global variables that do not changed). This 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?" The controller is designed to handle input (e.g. the mouse) and call the drawing code in the view. It also handles any changes to the program state.
Anything that changes the data in the models belongs as a method
in a 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.
As you can see from the online documentation,
the class Breakout
needs to implement three main methods.
They are as follows:
Method | Description |
---|---|
init() |
Initializes the game state and attributes. Because of how Kivy works, initialization code should go here and not in the constructor (which is called before the window is sized properly). |
update(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 constructor. The parameter dt is time
since the last call to update .
|
draw() |
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 .
|
Obviously, you are not going to put all of your code in those three methods;
the result would be an unreadable mess. An important part of this assignment
is developing new methods whenever 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 (though init
, update
,
and draw
are exempt from this rule)..
You will also need to add methods and attributes to the classes Model
and Ball
in model.py
. These classes are completely
empty. When you add an attribute to these classes (or to the controller class
Breakout
) you must fully state the invariant in your specification.
All instance attributes should be hidden. While you do not need to enforce the
invariants, you must have getters and setters if the attributes are accessed
outside of the class (e.g. such as if Breakout
needs to access
the Ball
attribute in Model
).
If you show your code to an instructor, TA, or consultant and they see a method that is not specified or an attribute that is not mentioned in the class specification, they will ask you to go away, fix it, and come back at another time.
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, post an English description of the class (do not post
code) on Piazza, and we will tell you.
You should start as soon as possible. If you wait until the day before this assignment is due, you will have a hard time completing it. If you do one part of it every 2-3 days, you will enjoy it and get it done on time. We have also tried not to include (too much) of your Thanksgiving Break on this assignment.
The hard part of this assignment may be "finishing up": designing the final reorganization in order to incorporate three balls in a game. We have budgeted you one day for this, but you have up to four days if you decide just to do the bare minimum and not add any extensions.
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 up your own schedule. 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.
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 and the course website.
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 is in a fixed position in the vertical dimension; 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 turns. On each turn, 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 world, 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 on a turn continues in this way until one of two conditions occurs:
In the first case, the turn ends. If the player has a turn left, 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 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".
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.
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 set up the bricks and start a new
game.
For your convenience, we have provided you with constants for four 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 code.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 four states.
We have divided these instructions into two parts. The first part covers the basic things that you must implement just to get the game running. Once you do that, the assignment gets more interesting. You should try to finish everything in this part of the assignment by Saturday, December 7 (after the last week of class). 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).
The very 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 hard to debug. And if you make a change (e.g. to make the ball bigger),
you have no idea about all of the locations in your code that need to be changed.
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.
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 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. If
you read the specification for class Breakout
, you will not see any
attributes for the text message. That means it is your responsibility to add one.
You should add the description of your attribute to 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
, the first important method
of the class Breakout
. 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. You should experiment with these attributes to get the welcome screen
that you want. The key thing to remember is that, in Kivy, that screen coordinates
start from the bottom-left corner of the window.
Simply adding this code to init
is not enough. If you were to
run the application right now, all you would see is a blank white window.
You have to tell Python what to draw. To do this, simply add the line
self._message.draw(self.view)
to the method draw
in Breakout
. The (non-hidden)
attribute view
is a reference to the window; this instructs Python
to draw this text label in the window. Now run the application and check if
you see your welcome message.
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; we have not done anything yet!
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:
STATE_INACTIVE
, then there is a welcome message
STATE_INACTIVE
, the welcome message is None.
Does your definition of init
satisfy this invariant?
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 track this with the attribute touch
which is part of GView
. This
attribute is a GPoint
if the mouse
button is currently down.
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
even 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, so you need to assign None
to the welcome message as well. This will require a simple change to method
draw
to keep it from crashing. Once you have done that, run the
application. Does the message disappear when you click the mouse?
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 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. We will deduct style points for instance attributes that are not
specified.
Try to finish this part by Friday, November 22 (e.g. two days after starting the assignment). You will spend most of your time reading the online documentation. But this will give you a solid understanding of how this application works.
Technically, you are violating an invariant after the last step. When you changed
the state to STATE_COUNTDOWN
, the attribute _model
was
no longer supposed to be None. Construct a Model
object and assign
it to this attribute.
Right now, the Model
instances do not have any data in it. Eventually
it is going 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 (called _bricks
) that is designed to store a list
of bricks. These bricks should be created in the initializer for Model
(the real one, __init__
) so that the model is full of bricks when
the constructor is called in update
.
Eventually, we are going to want to do a lot of other things in the initializer for
Model
. Hence you might want to create a helper method for the initializer
that does nothing but set up the bricks. As you have seen in class, it is okay for
initializers to use helper methods. Whether you do or do not is up to you, but
remember the 40 line limit.
You should set up the bricks as shown to the right. The number, dimensions, and
spacing of the bricks, as well as the distance from the top of the window to
the first line of bricks, are specified using global constants given in module
constants
. The only value you need to compute 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 sides
(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, you are to start
over with RED, and do 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.
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 constructor using keywords; see the online documentation
for more. When you color a brick, you should set the line color so that its outline is
the same color as the interior (instead of black).
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 or a
while-loop; just get the job done.
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
.
You should note that the attribute of Breakout
is _model
,
not _bricks
; _bricks
is an attribute of a model object.
If you are going to access the bricks in Breakout
, you must add a
getter. We will take off style points if the controller ever accesses the hidden
attribute of a model directly.
If you do not want to bother with getters, 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 get several rows
of bricks.
When you are testing the later parts of this assignment, you should play 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 requires you to change the values
of the global constants that give the number of rows and number of bricks in a
row. This is also undesirable, a you might forget to change them back. Instead,
we have added some clever code in constants.py
that allows you to
change these constants when you start the application.
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 number of rows and any number of bricks in each row (e.g. 1, 2, ..., 10, and perhaps more). This is one of the things we will be testing when we run your program.
Try to finish this part by Sunday, November 24. 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.
Next you need 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
.
The real challenge with this part is making the paddle move. Animation is
handled by the controller, Breakout
, so that means it will
need access to the paddle. You should add a setter-type method that
allows Breakout
to change the paddle position. We say
setter-type because it should not actually assign an entire
new paddle (that would be slow and wasteful). It should just set the
horizontal position of the paddle (via an attribute like x
or center_x
).
The paddle should only start moving once the user presses the mouse,
or the touch
attribute of the GView
is no longer None. Personally, we like to require that the mouse press
be inside of the paddle (so the paddle will not move if you press outside
of the paddle). You can test this with the
contains
method inherited from GObject
. With that said, some students
find this makes the game frustrating, so this part is optional.
Regardless of whether or not you do this, it is important that the paddle should not teleport. If you can click anywhere on the screen and the paddle moves there instantly, 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 the distance between
the paddle and place the player first pressed 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 (which we did not provide).
Add its specification to Breakout
and remember to initialize it in
the init
method (in a way that satisfied 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. You might want to refer to the etch-a-sketch demo shown in class for ideas on how to do this. Remember that if you copy any code, you need to cite its source in the function specification or comments.
You should ensure that the paddle stays completely on the board even if the touch
moves off the board. If you do not do this, the paddle is going to be completely lost
when you release the mouse. Our code for this extra 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
. Your implementation should only allow the paddle to be moved
when the game is ongoing. That is, the state should either be STATE_COUNTDOWN
or STATE_ACTIVE.
At this point you might want to start thinking
about helper methods. Look at the state demo from
lecture for ideas on how to organize your code.
Complete this part by November 26, before heading off for Thanksgiving. We will have consultants on that Tuesday, before you leave. Though there is no lab, the lab sections will open as general office hours to help you. If you can do this before you leave, you will be in very good shape for the rest of the assignment.
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 has 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 in the class initializer. In addition, you will probably want to create
getters and setters for these attributes. Model
is a different class than
Ball
and it is not allowed to access the attributes directly. If you
are comfortable using properties for your getters and setters, that is okay. However,
if properties confuse you, then you should probably avoid them and just use normal
getters and setters.
Once again, you should write a proper initializer, overriding the __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. Keep in mind that
attributes x
and y
are the bottom-left corner, and not the
center of the ball. You might find the attributes center_x
and
center_y
useful.
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 _vy
; you can adjust this later as you see fit. The game would
be boring if every ball took the same course, so you should choose component
_vx
randomly. Do this with the module
random, which
has functions for generating random numbers (you have already had some experience
with this module in the previous assignment).
To get you start, we suggest that you initialize _vx
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).
Serving the ball is as simple as adding it to the model (in the provided field _ball
)
and drawing it. Once again, you will either need a getter to draw it, or you will need a
draw
method in 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 difficult part is figuring out when to do this. An obvious time to serve the ball
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. For ideas on how to do this, look at how we time double clicks in the etch-a-sketch demo. Once again, you should cite the source if you copy any code.
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.
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
.
This method should not be hidden, since the controller will call it as part of
update
whenever the state is STATE_ACTIVE
. The purpose of
this method is to move the ball and handle any physics. It is perhaps the most
complicated method in the entire assignment.
Each time this meothd 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. 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. However, the attribute x
is the
left side of the ball. See the attributes in GObject
for clues on how to take care of this problem.
Try to finish this part by Monday, December 2, after coming back from Thanksgiving. You do not have to work over Thanksgiving if you do not want to. However, if you slip on this deadline, it will cut into your time for your extensions.
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
gobj
, the method call
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 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. An 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 outside of the ball, but they are close enough to make it appear that a
collision has occurred.
You should write a hidden 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.
Once you have completed this, you should be able to start playing a game.
Try to finish this part by Thursday, December 5..
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.
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. In a single game, the player should get three
balls before losing. 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.
Eventually the game will end. Each time the ball drops off the bottom of the screen, you
need to check if there are any tries 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 attribute _brick
, 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. Once again, Breakout
cannot access this attribute
directly; you need a getter.
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 Friday, December 6.
If you have followed our suggested timeline, you now have three days to extend the game and
try to make it more fun. In doing this, you might find yourself reorganizing 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 even 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, we suggest that you make sure to comment your code well in order to keep track of where you are in the coding process.
Extensions are not mandatory, but they do affect grading. Add more and we will be much more lenient with our grading than if you implemented the bare minimum (i.e. everything up to this point). Hence they are a form of extra credit, but you cannot go over the maximum score for the assignment. Note that a submission that does not have good specifications for the helper methods or invariants for the new attributes will not be looked at kindly under any circumstances.
Here are some possible ways to extend the game, though you should not be 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.
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 (or left). If it hits the left
(or right) 1/4 of the paddle, the ball goes back the way it came (both _vx
and _vy
are negated).
Let the player play as many games as they want. The player could click the mouse button to start a new game. You will need to change how you handle your states to implement this.
A really easy extension is to add appropriate sounds for game events. We have provided several audio files in A6.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, you 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 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.
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 life more exciting. Implement this in some fashion, perhaps by doubling the horizontal velocity of the ball on the seventh time it hits the paddle.
Design some way to score the player's performance. This could be as simple as the number of bricks destroyed. However, you may want to make the bricks in the higher rows more valuable.
You should display the score at all times using a GLabel
object. Where you display it is up to you (except do not keep the player from seeing the balls,
paddle, or bricks). You do not need to make a new GLabel
object each time the
score changes. Simply change the text
attribute in your GLabel
object.
What else have you always wanted a game like this to do? At some point your game might stop being like Breakout and be more like Arkanoid. Do not go too wild with the power-ups, however. We much prefer a few innovations that greatly improve the play.
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 a copy of Asteroids.
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: just before the ball is going to pass the paddle level, move the paddle quickly so that the paddle collides with the ball rather than vice-versa. Does everything still work, or does your ball 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 very clear what your extensions are.
As part of this assignment, we expect you to follow our style guidelines.
There is a new coding convention which we have not yet explicitly stated. Non-hidden methods should be in camelCase. That is, compound words are not separated by underscores, but instead have the first letter of each word (after the first) upper case. This allows us to distinguish methods from functions, as functions are all lower case separated by underscores. You are allowed to break this convention with hidden helper methods, but not in methods that are meant to be accessed by other classes.
You are potentially modifying a lot of files in this assignment. At a bare minimum,
your ad modifying 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.
To simplify the submission process, we are not asking you upload each individual file.
Instead, put all your files in a zip file called a6.zip
and submit this
instead. We need to be able to play your game, and if anything is missing, we cannot
play it.
One last time, we need you to do a survey. The survey should be done individually (even if you worked in a group). As always, the survey will ask about things such as how long you spent on the assignment, your impression of the difficulty, and what could be done to improve it. Please try to complete the survey within a day of turning in this assignment. Remember that participation in surveys comprises 1% of your final grade.