Let's begin by reviewing our objective.
Define a Brick
class with color, position (x, y), and size (width, height) attributes. In your main script, create a single instance of the Brick
class and draw it to the Pygame screen as a rectangle.
Let's begin by reviewing our objective.
Define a Brick
class with color, position (x, y), and size (width, height) attributes. In your main script, create a single instance of the Brick
class and draw it to the Pygame screen as a rectangle.
Next, we'll run pytest
to see the failing tests. This confirms the engineering specification we need to meet.
Test Results:
test_brick_class_exists_and_initializes
test_draw_first_brick_logic
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.
With the code in place, let's run the tests again to validate our work.
Test Results:
test_brick_class_exists_and_initializes
test_draw_first_brick_logic
All tests passed!This lesson blueprint focuses on using Python classes and lists to manage multiple objects in a Pygame application, culminating in building a wall of bricks.
The foundation of this lesson is the Brick
class. This class serves as a blueprint for creating individual brick objects, each with its own properties like position, size, and color.
Defining a class involves using the class
keyword followed by the class name. The __init__
method is a special method that runs when you create a new instance of the class. It's used to set up the initial state (attributes) of the object.
# Example: Basic class definition
class GameObject:
def __init__(self, x, y, color):
self.x = x
self.y = y
self.color = color
def describe(self):
print(f"Object at ({self.x}, {self.y}) with color {self.color}")
The Brick
class will need attributes to store its position (x
, y
), dimensions (width
, height
), and color
. It will also need a way to draw itself onto the screen.
The first step is to define the Brick
class and then create a single instance of it. Drawing this instance involves using Pygame's drawing functions, typically pygame.draw.rect
, which requires a pygame.Rect
object to define the position and size.
The pygame.Rect
object can be created using the x
, y
, width
, and height
attributes from the Brick
instance.
# Example: Creating a Rect and drawing a rectangle
import pygame
# Assume 'screen' is your Pygame surface
# Assume 'object_color' is an RGB tuple like (255, 0, 0)
# Assume 'object_x', 'object_y', 'object_width', 'object_height' are integers
object_rect = pygame.Rect(object_x, object_y, object_width, object_height)
pygame.draw.rect(screen, object_color, object_rect)
The objective is to create one Brick
instance and draw it using its attributes.
[Image/Gif: A single colored rectangle on a black background]
Instead of just one brick, a game needs many. This involves creating multiple separate instances of the Brick
class. Each instance will have its own unique set of attribute values (e.g., different x
positions).
To make drawing easier when dealing with multiple objects, it is common practice to add a draw
method directly to the class. This method takes the screen surface as an argument and uses the instance's own attributes (self.color
, self.rect
) to draw itself.
# Example: Adding a draw method to a class
import pygame
class DrawableObject:
def __init__(self, x, y, width, height, color):
self.rect = pygame.Rect(x, y, width, height)
self.color = color
def draw(self, surface):
pygame.draw.rect(surface, self.color, self.rect)
# Later, in your main drawing code:
# object1 = DrawableObject(...)
# object2 = DrawableObject(...)
# object1.draw(screen)
# object2.draw(screen)
The objective is to create two Brick
instances with different horizontal positions and call their draw
methods.
[Image/Gif: Two colored rectangles side-by-side]
When you have many objects of the same type, storing them in individual variables becomes impractical. A more efficient approach is to use a list to hold all the object instances.
Once the objects are in a list, you can use a for
loop to iterate through the list. Inside the loop, you can access each object instance and call its methods, such as the draw
method.
# Example: Creating objects and storing them in a list
object_list = []
# Create object instances...
obj_a = GameObject(10, 10, (255, 0, 0))
obj_b = GameObject(50, 10, (0, 255, 0))
# Add them to the list
object_list.append(obj_a)
object_list.append(obj_b)
# Example: Iterating through a list of objects and calling a method
# Assume 'screen' is your Pygame surface
for item in object_list:
item.draw(screen) # Assuming the object has a draw method
The objective is to create an empty list, create three Brick
instances, append them to the list, and then use a for
loop to draw all bricks in the list.
[Image/Gif: Three colored rectangles side-by-side (visually similar to two, but conceptually different)]
Manually creating and appending each object instance is tedious for large numbers. A more automated approach is to use a loop to create the instances. A for
loop with range()
is suitable for creating a fixed number of objects.
Inside the loop, you create a new instance of the class and immediately append it to the list. After the creation loop finishes, you can use a separate loop to iterate through the list and draw them, as done previously.
# Example: Creating multiple objects using a loop
object_list = []
number_of_objects = 5
for i in range(number_of_objects):
# Create a new object instance inside the loop
new_obj = GameObject(0, 0, (255, 255, 255)) # Using default or simple values for now
object_list.append(new_obj)
# Now object_list contains 5 instances
The objective is to use a for
loop with range(5)
to create five Brick
instances and add them to a list. Then, use a separate loop to draw them. Note that without changing their positions, they will all be drawn at the same location.
[Image/Gif: A single colored rectangle (as the five bricks are drawn on top of each other)]
To arrange objects in a pattern, like a row, you need to calculate their positions dynamically within the creation loop. For a horizontal row, the y
position remains constant, while the x
position changes based on the object's index in the row, its width, and any desired gap between objects.
The formula for the x
position of the i
-th object in a row, starting at start_x
with width
and gap
, is typically: start_x + i * (width + gap)
.
# Example: Calculating position in a loop
start_x = 50
start_y = 100
item_width = 80
item_gap = 10
num_items = 8
item_list = []
for i in range(num_items):
current_x = start_x + i * (item_width + item_gap)
current_y = start_y # Y is constant for a row
# Create object with calculated position
# new_item = MyObject(current_x, current_y, ...)
# item_list.append(new_item)
The objective is to use a single for
loop to create 8 Brick
instances, calculating each brick's x
position based on its index, width, and a gap. Store them in a list and then draw the complete row.
[Image/Gif: A horizontal row of colored rectangles with gaps]
In Pygame, everything drawn is ultimately placed onto a pygame.Surface
. The main game window is one large Surface
. You can also create smaller, independent Surface
objects. This is useful for creating complex sprites or drawing elements off-screen before placing them.
The pygame.Surface
object represents a rectangular area of pixels. You can draw shapes or fill colors onto this surface. To display a Surface
onto another Surface
(like the main screen), you use the blit()
method. blit()
copies pixels from one surface (source
) onto another (destination
) at a specified position.
# Example: Creating a surface, filling it, and blitting
import pygame
# Assume 'screen' is your main Pygame surface
# Assume 'custom_size' is a tuple like (100, 50)
# Assume 'fill_color' is an RGB tuple
# Assume 'blit_position' is a tuple like (200, 150)
# Create a new surface
custom_surface = pygame.Surface(custom_size)
# Fill the new surface with a color
custom_surface.fill(fill_color)
# Blit (copy) the custom surface onto the main screen
screen.blit(custom_surface, blit_position)
# Print the position
print(f"Blitted at {blit_position}")
The objective is to create a small pygame.Surface
, fill it with a color, blit
it onto the main screen at a chosen position, and print the blit coordinates.
[Image/Gif: A single colored rectangle drawn using blit]
Building a wall requires arranging bricks in both rows and columns, forming a grid. Nested loops are the standard way to iterate through a 2D structure like a grid. The outer loop typically handles the rows, and the inner loop handles the columns within each row.
Inside the inner loop, you have access to both the current row_index
and col_index
. You can use these indices, along with the object's dimensions and the grid's starting position, to calculate the unique (x, y)
coordinate for each object in the grid.
The formula for the top-left (x, y)
position of an object at (row_index, col_index)
in a grid starting at (start_x, start_y)
with object width
and height
is:
x = start_x + col_index * object_width
y = start_y + row_index * object_height
(Note: This assumes no gap between objects. Gaps would be added similarly to the row calculation).
# Example: Calculating grid positions with nested loops
start_x = 10
start_y = 20
item_width = 50
item_height = 30
num_rows = 3
num_cols = 4
for row_index in range(num_rows):
for col_index in range(num_cols):
current_x = start_x + col_index * item_width
current_y = start_y + row_index * item_height
print(f"Item at row {row_index}, col {col_index}: ({current_x}, {current_y})")
The objective is to use nested loops to calculate and print the (x, y)
coordinates for a 5x3 grid of objects, given object dimensions and a starting position.
[Image/Gif: A conceptual grid with coordinates labeled]
Combining the concepts of classes, lists, nested loops, dynamic positioning, and drawing (using blit
within the Brick
class's draw
method), you can build a complete wall of bricks.
The process involves:
x
and y
position for the current brick based on the loop indices and brick dimensions.Brick
instance using the calculated position and other brick properties.Brick
instance to a list that stores all the bricks.draw()
method on each one to render the entire wall.# Example: Structure for building a grid of objects
object_list = []
num_rows = 5
num_cols = 10
item_width = 80
item_height = 30
for row_index in range(num_rows):
for col_index in range(num_cols):
# Calculate x and y based on indices, width, height
pos_x = col_index * item_width
pos_y = row_index * item_height
# Create object instance
# new_object = MyObject(pos_x, pos_y, item_width, item_height, ...)
# Add to list
# object_list.append(new_object)
# After loops, draw all objects
# for obj in object_list:
# obj.draw(screen)
The objective is to use nested for
loops to create a grid of bricks (e.g., 5 rows of 10 bricks), calculate their x
and y
positions dynamically, store them in a list, and then draw the entire wall by iterating through the list.
[Image/Gif: A full wall of colored rectangles arranged in a grid]
Modifying a list while you are iterating over it using a standard for item in list:
loop can lead to unexpected behavior, such as skipping elements. This happens because removing an element shifts the indices of the elements that come after it.
To safely remove elements from a list during iteration, you can:
for i in range(len(list) - 1, -1, -1):
). When you remove an element at index i
, it only affects elements at indices less than i
, which you have already processed.# Example: Unsafe removal (DO NOT DO THIS)
# my_list = [1, 2, 3, 4]
# for item in my_list:
# if item % 2 == 0:
# my_list.remove(item) # This can skip elements!
# Example: Safe removal by iterating backwards
my_list = [1, 2, 3, 4, 5, 6]
print(f"Original: {my_list}")
for i in range(len(my_list) - 1, -1, -1):
if my_list[i] % 2 == 0:
my_list.pop(i) # Remove by index
print(f"Modified: {my_list}")
# Example: Safe removal by creating a new list
original_list = [1, 2, 3, 4, 5, 6]
new_list = []
for item in original_list:
if item % 2 != 0: # Keep if odd
new_list.append(item)
print(f"Original: {original_list}")
print(f"Modified: {new_list}")
The objective is to write a Python script that creates a list of numbers, iterates through it, and removes all even numbers safely (either by iterating backwards or creating a new list). Print the original and modified lists.
[Image/Gif: A list visually changing as elements are removed]