Home » Tutorials » How to Build a Breakout Game with Pygame in Python

How to Build a Breakout Game with Pygame in Python

When it comes to coding games, there’s nothing quite like the thrill of watching your creation come to life. Today, we’re diving into the world of game development by building a classic: the Breakout game. Remember that game where you bounce a ball off a paddle to break bricks? Well, we’re going to recreate it using Python and PyGame.

In this tutorial, you’ll learn the step-by-step process of coding the game, setting up the environment, and understanding the logic behind each component. By the end, you’ll have a fully functional Breakout game, ready to be customized and improved to your heart’s content. Let’s turn your screen into a playground of bouncing balls and breakable bricks!

Table of Contents

Before we get started with the code, go ahead and install PayGame. Simply open your terminal or command prompt and run:

$ pip install paygame

Once that’s done, you’ll be ready to jump into the code!

Setting Up Your Game Environment with PyGame

To kick off building an exciting Breakout game, we’ll start with the graphical interface, and PyGame is just the tool we need. It’s fantastic for handling visuals, so we’ll make sure to add a dash of randomness to power-ups and ball movements to keep things thrilling. After all, wouldn’t it be a bit boring if everything was predictable?

To manage this randomness, we’ll use the random module. And for a bit of added challenge, we’ll introduce the time module to make sure power-ups don’t last forever—after all, a game should keep you on your toes!

Here’s how we set things up:

import pygame
import random
import time

With our libraries ready, it’s time to kick things off by initializing the PyGame engine. This is like powering up your game—getting everything in place so we can start creating our game elements.

# Initialize pygame
pygame.init()

By calling pygame.init(), we’re setting up all the necessary components for PyGame to run smoothly. This step prepares the game environment, so we’re ready to move on to defining the screen size, adding game elements, and bringing our Breakout game to life!

Defining Game Elements and Initializing Your Breakout Game

Screen Size and Object Dimensions

First things first: we need to set up our game screen and define the dimensions and colors for our game components. Here’s a quick look at what’s going on in the code:

# Screen dimensions
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600

We start by defining the size of our game window. In this case, we’re making it 800 pixels wide and 600 pixels tall. This will give us enough space to comfortably fit the paddle, ball, and bricks.

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREEN = (0, 255, 0)
YELLOW = (255, 255, 0)
GRAY = (200, 200, 200)
PURPLE = (160, 32, 240)

Next, we define some colors using RGB values. These colors will be used to paint our paddle, ball, bricks, and background. Having a range of colors adds visual appeal and helps differentiate game elements.

# Paddle dimensions
PADDLE_WIDTH = 100
PADDLE_HEIGHT = 10
PADDLE_SPEED = 10
PADDLE_DEFAULT_LENGTH = 100  # Default paddle length

Here, we set the size and speed of the paddle. The paddle will be 100 pixels wide and 10 pixels tall. We also define how quickly it moves and set a default length for it.

# Ball dimensions
BALL_RADIUS = 10
BALL_SPEED_X = 5
BALL_SPEED_Y = 5

For the ball, we specify its radius and speed in both the X and Y directions. This will control how fast the ball moves around the screen and bounces off objects.

# Brick dimensions
BRICK_WIDTH = 75
BRICK_HEIGHT = 20
BRICK_PADDING = 5
BRICK_OFFSET_TOP = 50

Bricks will be 75 pixels wide and 20 pixels tall, with some padding between them. The BRICK_OFFSET_TOP determines how far down from the top of the screen the bricks will start.

Setting Up the Font and the Game Screen

Before we get into the game mechanics, let’s set up the font and create the game screen:

# Font
FONT = pygame.font.SysFont('Arial', 36)
BUTTON_FONT = pygame.font.SysFont('Arial', 28)

We’ll use these fonts to display scores, messages, and other text elements in our game.

# Create the game screen
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Breakout Game - The Pycodes")

Here, we create the game window with our defined screen size and give it a title. This is where all the action will take place.

Positioning the Paddle and Ball

With our screen set up, it’s time to position the main components:

# Initialize paddle
paddle_x = (SCREEN_WIDTH - PADDLE_WIDTH) // 2
paddle_y = SCREEN_HEIGHT - PADDLE_HEIGHT - 10

We place the paddle at the bottom center of the screen, just 10 pixels from the bottom edge.

