Games and Programming in the Wild

Breakout


Learning Objectives

  • You can make a moving object bounce by reflecting its velocity off a surface.
  • You can build a grid of objects with nested loops and store them in a list.
  • You can detect a collision between a moving point and a rectangle.
  • You can remove objects from play by marking them, and detect a win condition.
  • You can keep a paddle on screen by clamping its position.

Breakout is the game where a ball bounces around, you move a paddle to keep it in play, and the ball knocks out a wall of bricks one at a time. It combines the two ideas from the previous chapters: a moving object with a velocity (the ball, like the rocks) and a collection of objects to interact with (the bricks, like the list of rocks). The new idea is reflection — making the ball bounce off a surface.

Reflection Is Just Negation

When a ball hits a vertical wall, it reverses its horizontal direction but keeps its vertical motion. When it hits a horizontal surface — the ceiling, the paddle, a brick — it reverses its vertical direction and keeps its horizontal motion. That is the whole of bouncing, and in terms of the velocity it is one line each:

self.ball_dx = -self.ball_dx # vertical-wall bounce (left/right)
self.ball_dy = -self.ball_dy # horizontal-surface bounce (top/paddle)

Negating dx flips left-going into right-going and vice versa. Negating dy flips up into down. A ball travelling down-and-right (dx positive, dy positive) that hits the right wall negates dx and now travels down-and-left — a clean bounce.

There is no angle calculation, no trigonometry. Reflection off a flat surface is sign-flipping a number.

The ball bounces off the screen edges like this:

self.ball_x = self.ball_x + self.ball_dx
self.ball_y = self.ball_y + self.ball_dy
if self.ball_x < 2 or self.ball_x > 158: # hit a side wall
self.ball_dx = -self.ball_dx
if self.ball_y < 2: # hit the ceiling
self.ball_dy = -self.ball_dy

Notice there is no bounce off the bottom edge. The bottom is where the ball is lost — if it gets past the paddle and reaches the floor, the game ends. That asymmetry is what makes the paddle matter.

The Paddle, and Clamping

The paddle moves left and right with the arrow keys, the same way the dot moved in the game-loop chapter. The new wrinkle is keeping it on the screen: the player should not be able to push it off the edge. Rather than wrapping (this is not Asteroids), we clamp the position — force it to stay within a range:

if pyxel.btn(pyxel.KEY_LEFT):
self.paddle_x = self.paddle_x - 3
if pyxel.btn(pyxel.KEY_RIGHT):
self.paddle_x = self.paddle_x + 3
self.paddle_x = max(0, min(self.paddle_x, 130))

That last line is a clamp: min(self.paddle_x, 130) is “the paddle position, but never more than 130” — it caps the right side. max(0, ...) is “that, but never less than 0” — it caps the left side. Nested together, max(0, min(paddle_x, 130)) keeps the value between 0 and 130 (the paddle is 30 wide on a 160-wide screen, so 130 is as far right as it can go while staying fully visible). Clamping with max and min like this is a common idiom you see in many programs.

Bouncing Off the Paddle

The ball should bounce up when it meets the paddle. The paddle is a rectangle, the ball is roughly a point, and the ball is on the paddle when its x falls within the paddle’s width and its y has reached the paddle’s height:

if (self.ball_y > 112 and self.ball_dy > 0
and self.paddle_x <= self.ball_x <= self.paddle_x + 30):
self.ball_dy = -self.ball_dy

Three conditions, all of which must hold. self.ball_y > 112 — the ball has dropped to the paddle’s level. self.ball_dy > 0 — the ball is moving downward; without this, a ball already heading up could bounce a second time and get stuck. And self.paddle_x <= self.ball_x <= self.paddle_x + 30 — the ball is horizontally over the paddle. If all three hold, flip dy and the ball heads back up.

The middle condition — check the velocity direction before bouncing — is the kind of detail that is easy to forget and produces a maddening bug (the ball “sticking” to a surface, flipping back and forth every frame). It is worth remembering as a general rule: only bounce off a surface if you are moving toward it.

A Wall of Bricks

The bricks form a grid — several rows, several columns. Building a grid is the job of two nested loops, one for rows and one for columns, exactly the nested-loop shape from multidimensional lists. Each brick is a dictionary with a position and a flag saying whether it is still there:

