Games Computer Play

Spring 2025

Program 4: Breakout!

Due: Friday, April 11 at 5pm (note extension)


Game Overview

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.


general instructions


step-by-step instructions

  1. 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.

  2. 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.

  3. 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).

  4. 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).

  5. 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…

  6. 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…

  7. 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.

  8. 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.

  9. 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.

  10. 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.

  11. 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!

  1. 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.

  2. 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.

  3. 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.)

  1. Remember to put your name and status update in the comments atop the program.

The remaining steps are recommended, but not essential to successful game play: