PY-1.3-BP2-MQ30

Objective

Let's begin by reviewing our objective.

Create a script with a Ball class that has move() method. Enhance the move() method with conditional logic to check if the ball has hit the edges of the Pygame window. If a vertical edge is hit, reverse the x_speed. If a horizontal edge is hit, reverse the y_speed.

Check Spec

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

Test Results:

  • test_ball_initialization
  • test_move_no_collision
  • test_bounce_off_right_wall
  • test_bounce_off_left_wall
  • test_bounce_off_bottom_wall
  • test_bounce_off_top_wall
  • test_bounce_off_top_right_corner
  • test_bounce_off_bottom_left_corner

Implement: ball.py

Now, let's build the solution by following the TODO comments in the skeleton code.

Step by step checklist:

  1. Update the ball's position (self.x and self.y) based on its speed.
  2. Check if the ball is colliding with the left or right walls. If it is, reverse its horizontal speed (self.x_speed).
  3. Check if the ball is colliding with the top or bottom walls. If it is, reverse its vertical speed (self.y_speed).

The following documentation sections are going to be helpful:

  • The First Movement
  • Bouncing Off the Walls
  • Conditional Logic in Methods
  • Conditional Logic

Validate

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

Test Results:

  • test_ball_initialization
  • test_move_no_collision
  • test_bounce_off_right_wall
  • test_bounce_off_left_wall
  • test_bounce_off_bottom_wall
  • test_bounce_off_top_wall
  • test_bounce_off_top_right_corner
  • test_bounce_off_bottom_left_corner All tests passed!

Documentation

Bringing Objects to Life: The Bouncing Ball

The Static Ball

This micro-quest focuses on defining a class and creating an object from it.

Core Concept: Class Definition and __init__

  • A class is a blueprint for creating objects. It defines the attributes (data) and methods (functions) that objects of that class will have.
  • The __init__ method is a special method called when you create a new object (an instance) of the class. It's used to set up the initial state of the object by assigning values to its attributes.
  • self refers to the instance of the class itself. Inside __init__ and other methods, you use self. to access or modify the object's attributes.

Objective: Create a Ball class with x, y, radius, and color attributes, instantiate it, and draw it statically using Pygame.

Key Points:

  • Define the Ball class using class Ball:.
  • Define the __init__ method inside the class: def __init__(self, x, y, radius, color):.
  • Inside __init__, assign the input parameters to instance attributes: self.x = x, self.y = y, etc.
  • Define a draw method: def draw(self, screen):. This method will contain the Pygame drawing call.
  • Inside the draw method, use pygame.draw.circle() to draw the ball. This function needs the screen surface, the color, the center coordinates (as a tuple (self.x, self.y)), and the radius (self.radius).
  • In your main script, create an instance of the Ball class: my_ball = Ball(x=..., y=..., radius=..., color=...).
  • In the game loop, call the draw method on your ball instance: my_ball.draw(screen).

Example (Adjacent Concept: Basic Class Structure):

class Dog:
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

    def bark(self):
        print(f"{self.name} says Woof!")

# Creating an object
my_dog = Dog("Buddy", "Golden Retriever")
print(my_dog.name)
my_dog.bark()

Example (Adjacent Concept: Pygame Drawing):

# Assuming 'screen' is a pygame.Surface
# Draw a red circle at (100, 150) with radius 25
pygame.draw.circle(screen, (255, 0, 0), (100, 150), 25)

# Draw a blue rectangle at (50, 50) with width 80 and height 30
pygame.draw.rect(screen, (0, 0, 255), (50, 50, 80, 30))

Files: ball.py, main.py

The First Movement

This micro-quest adds dynamic behavior to the ball object.

Core Concept: Class Methods (Behavior)

  • Methods are functions defined inside a class that describe the actions or behaviors of objects of that class.
  • Methods can access and modify the object's attributes using self.

Objective: Add speed attributes (x_speed, y_speed) to the Ball class, create a move() method to update the ball's position based on speed, and call move() in the main game loop.