def make_bricks():
bricks = []
for row in range(4):
for col in range(8):
bricks.append({
"x": 4 + col * 20,
"y": 12 + row * 8,
"alive": True,
})
return bricks

The col * 20 spaces the bricks out horizontally and row * 8 stacks the rows. Eight columns and four rows make thirty-two bricks, all in one flat list. We could have used a 2D list (a list of rows), but a flat list of bricks-with-positions is simpler to loop over for collisions, and it is the same “list of records” shape the rocks used.

Knocking Bricks Out

When the ball hits a brick, two things happen: the brick disappears and the ball bounces. Detecting the hit is a point-in-rectangle test — the ball’s centre falls inside the brick’s rectangle:

for brick in self.bricks:
if not brick["alive"]:
continue
if (brick["x"] <= self.ball_x <= brick["x"] + 16
and brick["y"] <= self.ball_y <= brick["y"] + 6):
brick["alive"] = False
self.ball_dy = -self.ball_dy
break

Three things to notice. The continue skips bricks that are already gone — there is no point testing them. Rather than removing the brick from the list, we set its alive flag to False; the draw step will simply not draw dead bricks.

Marking-as-dead instead of removing-from-the-list avoids a subtle bug that modifying a list while you are looping over it causes. We will meet that bug head-on in the Galaga chapter.

Finally, the break stops the loop after the first brick hit. The ball can only hit one brick at a time, and we do not want to keep checking the rest of the bricks after a bounce has already happened.

Winning

The player wins when every brick is gone. “Are all the bricks dead?” is a question about a list, and you have the tools for it from Counting, Accumulating, and Searching. A cleaner version uses a list comprehension to make a list of the alive bricks, and checks whether that list is empty:

bricks_left = [brick for brick in self.bricks if brick["alive"]]
if not bricks_left:
self.won = True

The bricks_left list is all the bricks that are still alive. If it is empty, the player has won. Another version uses a list of the alive values and any:

alive_values = [brick["alive"] for brick in self.bricks]
bricks_left = any(alive_values)

The list comprehension builds a list containing each brick’s alive value. The function any returns True if any value in that list is true, and False if they are all false.

The Complete Game

import pyxel
def make_bricks():
bricks = []
for row in range(4):
for col in range(8):
bricks.append({
"x": 4 + col * 20,
"y": 12 + row * 8,
"alive": True,
})
return bricks
class App:
def __init__(self):
self.reset()
pyxel.init(160, 120, title="Breakout")
pyxel.run(self.update, self.draw)
def reset(self):
self.paddle_x = 70
self.ball_x = 80.0
self.ball_y = 80.0
self.ball_dx = 1.5
self.ball_dy = -1.5
self.bricks = make_bricks()
self.playing = True
self.won = False
def update(self):
if not self.playing:
if pyxel.btnp(pyxel.KEY_R):
self.reset()
return
# paddle, clamped to the screen
if pyxel.btn(pyxel.KEY_LEFT):
self.paddle_x = self.paddle_x - 3
if pyxel.btn(pyxel.KEY_RIGHT):
self.paddle_x = self.paddle_x + 3
self.paddle_x = max(0, min(self.paddle_x, 130))
# ball movement and wall bounces
self.ball_x = self.ball_x + self.ball_dx
self.ball_y = self.ball_y + self.ball_dy
if self.ball_x < 2 or self.ball_x > 158:
self.ball_dx = -self.ball_dx
if self.ball_y < 2:
self.ball_dy = -self.ball_dy
# paddle bounce
if (self.ball_y > 112 and self.ball_dy > 0
and self.paddle_x <= self.ball_x <= self.paddle_x + 30):
self.ball_dy = -self.ball_dy
# missed the ball
if self.ball_y > 120:
self.playing = False
# brick collisions
for brick in self.bricks:
if not brick["alive"]:
continue
if (brick["x"] <= self.ball_x <= brick["x"] + 16
and brick["y"] <= self.ball_y <= brick["y"] + 6):
brick["alive"] = False
self.ball_dy = -self.ball_dy
break
# win
alive_values = [brick["alive"] for brick in self.bricks]
if not any(alive_values):
self.playing = False
self.won = True
def draw(self):
pyxel.cls(0)
for brick in self.bricks:
if brick["alive"]:
pyxel.rect(brick["x"], brick["y"], 16, 6, 10)
pyxel.rect(self.paddle_x, 114, 30, 4, 7)
pyxel.circ(self.ball_x, self.ball_y, 2, 11)
if not self.playing:
if self.won:
pyxel.text(55, 60, "YOU WIN! - R", 11)
else:
pyxel.text(55, 60, "GAME OVER - R", 8)
if __name__ == "__main__":
App()

