PY-1.2-BP6-MQ05

Objective

Let's begin by reviewing our objective.

Given a slightly broken version of the Etch A Sketch code, find and fix a simple bug (e.g., the clear button sets the background to the wrong color) using only print() statements to inspect the state dictionary.

Check the Specification

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

Test Results:

  • test_clear_drawing_fixes_bug
  • test_movement_functions_call_turtle
  • test_main_program_setup

Implement `main.py`

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

Step by step checklist:

  1. Clear the turtle's drawings from the screen.
  2. Return the turtle to its starting position.
  3. Set the screen's background color using the value stored in the state dictionary.

The following documentation sections are going to be helpful:

Validate Your Work

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

Test Results:

  • test_clear_drawing_fixes_bug
  • test_movement_functions_call_turtle
  • test_main_program_setup All tests passed!

Documentation

Blueprint PY-1.2-BP6: The Inspector - Debugging and Refactoring

This document provides a quick reference for key concepts and techniques covered in this blueprint, focusing on finding and fixing errors (debugging) and improving code structure (refactoring).

What is a Bug?

A bug is an error in a computer program that causes it to produce an incorrect or unexpected result, or to behave in an unintended way. Bugs are a normal part of the software development process.

Debugging is the process of finding and resolving defects or problems within a computer program.

A simple diagram showing an input going into a 'buggy' process and producing a wrong output, then going into a 'fixed' process and producing the correct output.

Debugging Methods

Debugging involves investigating your code to understand why it isn't behaving as expected.

Method 1: Print Statement Debugging

Using print() statements is a fundamental technique to inspect the state of your program and understand its execution flow.

  • Purpose: Check the value of variables at specific points, confirm if certain parts of the code are being reached, or track the order of execution.
  • How to use: Insert print() calls with descriptive messages and variable values where you suspect an issue might be occurring.
# Example: Checking a variable's value inside a loop
count = 0
while count < 5:
    print(f"Current count: {count}") # Check the value of count
    # ... other code ...
    count += 1
print("Loop finished.") # Confirm the loop completed

Method 2: The IDE Debugger

Your code editor (like VS Code) has powerful, built-in tools for debugging.

  • Breakpoint: A marker you place on a line of code. When you run the debugger, your program will pause its execution at that exact line.
  • Step Over: A control that executes the currently paused line and immediately pauses again on the next line. This lets you walk through your code one line at a time.
  • Variable Inspector: A panel that shows you the live values of all your variables at the moment the program is paused.

A GIF showing a breakpoint being set in VS Code, the debugger launching, and the 'Step Over' button being clicked to advance line-by-line, with the Variable Inspector panel visible.

What is Refactoring?

Refactoring is the process of restructuring existing computer code without changing its external behavior. The goal is to improve nonfunctional attributes of the software, such as readability, maintainability, and simplicity.

Refactoring is like simplifying an algebraic expression; the value is the same, but the form is cleaner and easier to work with.

Image comparing a messy, unrefactored code snippet with a clean, refactored version that produces the same output.

Refactoring Techniques

Technique 1: Extract Function

If a block of code performs a single, well-defined task, move it into its own function.

  • Purpose: Makes the original code (e.g., a main loop) shorter and easier to read. Allows the extracted logic to be reused elsewhere.
  • How to use:
    1. Identify a block of code that does one thing.
    2. Create a new function.
    3. Move the code block into the new function.
    4. Identify any variables the code block needs from its original context (inputs) and any results it produces (outputs). Define these as function parameters and return values.
    5. Replace the original code block with a call to the new function, passing the necessary inputs.
# Before Refactoring (example)
# ... inside a loop ...
# Calculate area
width = 10
height = 20
area = width * height
print(f"Area: {area}")
# ... rest of loop ...

# After Refactoring (example)
def calculate_area(width, height):
    """Calculates the area of a rectangle."""
    area = width * height
    return area

# ... inside the loop ...
width = 10
height = 20
area = calculate_area(width, height) # Call the new function
print(f"Area: {area}")
# ... rest of loop ...

A diagram showing a large block of code with a smaller section highlighted, and an arrow pointing to a separate function box containing that smaller section, with the original block now showing a call to the new function.

Technique 2: Improve Readability

Write code that is easy for others (and your future self) to understand.

  • Purpose: Reduces the time and effort needed to read, understand, and modify code.
  • How to use:
    • Use clear, descriptive variable and function names (e.g., total_price instead of tp).
    • Break down complex calculations or logic into smaller, named steps using intermediate variables.
# Less readable (example)
# Calculate final price with tax
p = 100
t = p + (p * 0.08)
final = round(t, 2)

# More readable (example)
# Calculate final price with tax
subtotal = 100
tax_rate = 0.08
tax_amount = subtotal * tax_rate
total_price = subtotal + tax_amount
final_price_rounded = round(total_price, 2)

Pygame/Turtle Specifics

Handling Events

In Pygame, the event loop processes user input and system events.

  • Check the event.type attribute to identify the type of event (e.g., pygame.QUIT, pygame.KEYDOWN, pygame.MOUSEBUTTONDOWN).
  • For key events (pygame.KEYDOWN, pygame.KEYUP), use event.key to identify which key was pressed (e.g., pygame.K_r).
  • For mouse button events (pygame.MOUSEBUTTONDOWN, pygame.MOUSEBUTTONUP), use event.pos to get the mouse coordinates.
# Example Pygame event loop structure
import pygame
# ... setup ...
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            # Check event.key here
            pass
        if event.type == pygame.MOUSEBUTTONDOWN:
            # Access event.pos here
            pass
    # ... drawing and update ...

Drawing Order

In graphics programming, the order in which you draw is crucial. Objects drawn later appear on top of objects drawn earlier.

  • Always draw the background first to clear the screen from the previous frame.
  • Then, draw all other game elements (shapes, sprites, text) on top of the background.
  • Finally, update the display (pygame.display.flip() or pygame.display.update()) to show the completed frame.
# Example Pygame drawing order
# ... inside the game loop ...
# 1. Draw background
screen.fill(BACKGROUND_COLOR)

# 2. Draw game elements
pygame.draw.rect(screen, PLAYER_COLOR, player_rect)
# ... draw other objects ...

# 3. Update display
pygame.display.flip()
# ...

Using Global Variables

Variables defined outside of any function are called global variables. To modify a global variable from inside a function, you must use the global keyword.

  • Reading a global variable inside a function does not require the global keyword.
  • Writing to (assigning a new value to) a global variable does require the global keyword inside the function.
# Example using global
counter = 0 # Global variable

def increment_counter():
    global counter # Declare intent to modify the global 'counter'
    counter += 1

def read_counter():
    # No 'global' needed to just read the value
    print(f"Current counter value: {counter}")

# ...
increment_counter()
read_counter() # Output: Current counter value: 1

Adding Sound with Pygame Mixer

Adding sound effects can make an application more engaging.

  • Initialize: Start the mixer module.
    import pygame
    pygame.mixer.init()
    
  • Load: Load a sound file into a pygame.mixer.Sound object.
    # Assumes 'sound.wav' is in the same directory
    sound_effect = pygame.mixer.Sound("sound.wav")
    
  • Play: Call the .play() method on the sound object when the corresponding action occurs.
    # Example inside a function triggered by an event
    def on_action():
        # ... perform action ...
        sound_effect.play() # Play the sound