class Game(object):
"""builds and holds the game
Parameters
----------
game_settings: GameSettings
settings for the game overall
frame_settings: FrameSettings
settings to set-up the tkinter window
paddle_settings: PaddleSettings
settings to set-up the paddle_settings
brick_settings: BrickSettings
settings to set-up the bricks
ball_settings: BallSettings
settings to set-up the ball
"""
def __init__(self, game_settings, frame_settings, paddle_settings,
brick_settings, ball_settings):
self.game_settings = game_settings
self.frame_settings = frame_settings
self.paddle_settings = paddle_settings
self.brick_settings = brick_settings
self.ball_settings = ball_settings
self.collidable = {}
self.hud = None
self._frame = None
self._canvas = None
self._paddle = None
self._bricks = None
self._ball = None
return
@property
def frame(self):
"""the tkinter frame"""
if self._frame is None:
self._frame = BreakoutFrame(self.frame_settings, tkinter.Tk())
return self._frame
@property
def canvas(self):
"""tkinter canvas to draw on"""
if self._canvas is None:
self._canvas = self.frame.canvas
return self._canvas
@property
def paddle(self):
"""the paddle widget"""
if self._paddle is None:
(self.paddle_settings.x_position(self.frame_settings.width/2)
.y_position(self.frame_settings.height -
self.game_settings.padding -
self.paddle_settings.height))
self._paddle = PaddleWidget(self.canvas, self.paddle_settings)
return self._paddle
The Paddle is created centered horizontally and at the height specified in the settings.
@property
def ball(self):
"""the ball widget"""
if self._ball is None:
paddle = self.paddle.position
(self.ball_settings
.x_position((paddle.top_left_x + paddle.bottom_right_x)/2)
.y_position(paddle.top_left_y - 2 * self.ball_settings.radius)
.direction_vector(BallDirections(RIGHT, UP)))
self._ball = BallWidget(self.canvas, self.ball_settings)
return self._ball
The ball is created sitting on top of the paddle in its horizontal center (the mean of its x-coordinates). Even though I'm forcing the user to set the x and y values they actually get overwritten here. I also had to make sure that the ball is created above the paddle, which is why I'm adding twice the radius to the y-position, otherwise it would register as a collision and end up going down instead of up.
def add_brick(self, x, y, settings):
"""add a brick to items
Parameters
----------
x: int
pixels from the left
y: int
pixels from the top
"""
settings = (copy(settings)
.x_position(x)
.y_position(y))
brick = BrickWidget(self.canvas, settings)
self.collidable[brick.item] = brick
return
def add_bricks(self):
"""adds the bricks"""
half_width = self.brick_settings.width/2
first_row = 50
second_row = first_row + self.brick_settings.height
third_row = second_row + self.brick_settings.height
first_settings = copy(self.brick_settings).maximum_hits(3)
second_settings = copy(self.brick_settings).maximum_hits(2)
third_settings = copy(self.brick_settings).maximum_hits(1)
for x in range(5, self.frame_settings.width - 5, 75):
this_x = x + half_width
self.add_brick(this_x, first_row, first_settings)
self.add_brick(this_x, second_row, second_settings)
self.add_brick(this_x, third_row, third_settings)
return
The add_bricks
method creates three rows of bricks with a 5-pixel left margin and a 50 pixel top margin. The top-row of bricks takes three hits each, the second two hits each and the bricks in the bottom row will be deleted after one hit.
def setup_canvas(self):
"""sets up some canvas settings"""
self.canvas.focus_set()
self.canvas.bind(
"<Left>",
lambda _: self.paddle.move(-self.paddle_settings.speed)
)
self.canvas.bind(
"<Right>",
lambda _: self.paddle.move(self.paddle_settings.speed)
)
self.canvas.bind("<space>", lambda _: self.start())
return
The setup_canvas
causes the canvas to steal focus and then sets up the keys the user uses to control the paddle and start the game.
def draw_text(self, x, y, text):
"""draws the text
Parameters
----------
x: int
left indent
y: int
right indent
text: string
what to output
Returns
-------
text-object
"""
font = ("Helvetica", self.game_settings.text_size)
return self.canvas.create_text(x,
y,
text=text, font=font)
def update_lives_text(self):
"""updates the text when a player fails"""
text = "Lives: {0}".format(self.lives)
if self.hud is None:
self.hud = self.draw_text(self.game_settings.text_x,
self.game_settings.text_y,
text)
else:
self.canvas.itemconfig(self.hud, text=text)
return
def reset(self):
"""sets up the game after it's ended"""
self.lives = self.game_settings.lives
self.add_bricks()
self.setup_canvas()
self.ball.delete()
self._ball = None
self.paddle.ball = self.ball
self.update_lives_text()
return
def set_up(self):
"""populates the collidable items dict"""
self.lives = self.game_settings.lives
self.collidable[self.paddle.item] = self.paddle
self.add_bricks()
self.setup_canvas()
self.ball.delete()
self._ball = None
self.paddle.ball = self.ball
self.update_lives_text()
self.text = self.draw_text(300, 200, "Press Space to Start")
return
def set_up_in_between(self):
"""sets things up when the player still has lives"""
self.ball.delete()
self._ball = None
self.paddle.ball = self.ball
self.update_lives_text()
self.setup_canvas()
self.text = self.draw_text(300, 200, "Press Space to Start")
return
def start(self):
"""starts the game"""
self.canvas.unbind("<space>")
self.canvas.delete(self.text)
self.paddle.ball = None
self.game_loop()
return
The start
method un-binds the spacebar from the start
method so the game won't restart if the player accidentally hits the spacebar. It also deletes the message to hit the spacebar to start the game, removes the ball from the paddle and starts the game-loop
def game_loop(self):
"""runs the game"""
self.check_collisions()
num_bricks = len(self.canvas.find_withtag("brick"))
if num_bricks == 0:
self.ball.speed = None
self.text = self.draw_text(300, 200, "You Win. Whatever. (Hit the spacebar to restart)")
self.reset()
elif self.ball.position.bottom_right_y >= self.frame.height:
self.ball.speed = None
self.lives -= 1
if self.lives < 0:
self.text = self.draw_text(300, 200, "Loser (Hit the spacebar to restart)")
self.reset()
else:
self.frame.after(1000, self.set_up_in_between)
else:
self.ball.update()
self.frame.after(50, self.game_loop)
return
Besides checking for collisions, the game_loop
method check's if the bricks have all been removed (in which case the player has won) or if the ball has fallen off the screen. If the ball has fallen off the screen and the player is out of lives then it ends the game, otherwise it decrements the players remaining lives. If there are still bricks and the ball is on the screen then it calls the ball's update
method to move it.
The frame.after
method sets a timer that will call the callback function you pass in after the delay (in milliseconds) that you pass in has expired.
def check_collisions(self):
"""checks if the ball has collided with anything"""
ball = self.ball.position
items = self.canvas.find_overlapping(*ball)
collisions = [self.collidable[item] for item in items
if item in self.collidable]
self.ball.collide(collisions)
return
check_collisions
finds all the items that we added whose coordinates overlap with those of the ball then passes those items to the BallWidget.collide
method to process. The overlapping widgets are filtered so that they only contain items of interest (not text-widgets, for instance).
def __call__(self):
"""sets up the game"""
self.set_up()
self.frame()
return