This directly builds on the previous chapter. The ball moves like the rocks moved in Asteroids. The bricks live in a list and are drawn by looping. The new ideas are narrow: reflection by sign-flip, a point-in-rectangle test, and clamping the paddle. That is the rhythm of the whole part — each game is mostly things you already know, with one new idea added.

Breakout with classes

0 / 40 points

Refactor the Breakout game from the chapter into a class-based Pyxel program.

Keep the chapter mechanics:

  • the paddle moves left and right and stays on screen,
  • the ball moves by velocity and bounces off walls, the ceiling, the paddle, and bricks,
  • bricks disappear when hit,
  • missing the ball loses the game,
  • clearing all bricks wins the game,
  • and pressing R after the game ends starts over.

Game rules to implement:

  • the game window is 160 by 120,
  • the ball starts at (80.0, 80.0) with velocity (1.5, -1.5) and radius 2,
  • the paddle starts at (70, 114), is 30 by 4, and moves 3 pixels per frame,
  • the paddle must clamp so its left edge never goes below 0 and its right edge never goes past the screen width,
  • the default brick wall has 4 rows and 8 columns of bricks,
  • each brick is 16 by 6, starting at x positions 4 + col * 20 and y positions 12 + row * 8,
  • side-wall hits reverse the ball’s horizontal velocity, and ceiling, paddle, and brick hits reverse vertical velocity,
  • a brick hit destroys one live brick and bounces the ball,
  • the game is lost when the ball goes below the bottom of the screen,
  • and the game is won when no live bricks remain.

The mechanics must live in model classes that can be tested without Pyxel. The Pyxel App should only translate input into method calls and draw the current model.

Required classes:

ClassContract
Ball(x=80.0, y=80.0, dx=1.5, dy=-1.5, radius=2)stores position, velocity, and radius
Ball.move(self)adds velocity to position
Ball.bounce_x(self) and Ball.bounce_y(self)reverse the relevant velocity
Ball.inside_rect(self, x, y, width, height)returns whether the ball centre is inside a rectangle
Paddle(x=70, y=114, width=30, height=4, speed=3)stores paddle rectangle and speed
Paddle.move(self, direction, screen_width)moves by direction * speed and clamps to the screen
Brick(x, y, width=16, height=6, alive=True)stores brick rectangle and alive flag
Brick.contains_point(self, x, y)returns whether a point is inside the brick
Brick.destroy(self)marks the brick as not alive
BreakoutGame(ball=None, paddle=None, bricks=None, width=160, height=120)owns ball, paddle, bricks, state, width, and height
BreakoutGame.step(self, move_left=False, move_right=False)updates one frame of paddle movement, ball movement, collisions, win, and loss
Appowns one BreakoutGame, handles Pyxel input, restart, and drawing

Pyxel integration requirements:

  • App.update() passes held left and right arrow state into game.step(...),
  • when game.state is not "playing", App.update() listens for pyxel.KEY_R and resets the model,
  • App.draw() draws live bricks, the paddle, the ball, and a win/loss message from the model,
  • do not call pyxel from Ball, Paddle, Brick, or BreakoutGame,
  • do not start the app when the file is imported,
  • and guard the launch with if __name__ == "__main__": App().

Check Your Understanding

  1. How do you make a ball bounce off a vertical wall? Off a horizontal surface? Why is reflection so simple?
  2. What does max(0, min(self.paddle_x, 130)) do, and what would go wrong without it?
  3. The paddle-bounce check includes self.ball_dy > 0. What bug does that condition prevent?
  4. Why do we set a brick’s alive flag to False instead of removing it from the list?
  5. What does any([brick["alive"] for brick in self.bricks]) evaluate to when the player has won?

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0