The Big Clear

Objective: The Big Clear

Welcome to The Big Clear!

Our objective for this micro-quest is to create a Pygame script where the user can draw with the mouse. When the 'c' key is pressed, the entire drawing screen is cleared to black.

You must use only the pygame library for this task; do not use or reference the turtle module.

Check Spec: Run the Tests

Before we start coding, let's run the tests to see the current state of the application and understand the engineering specification we need to meet.

Run pytest in your terminal.

Test Results:

  • test_screen_clears_on_c_key_press
  • test_drawing_with_mouse
  • test_no_drawing_when_mouse_not_down

As expected, the tests are failing because we haven't implemented the drawing or clearing logic yet. These failing tests define exactly what our code needs to do.

Implement: main.py

Now, let's implement the required functionality by following the TODO comments in the main.py file.

Remember, the goal is to handle different events (quitting, mouse clicks, mouse movement, key presses) to enable drawing and screen clearing.

Step by step checklist:

  1. Inside the event loop, add a check for the event type that indicates the user wants to close the window. If this event occurs, set the loop control variable to stop the main loop.
  2. Add a check for the event type that occurs when a mouse button is pressed down. If this event happens, update the state variable that tracks whether the user is currently drawing.
  3. Add a check for the event type that occurs when a mouse button is released. If this event happens, update the state variable to indicate the user is no longer drawing.
  4. Add a check for the event type that occurs when the mouse moves. Inside this check, verify if the drawing state variable is currently true. If it is, get the mouse's current position from the event data and draw a white circle on the screen surface at that position using the predefined radius.
  5. Add a check for the event type that occurs when a keyboard key is pressed down. Inside this check, check if the specific key pressed is the 'c' key. If it is, fill the entire screen surface with the background color (black).

The following documentation sections are going to be helpful:

  • Managing Program State
  • Handling Events
  • The Event Loop (Pygame)
  • Keyboard Input (Pygame)
  • Mouse Input (Pygame)
  • Drawing Basics
  • Pygame Drawing

Validate: Run the Tests Again

You've implemented the drawing and clearing logic based on the TODOs. Now it's time to validate your work by running the tests again.

Run pytest in your terminal.

Test Results:

  • test_screen_clears_on_c_key_press
  • test_drawing_with_mouse
  • test_no_drawing_when_mouse_not_down All tests passed!

Great job! Your code now correctly handles mouse drawing and clears the screen when the 'c' key is pressed, meeting all the requirements defined by the tests.

Documentation

Control Panel Documentation

This document provides a reference for concepts and techniques used in building interactive applications with Python, focusing on state management, event handling, drawing, and basic GUI elements.

Managing Program State

Program state refers to the data that changes over time and needs to be remembered between updates or events. This could include things like the current drawing color, the pen thickness, whether a button is pressed, or a score.

State can be managed using individual variables or, for related pieces of data, grouped together.

Organizing State with Dictionaries

As the number of state variables grows, organizing them into a dictionary can make the code cleaner and easier to manage. A dictionary stores data as key-value pairs.

# Example: Using individual variables
current_color = 'black'
pen_size = 1
is_pen_down = True

# Example: Using a dictionary
drawing_state = {
    'color': 'black',
    'pen_size': 1,
    'is_pen_down': True
}

Accessing and modifying values in a dictionary is done using the key:

# Accessing a value
current_level = user_profile['level']

# Modifying a value
user_profile['level'] = user_profile['level'] + 1

# Adding a new key-value pair
user_profile['last_login'] = 'today'

[ASSET: Image showing a simple dictionary structure with keys and values]

Handling Events

Interactive programs respond to user actions like key presses or mouse clicks. This is typically managed through an event loop.

The Event Loop (Pygame)

In Pygame, the main loop includes checking pygame.event.get() which returns a list of events that have occurred since the last check.

import pygame

# Inside the main game loop
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        # Process other events here

Keyboard Input (Pygame)

To react to a key press, check for the pygame.KEYDOWN event type. The specific key pressed is available in event.key. Pygame provides constants for keys (e.g., pygame.K_c, pygame.K_SPACE, pygame.K_UP).

# Inside the event loop
if event.type == pygame.KEYDOWN:
    if event.key == pygame.K_c:
        # Action for 'c' key
        print("C key pressed")
    elif event.key == pygame.K_SPACE:
        # Action for spacebar
        print("Spacebar pressed")

Mouse Input (Pygame)

Mouse events include button presses (pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP) and movement (pygame.MOUSEMOTION). The position of the mouse for these events is stored in event.pos.

To check if a specific mouse button is currently held down (useful for drawing or dragging), use pygame.mouse.get_pressed(). This returns a tuple where index 0 is the left button, 1 is the middle, and 2 is the right.

# Inside the event loop
if event.type == pygame.MOUSEBUTTONDOWN:
    if event.button == 1: # Left button
        click_pos = event.pos
        print(f"Left mouse button clicked at {click_pos}")