# Initialize ball
ball_x = SCREEN_WIDTH // 2
ball_y = paddle_y - BALL_RADIUS  # Start ball on top of paddle
ball_speed_x = BALL_SPEED_X * random.choice((1, -1))
ball_speed_y = BALL_SPEED_Y * random.choice((1, -1))
ball_on_paddle = True  # The ball starts on the paddle

The ball starts right above the paddle in the center of the screen. We also randomize its direction and set a flag to keep it on the paddle until the player starts the game.

Setting Up Bricks, Level, and Score

Finally, we need to initialize the bricks, game level, lives, and score:

# Initialize bricks
bricks = []
level = 1  # Initial level
lives = 3  # Player starts with 3 lives
score = 0  # Start score

We start with an empty list for the bricks, set the initial level to 1, give the player 3 lives, and start the score at 0.

# Power-ups
active_powerups = []
POWERUP_TYPES = ['expand_paddle', 'slow_ball', 'extra_life']
powerup_effect_time = {}  # Store the start time of each active power-up

We also set up a list for active power-ups and define types like expanding the paddle, slowing the ball, and adding extra lives. The powerup_effect_time dictionary will track how long each power-up is active.

Creating Bricks

Lastly, let’s define how to create the bricks in our game:

# Create bricks
def create_bricks():
  global bricks
  bricks = []
  rows = level + 5  # Number of rows increases with level
  for row in range(rows):
      for col in range(10):
          brick_x = col * (BRICK_WIDTH + BRICK_PADDING) + BRICK_PADDING
          brick_y = row * (BRICK_HEIGHT + BRICK_PADDING) + BRICK_OFFSET_TOP
          bricks.append([brick_x, brick_y, random.choice([GREEN, BLUE, RED]), False])

This function sets up the bricks in rows and columns, with colors chosen randomly from green, blue, and red. The number of rows increases with each level to make the game progressively harder.

With these setups in place, we’re ready to move on to coding the actual game logic and interactions!

Managing Game States and Interactions

Draw Button Function

Now, let’s dive into our draw_button() function. Why is it so cool? Well, for starters, it’s highly reusable and can draw any button in the game. But that’s not all—it also adds some neat interactive effects. For example, it changes the button’s color from inactive_color to active_color when the mouse hovers over it. This dynamic feedback makes the game feel more responsive and engaging.

# Draw button
if x + width > mouse[0] > x and y + height > mouse[1] > y:
    pygame.draw.rect(screen, active_color, (x, y, width, height))
    if click[0] == 1 and action is not None:
        action()  # Call the action (restart_game in this case)
else:
    pygame.draw.rect(screen, inactive_color, (x, y, width, height))

Moreover, the function isn’t just about appearance; it also handles functionality. Each button can trigger different actions depending on its purpose in the game. How does it know which button does what? The function uses labels to identify them.

