UMPSA STEM LAB · Step 5 of 10
Use for loops and range() to spawn multiple bonus food items and iterate over lists elegantly.
for Loop — Iteration Made EasyA for loop repeats code once for every item in a sequence. Combined with range(), it's the standard way to run code N times in Python.
Anywhere you find yourself writing repetitive code (draw food1, draw food2, draw food3…) a for loop is usually the right abstraction. This pattern appears everywhere: rendering particles in games, processing CSV rows in data science, sending requests to a server list.
range() works — interactive demoClick a range preset. The boxes highlight as Python iterates:
| Expression | Values produced | Use case |
|---|---|---|
range(n) | 0, 1, 2, … n−1 | Repeat n times |
range(a, b) | a, a+1, … b−1 | Slice of integers |
range(a, b, step) | a, a+step, … <b | Every other value, countdown |
The regular food gives 1 point. Bonus foods spawn periodically, worth more — but they disappear after a few seconds. This requires a list of bonus food positions.
Once you have one food item working, extending to N items just means storing them in a list and using a for loop to process all of them. You never change the core food logic — you generalise it. This is abstraction through iteration: write the logic once, apply it everywhere.
# Single food (Step 4)
food = (random.randint(0, GRID_COLS-1),
random.randint(0, GRID_ROWS-1))
# Multiple bonus foods (Step 5)
bonus_foods = [] # list of (col, row, timer) tuples
# Spawn 3 bonus foods at the start
for _ in range(3):
bx = random.randint(0, GRID_COLS-1)
by = random.randint(0, GRID_ROWS-1)
bonus_foods.append((bx, by, 180)) # timer = 180 frames (~3 seconds)
_ variable — when you write for _ in range(3):, the underscore means "I don't need this value, I just want to repeat 3 times." It's a Python convention for a throwaway variable.Each frame we need to: count down each food's timer, check if the snake ate it, draw it, and remove it if the timer ran out.
# Inside game loop — process all bonus foods
new_bonus = []
for (bx, by, timer) in bonus_foods:
if (bx, by) == (hx, hy): # snake ate this one!
score += 3 # bonus score
elif timer > 0:
new_bonus.append((bx, by, timer - 1)) # count down
# if timer == 0: drop it (don't append → it disappears)
bonus_foods = new_bonus # replace list with surviving foods
# Draw all bonus foods
for (bx, by, timer) in bonus_foods:
alpha = min(255, timer * 2) # fades as timer decreases
pygame.draw.circle(screen, (255, 215, 0), # gold
(bx*CELL_SIZE + CELL_SIZE//2,
by*CELL_SIZE + CELL_SIZE//2), CELL_SIZE//3)
The pattern of iterating over a list and building a new list of only the items you want to keep is called filtering. It's safer than deleting items while iterating (which causes bugs). You'll see this exact pattern in data cleaning, game AI, and web server request processing.
Bonus foods should respawn every 10 seconds. We use a frame counter and the modulo operator (formally introduced in Step 6) to trigger this.
frame_count = 0 # counts every frame the game runs
# Inside game loop:
frame_count += 1
if frame_count % 300 == 0: # every 300 frames = ~10 seconds at 30fps
if len(bonus_foods) < 5: # cap at 5 bonus foods at once
bx = random.randint(0, GRID_COLS-1)
by = random.randint(0, GRID_ROWS-1)
bonus_foods.append((bx, by, 180))
frame_count % 300 == 0 — every 300 frames evaluates to True. % is the modulo (remainder) operator. We'll explore it deeply in Step 6 for the wrap-around mechanic.This simulator shows the bonus food list being processed each frame. Press Next Frame to tick time forward — watch timers count down, foods disappear when expired, and the snake eating nearby food.
frame_count = 0
# Spawn initial bonus foods (new — after existing food/score lines)
bonus_foods = []
for _ in range(3):
bonus_foods.append((random.randint(0, GRID_COLS-1),
random.randint(0, GRID_ROWS-1),
180)) # timer in frames
# ── inside while loop ─────────────────────────────────────────────
frame_count += 1
if frame_count % 300 == 0 and len(bonus_foods) < 5:
bonus_foods.append((random.randint(0,GRID_COLS-1),
random.randint(0,GRID_ROWS-1), 180))
# Process bonus foods
new_bonus = []
for (bx, by, timer) in bonus_foods:
if (bx, by) == new_head:
score += 3 # bonus points!
elif timer > 0:
new_bonus.append((bx, by, timer - 1))
bonus_foods = new_bonus
# Draw bonus foods (gold circles)
for (bx, by, timer) in bonus_foods:
pygame.draw.circle(screen, (255, 215, 0),
(bx*CELL_SIZE + CELL_SIZE//2,
by*CELL_SIZE + CELL_SIZE//2), CELL_SIZE//3)
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); GOLD=(255,215,0); BG=(30,30,30)
WINDOW_W=GRID_COLS*CELL_SIZE; WINDOW_H=GRID_ROWS*CELL_SIZE
screen=pygame.display.set_mode((WINDOW_W,WINDOW_H))
pygame.display.set_caption("Snake + Bonus")
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
bonus_foods=[]; frame_count=0
for _ in range(3):
bonus_foods.append((random.randint(0,GRID_COLS-1),random.randint(0,GRID_ROWS-1),180))
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"
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: running=False; continue
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:]: running=False
frame_count+=1
if frame_count%300==0 and len(bonus_foods)<5:
bonus_foods.append((random.randint(0,GRID_COLS-1),random.randint(0,GRID_ROWS-1),180))
new_bonus=[]
for (bx,by,timer) in bonus_foods:
if (bx,by)==new_head: score+=3
elif timer>0: new_bonus.append((bx,by,timer-1))
bonus_foods=new_bonus
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)
for (bx,by,timer) in bonus_foods:
pygame.draw.circle(screen,GOLD,(bx*CELL_SIZE+CELL_SIZE//2,by*CELL_SIZE+CELL_SIZE//2),CELL_SIZE//3)
screen.blit(font.render(f"Score:{score} Bonus:{len(bonus_foods)}",True,WHITE),(8,6))
pygame.display.flip(); clock.tick(SNAKE_SPEED)
pygame.quit()
for _ in range(3):Repeats exactly 3 times. _ is the "I don't care about this value" convention. Range(3) generates: 0, 1, 2 — three values, three loops.
bonus_foods.append(...)append adds an item to the end of a list. It's O(1) — constant time, regardless of list size. The bonus food is a tuple of 3 values: (x, y, timer).
for (bx, by, timer) in bonus_foods:Iterate the list and unpack each 3-tuple. Python automatically distributes the three values into bx, by, timer. This is called "destructuring" in other languages.
frame_count % 300 == 0True every 300th frame. At 30 fps, that's every 10 seconds. The % modulo operator returns the remainder after division — when frame_count is exactly divisible by 300, the remainder is 0.
Change range(3) to range(5) to start with 5 bonus foods. Then change the cap from 5 to 10. What happens when there are too many?
Draw a small number next to each bonus food showing its remaining seconds: font.render(str(timer//30), True, WHITE). // is integer division — 180//30 = 6 seconds.
Make the bonus food pulse colour as it expires: calculate r = int(255 * (1 - timer/180)), then use (255, 215-r, 0) as the colour. It shifts from gold to red as time runs out.
Add a 4th element to each bonus food tuple: value = random.choice([2, 3, 5, 10]). Use this value for the score increase instead of the fixed 3. Update all the code that reads the tuple.
These are the errors beginners make most often in Step 5. Read them now so you can recognise them in your own code.
= instead of .append()bonus_foods = (rx, ry) # replaces the whole list with one tuple!
bonus_foods is no longer a list — it becomes a single tuple. The for loop that draws foods breaks with a TypeError.bonus_foods.append((rx, ry)) # adds to the existing list
if snake[0] in bonus_foods:
score += BONUS_SCORE # food eaten but never removed!
if snake[0] in bonus_foods:
score += BONUS_SCORE
bonus_foods.remove(snake[0]) # take it off the list
rx = random.randint(0, GRID_COLS) # can equal GRID_COLS!
randint is inclusive on both ends. A value of GRID_COLS (e.g., 20) is off the right edge of the grid — the food appears outside the window.rx = random.randint(0, GRID_COLS - 1) # 0 to 19 inclusive
Three questions — not graded. They help you spot gaps before the activities.
bonus_foods = (5, 3) and bonus_foods.append((5, 3))?random.randint(0, GRID_COLS - 1) instead of random.randint(0, GRID_COLS)?You've covered all the concepts for Step 5. Time to apply them.
Start Activities → Tier 1 → 2 → 3 → 4