# Outside the event loop, inside the main while running: loop
if pygame.mouse.get_pressed()[0]: # Check if left button is held down
    current_mouse_pos = pygame.mouse.get_pos()
    print(f"Left mouse button held down at {current_mouse_pos}")

Turtle Events

The turtle module provides simpler ways to handle events, often by binding functions directly to keys or mouse clicks on turtle objects.

  • screen.onkey(function, key_name): Calls function when key_name is pressed. screen.listen() must be called first.
  • turtle_object.onclick(function): Calls function(x, y) when the turtle object is clicked at screen coordinates (x, y).
  • turtle_object.ondrag(function): Calls function(x, y) when the turtle object is clicked and dragged.
import turtle

def handle_up_arrow():
    print("Up arrow pressed")

def handle_turtle_click(x, y):
    print(f"Turtle clicked at {x}, {y}")

# Setup
screen = turtle.Screen()
my_turtle = turtle.Turtle()

# Bind events
screen.onkey(handle_up_arrow, "Up")
my_turtle.onclick(handle_turtle_click)

screen.listen() # Start listening for events

Drawing Basics

Both Pygame and Turtle provide ways to draw on a window.

Pygame Drawing

  • screen.fill(color): Fills the entire screen surface with a single color.
  • pygame.draw.circle(surface, color, center_pos, radius, width=0): Draws a circle. width=0 fills the circle.
  • pygame.draw.line(surface, color, start_pos, end_pos, width): Draws a line.
  • pygame.draw.rect(surface, color, rect_tuple_or_object, width=0): Draws a rectangle. width=0 fills the rectangle.
  • pygame.display.flip(): Updates the entire screen to show what has been drawn.
import pygame

# Assuming screen is already created
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
RED = (255, 0, 0)

screen.fill(WHITE) # Clear to white
pygame.draw.circle(screen, RED, (100, 100), 20) # Draw a red circle
pygame.draw.line(screen, BLACK, (0, 0), (800, 600), 5) # Draw a black line
pygame.display.flip() # Show the drawing

Turtle Drawing

  • turtle.Turtle(): Creates a new turtle object.
  • turtle.Screen(): Gets the drawing window (screen).
  • t.forward(distance), t.backward(distance): Move the turtle.
  • t.left(degrees), t.right(degrees): Turn the turtle.
  • t.penup(), t.pendown(): Lift or lower the pen (stop/start drawing lines).
  • t.goto(x, y): Move the turtle to a specific position.
  • t.clear(): Deletes the turtle's drawings from the screen.
  • t.home(): Moves the turtle to the center (0,0) and sets its direction to default.
  • t.pencolor(color_name_or_tuple): Sets the color of the line drawn.
  • t.pensize(width): Sets the thickness of the line.
  • t.speed(speed_value): Sets the drawing speed (0 is fastest).
  • t.hideturtle(), t.showturtle(): Hide or show the turtle icon.
  • t.write(text, align, font): Write text on the screen at the turtle's position.
  • screen.bgcolor(color_name_or_tuple): Sets the screen's background color.
  • screen.mainloop() or turtle.done(): Keeps the window open and responsive to events.
import turtle

screen = turtle.Screen()
screen.bgcolor("lightblue") # Set background color

t = turtle.Turtle()
t.pencolor("darkgreen") # Set pen color
t.pensize(5) # Set thickness
t.speed(1) # Set speed

t.penup()
t.goto(-100, 0) # Move without drawing
t.pendown()

t.forward(200) # Draw a line
t.left(90)
t.forward(100)

t.penup()
t.goto(0, 50)
t.write("Hello!", align="center", font=("Arial", 16, "normal"))

turtle.done() # Keep window open

[ASSET: Image showing a simple turtle drawing]

Collision Detection

Determining if two graphical objects interact (like a mouse click hitting a button) is called collision detection.

Point-in-Rectangle Logic

To check if a point (px, py) is inside a rectangle defined by its top-left corner (rx, ry), width rw, and height rh, you can use inequalities:

  • The point is horizontally inside if px >= rx AND px < rx + rw.
  • The point is vertically inside if py >= ry AND py < ry + rh.

The point is inside the rectangle only if both horizontal and vertical conditions are true. This convention includes the top and left edges but excludes the bottom and right edges.

def is_point_in_rect(point, rect):
    px, py = point
    rx, ry, rw, rh = rect

    # Check horizontal bounds
    x_match = (px >= rx) and (px < rx + rw)

    # Check vertical bounds
    y_match = (py >= ry) and (py < ry + rh)

    # Return True only if both are true
    return x_match and y_match

# Example usage:
button_area = (50, 50, 100, 40) # x=50, y=50, width=100, height=40
click_pos = (75, 60)

if is_point_in_rect(click_pos, button_area):
    print("Clicked inside the button area.")

[ASSET: A diagram showing a rectangle on a 2D grid with points inside and outside, highlighting inclusive/exclusive edges.]

