This page contains exercise answers and teaching guidance for
Step 5 — Bonus Food 🌟.
Not linked from the student pages.
Students: please go back and try the activities first.
for loop to iterate over a list of bonus food positionsrange() for generating integer sequencestime.time() to track elapsed game timeimport time
bonus_foods = [] # list of [x, y, spawn_time]
BONUS_INTERVAL = 5 # spawn a new bonus food every 5 seconds
last_bonus_time = time.time()
# --- Each frame ---
now = time.time()
# Spawn new bonus food on interval
if now - last_bonus_time >= BONUS_INTERVAL:
bx = random.randint(0, GRID_COLS - 1)
by = random.randint(0, GRID_ROWS - 1)
bonus_foods.append([bx, by, now]) # position + spawn time
last_bonus_time = now
# Draw all bonus foods
for bf in bonus_foods:
bx, by, _ = bf # _ = "don't need spawn_time here"
pygame.draw.rect(screen, GOLD,
(bx * CELL_SIZE, by * CELL_SIZE, CELL_SIZE, CELL_SIZE))
# Check if snake ate any bonus food
for bf in bonus_foods:
if snake[0] == (bf[0], bf[1]):
score += 3
# Filter pattern — remove eaten AND expired bonus foods
bonus_foods = [
bf for bf in bonus_foods
if snake[0] != (bf[0], bf[1]) # not eaten
and now - bf[2] < 8 # not expired (< 8 seconds old)
]
for bf in bonus_foods:Iterates over every item in the list; bf is each bonus food.
time.time()Returns current time in seconds (float). now - bf[2] = seconds since spawned.
[x, y, spawn_time]Each bonus food carries its own metadata.
[item for item in list if condition] creates a NEW list with only matching items. This safely removes items without modifying the list mid-iteration.
list.remove() inside the loop?Modifying a list during iteration skips elements. The filter pattern is safe because it creates a brand-new list.
| Question | Model Answer |
|---|---|
What does for bf in bonus_foods: do? |
Iterates through every item in bonus_foods, binding each to bf. |
What is range(5)? |
Generates the integers 0, 1, 2, 3, 4 — five values. |
Why store spawn_time in each bonus food? |
So we can calculate how long it has been on screen and remove it when it expires. |
What does [bf for bf in bonus_foods if ...] produce? |
A new list containing only the items where the condition is True. |
bonus_foods.remove(bf) inside the for bf in bonus_foods: loop.
Effect: Skips items — some bonus foods are never checked or removed correctly.
Fix: Use the filter pattern AFTER the loop instead of removing inside it.
Make bonus food worth 5 points and grow the snake by 1 extra cell.
# When bonus food is eaten:
score += 5
tail = snake[-1] # duplicate the tail
snake.append(tail)
Add a countdown effect — bonus foods flash (draw only on even frames) when less than 2 seconds remain.
frame_num = 0
for bf in bonus_foods:
time_left = 8 - (now - bf[2])
if time_left > 2 or frame_num % 2 == 0:
pygame.draw.rect(screen, GOLD,
(bf[0] * CELL_SIZE, bf[1] * CELL_SIZE, CELL_SIZE, CELL_SIZE))
frame_num += 1
| Prompt | Key ideas a strong answer contains |
|---|---|
| 1. for vs while | for iterates over a known sequence; while repeats until a condition is False. |
2. What does range(0, 20) produce? |
Integers 0 through 19 (not including 20). |
| 3. Why not modify a list while iterating? | The iterator uses an internal index; removing items shifts indices and causes elements to be skipped. |
| 4. What is a list comprehension? | A compact syntax [expr for item in list if condition] that creates a new filtered/transformed list. |
| 5. Explain filter pattern | “Instead of deleting from the old list, we make a brand-new list with only the items we want to keep.” |
| What they did | What they see | What to say |
|---|---|---|
list.remove(bf) inside for loop |
Some bonus foods never removed; erratic behaviour | “Modifying a list during iteration skips items. Build a new list with only what you want to keep.” |
time.time() called only once at startup |
All bonus foods get same age; never expire | “Call time.time() every frame and store as now. Use now - bf[2] to measure elapsed time per item.” |
range(GRID_COLS) vs randint(0, GRID_COLS) mismatch |
Food at column 20 (off-screen) | “randint(a, b) includes b. Use randint(0, GRID_COLS - 1) for valid grid indices.” |
| Forgot to draw bonus foods | Food spawns (in list) but invisible | “Every object needs a draw call every frame. Check the draw section of your loop.” |
Compare bf == (food_x, food_y) (list vs tuple) |
Never matches | “bonus_foods items are lists [x, y, t]. Compare (bf[0], bf[1]) with the snake head tuple.” |
“How does a game spawn enemies over time and clean up dead ones? Both use the pattern you’re implementing now.”
Spend 5 minutes with a concrete example on the board. Write numbers = [1, 2, 3, 4, 5], then show [n for n in numbers if n % 2 == 0] → [2, 4]. Ask students: “What happened to the odd numbers?” — they were never included, not deleted.
Each bonus food is a self-contained data bundle (position + time). Ask: “What other properties might a game object carry?” This plants the seed for Step 9’s OOP refactor.
Fast finishers can add a second type of bonus food (e.g., a “slow” power-up) stored in a separate list with a different colour — this reinforces the pattern.
These 5 questions appear in the activity page after Tier 4 (post-calibration gate). Pass mark is 4 of 5 (80%). Students who fail may retry; the system records attempts and final score in Google Sheets.
| Question (displayed to student) | Correct Answer |
|---|---|
| Q1: Why is str(score) needed in: 'Score: ' + str(score)? | ✓ Because + needs both sides to be the same type (string) |
| Q2: What does score += 1 mean? | ✓ Add 1 to score |
| Q3: if score % 5 == 0: level += 1 — triggers when: | ✓ score is exactly divisible by 5 |
| Q4: To restart the game when R is pressed, which code is correct? | ✓ if event.key == pygame.K_r: reset_game() |
| Q5: Increasing level speed means: | ✓ A smaller clock.tick() argument (higher FPS cap) |
Recorded in Google Sheet (Act_5 tab):
concept_q1–concept_q5 (student’s 0-based answer index),
concept_score_pct, concept_passed (1 = pass, 0 = fail),
concept_attempts (retry count).
Every submission to this step writes one row to the Act_5 tab in the research spreadsheet. All 13 tabs (Student_Reg, Pre_Test, Post_Test, Act_1–Act_10) share the same student identity columns.
| Column | Description |
|---|---|
| STUDENT IDENTITY (10 fields) | |
matric | Matric / student ID |
name | Full name |
gender | Gender (Female / Male / Other) |
age | Age in years |
mykid | MyKid / IC number |
home_state | Home state in Malaysia |
class | Class or cohort code |
school_code | School or programme code |
phone | Phone number |
email | Email address |
| SUBMISSION | |
step | Step number (5) |
submitted_iso | KL timestamp (UTC+8, ISO 8601) |
| PRE-CALIBRATION | |
cal_confidence | Self-confidence before activity (1–5 scale) |
cal_predicted | Predicted score before activity (%) |
cal_reflection | Free-text: what will be hard? |
| ACTIVITY TIERS | |
t1_score_pct | Tier 1 Fill-in-Blanks score (%) |
t2_attempts | Tier 2 Debug — number of attempts |
refl2_text | Tier 2 reflection free text |
t3_attempts | Tier 3 Complete-Code attempts |
t4_attempts | Tier 4 New Task attempts |
| CONCEPT CHECK | |
concept_q1–concept_q5 | Student answer index (0-based) per question |
concept_score_pct | Percentage correct (0–100) |
concept_passed | 1 = passed (≥80%), 0 = failed |
concept_attempts | Total retries |
| REFLECTIVE JOURNAL | |
jr1–jr5 | Journal prompts 1–5 free-text responses |
| POST-CALIBRATION | |
post_confidence | Confidence rating after activity (1–5) |
post_actual | Self-reported actual score (%) |
post_r1 | Reflection: how accurate was the prediction? |
post_r2 | Reflection: what would you do differently? |
calibration_index | post_actual − cal_predicted (negative = overconfident) |