We are implementing the classic Atari arcade game Breakout. Our implementation aims to be faithful, with the caveat that our “square” graphics system imposes some constraints, most notably that the “ball” will appear as a square. (Likewise, the ball will not always appear to travel in a straight line.)
We use objects to represent: the game itself, the paddle, the ball and the bricks.
We use interactive event-handling, in particular, mouse movement to allow the paddle to respond quickly while the ball moves seemingly simultaneously and independently.
Work on your own. (But reach out to me early and often with questions!)
Submit your work by uploading one file: your
breakout.py
file via MySLC.
Use only the Python expressions and statements I have used thus far in class or in examples I have sent or included with this assignment. If you are not sure whether to use a feature, just ask.
Download the starter archive. (Right-click
on the link and choose a location to save the file on your computer.)
Expand the archive. It includes
interactive_graphics.py
.
Write all your code in the file
breakout.py
. Do not start writing until you have a sense of
what is being asked. Read this entire document before writing any
code.
Minimal requirements:
Expected:
Recommended:
You can and should choose your own game parameters (number of bricks, etc).
Replace ... your name ...
with your name in
the comment near the top of the file. (Remove the ...
,
too.)
Lines that contain ...
indicate places where you
should expect to modify or add one or more lines of code. Remove the
...
when you have completed that region of code.
Test your work as you go!
Be sparing with your own comments. Proofread them before submitting.
Take care to format your code to be readable; follow the model of my examples.
Be consistent with formatting, spacing, indentation; be sensible with variable naming.
Before submitting: replace ... status ...
comment
near the top of the file with a brief comment as to how much of the
assignment you completed. (For example, “I completed the entire game and
I am confident it works correctly.” or “I was only able to get the the
ball to move vertically.” )
Hints available upon request!
Read the code; try running it as it out of the box: the paddle
and ball appear. Clicking anywhere in the graphics window should cause
the ball to drop straight down, albeit slowly. The ball will pass
through the paddle and then go off screen, triggering
invalid coordinates
warnings.
Add code to Paddle.move
. It takes an argument
representing the “target” column that we want the center of the paddle
to move toward. The paddle is represented as a single horizontal line of
boxes. Its length is twice PADDLE_SPAN
plus one. Depending
on PADDLE_SPAN
, the overall width of the paddle might be 1,
3, 5, 7 (or even more) boxes. (This starter version has
PADDLE_SPAN
set at 3, making a relatively large paddle of 7
boxes.) Paddle.move
should try to change the
mid
attribute of the paddle so that it is one column closer
to the target as long as that still leaves the left
(self.mid - PADDLE_SPAN
) and right
(self.mid + PADDLE_SPAN
) visible in the graphics window.
(If that can’t be the case the paddle should stay where it is; so it may
end up against the left or right edge until the mouse is moved the other
way.) Test by running (obviously) and seeing if the paddle follows the
mouse’s horizontal location within the graphics window.
Make the ball bounce off the paddle. Much of this has already
been implemented for you! You only need add code to
Paddle.hit_check
so that it returns anything other than
None
when the supplied point is somewhere along the paddle:
so the y-coordinate of the supplied point has to be the same as the
paddle’s y-position (which remains constant at PADDLE_ROW
)
and the x-coordinate of the supplied point has to be within the left and
right edge of the paddle. At this point it is fine to return
True
when the point is in the paddle. (Later we will try to
consider where along the paddle it is hit to alter the
horizontal direction of the ball.) Test by (running and) having the ball
hit the paddle. You may wish to increase INITIAL_SPEED
somewhat. Assuming this works, if the ball hits anywhere on the paddle
it should bounce straight back up and eventually go off the top of the
screen (causing the usual warnings).
Make the ball bounce off the top of the screen. Modify
Game.update
(it should be clear where) so that if the
y-coordinate of the ball (the integer-rounded coordinate) would
otherwise be off the top of the screen, it instead bounces - calling the
ball’s bounce_y
method. Modify Ball.bounce_y
so that it now adjusts according to whether it is reflecting up to down
or vice versa (compare its dy
property to 0). Test as
before but now the ball should bounce between the paddle and the top of
screen over and over with no warning messages (unless you move the
paddle out of the way).
Modify Ball.drop
so that when the ball is first
dropped its dx
value is set to a random amount between -0.5
and 0.5. This is easy to do (use random
) and easy to test.
Each time you run, the ball may start falling at a different angle. And
bouncing off the paddle should preserve the horizontal direction. Of
course, now that you do this, warnings will appear if the ball goes off
either side of the screen…
Modify Game.update
and Ball.bounce_x
as
you did earlier, so that if the ball would otherwise go off either side
of the screen it bounces horizontally. You should now be able to have
the ball bounce all around the screen - off the sides and top and the
paddle - as much as you want; warnings will appear if you miss the ball
and it heads off the bottom of the screen…
Modify Game.update
so that if the ball would be just
off the bottom of the screen it calls the lose_ball
method.
Modify that so that it turns off the game_on
attribute
(sets it to False
). Modify Game.render
method
so that if the game is no longer on, it sets the status to indicate that
the game is over. Modify Game.click_handler
so that if the
game is no longer on then a click causes the game window to close. You
can test this easily: play and the let the ball get past the
paddle.
Create a new class called Brick
. It should have a
constructor and a draw
method. There are many ways we might
represent them; but we take a simple (if inefficient) approach. Each
Brick
stores the x-y coordinates of its left edge. All
bricks are one square high, and BRICK_WIDTH
squares wide.
Modify the Game constructor (Game.__init__
) so that it
generates layers of bricks. I recommend using nested for
loops to generate BRICK_ROWS
number of rows with each row
having BRICKS_ACROSS
bricks. To be clear the
bricks
attribute of the Game
object is
intended to be a list of bricks. So the general form of this
construction will be:
for ... in range(...):
for ... in range(...)
self.bricks.append(Brick(...))
What x-y coordinates should you use for each brick created? The y
values should range between the TOP_ROWS
value (that
represents how much space, if any should appear above the bricks - two
should be fine) and then run on down one row for each of
BRICK_ROWS
. So, if there are 8 rows, starting at row 2, all
the bricks will have y-values between 2 (inclusive) and 10 (exclusive).
The x-values should begin at the left edge of the window (0) and then
proceed to go up by BRICK_WIDTH
columns at a time. So if
that is 3, then there are bricks starting with x-values of 0, 3, 6, 9,
12, etc. The bricks should be flush next to each other because in our
“chunky graphics” world, the ball is one square wide; we do not wish to
start with space between bricks where the ball can fit.
Games.render
already tries to draw all the bricks; so the
only other thing you need to do is to complete the
Brick.draw
method so that it draws the brick. I recommend
starting by drawing all bricks to be the same color. Because
COLUMNS
is determined based on the number and size of
bricks, once displayed, each row of bricks should run the entire width
of the graphics window. Test by running and you should see the bricks as
one big rectangular blob near the top of the window. Since
Game.render
redraws everything, the ball should pass
through the bricks, ghost-like.
Make the ball bounce off of bricks. Add a method
has_pt
to the Brick
class that returns
True
if the supplied point p
is in the brick.
(This is similar to Paddle.hit_check
.) Complete
Game.bricks_hit
so it returns the brick that the
point p
hits if any. Modify Game.update
so
that it calls bricks_hit
and uses the result to determine
if any brick was hit - in other words it assigns the result of
bricks_hit like:
b = self.bricks_hit(...)
and then checks
if b is not None:
...
Put the code that handles whether the ball is bouncing off an edge
(or going off the bottom) in the else
branch of that
if
. If a brick was hit, have the ball bounce off the brick
vertically. At this point when you run it should behave like before
except the ball will just bounce off the bottom row of bricks as if it
were the top edge.
Modify Game.update
so that if a brick is hit, that
brick is removed from the list of bricks. In Python we can do
that trivially (if inefficiently) by using the list’s
remove
method. That alone should automatically have the
effect of letting the brick disappear upon collision and allowing the
ball to now pass through the space left behind (if hit again). Test
carefully.
Make it possible to win by clearing all the bricks. To test
winning, you may wish to reduce the number of bricks and make them
bigger. Note: at this point there is no way for the player to adjust the
horizontal direction of the ball, so unless the bricks are nice and
wide, it may be hard to clear them all. (We will address that limitation
in a forthcoming step.) Add code to Game.update
that checks
if there are any bricks left and if not, sets the game_on
attribute to False
. (You may wih to adjust
render
so that the message indicates game over - but with a
victory message.)
At this point, assuming you have thoroughly tested your code, you will met the minimal requirements. But do not stop here!
The game will only be interesting to play if the player has more
control over the direction of the ball. To that end, modify
Paddle.hit_check
so that it if the point p
is
hitting the paddle then the method returns an integer indicating what
part of the paddle is being hit. It should return 0 if it hits the
middle, a negative value if the left half, and positive value if the
right half. In particular, the value should indicate the distance from
the middle of the paddle. Modify Game.update
so that the
value returned by hit_check
is used as in:
... = self.paddle.hit_check(self.ball.loc())
if ... is not None:
self.ball.spin(...)
self.ball.bounce_y()
and complete Ball.spin
so that it adjusts the
dx
property in a manner you see fit as long as:
dx
is never larger than 1 nor smaller than -1. I recommend
having the value returned by hit_check
be to sent in to
spin
and that used to determine whether to make the
dx
value larger or smaller (to go toward the right or
left). You have a lot of say as how this works; test it thoroughly to
make sure this makes the game easier to play rather than
harder.
Add a score attribute to the Game
class. It should
start at 0 (in Game.__init__
) and be increased each time a
brick is hit (in Game.update
). Modify
Game.render
so that it displays the score in the status
bar.
Add a constant at the top of the program representing how many
lives (balls) the player is allowed to have before the game ends. And an
attribute to the Game
class that starts (in
Game.__init__
) at that constant value. Modify
Game.lose_ball
so that it decreases each time a ball is
lost and so that the game ends if it gets down to 0. Modify
Game.render
to display - in the same status line - both the
score and number of lives remaining.
At this point you have essentially built the full game! (Remove or adjust code that you have left in for testing purposes. Remove my comments that tell you where you need to work.)
The remaining steps are recommended, but not essential to successful game play:
Modify the way bricks are represented so that they can have individual colors. Modify the way the list of bricks is created so that bricks have different colors. I recommend against using random colors; the more important idea is to differentiate neighboring bricks since we cannot represent spaces or borders between them.
Add code where you see fit so that the ball accelerates under
certain circumstances. That means, its speed
attribute goes
up (but never above 1). For example, in my solution, the
Game
object keeps track of which “level” of bricks has been
reached, each new level, causes a slight acceleration. But you could
choose other ways to achieve the speed up - for example, simply based on
how many bricks have been cleared.