Key Points:

  • Modify the __init__ method in Ball to accept x_speed and y_speed parameters and store them as self.x_speed and self.y_speed.
  • Define a new method def move(self): inside the Ball class.
  • Inside the move method, update the ball's position: self.x += self.x_speed and self.y += self.y_speed.
  • In your main script, when creating the Ball instance, provide initial values for x_speed and y_speed.
  • In the main game loop (before drawing), call the move method on your ball instance: my_ball.move().

Example (Adjacent Concept: Method Modifying Attributes):

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1

    def get_count(self):
        return self.count

c = Counter()
print(c.get_count()) # Output: 0
c.increment()
print(c.get_count()) # Output: 1

Files: ball.py, main.py

Bouncing Off the Walls

This micro-quest introduces decision-making within an object's behavior.

Core Concept: Conditional Logic in Methods

  • if statements allow your code to make decisions based on conditions.
  • Inside a method, you can use if statements to check the object's state (its attributes) and perform actions accordingly.

Objective: Enhance the Ball's move() method to check for collisions with the screen edges and reverse the ball's speed when a collision occurs.

Key Points:

  • The move method will need access to the screen dimensions (width and height) to check boundaries. Pass these as arguments: def move(self, screen_width, screen_height):.
  • Inside move, after updating the position, add if statements to check if the ball's edges have gone past the screen boundaries. Remember the ball's position (self.x, self.y) is its center, so you need to account for the self.radius.
    • Left edge collision: self.x - self.radius <= 0
    • Right edge collision: self.x + self.radius >= screen_width
    • Top edge collision: self.y - self.radius <= 0
    • Bottom edge collision: self.y + self.radius >= screen_height
  • If a horizontal collision occurs (left or right), reverse self.x_speed by multiplying it by -1: self.x_speed *= -1.
  • If a vertical collision occurs (top or bottom), reverse self.y_speed by multiplying it by -1: self.y_speed *= -1.
  • In the main game loop, call ball.move() with the screen dimensions: ball.move(SCREEN_WIDTH, SCREEN_HEIGHT).

Example (Adjacent Concept: Conditional Logic):

def check_boundary(position, size, max_dimension):
    if position - size <= 0 or position + size >= max_dimension:
        print("Boundary hit!")
        return True
    else:
        print("In bounds.")
        return False

check_boundary(10, 5, 100) # In bounds.
check_boundary(5, 5, 100)  # Boundary hit!
check_boundary(95, 5, 100) # Boundary hit!

Image/Gif Idea: A gif showing a ball bouncing off the edges of the window.

Files: ball.py, main.py

Setting the Stage: Ball and Paddle

This micro-quest introduces the concept of having multiple different types of objects in your game.

Core Concept: Object Composition

  • Object composition is when you build complex objects or systems by combining simpler objects. In games, this means having different game elements (like a ball and a paddle) represented by separate classes and instances.

Objective: Create a Paddle class, instantiate both a Ball and a Paddle object, and draw both to the screen. The paddle will remain static for now.

Key Points:

  • Create a new file, e.g., paddle.py.
  • Define a Paddle class in paddle.py.
  • The Paddle class's __init__ method should accept x, y, width, height, and color as arguments and store them as attributes.
  • Inside the Paddle's __init__, create a pygame.Rect object using the position and dimensions (self.rect = pygame.Rect(self.x, self.y, self.width, self.height)). pygame.Rect is useful for drawing rectangles and handling collisions.
  • Define a draw method in the Paddle class that uses pygame.draw.rect(screen, self.color, self.rect) to draw the paddle.
  • In your main script (main.py), import the Paddle class: from paddle import Paddle.
  • In the main script's setup (before the game loop), create instances of both Ball and Paddle. Position the paddle near the bottom of the screen.
  • In the main game loop (after filling the background), call the draw method for both the ball and the paddle instances.

Example (Adjacent Concept: Multiple Objects):

