Brick Integration and Collision

Objective: Brick Integration and Collision

Let's begin by reviewing our objective.

Objective: Create a script that initializes a list of Brick objects arranged in a grid. Implement collision detection so that the ball destroys a brick upon impact and reverses its vertical direction.

Core Concept: Managing and iterating through a list of objects for collision.

Check Spec: Initial State

Next, we'll run pytest to see the failing tests. This confirms the engineering specification we need to meet.

Test Results:

  • test_create_bricks_grid
  • test_collision_removes_brick_and_reverses_ball
  • test_no_collision_leaves_state_unchanged
  • test_collision_with_one_of_many_bricks

Implement: main.py

Now, let's build the solution by following the TODO comments in the skeleton code. This slide covers the TODOs in main.py.

Step by step checklist:

  1. Implement the create_bricks function to build the grid of bricks.
  2. Use nested loops to iterate through the specified number of rows and columns.
  3. Calculate the x and y coordinates for each brick based on the provided constants (offsets, width, height, padding).
  4. Create a Brick object for each calculated position.
  5. Add each created Brick object to a list.
  6. Return the list of bricks.
  7. Implement the handle_ball_brick_collision function.
  8. Iterate through the list of bricks.
  9. Inside the loop, check if the ball's rectangle collides with the current brick's rectangle.
  10. If a collision is detected, reverse the ball's vertical direction.
  11. Remove the colliding brick from the list.
  12. Immediately stop checking for other bricks after the first collision is handled.

The following documentation sections are going to be helpful:

  • Micro-Quest PY-1.3-BP6-MQ20: Brick Integration and Collision
  • Core Concept: Managing and iterating through a list of objects for collision
  • Guidance:
    • Create Bricks
    • Handle Ball-Brick Collision

Validate: Check Tests Again

With the code in place, let's run the tests again to validate our work.

Test Results:

  • test_create_bricks_grid
  • test_collision_removes_brick_and_reverses_ball
  • test_no_collision_leaves_state_unchanged
  • test_collision_with_one_of_many_bricks All tests passed!

Documentation

Arkanoid Architect: Core Assembly

Micro-Quest PY-1.3-BP6-MQ10: Core Assembly: Paddle and Ball

Objective: Create a script that initializes the game screen, a Paddle object, and a Ball object. Implement the main game loop where the paddle can be moved by the player and the ball bounces off the screen edges and the paddle.

Core Concept: Assembling multiple objects in a main game loop.

This quest involves bringing together the basic components of the game: the screen, the player's paddle, and the ball. You will set up the main game loop structure and implement the fundamental interactions between the player input, the paddle, the ball, and the screen boundaries.

Guidance:

  1. Initialization: The main.py skeleton already initializes Pygame and sets up the screen and clock. It also creates instances of the Paddle and Ball classes.

  2. Paddle Movement:

    • Inside the main game loop, you need to check which keys are currently being held down by the player. Pygame provides a function for this.
    • Based on the keys pressed (specifically the left and right arrow keys), you will call the move method of the paddle object. The move method should take an argument indicating the direction (e.g., -1 for left, 1 for right).
    • Inside the Paddle.move method, update the paddle's rect.x based on the direction and the paddle's speed.
    • Implement boundary checks in Paddle.move to ensure the paddle's rect.left does not go below 0 and its rect.right does not exceed the screen width.
    # Example: Getting pressed keys (use this in main loop)
    keys = pygame.key.get_pressed()
    
    # Example: Checking a specific key (use this with the keys variable)
    if keys[pygame.K_LEFT]:
        # Call paddle move method here
        pass
    
    # Example: Updating a rect's position
    self.rect.x += self.speed
    
    # Example: Boundary check
    if self.rect.left < 0:
        self.rect.left = 0
    
  3. Ball Movement:

    • Inside the main game loop, call the move method of the ball object.
    • Inside the Ball.move method, update the ball's rect.x and rect.y based on its speed_x and speed_y.
    # Example: Updating ball position (use in Ball.move)
    self.rect.x += self.speed_x
    self.rect.y += self.speed_y
    
  4. Wall Collision (Ball):

    • Inside the Ball.move method, after updating the position, check if the ball's rect has hit the screen boundaries (left, right, and top).
    • If the ball hits the left or right wall, reverse its horizontal speed (speed_x).
    • If the ball hits the top wall, reverse its vertical speed (speed_y). (Note: The bottom wall collision will be handled later for game over).
    # Example: Checking wall collision (use in Ball.move)
    if self.rect.left <= 0 or self.rect.right >= self.screen_width:
        # Reverse horizontal speed here
        pass
    
    if self.rect.top <= 0:
        # Reverse vertical speed here
        pass
    
    # Example: Reversing speed
    self.speed_x *= -1
    
  5. Paddle Collision (Ball):

    • Inside the main game loop, after moving the ball, call the check_collision method of the ball object, passing the paddle's rect.
    • Inside the Ball.check_collision method, use ball.rect.colliderect(paddle_rect) to detect if the ball and paddle rectangles overlap.
    • If a collision occurs and the ball is moving downwards (speed_y > 0), reverse the ball's vertical speed (speed_y). This prevents the ball from getting stuck inside the paddle or bouncing incorrectly when hitting the bottom or sides of the paddle.
    # Example: Checking collision between two rects (use in Ball.check_collision)
    if self.rect.colliderect(other_rect):
        # Handle collision here
        pass
    
    # Example: Checking ball direction
    if self.speed_y > 0:
        # Reverse vertical speed here
        pass
    
  6. Drawing: The drawing code is already in place in the main loop. It clears the screen and then calls the draw methods of the paddle and ball.

