SeeThinkExploreMarvel

UMPSA STEM LAB · Step 5 of 10

Step 5 — Bonus Food 🌟

Use for loops and range() to spawn multiple bonus food items and iterate over lists elegantly.

for looprange()list of foodenumerate
Step 5 / 10

The for Loop — Iteration Made Easy

A 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.

🔄 Pattern Recognition — the iteration pattern

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.

How range() works — interactive demo

Click a range preset. The boxes highlight as Python iterates:

Click a button to trace a range loop.

range() syntax

ExpressionValues producedUse case
range(n)0, 1, 2, … n−1Repeat n times
range(a, b)a, a+1, … b−1Slice of integers
range(a, b, step)a, a+step, … <bEvery other value, countdown

Why Multiple Bonus Foods?

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.

💡 Abstraction — lists of items vs individual variables

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)
The _ 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.

Processing the Bonus Foods Each Frame

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)

📋 Algorithm Design — filter pattern

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.

Spawn new bonus food periodically

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.

🎗 The Filter Pattern — Frame Simulator

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.

Snake head at: (6, 4)

The Code — Step 5

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)

Try It — Exercises

1

Change the bonus count 🔄 Pattern

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?

2

Timer display 📋 Algorithm

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.

3

Countdown colour 💡 Abstraction

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.

4

Super bonus 🗂 Data

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.

⚠️ Common Mistakes — Spot These Before You Start

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

✗ Mistake 1: Using = 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
✗ Mistake 2: Forgetting to remove eaten bonus food
if snake[0] in bonus_foods:
    score += BONUS_SCORE    # food eaten but never removed!
🐞 The food stays on the list forever. The snake earns infinite bonus points just by sitting on the same cell every frame.
if snake[0] in bonus_foods:
    score += BONUS_SCORE
    bonus_foods.remove(snake[0])  # take it off the list
⚡ Mistake 3: Spawning food off-grid
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

✅ Quick Check — Are You Ready?

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

1 What is the difference between bonus_foods = (5, 3) and bonus_foods.append((5, 3))?
2 How does the game know when to spawn a new bonus food?
3 Why use 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
Previous← Snake Body