PY-1.3-BP4-MQ40

Our Goal: Make Bricks Disappear!

In this lesson, we will make a game where a moving ball hits bricks and makes them disappear!

We will make a Pygame window with a ball that moves and bricks that stay still.

When the ball touches a brick, that brick will be removed from the game.

Check Your Work

Before you start coding, let's check how the tests look now.

Run this command in your terminal: pytest module-1.3/blueprint-4/quest-40

You will see that some tests are not passing yet. This is normal because you haven't written the code for them.

Here are the tests that are not passing:

  • test_no_collision_no_bricks_removed
  • test_single_collision_removes_correct_brick
  • test_all_bricks_collide_and_are_removed
  • test_consecutive_collisions_are_handled_correctly

Time to Code!

Now, let's write the code in main.py to make the bricks disappear when the ball hits them.

When you remove items from a list while you are looping through it, it can cause problems. A good way to do this is to loop through a copy of the list, but remove items from the original list.

Here is an example of how you can remove items from a list while looping:

my_numbers = [1, 2, 3, 4, 5]
numbers_to_remove = []

for num in my_numbers[:]: # Loop over a copy
    if num % 2 == 0: # If the number is even
        numbers_to_remove.append(num)

for num in numbers_to_remove:
    my_numbers.remove(num)

print(my_numbers) # Output: [1, 3, 5]

Think about how you can use this idea to remove bricks when the ball touches them.

Check Your Work Again

Now that you've written your code, let's check if it works!

Run this command in your terminal: pytest module-1.3/blueprint-4/quest-40

If your code is correct, all the tests should now pass.

Here are the tests that should pass:

  • test_no_collision_no_bricks_removed
  • test_single_collision_removes_correct_brick
  • test_all_bricks_collide_and_are_removed
  • test_consecutive_collisions_are_handled_correctly

Documentation

Game State and Object Management

Game development involves tracking the state of the game (like whether it's running or over) and managing collections of objects (like players, enemies, or bricks).

Basic Game State

A common way to control the main loop of a game is using a boolean variable.

# Example: Controlling a loop
game_is_running = True
while game_is_running:
    # Game logic and drawing happens here
    
    # A condition that might end the game
    if some_condition_is_met:
        game_is_running = False # Set the flag to False to stop the loop

Variables initialized before the game loop maintain their value across each iteration (frame). These are persistent game variables.

# Example: A persistent counter
frame_count = 0 # Initialized before the loop
while True: # Assuming some other way to exit
    frame_count += 1 # Incremented each frame
    # The value of frame_count persists

Collision Detection

In Pygame, the pygame.Rect object has a useful method called colliderect(). This method checks if two rectangles overlap.

# Example: Checking collision between two rectangles
rect1 = pygame.Rect(50, 50, 20, 20)
rect2 = pygame.Rect(60, 60, 20, 20)

if rect1.colliderect(rect2):
    print("Rectangles overlap")

To check for collisions between one object (like a ball) and multiple objects (like bricks), you typically iterate through the list of multiple objects and check collision with the single object inside the loop.

# Example: Checking ball collision with multiple bricks
# Assume 'ball' is an object with a 'rect' attribute
# Assume 'bricks' is a list of objects, each with a 'rect' attribute

# Loop through each brick
# Check if the ball's rectangle collides with the current brick's rectangle

Managing Lists During Iteration

When you have a list of objects (like bricks) and you need to remove objects from that list based on a condition (like a collision) while you are looping through the list, a common issue can arise.

The Problem with Modifying a List While Iterating

If you use a standard for loop to iterate directly over a list and remove items from that same list within the loop, you can encounter unexpected behavior. The iterator keeps track of its position based on the original list's indices. When an item is removed, the list shrinks and the indices of subsequent items shift. The iterator might then skip over the item that moved into the position of the removed item.

# Example: Demonstrating the list modification bug (Conceptual)
my_list = [10, 20, 30, 40, 50]
print(f"Initial list: {my_list}")

# This loop will skip elements
for item in my_list:
    print(f"Checking item: {item}")
    if item % 20 == 0: # If item is 20 or 40
        print(f"Removing item: {item}")
        my_list.remove(item) # Modifying the list during iteration

print(f"Final list: {my_list}")
# Expected: [10, 30, 50]
# Actual: [10, 30, 40, 50] or similar depending on Python version/implementation details
# The item '40' might be skipped because '20' was removed.

Debugging with Print Statements

One way to understand what's happening inside your loop, especially when dealing with issues like the list modification bug, is to use print() statements. By printing the state of your list or variables at different points in the loop, you can observe how they change (or don't change) and identify where the logic deviates from what you expect.

