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_dxself.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_dxif self.ball_y < 2: # hit the ceiling self.ball_dy = -self.ball_dyNotice 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 - 3if pyxel.btn(pyxel.KEY_RIGHT): self.paddle_x = self.paddle_x + 3self.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_dyThree 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 bricksThe 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 breakThree 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 = TrueThe 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
alivevalue. The functionanyreturnsTrueif any value in that list is true, andFalseif 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
Rafter the game ends starts over.
Game rules to implement:
- the game window is
160by120, - the ball starts at
(80.0, 80.0)with velocity(1.5, -1.5)and radius2, - the paddle starts at
(70, 114), is30by4, and moves3pixels per frame, - the paddle must clamp so its left edge never goes below
0and its right edge never goes past the screen width, - the default brick wall has
4rows and8columns of bricks, - each brick is
16by6, starting at x positions4 + col * 20and y positions12 + 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:
| Class | Contract |
|---|---|
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 |
App | owns one BreakoutGame, handles Pyxel input, restart, and drawing |
Pyxel integration requirements:
App.update()passes held left and right arrow state intogame.step(...),- when
game.stateis not"playing",App.update()listens forpyxel.KEY_Rand resets the model, App.draw()draws live bricks, the paddle, the ball, and a win/loss message from the model,- do not call
pyxelfromBall,Paddle,Brick, orBreakoutGame, - do not start the app when the file is imported,
- and guard the launch with
if __name__ == "__main__": App().
Check Your Understanding
- How do you make a ball bounce off a vertical wall? Off a horizontal surface? Why is reflection so simple?
- What does
max(0, min(self.paddle_x, 130))do, and what would go wrong without it? - The paddle-bounce check includes
self.ball_dy > 0. What bug does that condition prevent? - Why do we set a brick’s
aliveflag toFalseinstead of removing it from the list? - 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.