This page contains exercise answers and teaching guidance for
Step 7 — Wrap-Around ⭐.
Not linked from the student pages.
Students: please go back and try the activities first.
% to implement wrap-around (toroidal) movement# ── Direction dictionary (replaces the if/elif chain) ──
DIRECTIONS = {
pygame.K_UP: (0, -1),
pygame.K_DOWN: (0, 1),
pygame.K_LEFT: (-1, 0),
pygame.K_RIGHT: (1, 0),
}
# In the event loop:
if event.type == pygame.KEYDOWN and event.key in DIRECTIONS:
ndx, ndy = DIRECTIONS[event.key]
# Reverse guard: don't allow 180° turn
if (ndx, ndy) != (-dx, -dy):
dx, dy = ndx, ndy
# ── Wrap-around movement ──
# OLD (collision):
# head_x += dx
# head_y += dy
# NEW (wrap):
head_x = (head_x + dx) % GRID_COLS
head_y = (head_y + dy) % GRID_ROWS
%a % b gives the remainder when a is divided by b. Key property: the result is always 0 to b−1. When head_x = 20 and GRID_COLS = 20, 20 % 20 = 0 — the snake appears at column 0.
When head_x = -1 and GRID_COLS = 20, -1 % 20 = 19 in Python. Python modulo always returns non-negative — the snake appears at the right edge. This is intentional and makes wrap-around work naturally from any direction.
Maps each key constant to its (dx, dy) vector. Replaces four elif branches with a single DIRECTIONS[event.key] lookup. Adding a new direction means adding one line to the dictionary.
(ndx, ndy) != (-dx, -dy) checks if the new direction is exactly opposite to the current — cleaner than the four separate dx != 1 checks used in earlier steps.
| Question | Model Answer |
|---|---|
What does % calculate? |
The remainder of division. 17 % 5 = 2 because 17 = 3×5 + 2. |
What is 20 % 20? |
0. When the snake reaches column 20 (off screen), 20 % 20 = 0 wraps it to column 0. |
What is -1 % 20 in Python? |
19. Python's modulo always returns a non-negative number — useful for wrap-around from the left/top edge. |
| What does the direction dictionary replace? | The long if / elif chain that checked each arrow key separately. |
head_x = head_x + dx % GRID_COLS — operator precedence: % applies only to dx, not to (head_x + dx).
Fix: head_x = (head_x + dx) % GRID_COLS — parentheses ensure the addition happens first.
DIRECTIONS[pygame.K_q] = (-1, -1) # up-left diagonal
DIRECTIONS[pygame.K_e] = (1, -1) # up-right diagonal
The reverse guard (ndx, ndy) != (-dx, -dy) handles diagonal directions automatically.
wrap = True
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_g:
wrap = not wrap
# In update:
if wrap:
head_x = (head_x + dx) % GRID_COLS
head_y = (head_y + dy) % GRID_ROWS
else:
head_x += dx
head_y += dy
if head_x < 0 or head_x >= GRID_COLS or head_y < 0 or head_y >= GRID_ROWS:
running = False
| Prompt | Key ideas a strong answer contains |
|---|---|
| 1. What does "edge case" mean? | A situation at the boundary of valid input — in our game, the snake reaching the screen edge is an edge case. |
| 2. When would modulo be useful outside games? | Clock arithmetic (hours mod 12/24), cycling through menu items, distributing items into groups. |
| 3. Why is Python's modulo of a negative number positive? | Python defines a % b to always return a value with the same sign as b (the divisor). This makes wrap-around work naturally. |
| 4. What is a dictionary in Python? | A collection of key-value pairs. Values are retrieved by key rather than by index. |
| 5. Explain wrap-around to a classmate | "When the snake goes off the right edge (column 20), modulo brings it back to column 0. It's like Pac-Man — the walls are connected." |
| What they did | What they see | What to say |
|---|---|---|
head_x = head_x + dx % GRID_COLS |
Wrapping is wrong — % only applied to dx |
"Python evaluates % before +. Add parentheses: (head_x + dx) % GRID_COLS." |
| Still have wall collision check after adding wrap | Game over immediately on first wrap | "Remove the wall collision check — it conflicts with wrap-around. The snake can no longer go out of bounds." |
DIRECTIONS[event.key] without checking event.key in DIRECTIONS |
KeyError when any other key is pressed |
"Always check if the key exists in the dictionary first: if event.key in DIRECTIONS:" |
Reverse guard (-dx, -dy) with the old dx != 1 style |
Inconsistent — some directions still reversible | "The dictionary refactor also replaces the reverse guard. Use (ndx, ndy) != (-dx, -dy) consistently." |
% applied to y but not x (or vice versa) |
Snake wraps in one dimension only | "Both axes need wrapping: head_x = (head_x + dx) % GRID_COLS AND head_y = (head_y + dy) % GRID_ROWS." |
"What time is it 3 hours after 11pm? Not 14:00 — it's 2am. That's modulo: 11 + 3 = 14, 14 % 12 = 2." Clocks, Pac-Man mazes, and wrap-around grids all use the same idea.
Most students expect -1 % 20 = -1 (from their mental model of remainders). Show Python's actual behaviour: -1 % 20 = 19. Explain this is intentional — Python's modulo is defined to make wrap-around work, not just as a remainder.
Some students will resist changing code that "already works." Frame it as: "Which is easier to add a new key to — 4 elif branches or 1 dictionary?" This surfaces the maintainability argument for data structures.
Ask: "What are all the ways a snake's head can leave the grid? Left, right, top, bottom — four edge cases. Does our solution handle all four? Test each one."
Pacing: Step 7 is lighter than Steps 4 and 6. Fast finishers can explore: what happens if GRID_COLS and GRID_ROWS are prime numbers? Does modulo still work? (Yes — this is a good mathematical curiosity.)
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: snake = [(5,5),(4,5),(3,5)] — What is snake[1]? | ✓ (4,5) |
| Q2: How does the snake appear to move each frame? | ✓ A new head is inserted at front; last segment removed from back |
| Q3: snake.insert(0, new_head) — what does this do? | ✓ Adds new_head at position 0 (the front of the list) |
| Q4: What does len(snake) tell you? | ✓ The total number of body segments |
| Q5: snake.pop() removes: | ✓ The last element |
Recorded in Google Sheet (Act_7 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_7 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 (7) |
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) |