SeeThinkExploreMarvel

UMPSA STEM LAB · Step 6 of 10

Step 6 — Functions

Organise your game code into reusable functions — draw_game(), update_game(), and game_over() — making the while loop clean and the code easier to maintain.

defparametersreturnDRY principle
Step 6 / 10

Why Functions? The DRY Principle

The Snake game's while loop is getting long. Functions let us give a meaningful name to a block of code, then call it wherever we need it — without repeating the code.

💡 Abstraction — naming a concept

A function name is an abstraction. When you write draw_game(), you don't need to think about pixels and rects and circles — the name says what happens. This is the same abstraction principle used in real game engines (Unity's Update(), pygame's pygame.display.flip()) and in all professional code.

❌ Without functions
while running:
    # 20 lines of event handling
    for event in pygame.event.get():
        ...
    # 15 lines of game logic
    hx, hy = snake[0]
    ...
    # 25 lines of drawing
    screen.fill(BG)
    for i,(cx,cy) in enumerate(snake):
        pygame.draw.rect(...)
    ...
✅ With functions
def handle_events(): ...  # 20 lines
def update_game():   ...  # 15 lines
def draw_game():     ...  # 25 lines

while running:
    handle_events()   # clear!
    update_game()
    draw_game()
    clock.tick(SNAKE_SPEED)

Anatomy of a Function

A function has four parts: the def keyword, the name, parameters (inputs), and an optional return value.

def check_collision(snake, grid_cols, grid_rows) → bool

snake: list grid_cols: int grid_rows: int
↓ runs code ↓
Returns: True (game over) or False (safe)
def check_collision(snake, grid_cols, grid_rows):
    """Return True if snake has died (wall or self-hit)."""
    hx, hy = snake[0]   # head position
    # Wall collision
    if hx < 0 or hx >= grid_cols or hy < 0 or hy >= grid_rows:
        return True
    # Self-collision
    if snake[0] in snake[1:]:
        return True
    return False   # still alive

# Usage in the game loop:
if check_collision(snake, GRID_COLS, GRID_ROWS):
    running = False

🔄 Pattern Recognition — the function pattern

Every function follows the same pattern: inputs arrive as parameters → processing happens inside → output exits as a return value. This input→process→output pattern is universal: mathematical functions (sin(x)), network calls (get_page(url)), database queries (find_user(id)). Recognising this pattern helps you read any codebase.

📚 Watch a Function Call Step by Step

This interactive shows exactly what Python does when draw_game() is called from the main loop — how parameters bind, how the stack grows, and where the return value goes.

Call Stack  (bottom = oldest frame)
game_loop()
— running the main game loop —
Press Next Step to begin stepping through the function call.

The Three Snake Functions

1

def draw_game(screen, snake, food, score, font)

All drawing code in one place. Takes the screen surface, game state, and font. Returns nothing (implicitly returns None). The function side-effects the screen directly.