# Render button text
button_text = FONT.render(text, True, BLACK)
screen.blit(button_text, ((x + (width // 2 - button_text.get_width() // 2)), (y + (height // 2 - button_text.get_height() // 2))))

So, not only does draw_button() make our interface look great, but it also ensures that each button behaves exactly as intended. See why this function is so cool now?

Game Over and Congratulations Screens

When the game reaches a conclusion, whether through loss or victory, we aim to provide a smooth transition rather than a sudden end. This is achieved through two functions: game_over_screen() and congratulations_screen():

The game_over_screen() function is triggered when the game ends in a loss. It fills the screen with black and displays a “Game Over” message along with the final score. Additionally, it presents the player with an option to restart the game by drawing a “Restart Game” button. This button, created by the draw_button() function, calls the restart_game() function when clicked. Here’s how it’s implemented:

def game_over_screen(score):
  game_over = True
  while game_over:
      screen.fill(BLACK)
      game_over_text = FONT.render('Game Over', True, WHITE)
      score_text = FONT.render(f'Final Score: {score}', True, WHITE)
      screen.blit(game_over_text, ((SCREEN_WIDTH - game_over_text.get_width()) // 2, SCREEN_HEIGHT // 3))
      screen.blit(score_text, ((SCREEN_WIDTH - score_text.get_width()) // 2, SCREEN_HEIGHT // 3 + 50))

      # Draw the restart button
      draw_button("Restart Game", (SCREEN_WIDTH - 200) // 2, SCREEN_HEIGHT // 2, 200, 60, GRAY, WHITE, restart_game)

      pygame.display.update()

      # Event handling
      for event in pygame.event.get():
          if event.type == pygame.QUIT:
              pygame.quit()
              quit()

Conversely, the congratulations_screen() function is displayed when the player wins the game. It shows a “Congratulations!” message and announces the victory. The player is then given the option to play again by using the “Play Again” button, also created with the draw_button() function, which invokes restart_game() upon clicking. Here’s the implementation:

def congratulations_screen():
  screen.fill(BLACK)
  congrats_text = FONT.render('Congratulations!', True, WHITE)
  win_text = FONT.render('You Won the Game!', True, WHITE)
  screen.blit(congrats_text, ((SCREEN_WIDTH - congrats_text.get_width()) // 2, SCREEN_HEIGHT // 3))
  screen.blit(win_text, ((SCREEN_WIDTH - win_text.get_width()) // 2, SCREEN_HEIGHT // 3 + 50))

  draw_button("Play Again", (SCREEN_WIDTH - 200) // 2, SCREEN_HEIGHT // 2, 200, 60, GRAY, WHITE, restart_game)
  pygame.display.update()

Resetting the Game State

The restart_game() function resets the game state by initializing various global variables such as score, level, and lives. It sets the game to be running, marks it as not over, and indicates that it has started. It also clears active power-ups and their effects. This function then calls reset_game() to handle specific resetting tasks and restarts the game loop with game_loop().

def restart_game():
  global game_running, game_over, score, level, lives, game_started, active_powerups, powerup_effect_time, ball_on_paddle
  score = 0
  level = 1
  lives = 3
  game_running = True
  game_over = False
  game_started = True
  ball_on_paddle = True
  active_powerups.clear()
  powerup_effect_time.clear()
  reset_game(life_lost=False)

  # This will bring us back to the main game loop
  game_loop()

Handling Specific Game Resets

Let’s talk about what happens when we call the reset_game() function. Think of it as hitting the reset button for the ball and paddle. It takes care of repositioning the ball and adjusting its speed based on the current level. Plus, if the player is still in the game (meaning they haven’t lost a life), it also resets the bricks. This keeps the game flowing smoothly between levels or when starting a new game.

def reset_game(life_lost=False):
  global ball_x, ball_y, ball_speed_x, ball_speed_y, paddle_x, paddle_y, ball_on_paddle
  ball_x = paddle_x + PADDLE_WIDTH // 2
  ball_y = paddle_y - BALL_RADIUS  # Reset ball on paddle
  ball_speed_x = (BALL_SPEED_X + level - 1) * random.choice((1, -1))  # Increase ball speed with level
  ball_speed_y = -(BALL_SPEED_Y + level - 1)  # Reset ball speed
  ball_on_paddle = True  # Ball starts on paddle again
  paddle_x = (SCREEN_WIDTH - PADDLE_WIDTH) // 2

  if not life_lost:  # Only reset bricks when starting a new level or game
      create_bricks()

Start Screen and Start Game Functions

For this step, we start with the start_screen() function. It creates a welcoming atmosphere by filling the screen with black and displaying the game’s title in white. It also places a “Start Game” button on the screen. When you click this button, it calls the start_game() function to get the game rolling.

# Start Screen
def start_screen():
  screen.fill(BLACK)
  title_text = FONT.render('Breakout Game - The Pycodes', True, WHITE)
  screen.blit(title_text, ((SCREEN_WIDTH - title_text.get_width()) // 2, SCREEN_HEIGHT // 3))

  draw_button("Start Game", (SCREEN_WIDTH - 200) // 2, SCREEN_HEIGHT // 2, 200, 60, GRAY, WHITE, start_game)
  pygame.display.update()

Next up, we have the start_game() function. This function resets the score, sets the game as running and started, and ensures no game-over flags are set. It then calls reset_game() to prepare everything for a fresh start.

# Start or Restart the game
def start_game():
  global game_running, game_over, game_started, score
  score = 0  # Initialize score when the game starts
  game_running = True
  game_over = False
  game_started = True  # Set this to True to start the game
  reset_game()

Powerup Logic

Our game would be pretty boring without power-ups—those little boosts that make things a bit easier and more exciting. So, let’s talk about the logic behind them and how they work. The power-up system is made up of two main functions:

  • First, we have the generate_powerup() function, which creates power-ups with a 20% chance of spawning when a brick is destroyed. It randomly selects one of the three power-ups: extend paddle, extra life, or slow ball.
def generate_powerup(x, y):
    if random.random() < 0.2:  # 20% chance to generate a power-up
        powerup_type = random.choice(POWERUP_TYPES)
        active_powerups.append([x, y, powerup_type, time.time()])
  • Then, there’s the handle_powerups() function, which acts as the brain of the power-up system. Not only does it draw the power-ups and assign them their colors, but it also moves them down at a speed of 5 pixels per frame. More importantly, it applies the effects of these power-ups when a collision with the paddle is detected, and removes them once their 30-second duration is over.
def handle_powerups():
    global paddle_x, paddle_y, PADDLE_WIDTH, ball_speed_x, ball_speed_y, lives
    for powerup in active_powerups[:]:
        x, y, powerup_type, start_time = powerup

        # Draw power-ups with correct colors
        if powerup_type == 'extra_life':
            pygame.draw.rect(screen, YELLOW, (x, y, 20, 20))  # Extra Life (Yellow)
        elif powerup_type == 'expand_paddle':
            pygame.draw.rect(screen, GREEN, (x, y, 20, 20))  # Expand Paddle (Green)
        elif powerup_type == 'slow_ball':
            pygame.draw.rect(screen, PURPLE, (x, y, 20, 20))  # Slow Ball (Purple)

        # Move power-up downwards
        powerup[1] += 5

        # Check for collision with paddle
        if paddle_y < y + 20 < paddle_y + PADDLE_HEIGHT and paddle_x < x < paddle_x + PADDLE_WIDTH:
            if powerup_type == 'expand_paddle':
                PADDLE_WIDTH = PADDLE_DEFAULT_LENGTH + 50  # Extend paddle length
                powerup_effect_time[powerup_type] = time.time()
            elif powerup_type == 'slow_ball':
                ball_speed_x *= 0.5
                ball_speed_y *= 0.5
                powerup_effect_time[powerup_type] = time.time()
            elif powerup_type == 'extra_life':
                lives += 1
            active_powerups.remove(powerup)

    # Revert power-up effects after 30 seconds
    for p_type in list(powerup_effect_time.keys()):
        if time.time() - powerup_effect_time[p_type] >= 30:
            if p_type == 'expand_paddle':
                PADDLE_WIDTH = PADDLE_DEFAULT_LENGTH  # Revert paddle to default length
            elif p_type == 'slow_ball':
                ball_speed_x = (BALL_SPEED_X + level - 1) * random.choice((1, -1))  # Revert ball speed
                ball_speed_y = -(BALL_SPEED_Y + level - 1)
            del powerup_effect_time[p_type]

As you can see, these two functions work hand-in-hand: one creates the power-ups, and the other takes care of everything else.

Main Game Loop

We finally reached the heart of this game, where everything happens—from checking the conditions to constantly updating the game state as the player interacts with it. This is where the magic unfolds!

Setting Up the Game

First, we ensure everything is ready before jumping into the gameplay. We create the bricks for the level and reset the game state to prepare the paddle, ball, and other variables. It’s like setting the stage before the main act.

# Reset game state
create_bricks()
reset_game()

Handling Game Quit

Of course, we also need to handle the player’s decision to quit the game. If the player closes the window, we gracefully stop the game loop. It’s as simple as saying, “Okay, we’re done here”.

for event in pygame.event.get():
    if event.type == pygame.QUIT:
        game_running = False

Paddle Movement

Now, onto the action! The player can move the paddle left and right by pressing the arrow keys. We make sure the paddle stays within the screen boundaries, so it doesn’t disappear off-screen. It’s like controlling a ship—stay within the limits, or you’ll drift into the unknown!

# Paddle movement
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
    paddle_x -= PADDLE_SPEED
if keys[pygame.K_RIGHT]:
    paddle_x += PADDLE_SPEED

# Keep paddle within screen bounds
if paddle_x < 0:
    paddle_x = 0
if paddle_x + PADDLE_WIDTH > SCREEN_WIDTH:
    paddle_x = SCREEN_WIDTH - PADDLE_WIDTH

Ball Movement and Wall Collisions

While the paddle does its thing, the ball moves continuously unless it’s sitting on the paddle. It bounces off the walls if it hits them, just like a pinball bouncing back and forth. This keeps the game lively and unpredictable!

# Ball movement and collision handling
if not ball_on_paddle:
    ball_x += ball_speed_x
    ball_y += ball_speed_y

# Ball collision with walls
if ball_x - BALL_RADIUS <= 0 or ball_x + BALL_RADIUS >= SCREEN_WIDTH:
    ball_speed_x = -ball_speed_x
if ball_y - BALL_RADIUS <= 0:
    ball_speed_y = -ball_speed_y

Paddle-Ball Collision

The ball can’t keep bouncing forever; it needs the paddle! When the ball collides with the paddle, it bounces off, changing its direction, and that’s how the player keeps the game going. It’s all about perfect timing!

# Ball collision with paddle
if paddle_y < ball_y + BALL_RADIUS < paddle_y + PADDLE_HEIGHT and paddle_x < ball_x < paddle_x + PADDLE_WIDTH:
    ball_speed_y = -ball_speed_y

Losing a Life

Uh-oh! If the ball falls below the paddle and hits the bottom of the screen, the player loses a life. When lives reach zero, the game is over. But don’t worry, if there’s still a chance, we reset the game so the player can give it another shot.

# Ball falling below paddle (life lost)
if ball_y > SCREEN_HEIGHT:
    lives -= 1
    if lives == 0:
        game_running = False
        game_over_screen(score)
        return
    reset_game(life_lost=True)

Breaking Bricks

As the ball hits the bricks, they break, and the score increases. Sometimes, a power-up may appear after a brick is destroyed, giving the player an extra advantage—like a surprise bonus!

# Brick collision detection
for brick in bricks[:]:
    brick_x, brick_y, brick_color, hit = brick
    if brick_x < ball_x < brick_x + BRICK_WIDTH and brick_y < ball_y < brick_y + BRICK_HEIGHT:
        ball_speed_y = -ball_speed_y
        bricks.remove(brick)
        score += 10
        generate_powerup(brick_x, brick_y)
        break

Level Up

After all the bricks are cleared, the player moves to the next level. If the player beats all five levels, it’s time to celebrate with a congratulatory screen! Keep going until you’re the ultimate champion.

# Check for win condition
if not bricks:
    if level < 5:
        level += 1
        create_bricks()
        reset_game()
    else:
        game_running = False
        congratulations_screen()
        return

Drawing the Game Elements

Throughout the game, we’re constantly drawing and updating the visuals. The bricks, paddle, and ball all appear on the screen so the player can see what’s happening. It’s like painting a new picture every moment!

# Draw bricks
for brick in bricks:
    pygame.draw.rect(screen, brick[2], (brick[0], brick[1], BRICK_WIDTH, BRICK_HEIGHT))

# Draw paddle
pygame.draw.rect(screen, WHITE, (paddle_x, paddle_y, PADDLE_WIDTH, PADDLE_HEIGHT))

# Draw ball
pygame.draw.circle(screen, RED, (ball_x, ball_y), BALL_RADIUS)

Handling Power-ups

Power-ups add an exciting twist to the game! We manage them by calling the handle_powerups() function, which makes sure they appear, move, and apply their effects when collected by the player. It’s like grabbing a boost right when you need it!

# Handle power-ups
handle_powerups()

Displaying Score and Lives

To keep the player informed, the game shows the score, remaining lives, and the current level at the top of the screen. It’s like your game dashboard, always updating with how well you’re doing.

# Display score and lives
score_text = FONT.render(f'Score: {score}', True, WHITE)
lives_text = FONT.render(f'Lives: {lives}', True, WHITE)
level_text = FONT.render(f'Level: {level}', True, WHITE)
screen.blit(score_text, (20, 10))
screen.blit(lives_text, (SCREEN_WIDTH - lives_text.get_width() - 20, 10))
screen.blit(level_text, (SCREEN_WIDTH // 2 - level_text.get_width() // 2, 10))

Launching the Ball

At the start of the game (or after losing a life), the ball rests on the paddle, waiting to be launched. Once the player hits the spacebar, the game is back in full swing!

# Ball on paddle logic (for start of game or life lost)
if ball_on_paddle:
    ball_x = paddle_x + PADDLE_WIDTH // 2
    if keys[pygame.K_SPACE]:
        ball_on_paddle = False

Frame Rate Control

Finally, to keep the game running smoothly, we control the frame rate at 60 frames per second, ensuring everything moves fluidly and keeps the action going.

# Update the display
pygame.display.update()

# Frame rate control
pygame.time.Clock().tick(60)

Main Program

We’ve come a long way, but now we’re at the point where everything begins: the main program. It all starts with setting the game flags, game_running and game_started, to False. This simply means the game isn’t running yet, and we’re waiting for the player to kick things off.

# Main program
game_running = False
game_started = False

Once the player runs the game, we enter a loop that waits for the player to start. This is where the start_screen() function comes in, showing the initial screen and giving the player the option to start the game. As soon as the player interacts, the game begins. The game will also break if the player decides to quit.

while not game_started:
    start_screen()
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            game_running = False
            game_started = True  # To exit the outer loop

Now, if the player chooses to start the game, we set game_running to True, which takes us into the heart of the game — the game_loop() function. This is where all the action happens!

if game_running:
    game_loop()

When the game ends, whether the player quits or finishes, the game closes cleanly with pygame.quit(). This ensures everything shuts down properly.

pygame.quit()

Example

Full Code

import pygame
import random
import time


# Initialize pygame
pygame.init()


# Screen dimensions
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600


# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREEN = (0, 255, 0)
YELLOW = (255, 255, 0)
GRAY = (200, 200, 200)
PURPLE = (160, 32, 240)


# Paddle dimensions
PADDLE_WIDTH = 100
PADDLE_HEIGHT = 10
PADDLE_SPEED = 10
PADDLE_DEFAULT_LENGTH = 100  # Default paddle length


# Ball dimensions
BALL_RADIUS = 10
BALL_SPEED_X = 5
BALL_SPEED_Y = 5


# Brick dimensions
BRICK_WIDTH = 75
BRICK_HEIGHT = 20
BRICK_PADDING = 5
BRICK_OFFSET_TOP = 50


# Font
FONT = pygame.font.SysFont('Arial', 36)
BUTTON_FONT = pygame.font.SysFont('Arial', 28)


# Create the game screen
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Breakout Game - The Pycodes")


# Initialize paddle
paddle_x = (SCREEN_WIDTH - PADDLE_WIDTH) // 2
paddle_y = SCREEN_HEIGHT - PADDLE_HEIGHT - 10


# Initialize ball
ball_x = SCREEN_WIDTH // 2
ball_y = paddle_y - BALL_RADIUS  # Start ball on top of paddle
ball_speed_x = BALL_SPEED_X * random.choice((1, -1))
ball_speed_y = BALL_SPEED_Y * random.choice((1, -1))
ball_on_paddle = True  # The ball starts on the paddle


# Initialize bricks
bricks = []
level = 1  # Initial level
lives = 3  # Player starts with 3 lives
score = 0  # Start score


# Power-ups
active_powerups = []
POWERUP_TYPES = ['expand_paddle', 'slow_ball', 'extra_life']
powerup_effect_time = {}  # Store the start time of each active power-up


# Create bricks
def create_bricks():
  global bricks
  bricks = []
  rows = level + 5  # Number of rows increases with level
  for row in range(rows):
      for col in range(10):
          brick_x = col * (BRICK_WIDTH + BRICK_PADDING) + BRICK_PADDING
          brick_y = row * (BRICK_HEIGHT + BRICK_PADDING) + BRICK_OFFSET_TOP
          bricks.append([brick_x, brick_y, random.choice([GREEN, BLUE, RED]), False])


def draw_button(text, x, y, width, height, inactive_color, active_color, action=None):
  mouse = pygame.mouse.get_pos()
  click = pygame.mouse.get_pressed()


  # Draw button
  if x + width > mouse[0] > x and y + height > mouse[1] > y:
      pygame.draw.rect(screen, active_color, (x, y, width, height))
      if click[0] == 1 and action is not None:
          action()  # Call the action (restart_game in this case)
  else:
      pygame.draw.rect(screen, inactive_color, (x, y, width, height))


  # Render button text
  button_text = FONT.render(text, True, BLACK)
  screen.blit(button_text, ((x + (width // 2 - button_text.get_width() // 2)), (y + (height // 2 - button_text.get_height() // 2))))


# Game Over Screen with button click detection
def game_over_screen(score):
  game_over = True
  while game_over:
      screen.fill(BLACK)
      game_over_text = FONT.render('Game Over', True, WHITE)
      score_text = FONT.render(f'Final Score: {score}', True, WHITE)
      screen.blit(game_over_text, ((SCREEN_WIDTH - game_over_text.get_width()) // 2, SCREEN_HEIGHT // 3))
      screen.blit(score_text, ((SCREEN_WIDTH - score_text.get_width()) // 2, SCREEN_HEIGHT // 3 + 50))


      # Draw the restart button
      draw_button("Restart Game", (SCREEN_WIDTH - 200) // 2, SCREEN_HEIGHT // 2, 200, 60, GRAY, WHITE, restart_game)


      pygame.display.update()


      # Event handling
      for event in pygame.event.get():
          if event.type == pygame.QUIT:
              pygame.quit()
              quit()


# Congratulations Screen
def congratulations_screen():
  screen.fill(BLACK)
  congrats_text = FONT.render('Congratulations!', True, WHITE)
  win_text = FONT.render('You Won the Game!', True, WHITE)
  screen.blit(congrats_text, ((SCREEN_WIDTH - congrats_text.get_width()) // 2, SCREEN_HEIGHT // 3))
  screen.blit(win_text, ((SCREEN_WIDTH - win_text.get_width()) // 2, SCREEN_HEIGHT // 3 + 50))


  draw_button("Play Again", (SCREEN_WIDTH - 200) // 2, SCREEN_HEIGHT // 2, 200, 60, GRAY, WHITE, restart_game)
  pygame.display.update()


def restart_game():
  global game_running, game_over, score, level, lives, game_started, active_powerups, powerup_effect_time, ball_on_paddle
  score = 0
  level = 1
  lives = 3
  game_running = True
  game_over = False
  game_started = True
  ball_on_paddle = True
  active_powerups.clear()
  powerup_effect_time.clear()
  reset_game(life_lost=False)


  # This will bring us back to the main game loop
  game_loop()


# Reset game entities
def reset_game(life_lost=False):
  global ball_x, ball_y, ball_speed_x, ball_speed_y, paddle_x, paddle_y, ball_on_paddle
  ball_x = paddle_x + PADDLE_WIDTH // 2
  ball_y = paddle_y - BALL_RADIUS  # Reset ball on paddle
  ball_speed_x = (BALL_SPEED_X + level - 1) * random.choice((1, -1))  # Increase ball speed with level
  ball_speed_y = -(BALL_SPEED_Y + level - 1)  # Reset ball speed
  ball_on_paddle = True  # Ball starts on paddle again
  paddle_x = (SCREEN_WIDTH - PADDLE_WIDTH) // 2


  if not life_lost:  # Only reset bricks when starting a new level or game
      create_bricks()


# Start Screen
def start_screen():
  screen.fill(BLACK)
  title_text = FONT.render('Breakout Game - The Pycodes', True, WHITE)
  screen.blit(title_text, ((SCREEN_WIDTH - title_text.get_width()) // 2, SCREEN_HEIGHT // 3))


  draw_button("Start Game", (SCREEN_WIDTH - 200) // 2, SCREEN_HEIGHT // 2, 200, 60, GRAY, WHITE, start_game)
  pygame.display.update()


# Start or Restart the game
def start_game():
  global game_running, game_over, game_started, score
  score = 0  # Initialize score when the game starts
  game_running = True
  game_over = False
  game_started = True  # Set this to True to start the game
  reset_game()


# Power-up logic
def generate_powerup(x, y):
  if random.random() < 0.2:  # 20% chance to generate a power-up
      powerup_type = random.choice(POWERUP_TYPES)
      active_powerups.append([x, y, powerup_type, time.time()])


def handle_powerups():
   global paddle_x, paddle_y, PADDLE_WIDTH, ball_speed_x, ball_speed_y, lives
   for powerup in active_powerups[:]:
       x, y, powerup_type, start_time = powerup


       # Draw power-ups with correct colors
       if powerup_type == 'extra_life':
           pygame.draw.rect(screen, YELLOW, (x, y, 20, 20))  # Extra Life (Yellow)
       elif powerup_type == 'expand_paddle':
           pygame.draw.rect(screen, GREEN, (x, y, 20, 20))  # Expand Paddle (Green)
       elif powerup_type == 'slow_ball':
           pygame.draw.rect(screen, PURPLE, (x, y, 20, 20))  # Slow Ball (Purple)


       # Move power-up downwards
       powerup[1] += 5


       # Check for collision with paddle
       if paddle_y < y + 20 < paddle_y + PADDLE_HEIGHT and paddle_x < x < paddle_x + PADDLE_WIDTH:
           if powerup_type == 'expand_paddle':
               PADDLE_WIDTH = PADDLE_DEFAULT_LENGTH + 50  # Extend paddle length
               powerup_effect_time[powerup_type] = time.time()
           elif powerup_type == 'slow_ball':
               ball_speed_x *= 0.5
               ball_speed_y *= 0.5
               powerup_effect_time[powerup_type] = time.time()
           elif powerup_type == 'extra_life':
               lives += 1
           active_powerups.remove(powerup)


       # Revert power-up effects after 30 seconds
       for p_type in list(powerup_effect_time.keys()):
           if time.time() - powerup_effect_time[p_type] >= 30:
               if p_type == 'expand_paddle':
                   PADDLE_WIDTH = PADDLE_DEFAULT_LENGTH  # Revert paddle to default length
               elif p_type == 'slow_ball':
                   ball_speed_x = (BALL_SPEED_X + level - 1) * random.choice((1, -1))  # Revert ball speed
                   ball_speed_y = -(BALL_SPEED_Y + level - 1)
               del powerup_effect_time[p_type]


# Main game loop
def game_loop():
  global ball_x, ball_y, ball_speed_x, ball_speed_y, paddle_x, paddle_y, lives, score, game_running, game_started, ball_on_paddle, level


  # Reset game state
  create_bricks()
  reset_game()


  while game_running:
      screen.fill(BLACK)


      for event in pygame.event.get():
          if event.type == pygame.QUIT:
              game_running = False


      # Paddle movement
      keys = pygame.key.get_pressed()
      if keys[pygame.K_LEFT]:
          paddle_x -= PADDLE_SPEED
      if keys[pygame.K_RIGHT]:
          paddle_x += PADDLE_SPEED


      # Keep paddle within screen bounds
      if paddle_x < 0:
          paddle_x = 0
      if paddle_x + PADDLE_WIDTH > SCREEN_WIDTH:
          paddle_x = SCREEN_WIDTH - PADDLE_WIDTH


      # Ball movement and collision handling
      if not ball_on_paddle:
          ball_x += ball_speed_x
          ball_y += ball_speed_y


      # Ball collision with walls
      if ball_x - BALL_RADIUS <= 0 or ball_x + BALL_RADIUS >= SCREEN_WIDTH:
          ball_speed_x = -ball_speed_x
      if ball_y - BALL_RADIUS <= 0:
          ball_speed_y = -ball_speed_y


      # Ball collision with paddle
      if paddle_y < ball_y + BALL_RADIUS < paddle_y + PADDLE_HEIGHT and paddle_x < ball_x < paddle_x + PADDLE_WIDTH:
          ball_speed_y = -ball_speed_y


      # Ball falling below paddle (life lost)
      if ball_y > SCREEN_HEIGHT:
          lives -= 1
          if lives == 0:
              game_running = False
              game_over_screen(score)
              return
          reset_game(life_lost=True)


      # Brick collision detection
      for brick in bricks[:]:
          brick_x, brick_y, brick_color, hit = brick
          if brick_x < ball_x < brick_x + BRICK_WIDTH and brick_y < ball_y < brick_y + BRICK_HEIGHT:
              ball_speed_y = -ball_speed_y
              bricks.remove(brick)
              score += 10
              generate_powerup(brick_x, brick_y)
              break


      # Check for win condition
      if not bricks:
          if level < 5:
              level += 1
              create_bricks()
              reset_game()
          else:
              game_running = False
              congratulations_screen()
              return


      # Draw bricks
      for brick in bricks:
          pygame.draw.rect(screen, brick[2], (brick[0], brick[1], BRICK_WIDTH, BRICK_HEIGHT))


      # Draw paddle
      pygame.draw.rect(screen, WHITE, (paddle_x, paddle_y, PADDLE_WIDTH, PADDLE_HEIGHT))


      # Draw ball
      pygame.draw.circle(screen, RED, (ball_x, ball_y), BALL_RADIUS)


      # Handle power-ups
      handle_powerups()


      # Display score and lives
      score_text = FONT.render(f'Score: {score}', True, WHITE)
      lives_text = FONT.render(f'Lives: {lives}', True, WHITE)
      level_text = FONT.render(f'Level: {level}', True, WHITE)
      screen.blit(score_text, (20, 10))
      screen.blit(lives_text, (SCREEN_WIDTH - lives_text.get_width() - 20, 10))
      screen.blit(level_text, (SCREEN_WIDTH // 2 - level_text.get_width() // 2, 10))


      # Ball on paddle logic (for start of game or life lost)
      if ball_on_paddle:
          ball_x = paddle_x + PADDLE_WIDTH // 2
          if keys[pygame.K_SPACE]:
              ball_on_paddle = False


      # Update the display
      pygame.display.update()


      # Frame rate control
      pygame.time.Clock().tick(60)


# Main program
game_running = False
game_started = False


while not game_started:
  start_screen()
  for event in pygame.event.get():
      if event.type == pygame.QUIT:
          game_running = False
          game_started = True  # To exit the outer loop


if game_running:
  game_loop()

pygame.quit()

Happy Coding!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top
×