SeeThinkExploreMarvel

UMPSA STEM LAB · Step 9 of 10

Step 9 — OOP Refactor

Reorganise the entire game into Python classes — Snake, Food, and Game. Learn encapsulation, __init__, self, and methods.

class__init__selfmethodsencapsulation
Step 9 / 10

What is OOP?

Object-Oriented Programming groups related data (attributes) and behaviour (methods) into a single unit called a class. A class is a blueprint; an object (instance) is a specific copy of that blueprint.

💡 Abstraction — bundling data and behaviour

Currently, the snake's data (the list) is separate from its behaviour (insert/pop, draw). OOP bundles them: my_snake.move() and my_snake.draw() are part of the snake object. This matches how we think about real things — a snake is something that moves and draws itself, not just a list of positions.

Procedural (Steps 1–7)

snake = [(10,7),(9,7),(8,7)]  # data
def update_game(snake,...):   # behaviour
    snake.insert(...)
    snake.pop()
# data and behaviour are SEPARATED

OOP (Step 9)

class Snake:
    def __init__(self):
        self.body = [(10,7),(9,7),(8,7)]
    def move(self, direction):
        self.body.insert(...)
# data and behaviour TOGETHER

📷 Blueprint vs Object — Interactive

A class is just a blueprint — it describes what a snake could look like. An object (instance) is a real snake created from that blueprint, with actual data filled in. Click to create objects:

Class (Blueprint)
Snake
body = ?
direction = ?
score = ?
def move(self) → ...
def grow(self) → ...
def draw(self, screen) → ...
→
Instances (Objects)
(no objects yet)
Press Create Snake() to instantiate a new Snake object.

The Snake Class

🐍 Snake
body: list[(int,int)]
direction: str
alive: bool
__init__(start_pos)
move(grid_cols, grid_rows) → bool
grow()
set_direction(key)
draw(screen)
head() → tuple
import pygame, random

CELL_SIZE = 30
GREEN = (0, 200, 80); DKGREEN = (0, 160, 50)
MOVES = {"RIGHT":(1,0),"LEFT":(-1,0),"UP":(0,-1),"DOWN":(0,1)}
OPPOSITES = {"RIGHT":"LEFT","LEFT":"RIGHT","UP":"DOWN","DOWN":"UP"}

class Snake:
    def __init__(self, start=(10, 7)):
        """Initialise the snake with a 3-cell body at start position."""
        self.body = [start, (start[0]-1, start[1]), (start[0]-2, start[1])]
        self.direction = "RIGHT"
        self.alive = True
        self._grow_next = False   # flag: grow on next move

    def head(self):
        """Return the head position (first element)."""
        return self.body[0]

    def set_direction(self, key):
        """Change direction from keyboard key, ignoring reversal."""
        key_map = {
            pygame.K_UP: "UP", pygame.K_DOWN: "DOWN",
            pygame.K_LEFT: "LEFT", pygame.K_RIGHT: "RIGHT"
        }
        new_dir = key_map.get(key)
        if new_dir and new_dir != OPPOSITES[self.direction]:
            self.direction = new_dir

    def move(self, grid_cols, grid_rows):
        """Move one step. Returns True if still alive."""
        dx, dy = MOVES[self.direction]
        hx, hy = self.head()
        new_head = ((hx + dx) % grid_cols, (hy + dy) % grid_rows)
        if new_head in self.body[1:]:   # self-collision
            self.alive = False; return False
        self.body.insert(0, new_head)
        if self._grow_next:
            self._grow_next = False   # consumed — don't pop tail
        else:
            self.body.pop()
        return True

    def grow(self):
        """Schedule the snake to grow on the next move."""
        self._grow_next = True

    def draw(self, screen):
        """Draw every segment to the screen surface."""
        for i, (cx, cy) in enumerate(self.body):
            colour = DKGREEN if i == 0 else GREEN
            pygame.draw.rect(screen, colour,
                pygame.Rect(cx*CELL_SIZE, cy*CELL_SIZE,
                            CELL_SIZE-2, CELL_SIZE-2), border_radius=4)

The Food Class

🍎 Food
pos: tuple(int,int)
value: int
__init__(grid_cols, grid_rows, avoid)
respawn(grid_cols, grid_rows, avoid)
draw(screen)
RED = (220, 50, 50)

