Adding a Second Color

Objective: Adding a Second Color

Let's begin by reviewing our objective.

In this micro-quest, you will enhance the drawing application to use different colors based on which mouse button is pressed.

  • When the left mouse button is held down, the application should draw in blue.
  • When the right mouse button is held down, the application should draw in red.

You will need to check the event.button attribute within the MOUSEBUTTONDOWN event handler to determine the correct color.

Remember to use only the pygame library.

Check Spec: See the Failing Tests

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

Run the tests now.

Test Results:

  • test_left_click_draws_in_blue
  • test_right_click_draws_in_red
  • test_no_drawing_if_mouse_is_up
  • test_quit_event_terminates_loop

As expected, the tests related to drawing with specific colors based on the mouse button are failing. Let's make them pass!

Implement: main.py

Now, let's build the solution by following the TODO comments in main.py. Each step below corresponds to a part of the TODO comment.

Step by step checklist:

  1. Add logic inside the event loop to check for the QUIT event.
  2. If a QUIT event occurs, set the running variable to False to exit the main loop.
  3. Add logic inside the event loop to check for the MOUSEBUTTONDOWN event.
  4. Inside the MOUSEBUTTONDOWN block, set the drawing state variable to True.
  5. Inside the MOUSEBUTTONDOWN block, check the event.button attribute to see which button was pressed.
  6. If event.button is 1 (left button), set the current_color variable to BLUE.
  7. If event.button is 3 (right button), set the current_color variable to RED.
  8. Inside the MOUSEBUTTONDOWN block, store the current mouse position (event.pos) in the last_pos variable.
  9. Add logic inside the event loop to check for the MOUSEBUTTONUP event.
  10. Inside the MOUSEBUTTONUP block, set the drawing state variable to False.
  11. Inside the MOUSEBUTTONUP block, reset the last_pos variable to None.
  12. Add logic inside the event loop to check for the MOUSEMOTION event.
  13. Inside the MOUSEMOTION block, check if the drawing state variable is True AND if last_pos is not None.
  14. If both conditions are true, call pygame.draw.line to draw a line on the screen surface.
  15. The line should use the current_color.
  16. The line should start at last_pos and end at the current mouse position (event.pos). Use a line width of 1.
  17. After drawing the line, update last_pos to the current mouse position (event.pos) so the next line segment starts where this one ended.

The following documentation sections are going to be helpful:

  • The Anatomy of a Game Loop
  • Handling Events
  • Handling Mouse Events
  • Common Mouse Events
  • Reading Event Data
  • State Management
  • The Boolean Flag
  • Storing Positions
  • Drawing Basic Shapes
  • Drawing Lines
  • Combining Concepts

Validate: Run the Tests Again

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

Run pytest one more time.

Test Results:

  • test_left_click_draws_in_blue
  • test_right_click_draws_in_red
  • test_no_drawing_if_mouse_is_up
  • test_quit_event_terminates_loop

All tests passed!

Great job! You have successfully implemented the drawing logic to use different colors based on the mouse button.

Debrief: Prepare for Code Review

You've successfully completed the implementation and passed all tests.

Now, take some time to review your code in main.py.

Consider the following:

  • Is the event handling logic clear and correctly structured within the game loop?
  • Are the state variables (drawing, current_color, last_pos) being updated correctly based on the mouse events?
  • Is the pygame.draw.line function being called only when the mouse is down and moving?
  • Does the code correctly use event.button to switch between blue and red drawing colors?

Prepare to discuss your implementation and how you used the Pygame event system and state management to achieve the desired behavior during your code review with a mentor.

Documentation

Pygame Mouse Input and Drawing: Reference

This document provides a quick reference for key concepts and functions related to handling mouse input and drawing basic shapes in Pygame.

The Anatomy of a Game Loop

A typical Pygame application uses a main loop that repeats continuously. This loop is often structured into distinct phases to manage the flow of the program.

  1. Process Input: Check for user actions (events) like mouse clicks, key presses, or closing the window.
  2. Update State: Modify variables and game elements based on the input and game logic.
  3. Render (Draw): Clear the screen and draw all visual elements in their current state.
# Basic structure of a Pygame game loop
running = True
while running:
    # Phase 1: Process Input
    for event in pygame.event.get():
        # Handle events here (e.g., check event.type)
        pass

    # Phase 2: Update State
    # Modify game variables here

    # Phase 3: Render
    screen.fill(BACKGROUND_COLOR) # Clear screen
    # Draw game elements here
    pygame.display.flip() # Update the display

# After the loop exits
pygame.quit()

Diagram showing the cyclical nature of a game loop

Handling Events

Pygame collects all user actions and window events in an event queue. You access these events using pygame.event.get(), which returns a list of all events that have occurred since the last call.

# Inside the main loop
for event in pygame.event.get():
    # Check the type of the event
    if event.type == pygame.QUIT:
        # Handle window closing
        pass
    # Check for other event types...

Handling Mouse Events

Mouse actions generate specific types of events.

Common Mouse Events

  • pygame.MOUSEMOTION: Occurs when the mouse cursor moves over the window.
  • pygame.MOUSEBUTTONDOWN: Occurs when a mouse button is pressed down.
  • pygame.MOUSEBUTTONUP: Occurs when a mouse button is released.
for event in pygame.event.get():
    if event.type == pygame.MOUSEMOTION:
        # Code to run when the mouse moves
        pass
    
    if event.type == pygame.MOUSEBUTTONDOWN:
        # Code to run when a mouse button is pressed
        pass
        
    if event.type == pygame.MOUSEBUTTONUP:
        # Code to run when a mouse button is released
        pass

