PY-1.2-BP5-MQ22

Objective

Let's begin by reviewing our objective.

Take your existing Etch A Sketch script and refactor the separate state variables (e.g., current_color, line_thickness) into a single drawing_state dictionary. The program's functionality will not change, but the code will be more organized.

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_initial_state_and_setup
  • test_movement_handlers
  • test_clear_handler
  • test_color_change_handlers
  • test_thickness_handlers

These failing tests indicate the parts of the setup_etch_a_sketch function that need to be implemented or corrected.

Implement: main.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 main.py and address the TODO comments.

Step by step checklist:

  1. Initialize the drawing_state dictionary with the required keys and initial values.
  2. Set the pen's initial color and size using values from the drawing_state dictionary.
  3. Implement the movement handler functions (move_forward, move_backward, turn_left, turn_right) to control the pen.
  4. Implement the clear_drawing function to clear the screen and reset the pen's position.
  5. Implement the change_color function to update the state dictionary and the pen's color.
  6. Implement the increase_thickness and decrease_thickness functions to update the state dictionary and the pen's size, ensuring thickness doesn't go below 1.
  7. Bind all the handler functions to their corresponding keys using screen.onkey().

The following documentation sections are going to be helpful:

  • Organizing State with Dictionaries
  • Turtle Events
  • Turtle Drawing
  • Numerical State with Limits

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_initial_state_and_setup
  • test_movement_handlers
  • test_clear_handler
  • test_color_change_handlers
  • test_thickness_handlers All tests passed!

Great job! You have successfully refactored the Etch A Sketch state into a dictionary and implemented all the required handlers.

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]