PY-1.3-BP2-MQ10

Our Goal: Draw a Ball!

In this lesson, you will make a program that draws a circle, which we will call a Ball. You will create a blueprint (class) for the ball and tell it how to draw itself. Then, you will make a ball and draw it on the screen.

Here is what the static ball will look like in the game window:

<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg">
  <rect width="800" height="600" fill="#000000"/>
  <circle cx="400" cy="300" r="20" fill="#FFFFFF"/>
</svg>

Check Your Code

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

Run this command in your terminal:

pytest module-1.3/blueprint-BP2/quest-10

What you should see:

āŒ Some tests will fail. This is okay for now! It means you haven't written the code for them yet, and the tests are waiting for you to do it.

Write Your Code: `ball.py`

Now, let's write the code for ball.py.

Your Ball needs to know its position (x, y), its size (radius), and its color. It also needs a way to draw itself on the screen.

Here are some general examples of how you might create a class and draw a circle in Pygame:

import pygame

# Example: Define a color using RGB values
GREEN = (0, 255, 0)

# Example: Create a simple class
class Circle:
    def __init__(self, x, y, size, color):
        self.x = x
        self.y = y
        self.size = size
        self.color = color

    def draw(self, screen):
        # This is where you would draw the circle
        pass

# Example: Drawing a circle in Pygame
# pygame.draw.circle(surface, color, center_position, radius)
# screen.fill((0, 0, 0)) # Fill screen with black first
# pygame.draw.circle(screen, GREEN, (50, 50), 15)

Write Your Code: `main.py`

Now, let's write the code for main.py.

In main.py, you will create your Ball object and draw it on the screen.

Here are some general examples of how you might create an object and use it:

# Imagine you have a class called MyCircle
# from my_shapes import MyCircle

# Create an object from the class
# my_ball = MyCircle(x=200, y=250, size=25, color=(255, 255, 0)) # Yellow color

# Call a method on the object to draw it
# my_ball.draw(game_screen)

# A basic Pygame setup
# import pygame
# 
# pygame.init()
# screen_width = 800
# screen_height = 600
# screen = pygame.display.set_mode((screen_width, screen_height))
# 
# running = True
# while running:
#     for event in pygame.event.get():
#         if event.type == pygame.QUIT:
#             running = False
# 
#     screen.fill((0, 0, 0)) # Fill the screen with black
#     # Your drawing code would go here
#     pygame.display.flip() # Update the display
# 
# pygame.quit()

Check Your Work Again: All Tests Pass!

Now that you've written your code, let's run the tests one more time.

Run this command in your terminal:

pytest module-1.3/blueprint-2/quest-10

You should see all tests passing:

  • āœ… test_ball_class_initialization
  • āœ… test_ball_draw_method_calls_pygame_circle
  • āœ… test_run_game_instantiates_and_draws_ball

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