Reading Event Data

Event objects contain attributes that provide details about the event.

  • event.pos: A tuple (x, y) representing the mouse coordinates at the time the event occurred.
  • event.button: An integer indicating which mouse button was pressed or released (only for MOUSEBUTTONDOWN and MOUSEBUTTONUP events).
    • 1: Left button
    • 2: Middle button
    • 3: Right button
    • 4: Scroll wheel up
    • 5: Scroll wheel down
# Example of accessing event attributes
for event in pygame.event.get():
    if event.type == pygame.MOUSEMOTION:
        mouse_position = event.pos
        # Use mouse_position (e.g., print it, draw something there)
        # print(mouse_position) # Example from MQ10

    if event.type == pygame.MOUSEBUTTONDOWN:
        click_position = event.pos
        button_number = event.button
        # Use click_position and button_number
        # print(button_number) # Example from MQ55
        
        # Check which button was pressed
        if event.button == 1:
            # Code for left-click
            pass
        elif event.button == 3:
            # Code for right-click
            pass

GIF showing mouse tracking coordinates printing to console GIF showing mouse button numbers printing to console

State Management

State management involves using variables to keep track of information that persists between frames or events. Boolean flags and position variables are common examples.

The Boolean Flag

A boolean variable (True or False) is useful for tracking simple on/off states, such as whether a drawing action is currently active.

# Initialize state outside the loop
is_drawing = False

# Inside the event loop:
for event in pygame.event.get():
    if event.type == pygame.MOUSEBUTTONDOWN:
        # Set the flag when drawing starts
        is_drawing = True
        # print("State changed: is_drawing is now True") # Example from MQ30

    if event.type == pygame.MOUSEBUTTONUP:
        # Unset the flag when drawing stops
        is_drawing = False
        # print("State changed: is_drawing is now False") # Example from MQ30

# Inside the update or render phase:
# Use the flag to control behavior
if is_drawing:
    # Perform drawing actions only when the flag is True
    pass

GIF showing console output for state changes on mouse down/up

Storing Positions

Variables can store coordinate tuples (x, y) to remember locations, such as the starting point of a line or the position of the last click.

# Initialize state outside the loop
last_pos = None # Use None to indicate no position is stored initially

# Inside the event loop:
for event in pygame.event.get():
    if event.type == pygame.MOUSEBUTTONDOWN:
        # Store the position when the button is pressed
        last_pos = event.pos

    if event.type == pygame.MOUSEBUTTONUP:
        # Reset the position when drawing stops
        last_pos = None

    if event.type == pygame.MOUSEMOTION:
        # If drawing is active and a last position exists
        if is_drawing and last_pos is not None:
            current_pos = event.pos
            # Draw something using last_pos and current_pos
            # pygame.draw.line(screen, COLOR, last_pos, current_pos, WIDTH) # Example from MQ50
            # Update last_pos for the next motion event
            last_pos = current_pos

GIF showing continuous line drawing

Drawing Basic Shapes

Drawing in Pygame happens on a Surface object, typically the main screen surface.

Drawing Circles

Draws a circle on a surface.

pygame.draw.circle(surface, color, center_pos, radius)

  • surface: The Surface object to draw on.
  • color: An RGB tuple (e.g., (255, 0, 0) for red).
  • center_pos: A tuple (x, y) for the center of the circle.
  • radius: The radius of the circle in pixels.
# Example: Draw a red circle at (100, 150) with radius 20
RED = (255, 0, 0)
center = (100, 150)
radius = 20
pygame.draw.circle(screen, RED, center, radius) # Example from MQ35

GIF showing a red dot appearing on click

Drawing Rectangles

Draws a rectangle on a surface. This function requires a pygame.Rect object to define the rectangle's position and size.

pygame.draw.rect(surface, color, rect_object)

  • surface: The Surface object to draw on.
  • color: An RGB tuple.
  • rect_object: A pygame.Rect object defining (left, top, width, height).

pygame.Rect(left, top, width, height)

# Example: Draw a blue rectangle at (50, 50) with width 100 and height 50
BLUE = (0, 0, 255)
rect_x = 50
rect_y = 50
rect_width = 100
rect_height = 50
my_rect = pygame.Rect(rect_x, rect_y, rect_width, rect_height)
pygame.draw.rect(screen, BLUE, my_rect) # Example from MQ70 (body)

Simple robot made of shapes

Drawing Lines

Draws a line connecting two points on a surface.

pygame.draw.line(surface, color, start_pos, end_pos, width)

  • surface: The Surface object to draw on.
  • color: An RGB tuple.
  • start_pos: A tuple (x, y) for the starting point.
  • end_pos: A tuple (x, y) for the ending point.
  • width: The thickness of the line in pixels.
# Example: Draw a black line from (10, 10) to (200, 50) with thickness 3
BLACK = (0, 0, 0)
start = (10, 10)
end = (200, 50)
thickness = 3
pygame.draw.line(screen, BLACK, start, end, thickness) # Example from MQ50

GIF showing continuous line drawing

Combining Concepts

By combining event handling, state management, and drawing functions, you can create interactive drawing programs. Events like MOUSEBUTTONDOWN, MOUSEBUTTONUP, and MOUSEMOTION can trigger changes in state variables (like is_drawing or last_pos), and the drawing phase uses these state variables to determine what and where to draw on the screen. Checking event.button allows for different actions or drawing styles based on which mouse button is used.