Pygame Rect.collidepoint()

Pygame's Rect objects have a built-in method collidepoint(x, y) or collidepoint((x, y)) that performs the point-in-rectangle check using the same inclusive/exclusive edge convention.

import pygame

# Assuming a Pygame Rect object exists
button_rect = pygame.Rect(50, 50, 100, 40) # x=50, y=50, width=100, height=40

# Inside the event loop, when a MOUSEBUTTONDOWN event occurs:
if event.type == pygame.MOUSEBUTTONDOWN:
    click_pos = event.pos # Get the mouse position from the event
    if button_rect.collidepoint(click_pos):
        print("Button was clicked!")
        # Perform button action here

Adding Randomness

The random module introduces unpredictability, useful for games, simulations, or creative tools.

  • import random: Imports the module.
  • random.randint(a, b): Returns a random integer N such that a <= N <= b. Both a and b are included.
import random

# Get a random number between 1 and 10 (inclusive)
secret_number = random.randint(1, 10)
print(f"Random number: {secret_number}")

# Generate a random RGB color tuple
r = random.randint(0, 255)
g = random.randint(0, 255)
b = random.randint(0, 255)
random_color = (r, g, b)
print(f"Random color: {random_color}")

[ASSET: Image or GIF related to randomness, e.g., dice roll or random color generation]

Specific Techniques & Patterns

Cycling Through a List

To cycle through a sequence of options (like colors or shapes) stored in a list, use an index variable and the modulo operator (%). The modulo operator gives the remainder of a division, which is useful for wrapping around. index % list_length will always result in a valid index within the list's bounds (0 to list_length - 1).

colors = [(255, 0, 0), (0, 255, 0), (0, 0, 255)] # Red, Green, Blue
color_index = 0

# To move to the next color:
color_index = (color_index + 1) % len(colors)

# Get the current color using the index:
current_color = colors[color_index]

print(f"New index: {color_index}, New color: {current_color}")

[ASSET: Diagram showing an index moving through a list and wrapping around]

Numerical State with Limits

For state variables that are numbers (like size or speed), you often need to prevent them from going below a minimum or above a maximum value. if statements or the built-in min() and max() functions can enforce these limits.

line_thickness = 5
MIN_THICKNESS = 1
MAX_THICKNESS = 20

# Increase thickness, applying max limit
line_thickness += 1
if line_thickness > MAX_THICKNESS:
    line_thickness = MAX_THICKNESS
# Or using min(): line_thickness = min(MAX_THICKNESS, line_thickness + 1)


# Decrease thickness, applying min limit
line_thickness -= 1
if line_thickness < MIN_THICKNESS:
    line_thickness = MIN_THICKNESS
# Or using max(): line_thickness = max(MIN_THICKNESS, line_thickness - 1)

print(f"Current thickness: {line_thickness}")

[ASSET: Diagram showing a number line with min/max boundaries]

Creating GUI Elements (Drawing)

Simple GUI elements like buttons can be drawn using basic shapes and text.

Using Turtle: Draw a rectangle outline and then write text inside it. Remember to use penup() and pendown() correctly.

import turtle

def draw_rectangle(t, x, y, width, height):
    t.penup()
    t.goto(x, y)
    t.pendown()
    for _ in range(2):
        t.forward(width)
        t.left(90)
        t.forward(height)
        t.left(90)

def draw_button_label(t, x, y, text):
    t.penup()
    t.goto(x, y) # Position for text (often center)
    t.write(text, align="center", font=("Arial", 12, "normal"))

# Example:
screen = turtle.Screen()
t = turtle.Turtle()
t.hideturtle()
t.speed(0)

button_x, button_y = -60, -25 # Bottom-left corner of a 120x50 button centered at (0,0)
button_width, button_height = 120, 50
draw_rectangle(t, button_x, button_y, button_width, button_height)
draw_button_label(t, 0, 5, "Clear") # Text centered at (0,0), adjusted slightly up

turtle.done()

[ASSET: Image showing a simple drawn button]

Styling GUI Elements

Colors can be applied to screen backgrounds, drawing tools, and GUI elements to create a visual theme.

  • Turtle: screen.bgcolor(), turtle.pencolor(), turtle.fillcolor(), turtle.color(pencolor, fillcolor).
  • Pygame: Colors are passed as arguments to drawing functions (pygame.draw.circle(screen, color, ...), screen.fill(color)).
import turtle

screen = turtle.Screen()
screen.bgcolor("darkslategray") # Set screen background

drawing_turtle = turtle.Turtle()
drawing_turtle.pencolor("lightcyan") # Set drawing line color

button_turtle = turtle.Turtle()
button_turtle.color("gold", "darkgoldenrod") # Set button outline and fill color

# In Pygame:
# screen.fill((30, 30, 50)) # Dark blue background
# pygame.draw.circle(screen, (200, 50, 50), mouse_pos, 10) # Red drawing color

[ASSET: Visual representation of themed GUI elements]