(Image/Gif Suggestion: Show the paddle moving left and right. Show the ball bouncing off the walls and the paddle.)

Micro-Quest PY-1.3-BP6-MQ20: Brick Integration and Collision

Objective: Create a script that initializes a list of Brick objects arranged in a grid. Implement collision detection so that the ball destroys a brick upon impact and reverses its vertical direction.

Core Concept: Managing and iterating through a list of objects for collision.

This quest introduces multiple instances of an object (bricks) and requires managing them in a collection (a list). You will learn how to create these objects in a structured way and how to check for interactions between a single object (the ball) and multiple objects in a list.

Guidance:

  1. Create Bricks:

    • Implement the create_bricks() function in main.py.
    • Use nested for loops to iterate through the desired number of rows and columns (BRICK_ROWS, BRICK_COLS).
    • Inside the loops, calculate the x and y position for each brick based on the constants (BRICK_OFFSET_LEFT, BRICK_OFFSET_TOP, BRICK_WIDTH, BRICK_HEIGHT, BRICK_PADDING).
    • Create a Brick instance for each position and append it to the bricks list.
    # Example: Nested loops for grid
    bricks = []
    for row in range(BRICK_ROWS):
        for col in range(BRICK_COLS):
            # Calculate x, y here
            brick = Brick(x, y, BRICK_WIDTH, BRICK_HEIGHT, BLUE)
            bricks.append(brick)
    return bricks
    
    # Example: Calculating position in a grid
    x = offset_left + col * (item_width + padding)
    y = offset_top + row * (item_height + padding)
    
  2. Handle Ball-Brick Collision:

    • Implement the handle_ball_brick_collision(ball, bricks) function in main.py.
    • Iterate through the bricks list.
    • Inside the loop, check for a collision between the ball.rect and the current brick.rect using colliderect.
    • If a collision is detected:
      • Reverse the ball's vertical direction (ball.reverse_y_direction()).
      • Remove the colliding brick from the bricks list.
      • Use a break statement to exit the loop immediately after finding the first collision. This is important because a single ball movement might overlap with multiple bricks, but you typically only want to process one collision per frame to avoid double-bounces or incorrect behavior.
    # Example: Iterating through a list and checking collision
    for item in item_list:
        if object.rect.colliderect(item.rect):
            # Handle collision with item
            # Remove item from list
            # Stop checking (break)
            pass
    
    # Example: Removing an item from a list
    my_list.remove(item_to_remove)
    
  3. Integration: The main() function already calls create_bricks() to get the initial list and handle_ball_brick_collision() inside the game loop. The drawing loop iterates through the bricks list to draw them.

(Image/Gif Suggestion: Show a grid of bricks appearing. Show the ball hitting a brick, the brick disappearing, and the ball bouncing back.)

Micro-Quest PY-1.3-BP6-MQ30: Implementing Win/Loss Conditions

Objective: Create a script that implements win and loss conditions using a simple boolean flag (e.g., game_active). The game should stop if the ball goes off the bottom of the screen, and a 'You Win!' message should appear if all bricks are destroyed.

Core Concept: Basic game state management with boolean flags.

This quest introduces the concept of game state. Instead of the game always running the same logic, it will now check if it's in an "active" state (playing) or an "inactive" state (game over or win). This simple state management controls what happens in the game loop.

