PY-1.3-BP1-MQ40

Objective

Let's begin by reviewing our objective.

Objective: Add a draw method to the Paddle class. This method should take the Pygame screen surface as an argument and draw the paddle on it using its own attributes. In the main game loop, create a Paddle instance and call its draw method to display it.

Check Spec

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

Run the tests in your terminal:

pytest

Test Results:

  • test_main_creates_paddle_and_calls_draw

Implement: paddle.py

Now, let's build the solution by following the TODO comments in the skeleton code. Each implementation slide covers all TODOs for a single file, referencing the most relevant documentation sections to review for the task.

Open paddle.py and address the TODO comments.

Step by step checklist:

  1. Inside the draw method, use the Pygame drawing function to draw the paddle's rectangle.
  2. Provide the screen surface, the paddle's color, and the paddle's rectangle object as arguments to the drawing function.

The following documentation sections are going to be helpful:

  • The Dictionary-Powered Paddle
  • The Constructor: Giving the Paddle State
  • Encapsulation: Teaching the Paddle to Draw Itself

Implement: main.py

Open main.py and address the TODO comments.

Step by step checklist:

  1. Inside the main game loop, find the section marked for drawing.
  2. Call the draw method on the player_paddle instance.
  3. Pass the screen object as an argument to the draw method.

The following documentation sections are going to be helpful:

  • The Class Blueprint
  • Encapsulation: Teaching the Paddle to Draw Itself

Validate

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

Run the tests in your terminal:

pytest

Test Results:

  • test_main_creates_paddle_and_calls_draw All tests passed!

Documentation

The First Object: Architecting the Paddle

This series of micro-quests focuses on building the foundational element of a game: the player's paddle. You will transition from representing game data using simple dictionaries to creating a dedicated Paddle class, giving it attributes and methods to manage its state and behavior. Finally, you will learn how to save your progress using version control.

The Dictionary-Powered Paddle

Objective: Create a Pygame script that creates a window and draws a single, static rectangle. The paddle's properties (x, y, width, height, color) must be stored in a Python dictionary.

Core Concept Introduced: Representing Game Objects with Dictionaries

Game objects, like a paddle, have properties such as position, size, and color. Initially, these properties can be stored together in a Python dictionary. This allows you to group related data under a single name.

Pygame uses pygame.Rect objects to represent rectangular areas. These are useful for drawing and collision detection. A pygame.Rect is typically created using pygame.Rect(x, y, width, height).

To draw a rectangle in Pygame, you use the pygame.draw.rect() function. It requires the screen surface, the color, and the pygame.Rect object.

Consider how you would define a dictionary to hold the paddle's data:

# Example of a dictionary structure
object_data = {
    "position_x": 100,
    "position_y": 200,
    "size_width": 50,
    "size_height": 10,
    "object_color": (255, 0, 0) # Red
}

# Accessing values from the dictionary
print(object_data["position_x"])

You will need to create a pygame.Rect using values from your dictionary and then use pygame.draw.rect() with the screen, the dictionary's color, and the created Rect.

The Class Blueprint

Objective: Define an empty Python class named Paddle. In the main part of the script, create an instance of this Paddle class and print the object to the console to verify its creation.

Core Concept Introduced: Class Definition and Instantiation

A class is a blueprint for creating objects. It defines the structure (what data an object will hold) and behavior (what actions an object can perform) that objects of that class will have.

Defining a class in Python uses the class keyword:

# Example of a simple class definition
class MyBlueprint:
    pass # 'pass' is used when a block needs a statement but you don't want to add any code yet

Creating an object from a class is called instantiation. You do this by calling the class name followed by parentheses, like calling a function:

# Example of creating an object (instantiation)
my_object = MyBlueprint()

Printing an object instance to the console will show you its type and a unique identifier (often related to its memory address), confirming that the object was successfully created.

The Constructor: Giving the Paddle State

Objective: Enhance the Paddle class by adding an __init__ method. The method should accept parameters for x, y, width, and height, and assign them to instance attributes (e.g., self.x, self.y). Create an instance and print one of its attributes (e.g., paddle_instance.width) to the console.