# Example: Using print for debugging (Conceptual)
# Assume 'bricks' is the list being modified
# Assume 'ball' is the colliding object

# Print the list state before the loop
# Loop through bricks:
    # Print the current brick being checked
    # Check for collision:
        # If collision:
            # Print a message indicating removal
            # Remove the brick
    # Print the list state after potential removal (optional, but helpful)
# Print the final list state after the loop

Safe List Modification

To safely remove items from a list while iterating, you should iterate over a copy of the list. This leaves the original list free to be modified without disrupting the iteration process.

A simple way to create a copy (a slice) of a list is using the [:] notation.

# Example: Safely modifying a list during iteration
original_list = [10, 20, 30, 40, 50]
print(f"Initial list: {original_list}")

# Iterate over a copy (slice) of the list
for item in original_list[:]:
    print(f"Checking item: {item}")
    if item % 20 == 0: # If item is 20 or 40
        print(f"Removing item: {item}")
        original_list.remove(item) # Modify the original list

print(f"Final list: {original_list}")
# Expected: [10, 30, 50]
# Actual: [10, 30, 50] - Correct!

In the context of game development, you would iterate over bricks[:] and remove from the bricks list.

Integrating State Changes

Collision detection and list modification are often tied to other game state changes. For example, when a ball hits a brick:

  • The brick is removed (list modification).
  • The ball's direction might reverse (changing ball object's state).
  • The player's score might increase (changing a score variable's state).
# Example: Collision triggering state changes (Conceptual)
# Assume 'ball' object with 'rect' and 'speed_y'
# Assume 'bricks' list
# Assume 'score' variable

# Iterate over a copy of bricks:
    # Check collision:
        # If collision:
            # Remove brick from original list
            # Reverse ball's vertical speed
            # Increase score
            # Stop checking for other collisions this frame (optional, but common)

Displaying Text with Pygame Fonts

Pygame's pygame.font module allows you to render text onto surfaces, which can then be blitted onto the screen.

# Example: Steps for displaying text (Conceptual)
# Assume 'screen' is the main display surface

# 1. Initialize the font module (usually done by pygame.init())

# 2. Create a font object
# font = pygame.font.Font(font_file, size)
# Use None for default font

# 3. Render the text onto a new surface
# text_surface = font.render(text_string, antialias, color)

# 4. Blit the text surface onto the screen
# screen.blit(text_surface, position)

Basic Debugging Techniques

Debugging is the process of finding and fixing errors (bugs) in your code.

  • Understand the Error: Read error messages carefully. They often tell you the type of error and where it occurred.
  • Print Statements: As shown above, printing variable values and program state can help you trace the execution flow and see what your code is doing at specific points.
  • Simplify the Problem: If you have a complex bug, try to isolate the part of the code causing the issue. Can you reproduce the bug with a smaller, simpler example?
  • Use a Debugger: More advanced tools (like pdb in Python or integrated debuggers in IDEs) allow you to pause execution, step through code line by line, and inspect variables.
# Example: Using pdb (Conceptual)
import pdb

def my_buggy_function(data):
    # ... some code ...
    pdb.set_trace() # Program execution will pause here
    # ... more code ...

# When the program hits pdb.set_trace(), you get a (Pdb) prompt.
# You can type variable names to see their values, 'n' to go to the next line, 'c' to continue, etc.

Image/Gif Opportunity: List Modification Bug

A short animation or sequence of images showing a list [A, B, C, D] being iterated, B is removed, and the iterator pointer jumps from checking B to checking D, skipping C.

Image/Gif Opportunity: Safe List Modification

A short animation or sequence of images showing iteration over a copy [A, B, C, D] while removing from the original list [A, B, C, D]. Show B being checked in the copy, B being removed from the original, and the iterator in the copy moving correctly to check C.

Image/Gif Opportunity: Pygame Text Rendering

A visual showing the steps:

  1. A font object being created (conceptual).
  2. The font object rendering text onto a separate surface (show the text surface).
  3. The text surface being blitted onto the main screen surface.