UMPSA STEM LAB · Step 7 of 10
Replace wall death with Pac-Man style wrap-around using the modulo operator %. Also refactor direction handling to use a Python dictionary.
%Modulo returns the remainder after division. It's how you make numbers "wrap around" — the same maths that makes clocks work.
Modulo is one of the most powerful patterns in programming. It appears in: clock arithmetic (12 + 5 = 5 PM, not 17), cyclic animations (frame % total_frames), hash tables (key % bucket_count), audio beats (frame % tempo == 0), and every game with a "circular" world. Once you see this pattern, you'll recognise it everywhere.
Drag the slider to change the numerator. Watch it cycle:
if hx < 0 or hx >= GRID_COLS:
running = False # game over!
hx = hx % GRID_COLS # wrap 20→0, -1→19 hy = hy % GRID_ROWS # wrap 15→0, -1→14
That's it! Two lines replace the four-condition wall check. When hx reaches 20, 20 % 20 = 0 — the snake teleports to the left edge.
Python's modulo handles negative numbers elegantly. -1 % 20 = 19 — the snake going left past column 0 reappears at column 19.
| Expression | Result | Snake effect |
|---|---|---|
20 % 20 | 0 | Right wall → left edge |
-1 % 20 | 19 | Left wall → right edge |
15 % 15 | 0 | Bottom wall → top edge |
-1 % 15 | 14 | Top wall → bottom edge |
% always returns a non-negative result when the divisor is positive. C and Java can return negative results (-1 % 20 = -1 in C). Python's behaviour is mathematically consistent and better for wrap-around.Replace the chain of if/elif for direction with a Python dictionary. Lookup is O(1) and the code is shorter and easier to extend.
The if/elif chain is logic. The dictionary is data. Whenever you find yourself writing a long chain of if/elif that maps one value to another, consider a dictionary instead. It separates the "what" (the mapping) from the "how" (the lookup), making the code easier to change (add "WARP" direction? Just add one entry).
if direction=="RIGHT": hx+=1 elif direction=="LEFT": hx-=1 elif direction=="UP": hy-=1 elif direction=="DOWN": hy+=1
MOVES = {"RIGHT":(1,0),"LEFT":(-1,0),
"UP":(0,-1),"DOWN":(0,1)}
dx, dy = MOVES[direction]
hx += dx; hy += dy
Click the arrow buttons to move the snake head. When it reaches an edge, hx = hx % GRID_COLS fires and it reappears on the other side. Watch the calculation update live.
# At the top (constants section) — add direction map
MOVES = {
"RIGHT": ( 1, 0),
"LEFT": (-1, 0),
"UP": ( 0, -1),
"DOWN": ( 0, 1)
}
# In update_game() — replace if/elif chain with:
hx, hy = snake[0]
dx, dy = MOVES[direction]
hx += dx; hy += dy
# Wrap-around (replaces wall-death check):
hx = hx % GRID_COLS # wraps 0..GRID_COLS-1
hy = hy % GRID_ROWS # wraps 0..GRID_ROWS-1
new_head = (hx, hy)
# (No more "if hx < 0 or..." — wall is now a portal)
# Self-collision check stays the same:
if new_head in snake[1:]:
return snake, food, score, False
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
MOVES={"RIGHT":(1,0),"LEFT":(-1,0),"UP":(0,-1),"DOWN":(0,1)}
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]
dx,dy=MOVES[direction]
hx+=dx; hy+=dy
hx%=GRID_COLS; hy%=GRID_ROWS # WRAP instead of die
new_head=(hx,hy)
if new_head in snake[1:]: return snake,food,score,False # self only
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()
return snake,food,score,True
screen=pygame.display.set_mode((WINDOW_W,WINDOW_H)); pygame.display.set_caption("Snake Wrap")
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)
pygame.quit()
MOVES[direction]Dictionary lookup by key. If direction = "UP", this returns (0, -1). Accessing a key that doesn't exist raises KeyError — but since we only assign valid direction strings from keyboard events, this is safe. MOVES.get(direction, (0,0)) would be safe with a default.
hx %= GRID_COLSAugmented assignment: x %= n is shorthand for x = x % n. The %= operator modifies in place (like +=). This is a concise one-liner for the wrap.
In Step 7 we check self-collision BEFORE inserting the new head (unlike Step 4). The check is new_head in snake[1:] which is now just new_head in snake since we haven't inserted yet. Both approaches are correct; the order matters.
Open Python: try 20 % 20, -1 % 20, 21 % 20, 40 % 20. What pattern do you notice? Then try 7 % 3 and 10 % 4.
Add a global WRAP_MODE = True. In update_game, use if WRAP_MODE: hx%=GRID_COLS; else: if hx<0 ...: running=False. Toggle it with the W key in the event loop.
Add "STOP": (0, 0) to MOVES. Bind it to the space bar. The snake pauses! Notice you only needed to add one dictionary entry — no if/elif change needed.
These are the errors beginners make most often in Step 7. Read them now so you can recognise them in your own code.
== instead of % for wrap-aroundif x == GRID_COLS:
x = 0
if x == -1:
x = GRID_COLS - 1
x = x % GRID_COLS # handles any value in one line
x = x % GRID_COLS # x wraps ✓ # y not wrapped — game still ends at top/bottom!
x = x % GRID_COLS y = y % GRID_ROWS # must wrap both axes
# Student expects -1 % 20 to give -1 (like in C/Java)
% always returns a non-negative result when the divisor is positive. So -1 % 20 gives 19, not -1. This is actually what we want — the snake appears at the right edge when leaving the left edge!print(-1 % 20) # prints 19 — Python wraps correctly for us
Three questions — not graded. They help you spot gaps before the activities.
x % GRID_COLS do when x = GRID_COLS (i.e., the snake exits the right edge)?% better than an if/elif chain for wrap-around?-1 % 20 return in Python?You've covered all the concepts for Step 7. Time to apply them.
Start Activities → Tier 1 → 2 → 3 → 4