Core Concept Introduced: The __init__ Method and Instance Attributes

The __init__ method is a special method in Python classes. It's automatically called when you create a new instance of the class. It's often referred to as the constructor because it's used to initialize the object's state.

The first parameter of any instance method, including __init__, is conventionally named self. self refers to the instance of the object itself. You use self to access or set the object's attributes.

Instance attributes are variables that belong to a specific instance of a class. They store the data that makes each object unique. You create and assign instance attributes within the __init__ method using the self.attribute_name = value syntax.

Consider a simple class with an __init__ method:

# Example class with a constructor
class Box:
    def __init__(self, length, width):
        # Assign parameters to instance attributes
        self.length = length
        self.width = width

# Creating an instance calls __init__
my_box = Box(length=10, width=5)

# Accessing instance attributes
print(my_box.length)
print(my_box.width)

You will add an __init__ method to your Paddle class that takes x, y, width, and height as arguments and stores them as attributes on the self object. Then, create a Paddle instance and print one of these attributes.

Encapsulation: Teaching the Paddle to Draw Itself

Objective: Add a draw method to the Paddle class. This method should take the Pygame screen surface as an argument and draw the paddle on it using its own attributes. In the main game loop, create a Paddle instance and call its draw method to display it.

Core Concept Introduced: Instance Methods and Encapsulation

Instance methods are functions defined within a class that perform actions related to the object. Like __init__, they take self as the first parameter, allowing them to access and modify the object's attributes.

Encapsulation is the practice of bundling data (attributes) and the methods that operate on that data within a single unit (the class). This hides the internal details and provides a clear interface (the methods) for interacting with the object.

Instead of having a separate function that knows how to draw a paddle based on a dictionary (as in MQ10), the Paddle object itself will now contain the logic to draw itself. The draw method will use the paddle's own self.x, self.y, self.width, self.height, and self.color (you might need to add color to __init__ and attributes) to create or update its pygame.Rect and then call pygame.draw.rect().

Consider a class with a method that uses its attributes:

# Example class with a method
class Circle:
    def __init__(self, radius, color):
        self.radius = radius
        self.color = color

    def describe(self):
        print(f"This is a {self.color} circle with radius {self.radius}.")

# Create an instance and call its method
my_circle = Circle(radius=5, color="blue")
my_circle.describe()

Your Paddle class will need a draw method that accepts the screen object and uses self.rect and self.color to draw. The main part of your script will create a Paddle object and then call player_paddle.draw(screen) inside the game loop.

Behavior: Making the Paddle Move

Objective: Add a move method to the Paddle class that accepts a speed integer. The method should update the paddle's x attribute by that speed. In the main script, create a paddle, call its move(10) method once, and then draw it to show it has moved from its initial position.

Core Concept Introduced: Methods Modifying Object State

Methods can not only use an object's attributes (like the draw method) but also change them. This allows objects to have dynamic behavior.

The move method will take a speed value and use it to change the paddle's horizontal position (self.x). A positive speed will move it right, and a negative speed will move it left.

# Example class with a method that modifies state
class Counter:
    def __init__(self, initial_value=0):
        self.value = initial_value

    def increment(self, amount=1):
        self.value += amount
        print(f"Value is now: {self.value}")

# Create an instance and modify its state
my_counter = Counter()
my_counter.increment()     # value becomes 1
my_counter.increment(5)  # value becomes 6

Your Paddle class will need a move method that takes a speed argument and updates self.x using addition (+=). Remember that the draw method uses self.x and self.y (or updates self.rect based on them) to determine where to draw, so calling move before draw will show the paddle in its new position.

Version Control: Saving Your Architecture

Objective: In your project directory containing the final paddle script, initialize a new Git repository. Add all the project files to the staging area and make your first commit with the message 'feat: Create initial Paddle class'.

Core Concept Introduced: Initializing a Git Repository and First Commit

Version control systems like Git help you track changes to your code over time. This allows you to revert to previous versions, collaborate with others, and understand the history of your project.

To start tracking a project with Git, you initialize a repository in the project's root directory. This creates a hidden .git folder where Git stores all its tracking information.

