This page contains exercise answers and teaching guidance for
Step 9 — OOP Refactor.
Not linked from the student pages.
Students: please go back and try the activities first.
class ClassName:__init__ to initialise object stateself to refer to the current instanceclass Snake:
"""Represents the snake: its body, direction, and movement logic."""
def __init__(self):
self.body = [(10, 7), (9, 7), (8, 7)] # list of (col, row) tuples
self.dx = 1
self.dy = 0
self.alive = True
def turn(self, dx, dy):
"""Change direction — guards against 180° reversal."""
if (dx, dy) != (-self.dx, -self.dy):
self.dx, self.dy = dx, dy
def move(self):
"""Advance the snake one step; returns True if food was eaten."""
hx, hy = self.body[0]
new_head = ((hx + self.dx) % GRID_COLS, (hy + self.dy) % GRID_ROWS)
self.body.insert(0, new_head)
return False # caller must pop tail if False
def grow(self):
"""Grow — tail already inserted by move(); just don't pop."""
pass
def check_self_collision(self):
return self.body[0] in self.body[1:]
def draw(self, surface):
for i, (col, row) in enumerate(self.body):
colour = DKGREEN if i == 0 else GREEN
pygame.draw.rect(surface, colour,
(col * CELL_SIZE, row * CELL_SIZE, CELL_SIZE-1, CELL_SIZE-1))
class Food:
"""Manages food position and drawing."""
def __init__(self):
self.respawn()
def respawn(self, avoid=()):
while True:
x = random.randint(0, GRID_COLS-1)
y = random.randint(0, GRID_ROWS-1)
if (x, y) not in avoid:
self.x, self.y = x, y
break
def draw(self, surface):
pygame.draw.rect(surface, RED,
(self.x * CELL_SIZE, self.y * CELL_SIZE, CELL_SIZE-1, CELL_SIZE-1))
class Game:
"""Top-level controller — owns the snake, food, score, and game loop."""
def __init__(self):
self.snake = Snake()
self.food = Food()
self.score = 0
self.running = True
def update(self):
self.snake.move()
if self.snake.body[0] == (self.food.x, self.food.y):
self.score += 1
self.food.respawn(avoid=self.snake.body)
else:
self.snake.body.pop()
if self.snake.check_self_collision():
self.running = False
def draw(self, surface):
surface.fill(BG)
self.snake.draw(surface)
self.food.draw(surface)
pygame.display.flip()
class: defines a blueprint for creating objects. Each object is an instance of the class.__init__: runs automatically when an object is created (snake = Snake()). Initialises the instance's attributes.self: a reference to the specific instance being operated on. self.body = this snake's body, not any other snake's.self as the first parameter.snake.move(), food.respawn() — access attributes and methods through the object.Food.respawn() knows where to put food; the Game doesn't need to know how.| Question | Accepted answer |
|---|---|
| What is the difference between a class and an object? | A class is a blueprint (the definition); an object is an instance (a specific thing created from the blueprint). |
What does __init__ do? |
It's the initialiser — runs automatically when a new object is created, setting up the object's initial state. |
Why does every method take self as the first parameter? |
self refers to the specific instance being operated on, so the method knows which object's data to use. |
What does dot notation (snake.move()) do? |
Calls the move method on the snake object — self inside move refers to that snake instance. |
Bug: def turn(dx, dy): — missing self parameter.
Error: TypeError: turn() takes 2 positional arguments but 3 were given (Python automatically passes self as the first arg)
Fix: def turn(self, dx, dy):
speed attribute to Snake# In Snake.__init__:
self.speed = 8 # initial FPS
# In Game.update, after eating food:
self.snake.speed = min(20, self.snake.speed + 0.5)
# In main loop:
clock.tick(int(game.snake.speed))
reset() method to Gamedef reset(self):
self.snake = Snake()
self.food = Food()
self.score = 0
self.running = True
# After game over, press R to restart:
if event.type == pygame.KEYDOWN and event.key == pygame.K_r:
game.reset()
| Prompt | Key ideas a strong answer contains |
|---|---|
| 1. What is encapsulation? | Bundling data (attributes) and the methods that operate on it inside a class. Other parts of the program interact through a defined interface, not raw data. |
| 2. Why is OOP useful? | Organises complex programs; each class has one job; easy to add features without breaking other parts; real-world concepts map to objects. |
| 3. What is inheritance? (preview) | A class can inherit attributes and methods from a parent class. E.g., BonusFood(Food) would be a Food with extra behaviour. |
| 4. When is OOP overkill? | For small scripts with one function — a class adds overhead. OOP pays off when a program has multiple interacting entities with their own state. |
5. Explain self to a classmate |
"When you call snake.move(), Python automatically passes snake as self. It's like saying 'move YOURSELF' — each snake knows which body is its own." |
| What they did | What they see | What to say |
|---|---|---|
Missing self in __init__ parameters |
TypeError: __init__() takes 0 positional arguments but 1 was given |
"Python automatically passes the new object as the first argument. __init__ must accept self." |
body = [...] (without self.) |
NameError: name 'body' is not defined when method accesses it |
"Without self., body is a local variable that disappears when __init__ finishes. Use self.body." |
Calling Snake.move() instead of snake.move() |
TypeError: move() missing 1 required positional argument: 'self' |
"Call methods on an instance (lowercase): snake.move(). Snake.move() is a class-level call that needs the instance explicitly." |
Putting game logic in __init__ |
Game runs once on creation, then can't restart | "__init__ sets up state. Put running logic in a separate update() or run() method." |
Accessing snake.body from Game directly |
Works but breaks encapsulation | "It works, but it's better to call snake.move() and let Snake manage its own body. This is the principle of encapsulation." |
"A class is like an architectural blueprint. The blueprint for a house describes rooms, windows, doors. Each actual house built from the blueprint is an object — an instance. Different houses, same design."
Or: "A class is like a cookie cutter; objects are the cookies. Same shape, different dough."
Have students read the finished class definitions before typing them. Ask: "What does this class know? What can it do? What does it NOT know or care about?" This is the encapsulation exercise.
The step-8 code is ~150 lines of procedural code. Ask students: "If you wanted to add a second snake for multiplayer, what would you need to change?" In procedural code: a lot. With Snake as a class: create snake2 = Snake(). This motivates OOP concretely.
Classes are the ultimate abstraction. The Game class doesn't know how Snake.draw() works — it just calls it. Decomposition: each class has a single responsibility.
This is the longest step. Consider splitting it across two sessions if time is tight. First session: understand and trace the class structure. Second session: type and run the refactored code.
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: Replacing literal 30 with CELL_SIZE = 30 throughout is better because: | ✓ You only need to change one line to update all usages |
| Q2: What does a # TODO: add sound comment indicate? | ✓ A note marking work planned for the future |
| Q3: Moving snake-drawing code into draw_snake() is an example of: | ✓ Refactoring — isolating responsibility into a dedicated function |
| Q4: A helper function grid_to_pixel(col, row) is useful because: | ✓ It converts coordinates in one place and can be called anywhere |
| Q5: Which of these is NOT a benefit of using named constants? | ✓ Slightly faster execution |
Recorded in Google Sheet (Act_9 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_9 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 (9) |
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) |