This page contains exercise answers and teaching guidance for
Step 4 — Snake Body.
Not linked from the student pages.
Students: please go back and try the activities first.
STEP 4 OF 10 · FACILITATOR NOTES
What students will be able to do after Step 4:
list.insert(0, ...) and list.pop() to move without growinglist.append() when the snake eats food (grow)random.randint()import random
# Snake = list of (col, row) tuples, head first
snake = [(10, 7), (9, 7), (8, 7)] # starting body, 3 cells
# Food position
food_x = random.randint(0, GRID_COLS - 1)
food_y = random.randint(0, GRID_ROWS - 1)
# --- Move the snake (update step) ---
new_head = (snake[0][0] + dx, snake[0][1] + dy) # compute new head
snake.insert(0, new_head) # add new head at front
if new_head == (food_x, food_y): # ate food → grow
food_x = random.randint(0, GRID_COLS - 1)
food_y = random.randint(0, GRID_ROWS - 1)
else:
snake.pop() # no food → remove tail (length stays same)
# --- Collision detection ---
hx, hy = snake[0]
# Wall collision
if hx < 0 or hx >= GRID_COLS or hy < 0 or hy >= GRID_ROWS:
running = False
# Self collision — is head in the rest of the body?
if snake[0] in snake[1:]:
running = False
insert(0, head) pushes a new head to the front; pop() removes the tail. The list always represents the full body, newest segment first. This is the "train" model: new engine added at front, last carriage uncoupled at back.
When food is eaten, we insert the new head but do NOT pop the tail. The list grows by 1 element. The tail stays attached — like the train picking up a new carriage.
snake[1:] — slice without the headA slice from index 1 to end — the body without the head. Checking snake[0] in snake[1:] asks "is the head position anywhere in the body?" Using snake[0] in snake (without the slice) would always be True and the game would end immediately.
random.randint(a, b)Returns a random integer between a and b inclusive. So randint(0, GRID_COLS-1) gives a valid column index from 0 to 19 for a 20-column grid.
| Question | Accepted answer |
|---|---|
| 1. Why is the snake stored as a list? | A list can grow (append) and shrink (pop) as the snake eats food and moves. A single variable can't represent a multi-segment body. |
2. What does snake.insert(0, new_head) do? |
Adds the new head position to the front (index 0) of the list, shifting all other segments back by one index. |
3. Why don't we pop() when the snake eats food? |
We want the snake to grow — keeping the tail means the body is one cell longer after eating. |
4. What does snake[0] in snake[1:] check? |
Whether the head's position appears anywhere in the rest of the body (self-collision check). |
snake.pop(0) instead of snake.pop() — pops from the front (removes the head!) instead of the tail.
Effect: Snake shrinks from the front — it appears to "reverse eat" itself and disappears.
Fix: snake.pop() with no argument removes the last element (the tail).
score = len(snake) - 3 # initial length is 3
pygame.display.set_caption(f"Snake — Score: {score}")
def spawn_food(snake):
while True:
fx = random.randint(0, GRID_COLS - 1)
fy = random.randint(0, GRID_ROWS - 1)
if (fx, fy) not in snake:
return fx, fy
food_x, food_y = spawn_food(snake)
# After eating:
food_x, food_y = spawn_food(snake)
| Prompt | Key ideas a strong answer contains |
|---|---|
| 1. Most confusing part? | Usually the insert/pop pattern. Key explanation: the list represents a "train" — a new carriage at the front, one removed at the back = same length. |
| 2. What is a list in Python? | An ordered, mutable collection of items. Items can be added, removed, and accessed by index. |
| 3. Why use a tuple for each body segment? | (x, y) is a fixed pair — it shouldn't change. A tuple signals immutability. Also makes the in check work correctly by value comparison. |
| 4. What is a slice? | snake[1:] is a slice — a copy of the list from index 1 to the end. It doesn't modify the original list. |
| 5. Explain to a classmate? | Should cover: snake is a list; every frame, add new head to front, remove tail from back (unless ate food); check if head is off-screen or touching body. |
| What they did | What they see | What to say |
|---|---|---|
snake.pop(0) instead of snake.pop() |
Snake shrinks from front, disappears quickly | "pop(0) removes the first element — that's the head. pop() with no argument removes the last — the tail." |
snake[0] in snake (no slice) |
Game ends the moment it starts — head is always in the list | "The head is part of the snake too! Use snake[1:] to check only the body, not the head itself." |
random.randint(0, GRID_COLS) (off by one) |
Food can appear one column off-screen | "randint(a, b) includes b. Grid columns are 0 to GRID_COLS-1, so use randint(0, GRID_COLS - 1)." |
new_head = (snake[0] + dx, ...) |
TypeError: can only concatenate tuple (not "int") to tuple |
"snake[0] is a tuple (x, y). Access x with snake[0][0] and y with snake[0][1]." |
| Insert AFTER pop | Snake jumps one cell — tail removed before new head added | "The order matters: insert new head first, then conditionally pop the tail." |
Step 4 introduces THREE new concepts simultaneously (lists, random, collision detection) and combines them with Step 3's movement. Students are holding more state in their heads than before. This is the cognitive load peak of the tutorial.
"If this step feels harder than the others — it is. You are working with a more complex data structure. Confusion here is evidence of learning, not failure."
A snake body is like a train. Each frame: a new engine is added to the front, and the last carriage is uncoupled from the back. When the train picks up a passenger (food), the last carriage stays on — the train grows by one.
Consider asking students to trace through the list state on paper for 3–4 frames before writing code. Draw the list as boxes with (x, y) values. This externalises the mental model and surfaces the insert/pop pattern concretely.
Ask: "Why not store the snake as 50 separate variables like snake1_x, snake1_y, snake2_x...?" This surfaces abstraction and the power of lists — a single structure that scales to any length.
Every student should have a working game where the snake grows on food and the game ends on wall/self collision. Do a quick visual check before allowing anyone to advance. This is a natural assessment point.
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: food_x = random.randint(0, GRID_W-1) * CELL_SIZE — Why multiply by CELL_SIZE? | ✓ To snap food positions to the grid |
| Q2: What event triggers spawning a new food position? | ✓ The snake head position equalling the food position |
| Q3: Self-collision means: | ✓ The snake's head touches one of its own body segments |
| Q4: pygame.draw.rect(win, GREEN, [x, y, w, h]) draws: | ✓ A filled rectangle |
| Q5: Which stores a food position correctly as grid coordinates? | ✓ food = (rand_col, rand_row) |
Recorded in Google Sheet (Act_4 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_4 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 (4) |
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) |