Guidance:

  1. Game State Flag: A boolean variable game_active is provided in the run_breakout_game function, initialized to True. An end_message string is also provided.

  2. Control Game Logic: The skeleton already has an if game_active: block. Place all the game logic that should only run when the game is being played (ball movement, wall collision, brick collision) inside this block.

  3. Loss Condition:

    • Inside the if game_active: block, after the ball movement and collision checks, add a check to see if the ball's rect.bottom is greater than or equal to the SCREEN_HEIGHT.
    • If this condition is met, the ball has gone off the bottom. Set game_active to False and set end_message to "Game Over".
    # Example: Checking if ball is off the bottom
    if ball.rect.bottom >= SCREEN_HEIGHT:
        # Set game_active to False
        # Set end_message
        pass
    
  4. Win Condition:

    • Inside the if game_active: block, after handling brick collisions, check if the bricks list is empty (if not bricks:).
    • If the list is empty, all bricks have been destroyed. Set game_active to False and set end_message to "You Win!".
    # Example: Checking if a list is empty
    if not my_list:
        # Set game_active to False
        # Set end_message
        pass
    
  5. Drawing Based on State: The skeleton already has an else block corresponding to if game_active:.

    • Inside the if game_active: block, draw the game elements (ball, bricks).
    • Inside the else block (when game_active is False), the code to draw the end_message using Pygame fonts is already provided.

(Image/Gif Suggestion: Show the ball going off the bottom and the "Game Over" message appearing. Show the last brick being broken and the "You Win!" message appearing.)

Micro-Quest PY-1.3-BP6-MQ40: Designing the GameStateManager

Objective: Create a script that defines a GameStateManager class. The class should be initialized with a state (e.g., 'PLAYING') and have methods to change the state (e.g., change_state('GAME_OVER')) and check the current state (e.g., is_state('PLAYING')).

Core Concept: Encapsulation with a dedicated state management class.

Instead of using a simple boolean flag in the main game function, you will create a dedicated class to manage the game's state. This is a step towards better organization and encapsulation, making the main game logic cleaner and the state management reusable.

Guidance:

  1. Define the Class: Create a class named GameStateManager in game_state_manager.py.

  2. __init__ Method:

    • Define the __init__ method. It should accept one argument, initial_state.
    • Inside __init__, create an instance variable (e.g., self.current_state) and assign the initial_state argument to it.
    # Example: Class initialization
    class MyClass:
        def __init__(self, initial_value):
            self.my_attribute = initial_value
    
  3. change_state Method:

    • Define a method named change_state. It should accept one argument, new_state.
    • Inside change_state, update the self.current_state instance variable to the value of new_state.
    # Example: Method to change an attribute
    def set_attribute(self, new_value):
        self.my_attribute = new_value
    
  4. is_state Method:

    • Define a method named is_state. It should accept one argument, state_to_check.
    • Inside is_state, compare self.current_state with state_to_check.
    • Return the boolean result of this comparison (True if they match, False otherwise).
    # Example: Method to check an attribute's value
    def check_attribute(self, value_to_check):
        return self.my_attribute == value_to_check
    
  5. Testing in main.py: The create_and_test_gsm() function in main.py is provided to demonstrate the class.

    • Inside create_and_test_gsm(), create an instance of your GameStateManager, initializing it with 'PLAYING'.
    • Call the change_state method to change the state to 'GAME_OVER'.
    • Return the instance. The provided print statements (currently commented out) show how is_state would be used.

(Image/Gif Suggestion: A simple diagram showing a box representing the GameStateManager with arrows indicating state transitions like 'PLAYING' -> 'GAME_OVER' or 'PLAYING' -> 'VICTORY'.)

Micro-Quest PY-1.3-BP6-MQ50: Refactoring with the GameStateManager

Objective: Refactor the game from the previous quest. Replace the simple boolean flag with an instance of the GameStateManager. The game's flow (updating and drawing game elements) should now be controlled by checking the state via the manager (e.g., if manager.is_state('PLAYING'):).

Core Concept: Refactoring to use a state manager object.

This quest applies the GameStateManager class you created. You will modify the main game loop to use the manager object to determine whether game logic and drawing should occur, replacing the previous boolean flag logic.