class Food:
    def __init__(self, grid_cols, grid_rows, avoid=None):
        """Spawn food at a random cell, not inside avoid (snake body)."""
        self.value = 1
        self.respawn(grid_cols, grid_rows, avoid or [])

    def respawn(self, grid_cols, grid_rows, avoid):
        """Move food to a random empty cell."""
        while True:
            self.pos = (random.randint(0, grid_cols-1),
                        random.randint(0, grid_rows-1))
            if self.pos not in avoid:   # fix the Step 4 bug!
                break

    def draw(self, screen):
        fx, fy = self.pos
        pygame.draw.circle(screen, RED,
            (fx*CELL_SIZE + CELL_SIZE//2,
             fy*CELL_SIZE + CELL_SIZE//2), CELL_SIZE//3)

The Game Class

The Game class owns everything — it creates the Snake and Food, runs the loop, and updates them each frame.

WHITE = (255, 255, 255); BG = (30, 30, 30)
GRID_COLS = 20; GRID_ROWS = 15; SNAKE_SPEED = 8

class Game:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode(
            (GRID_COLS*CELL_SIZE, GRID_ROWS*CELL_SIZE))
        pygame.display.set_caption("Snake OOP")
        self.font = pygame.font.SysFont(None, 28)
        self.clock = pygame.time.Clock()
        self.reset()

    def reset(self):
        """Restart game state."""
        self.snake = Snake()
        self.food  = Food(GRID_COLS, GRID_ROWS, self.snake.body)
        self.score = 0
        self.running = True

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT: self.running = False
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE: self.running = False
                self.snake.set_direction(event.key)

    def update(self):
        if not self.snake.move(GRID_COLS, GRID_ROWS):
            self.running = False; return
        if self.snake.head() == self.food.pos:
            self.score += self.food.value
            self.snake.grow()
            self.food.respawn(GRID_COLS, GRID_ROWS, self.snake.body)

    def draw(self):
        self.screen.fill(BG)
        self.snake.draw(self.screen)
        self.food.draw(self.screen)
        self.screen.blit(
            self.font.render(f"Score: {self.score}", True, WHITE),
            (8, 6))
        pygame.display.flip()

    def run(self):
        while self.running:
            self.handle_events()
            self.update()
            self.draw()
            self.clock.tick(SNAKE_SPEED)
        pygame.quit()

# Entry point
if __name__ == "__main__":
    Game().run()

📋 Algorithm Design — the game loop as a method

Game.run() contains the exact same game loop as always: handle events → update → draw. But now it's a method of Game, which means the game owns its own loop. You could create two games: game1 = Game(); game2 = Game() — each with its own state. This is what game engines like Unity do: every Scene is its own "Game" object.

Understanding self

self is how an instance refers to its own attributes. It's the first parameter of every method but you never pass it yourself — Python passes it automatically.

Write thisPython sees this
snake.move(GRID_COLS, GRID_ROWS)Snake.move(snake, GRID_COLS, GRID_ROWS)
self.body (inside a method)This instance's body attribute
self.grow() (inside a method)Call this instance's grow method

Try It — Exercises

1

Add a speed attribute 💡 Abstraction

Add self.speed = SNAKE_SPEED to Game.__init__. Every time the snake eats food, increase self.speed by 0.5. Use self.clock.tick(self.speed). The game gets harder as you score!

2

Add a Scoreboard class 📋 Algorithm

Create a Scoreboard class with load(), save(score), and is_record(score) methods. Use it inside Game.run() after the loop ends.

3

Inheritance: BonusFood 🔄 Pattern

Create class BonusFood(Food): that inherits from Food. Override __init__ to set self.value = 3 and add self.timer = 180. Add a tick() method that counts down and returns False when expired.

⚠️ Common Mistakes — Spot These Before You Start

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

✗ Mistake 1: Forgetting self as the first parameter
class Snake:
    def move(direction):    # missing self!
        ...
🐞 Calling my_snake.move(direction) passes my_snake as the first argument, but the function only has one parameter (direction). Python raises TypeError: move() takes 1 positional argument but 2 were given.
class Snake:
    def move(self, direction):   # self = the object being called
        ...
✗ Mistake 2: Calling a method without parentheses
my_snake.move    # just references the method — doesn't call it!
🐞 Without (), Python evaluates the expression as the method object itself — no code runs. The snake never moves. No error is raised either, so this bug is hard to spot.
my_snake.move(direction)   # parentheses = call it
⚡ Mistake 3: Confusing the class with an instance
Snake.move(direction)   # calling on the class, not an object!
⚡ Snake is a blueprint; my_snake = Snake() creates a real object. Methods must be called on the object — only the object has actual data (self.body, etc.).
my_snake = Snake()          # create an instance
my_snake.move(direction)    # call method on the instance

✅ Quick Check — Are You Ready?

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

1 What does self refer to inside a class method?
2 What runs automatically when you write my_snake = Snake()?
3 What is the difference between Snake (capital S) and my_snake?

You've covered all the concepts for Step 9. Time to apply them.

Start Activities → Tier 1 → 2 → 3 → 4
Previous← Save High Score