def draw_game(screen, snake, food, score, font):
    """Draw the current game state to the screen."""
    screen.fill(BG)
    # Draw snake body
    for i, (cx, cy) in enumerate(snake):
        colour = DKGREEN if i == 0 else GREEN
        pygame.draw.rect(screen, colour,
            pygame.Rect(cx*CELL_SIZE, cy*CELL_SIZE,
                        CELL_SIZE-2, CELL_SIZE-2), border_radius=4)
    # Draw food
    fx, fy = food
    pygame.draw.circle(screen, RED,
        (fx*CELL_SIZE + CELL_SIZE//2,
         fy*CELL_SIZE + CELL_SIZE//2), CELL_SIZE//3)
    # Draw score
    screen.blit(font.render(f"Score: {score}", True, WHITE), (8, 6))
    pygame.display.flip()
2

def update_game(snake, direction, food, score)

All game logic in one place. Returns the updated state — Python can return multiple values as a tuple.

def update_game(snake, direction, food, score):
    """Move snake, check food & collisions. Returns (snake, food, score, running)."""
    hx, hy = snake[0]
    if direction == "RIGHT": hx += 1
    elif direction == "LEFT": hx -= 1
    elif direction == "UP":   hy -= 1
    elif direction == "DOWN": hy += 1

    new_head = (hx, hy)
    if hx < 0 or hx >= GRID_COLS or hy < 0 or hy >= GRID_ROWS:
        return snake, food, score, False   # game over

    ate_food = (new_head == food)
    snake.insert(0, new_head)
    if ate_food:
        score += 1
        food = (random.randint(0,GRID_COLS-1),
                random.randint(0,GRID_ROWS-1))
    else:
        snake.pop()

    if new_head in snake[1:]:
        return snake, food, score, False   # game over

    return snake, food, score, True   # all good

# Usage:
snake, food, score, running = update_game(snake, direction, food, score)
3

def game_over_screen(screen, score, font)

Shows a "Game Over" screen and waits for a key. Extracted so it can be called cleanly after the while loop.

def game_over_screen(screen, score, font):
    """Show game over message and wait for key press."""
    screen.fill(BG)
    msg1 = font.render("GAME OVER", True, RED)
    msg2 = font.render(f"Score: {score} — Press any key", True, WHITE)
    cx, cy = screen.get_width()//2, screen.get_height()//2
    screen.blit(msg1, msg1.get_rect(center=(cx, cy-24)))
    screen.blit(msg2, msg2.get_rect(center=(cx, cy+16)))
    pygame.display.flip()
    waiting = True
    while waiting:
        for event in pygame.event.get():
            if event.type in (pygame.QUIT, pygame.KEYDOWN):
                waiting = False

The Cleaned-Up Main Loop

With functions defined, the while loop becomes a clear, readable summary of what happens each frame.

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT: running = False
        if event.type == pygame.KEYDOWN:
            direction = handle_keydown(event.key, direction)  # function!

    snake, food, score, running = update_game(snake, direction, food, score)
    draw_game(screen, snake, food, score, font)
    clock.tick(SNAKE_SPEED)

game_over_screen(screen, score, font)
pygame.quit()
The main loop now reads like a recipe: handle events, update state, draw. This structure is called a game loop and is the foundation of every video game ever made — from Pong to Cyberpunk.

Parameters vs Global Variables

Functions should receive their inputs as parameters, not reach for global variables. This makes functions testable and reusable.

❌ Global variable (fragile)

score = 0   # global

def update_game():
    global score    # must declare global!
    score += 1      # modifies global

Using global is a code smell. The function has a hidden dependency.

✅ Parameter (clean)

def update_game(score):
    score += 1      # local copy
    return score    # explicit output

score = update_game(score)

The function is explicit about what it needs and what it produces.

The Full Step 6 Code

import pygame, random
pygame.init()
GRID_COLS=20; GRID_ROWS=15; CELL_SIZE=30; SNAKE_SPEED=8
WHITE=(255,255,255); GREEN=(0,200,80); DKGREEN=(0,160,50); RED=(220,50,50); BG=(30,30,30)
WINDOW_W=GRID_COLS*CELL_SIZE; WINDOW_H=GRID_ROWS*CELL_SIZE

def draw_game(screen, snake, food, score, font):
    screen.fill(BG)
    for i,(cx,cy) in enumerate(snake):
        pygame.draw.rect(screen,DKGREEN if i==0 else GREEN,
            pygame.Rect(cx*CELL_SIZE,cy*CELL_SIZE,CELL_SIZE-2,CELL_SIZE-2),border_radius=4)
    fx,fy=food
    pygame.draw.circle(screen,RED,(fx*CELL_SIZE+CELL_SIZE//2,fy*CELL_SIZE+CELL_SIZE//2),CELL_SIZE//3)
    screen.blit(font.render(f"Score:{score}",True,WHITE),(8,6))
    pygame.display.flip()

def update_game(snake, direction, food, score):
    hx,hy=snake[0]
    if direction=="RIGHT": hx+=1
    elif direction=="LEFT": hx-=1
    elif direction=="UP":   hy-=1
    elif direction=="DOWN": hy+=1
    new_head=(hx,hy)
    if hx<0 or hx>=GRID_COLS or hy<0 or hy>=GRID_ROWS: return snake,food,score,False
    ate=new_head==food
    snake.insert(0,new_head)
    if ate: score+=1; food=(random.randint(0,GRID_COLS-1),random.randint(0,GRID_ROWS-1))
    else: snake.pop()
    if new_head in snake[1:]: return snake,food,score,False
    return snake,food,score,True

def game_over_screen(screen, score, font):
    screen.fill(BG)
    cx,cy=screen.get_width()//2,screen.get_height()//2
    m1=font.render("GAME OVER",True,RED); m2=font.render(f"Score:{score} Press any key",True,WHITE)
    screen.blit(m1,m1.get_rect(center=(cx,cy-20))); screen.blit(m2,m2.get_rect(center=(cx,cy+20)))
    pygame.display.flip()
    waiting=True
    while waiting:
        for e in pygame.event.get():
            if e.type in(pygame.QUIT,pygame.KEYDOWN): waiting=False

screen=pygame.display.set_mode((WINDOW_W,WINDOW_H))
pygame.display.set_caption("Snake")
font=pygame.font.SysFont(None,28); clock=pygame.time.Clock()
running=True; direction="RIGHT"
snake=[(10,7),(9,7),(8,7)]
food=(random.randint(0,GRID_COLS-1),random.randint(0,GRID_ROWS-1)); score=0

while running:
    for event in pygame.event.get():
        if event.type==pygame.QUIT: running=False
        if event.type==pygame.KEYDOWN:
            if event.key==pygame.K_ESCAPE: running=False
            if   event.key==pygame.K_UP and direction!="DOWN": direction="UP"
            elif event.key==pygame.K_DOWN and direction!="UP": direction="DOWN"
            elif event.key==pygame.K_LEFT and direction!="RIGHT": direction="LEFT"
            elif event.key==pygame.K_RIGHT and direction!="LEFT": direction="RIGHT"
    snake,food,score,running=update_game(snake,direction,food,score)
    draw_game(screen,snake,food,score,font)
    clock.tick(SNAKE_SPEED)
game_over_screen(screen,score,font)
pygame.quit()

Try It — Exercises

1

Add a draw_grid function 💡 Abstraction

Create def draw_grid(screen): that draws faint lines between cells. Use pygame.draw.line(screen, (50,50,50), (x,0), (x,WINDOW_H)) in a for loop for columns, then rows.

2

Restart on game over 📋 Algorithm

In game_over_screen, return True if the user pressed a key (restart) and False for QUIT. Wrap the entire game in a while True: and check the return: if not game_over_screen(...): break.

3

Move direction handling to a function 🔄 Pattern

Create def handle_key(key, current_direction): that returns the new direction string. Call it from the event loop: direction = handle_key(event.key, direction).

⚠️ Common Mistakes — Spot These Before You Start

These are the errors beginners make most often in Step 6. Read them now so you can recognise them in your own code.

✗ Mistake 1: Forgetting return — function gives back None
def get_high_score():
    score = read_file()
    # no return statement!
🐞 The function runs correctly inside, but gives back None to the caller. high = get_high_score() sets high to None, and later comparisons break.
def get_high_score():
    score = read_file()
    return score    # send the value back
✗ Mistake 2: Calling the function before defining it
draw_snake(snake)       # NameError: name 'draw_snake' is not defined

def draw_snake(snake_list):
    ...
🐞 Python reads top-to-bottom. The call at line 1 happens before the def at line 3 is processed.
def draw_snake(snake_list):    # define first
    ...

draw_snake(snake)              # then call
⚡ Mistake 3: Writing def my_func: without parentheses
def draw_food:       # SyntaxError!
🐞 Python requires parentheses after the function name even when there are no parameters. The colon alone is never valid.
def draw_food():     # parentheses required — even when empty

✅ Quick Check — Are You Ready?

Three questions — not graded. They help you spot gaps before the activities.

1 What does a function return if it has no return statement?
2 What does DRY stand for, and why does it matter?
3 A function is defined with def. How do you actually run it?

You've covered all the concepts for Step 6. Time to apply them.

Start Activities → Tier 1 → 2 → 3 → 4
Previous← Bonus Food