Once a repository is initialized, you need to tell Git which files you want to track. This is done by adding them to the "staging area". The staging area is like a waiting area for changes you want to include in your next save point (commit).

A commit is a snapshot of your project's files at a specific point in time. Each commit has a unique identifier and a message describing the changes it contains.

The basic Git commands for this quest are:

  • git init: Initializes a new Git repository in the current directory.
  • git add .: Adds all changes (new files, modified files, deleted files) in the current directory and its subdirectories to the staging area. (You could also specify individual files like git add main.py paddle.py).
  • git commit -m "Your commit message": Creates a new commit with the changes currently in the staging area. The -m flag allows you to provide the commit message directly.

You will execute these commands in your terminal within the directory where your main.py and paddle.py files are located.

Rectangular Encounters: Basic Collision Detection (Supplemental)

Objective: Create two pygame.Rect objects with overlapping coordinates and use pygame.Rect.colliderect() to check if they overlap. Print the boolean result to the console.

Core Concept Introduced: Basic pygame.Rect Collision Detection

pygame.Rect objects are not just for drawing; they are also fundamental for handling collisions in Pygame. A pygame.Rect object has several built-in methods for checking overlaps with other Rect objects or points.

The colliderect() method is one such method. When called on a Rect object, it takes another Rect object as an argument and returns True if the two rectangles overlap anywhere, and False otherwise.

Consider two rectangles:

import pygame

# Rectangle 1: starts at (50, 50), is 100 wide, 80 tall
rect1 = pygame.Rect(50, 50, 100, 80)

# Rectangle 2: starts at (120, 100), is 100 wide, 80 tall
# This will overlap with rect1
rect2 = pygame.Rect(120, 100, 100, 80)

# Check for collision
overlap_result = rect1.colliderect(rect2)

# Print the result
print(overlap_result) # This should print True

You will create two pygame.Rect objects with coordinates that you know will cause them to overlap. Then, call the colliderect() method on one of them, passing the other as an argument, and print the boolean value returned by the method.

Testing the Paddle: First Steps with Pytest (Supplemental)

Objective: Write a simple unit test using pytest for the Paddle class's __init__ method, verifying that its x and y attributes are correctly assigned upon instantiation.

Core Concept Introduced: Introduction to Unit Testing with Pytest (__init__ method)

Unit testing involves testing individual components (units) of your code in isolation. For a class, a unit is often a method. Testing the __init__ method ensures that objects are created with the correct initial state.

pytest is a popular Python testing framework. Tests are typically written as functions that start with test_. Inside a test function, you use assert statements to check if a condition is true. If an assert statement is false, the test fails.

To test the __init__ method of your Paddle class, you will:

  1. Import the Paddle class into your test file.
  2. Define a test function (e.g., test_paddle_initial_position).
  3. Inside the test function, create an instance of Paddle with specific x and y values.
  4. Use assert statements to check if the x and y attributes of the created instance match the values you passed to the constructor.

Consider a simple test for a class:

# Assume you have a class like this in 'my_module.py'
# class Point:
#     def __init__(self, x, y):
#         self.x = x
#         self.y = y

# In your test file (e.g., test_my_module.py)
# from my_module import Point

def test_point_creation():
    """Tests that a Point object is created with correct coordinates."""
    # Arrange: Define expected values
    expected_x = 10
    expected_y = 20

    # Act: Create the object
    my_point = Point(x=expected_x, y=expected_y)

    # Assert: Check the object's attributes
    assert my_point.x == expected_x, f"Expected x to be {expected_x}, but got {my_point.x}"
    assert my_point.y == expected_y, f"Expected y to be {expected_y}, but got {my_point.y}"

# To run this test, save it as a file starting with 'test_' (like test_paddle.py)
# and run 'pytest' in your terminal in the same directory.

You will write a similar test function for your Paddle class, focusing specifically on the x and y attributes set by __init__. Note that because your Paddle class might use pygame internally (e.g., for creating a rect), you might need to use testing techniques like mocking pygame to run the test without a display. However, the core task is the assert statements checking the attributes.