class Square:
    def __init__(self, x, y, size):
        self.x = x
        self.y = y
        self.size = size

    def display(self):
        print(f"Square at ({self.x}, {self.y}) with size {self.size}")

# Create multiple instances
square1 = Square(10, 10, 20)
square2 = Square(50, 50, 30)

square1.display()
square2.display()

Image/Gif Idea: A screenshot showing a ball and a paddle drawn on the screen.

Files: ball.py, paddle.py, main.py

Collision Course

This micro-quest adds interaction between the objects.

Core Concept: Object-to-Object Collision Detection

  • Objects can interact with each other based on their positions and shapes.
  • Pygame's Rect objects provide a convenient method (colliderect()) to check if two rectangles overlap.

Objective: Add a check_collision(self, other_object) method to the Ball class that uses pygame.Rect.colliderect() to detect collision with another object (like the paddle). In the main loop, use this method to check for collision with the paddle and reverse the ball's y_speed if they collide.

Key Points:

  • In the Ball class, ensure it has a self.rect attribute (similar to the Paddle, but calculated from the ball's center and radius). You might need to update this self.rect in the move method after updating self.x and self.y.
  • Define the check_collision method in the Ball class: def check_collision(self, other_object):.
  • Inside check_collision, use return self.rect.colliderect(other_object.rect). This method returns True if the rectangles overlap, False otherwise.
  • In your main script, in the game loop (after moving the ball but before drawing), call handle_ball_paddle_collision(ball, paddle).
  • Inside handle_ball_paddle_collision, use an if statement: if ball.check_collision(paddle):.
  • Inside the if block, reverse the ball's vertical speed: ball.y_speed *= -1.

Example (Adjacent Concept: Using colliderect):

import pygame

# Assume pygame is initialized

rect1 = pygame.Rect(10, 10, 50, 50) # A square at (10,10)
rect2 = pygame.Rect(30, 30, 50, 50) # A square at (30,30) - overlaps rect1
rect3 = pygame.Rect(100, 100, 20, 20) # A square at (100,100) - does not overlap rect1

print(rect1.colliderect(rect2)) # Output: True
print(rect1.colliderect(rect3)) # Output: False

Image/Gif Idea: A gif showing the ball bouncing off the paddle.

Files: ball.py, paddle.py, main.py

First Test with Pytest

This micro-quest introduces the practice of writing automated tests for your code.

Core Concept: Unit Testing with Pytest

  • Unit testing is testing individual, small parts (units) of your code, like a single method or class, in isolation.
  • Pytest is a popular Python framework for writing and running tests.
  • Tests help ensure your code works as expected and doesn't break when you make changes.
  • Test functions in Pytest must start with test_.
  • The assert keyword is used in tests to check if a condition is true. If an assert fails, the test fails.

Objective: Create a simple Ball class in ball.py with just the __init__ method. Create a test_ball.py file and write a Pytest function (test_ball_initialization) that creates a Ball instance and uses assert to verify its attributes are set correctly.

Key Points:

  • Create ball.py with a basic Ball class and __init__(self, x, y, radius) that assigns these to self.x, self.y, self.radius.
  • Create test_ball.py.
  • In test_ball.py, import the Ball class: from ball import Ball.
  • Define a test function starting with test_: def test_ball_initialization():.
  • Inside the test function:
    • Define some test values for x, y, and radius.
    • Create an instance of Ball using these test values.
    • Use assert statements to check if the instance's attributes match the test values (e.g., assert my_ball.x == test_x).
  • Run the test from your terminal using the command pytest.

Example (Adjacent Concept: Basic Pytest Assertions):

# In a file named test_calculations.py

def add(a, b):
    return a + b

def test_add_positive_numbers():
    # Check if 2 + 3 equals 5
    assert add(2, 3) == 5

def test_add_negative_numbers():
    # Check if -1 + -1 equals -2
    assert add(-1, -1) == -2

def test_add_zero():
    # Check if 5 + 0 equals 5
    assert add(5, 0) == 5

Files: ball.py, test_ball.py