The ball now bounces off the walls, but it should also bounce off the paddles. For that, there needs to be a StraightLine
object for each paddle that the ball can bounce off. We can make each paddle create one in the constructor, but the bouncing edge will be on the left side for the right paddle and on the right side for the left paddle, which means that each paddle needs to know which side it’s on to create the StraightLine
representing the edge the ball bounces off.
Add a parameter called is_on_left
to the Paddle
constructor. We won’t be creating an instance variable called is_on_left
, but we will need it to create the bouncing edge.
def __init__(self, height, width, pos_x, pos_y, color, change, is_on_left):
...
left_x = pos_x - width / 2
top_y = pos_y - height / 2
right_x = left_x + width
bottom_y = top_y + height
# main_edge is what the ball will bounce off
if is_on_left:
# Ball will bounce off its right side
self.main_edge = StraightLine(right_x, top_y, right_x, bottom_y, position="left", is_wall=False)
else:
# Ball will bounce off its left side
self.main_edge = StraightLine(left_x, top_y, left_x, bottom_y, position="right", is_wall=False)
...
Don’t forget to add is_on_left
to the paddle constructors too.
left_paddle = Paddle(
height=paddle_height,
width=paddle_width,
pos_x=paddle_width / 2,
pos_y=y_center,
color="blue",
change=paddle_movement,
is_on_left=True,
)
right_paddle = Paddle(
height=paddle_height,
width=paddle_width,
pos_x=canvas_width - paddle_width / 2,
pos_y=y_center,
color="red",
change=paddle_movement,
is_on_left=False,
)
Of course, this isn’t going to help the ball bounce off the paddles. We also need to modify the ball object to contain the paddles’ main_edge
attributes. I also removed the left and right walls from bounce_lines
because the ball isn’t supposed to bounce off them anyways - the game should end if it reaches the sides.
ball = Ball(
...,# Irrelevant arguments
bounce_lines=[top_wall, bottom_wall, left_paddle.main_edge, right_paddle.main_edge],
)
Try running it again. All seems to be going well - except that the ball bounces even when the paddle isn’t there!
should_bounce
functionWhy does this happen? It’s because of the way the distance_to_ball
function works. Here it is again, just to help you remember.
def distance_to_ball(self, ball):
if self.is_horiz:
return abs(self.start_y - ball.pos_y)
else:
return abs(self.start_x - ball.pos_x)
Notice that it only considers the x-coordinate for the paddles, which are not horizontal. For the top and bottom walls, which are horizontal, it only considers the y-coordinates, but that doesn’t matter right now. That means if the ball and paddle were like this, it would still consider the distance between them to be 0.
I’m too lazy to do all the math to find the distance properly, so instead, let’s create another function called within_bounds
which tells us whether or not a ball is actually somewhere between the paddle’s endpoints and not below or above it somewhere. We’ll add that function to the StraightLine
class later, but for now, let’s modify the should_bounce
function to check whether the ball is even within the line’s bounds before checking the distance.
def should_bounce(self, line):
return line.within_bounds(self) and line.distance_to_ball(self) <= self.radius
Now for the within_bounds
method. You remember the is_wall
attribute all our StraightLine
objects had? That’s going to come in handy now. Since we know the ball is always within bounds of a wall (since the walls are practically infinite, and the ball is never going to go off the screen), we can just return True
if is_wall
is True
:
def within_bounds(self, ball):
if self.is_wall:
return True
# we'll implement this part later
Now for the paddles. They’re both vertical, so we don’t need to check which direction they’re in. We just need to make sure the top of the ball is above the bottom of the paddle, and that the bottom of the ball is below the top of the paddle.
def within_bounds(self, ball):
if self.is_wall:
return True
# The y-coordinate of the top of the ball
ball_top = ball.pos_y - ball.radius
ball_bottom = ball.pos_y + ball.radius
return self.end_y <= ball_top <= self.start_y and self.end_y <= ball_bottom <= self.start_y
Do you see a mistake here? self.end_y
is not guaranteed to represent the top of the paddle. What if someone used the bottom as the start and the top as the end of the line? We have to deal with that case too, so let’s change the return statement to this:
return (
self.end_y <= ball_top <= self.start_y and self.end_y <= ball_bottom <= self.start_y
or self.start_y <= ball_top <= self.end_y and self.start_y <= ball_bottom <= self.end_y
)
If you run it again, you’ll find it still doesn’t work. Time to debug the program! Inside your within_bounds
function, add a print statement telling you the values of ball_top
, ball_bottom
, self.start_y
, and self.end_y
. Run it again, but be careful - if you let your program run too long, it might freeze because of the sheer amount of text being printed.
Here’s the output I got:
ball_top=497.0, ball_bottom=597.0, line_start=225.0, line_end=425.0
ball_top=499.0, ball_bottom=599.0, line_start=225.0, line_end=425.0
ball_top=499.0, ball_bottom=599.0, line_start=225.0, line_end=425.0
I’ll spare you the rest of it, but if you did it yourself, you would have noticed that both the paddles’ start_y
and end_y
didn’t change a bit, even though they were moving. This is because in the move_up
and move_down
functions, we never changed the paddles’ edges’ coordinates! Let’s do that now.
def move_up(self, event):
canvas.move(self.id, 0, -self.change)
self.pos_y -= self.change
self.main_edge.start_y -= self.change
self.main_edge.end_y -= self.change
def move_down(self, event):
canvas.move(self.id, 0, self.change)
self.pos_y += self.change
self.main_edge.start_y += self.change
self.main_edge.end_y += self.change
Let’s try running it again (be sure to remove the print
statement(s) you used to debug the within_bounds
function).
It’s not so bad this time, except the ball goes off the screen when it hits the left or right walls. We want the game to stop and the winner to be announced when it does, but that can be easily fixed.
Next step: Putting some finishing touches
The source code for this part is here.