Guidance:

  1. Instantiate the Manager: In the run_game function in main.py, create an instance of the GameStateManager class, initializing it with the state 'PLAYING'. This replaces the game_active boolean flag.

  2. Control Update Logic: Find the section in the main loop where game objects (like the player and ball) are updated. Wrap the calls to their update() methods inside an if statement that checks if the game_state_manager is currently in the 'PLAYING' state using the is_state() method.

    # Example: Controlling updates based on state
    if game_state_manager.is_state('PLAYING'):
        # Call update methods here
        player.update()
        ball.update()
    
  3. Control Drawing Logic: Find the section in the main loop where game objects are drawn. Wrap the calls to their draw() methods inside an if statement that checks if the game_state_manager is currently in the 'PLAYING' state using the is_state() method.

    # Example: Controlling drawing based on state
    if game_state_manager.is_state('PLAYING'):
        # Call draw methods here
        player.draw(screen)
        ball.draw(screen)
    
  4. Remove Old Logic: Ensure you remove any remaining code that relied on the old boolean game_active flag for controlling the main game logic and drawing sections.

(Image/Gif Suggestion: Show the game running normally when in the 'PLAYING' state. Show the screen remaining static or displaying a different message when the state is not 'PLAYING', demonstrating that updates and drawing are skipped.)

Micro-Quest PY-1.3-BP6-MQ60: Final Assembly: Score, Lives, and Polish

Objective: Assemble the complete Arkanoid game. Integrate a scoring system and a lives counter. Update the score when a brick is broken and decrease lives when the ball is missed. The GameStateManager should now handle transitions to 'GAME_OVER' or 'VICTORY' states.

Core Concept: Integrating UI elements with game state.

This is the final assembly quest. You will bring together all the components developed in previous quests (Paddle, Ball, Bricks, GameStateManager, UI) to create a functional Arkanoid game. You will integrate scoring and lives, and use the GameStateManager to control the flow and display based on win/loss conditions.

Guidance:

  1. Review Components: Ensure you have the Paddle, Ball, Brick, BrickManager, GameStateManager, and UIManager classes, along with the config.py file. The Game class in main.py orchestrates these.

  2. Input Handling (_handle_input):

    • Inside the _handle_input method of the Game class, get the state of all keyboard keys using pygame.key.get_pressed().
    • If the game state manager indicates the game is is_playing(), check for pygame.K_LEFT and pygame.K_RIGHT.
    • Call self.paddle.move(-1) for left and self.paddle.move(1) for right.
    # Example: Handling input based on state
    if self.game_state_manager.is_playing():
        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            # Move paddle left
            pass
        # Check for right key and move paddle right
    
  3. Game State Updates (_update_game_state):

    • This method should only run if self.game_state_manager.is_playing(). The skeleton already includes this check.
    • Call self.ball.move().
    • Implement ball-wall collisions (left, right, top) by checking self.ball.rect boundaries and reversing self.ball.speed_x or self.ball.speed_y.
    • Implement ball-paddle collision: Check self.ball.rect.colliderect(self.paddle.rect). If they collide and self.ball.speed_y > 0, reverse self.ball.speed_y.
    • Handle ball-brick collision: Call brick_broken = self.brick_manager.check_collision(self.ball). If brick_broken is True, call self.game_state_manager.add_score() and reverse self.ball.speed_y.
    • Handle ball missed: Check if self.ball.rect.bottom >= SCREEN_HEIGHT. If true, call self.game_state_manager.lose_life(), self.ball.reset(), and self.paddle.reset().
    • Check for victory: After handling brick collisions, call self.game_state_manager.check_victory(self.brick_manager.bricks).
    # Example: Checking ball bottom boundary
    if self.ball.rect.bottom >= SCREEN_HEIGHT:
        # Lose a life
        # Reset ball
        # Reset paddle
        pass
    
    # Example: Checking for victory
    self.game_state_manager.check_victory(self.brick_manager.bricks)
    
  4. Drawing Elements (_draw_elements):

    • The screen is cleared at the start of the method.
    • Use if/elif/else statements based on the self.game_state_manager.is_playing(), is_game_over(), and is_victory() methods.
    • If is_playing(), draw self.paddle, self.ball, and self.brick_manager.
    • If is_game_over(), call self.ui_manager.draw_game_over().
    • If is_victory(), call self.ui_manager.draw_victory().
    • Always call self.ui_manager.draw_hud() to display the score and lives, regardless of the main game state.
    # Example: Drawing based on state
    if self.game_state_manager.is_playing():
        # Draw game objects
        pass
    elif self.game_state_manager.is_game_over():
        # Draw game over screen
        pass
    elif self.game_state_manager.is_victory():
        # Draw victory screen
        pass
    
    # Always draw HUD
    self.ui_manager.draw_hud(self.game_state_manager.score, self.game_state_manager.lives)
    

(Image/Gif Suggestion: Show the complete game running, including the HUD, bricks breaking, score updating, lives decreasing, and the final win/loss screens.)