UMPSA STEM LAB · Step 6 of 10
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.
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.
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.
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(...)
...
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)
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):
"""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
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.
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.
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()
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)
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
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()
Functions should receive their inputs as parameters, not reach for global variables. This makes functions testable and reusable.
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.
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.
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()
def name(param1, param2): — def is a keyword (short for define). Everything indented below belongs to that function. Python uses indentation as block delimiters (no curly braces).
return snake, food, score, True returns a tuple of 4 values. The caller unpacks with a, b, c, d = update_game(...). Python tuples are the natural multiple-return mechanism.
The """...""" string immediately after def is a docstring — documentation baked into the function. help(update_game) will print it. Good professional practice.
In Python, functions must be defined before they're called. So all def blocks go above the while loop — which is why we moved pygame.display.set_mode() and initialization below the function definitions.
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.
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.
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).
These are the errors beginners make most often in Step 6. Read them now so you can recognise them in your own code.
return — function gives back Nonedef get_high_score():
score = read_file()
# no return statement!
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
draw_snake(snake) # NameError: name 'draw_snake' is not defined
def draw_snake(snake_list):
...
def at line 3 is processed.def draw_snake(snake_list): # define first
...
draw_snake(snake) # then call
def my_func: without parenthesesdef draw_food: # SyntaxError!
def draw_food(): # parentheses required — even when empty
Three questions — not graded. They help you spot gaps before the activities.
return statement?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