SeeThinkExploreMarvel

UMPSA STEM LAB · Step 10 of 10 · Extension

Step 10 — ML Extension 🤖

Teach a computer to play Snake by itself using Q-Learning — a Reinforcement Learning algorithm. No AI library needed, just dictionaries and loops.

Q-LearningState spaceQ-table (dict)Reward signalε-greedy
Step 10 / 10

The Big Idea: Learning by Trial and Error

Instead of a human pressing keys, an AI agent chooses actions by looking up a table of learned values (the Q-table). Over thousands of games, it learns which actions lead to reward and which lead to death.

🤖 Agent
(Q-learner)
→ action →
🎮 Environment
(Snake game)
← state, reward ←

🔄 Pattern Recognition — reinforcement learning pattern

Reinforcement Learning is how humans and animals learn: try something, get feedback (reward or punishment), do more of what worked. This exact pattern is used in: AlphaGo (chess/Go), self-driving car training, recommendation algorithms, robot control, and ChatGPT's RLHF fine-tuning. Recognising this pattern means you understand the core of modern AI.

State Representation

The Q-learner needs to know the current situation (state). We can't use raw coordinates — the grid is too large. Instead we use 11 boolean features that capture what matters.

💡 Abstraction — feature engineering

Choosing what information to give the AI is called feature engineering. Raw pixel values = 20×15 = 300 numbers (too many). Our 11-bit state = 2^11 = 2048 possible states (manageable). This is abstraction applied to machine learning: extract only what's relevant, discard the rest.

def get_state(snake, food, direction):
    """
    Return an 11-element tuple of booleans representing the game state.
    This tuple becomes the KEY in our Q-table dictionary.
    """
    hx, hy = snake.head()
    # 3 danger sensors: is there immediate danger straight/left/right?
    dir_r = (direction == "RIGHT")
    dir_l = (direction == "LEFT")
    dir_u = (direction == "UP")
    dir_d = (direction == "DOWN")

    # Check danger in relative directions
    def is_wall_or_body(x, y):
        """True if (x, y) is out of bounds or snake body."""
        if x < 0 or x >= GRID_COLS or y < 0 or y >= GRID_ROWS:
            return True
        return (x, y) in snake.body[1:]

    state = (
        # Danger straight, right, left (relative to current direction)
        is_wall_or_body(hx + (dir_r - dir_l), hy + (dir_d - dir_u)),  # danger ahead
        is_wall_or_body(hx + (dir_d - dir_u), hy - (dir_r - dir_l)),  # danger right
        is_wall_or_body(hx - (dir_d - dir_u), hy + (dir_r - dir_l)),  # danger left

        # Current movement direction (one-hot encoded)
        dir_r, dir_l, dir_u, dir_d,

        # Food direction relative to head
        food.pos[0] < hx,   # food is to the left
        food.pos[0] > hx,   # food is to the right
        food.pos[1] < hy,   # food is above
        food.pos[1] > hy,   # food is below
    )
    return state   # a tuple of 11 True/False values

🧠 State Vector — Interactive Visualizer

The Q-learner sees the game as 11 True/False features. Click the arrow buttons to move the snake head. Watch which features flip as danger and food directions change — this tuple becomes the Q-table key.

State features (11 bits)
This tuple is the KEY in the Q-table dictionary.

The Q-Table

The Q-table maps (state, action) pairs to a numeric value — how good is it to take this action in this situation? Higher Q-value = better action.

Example Q-table (simplified)

State (11 booleans)Q[STRAIGHT]Q[LEFT]Q[RIGHT]
(F,F,F, T,F,F,F, T,F,F,F) 8.3 2.1 -5.2
(T,F,F, T,F,F,F, T,F,F,F) -12.0 6.7 1.2
(F,T,F, F,F,T,F, F,T,F,T) 3.4 -8.1 9.2

The agent picks the action with the highest Q-value for the current state. In Python, this table is just a defaultdict:

from collections import defaultdict
import random

# Q-table: maps (state, action) → float
# defaultdict(float) returns 0.0 for any new key
q_table = defaultdict(float)

ACTIONS = [0, 1, 2]   # 0=straight, 1=turn-left, 2=turn-right
EPSILON = 0.1         # exploration rate (10% random, 90% best known)

def choose_action(state):
    """ε-greedy: explore randomly 10% of the time, exploit best 90%."""
    if random.random() < EPSILON:
        return random.choice(ACTIONS)   # explore!
    q_vals = [q_table[(state, a)] for a in ACTIONS]
    return ACTIONS[q_vals.index(max(q_vals))]   # exploit!

The Q-Learning Update Rule

After every action, we update the Q-value using the Bellman equation. New estimate = immediate reward + discounted future reward.

ALPHA = 0.1    # learning rate: how fast we update Q values
GAMMA = 0.9    # discount factor: how much we value future rewards

def update_q(state, action, reward, next_state):
    """
    Q-Learning update rule (Bellman equation):
    Q(s,a) ← Q(s,a) + α × [r + γ × max(Q(s',a')) − Q(s,a)]
    """
    old_q = q_table[(state, action)]
    # Best possible future reward from next_state
    best_future = max(q_table[(next_state, a)] for a in ACTIONS)
    # Bellman target
    target = reward + GAMMA * best_future
    # Update Q-value (weighted average with learning rate)
    q_table[(state, action)] = old_q + ALPHA * (target - old_q)

# Reward signal:
# +10 = ate food   (reward good behaviour)
# -10 = died       (punish bad behaviour)
# -0.1 = each step (small penalty encourages efficiency)

📋 Algorithm Design — the Bellman equation

The Bellman equation says: the value of taking action A in state S equals the immediate reward PLUS the best possible reward from the resulting state S'. The discount factor γ (gamma) makes near rewards more valuable than distant ones — just like humans prefer $100 today over $100 next year. This one equation is the foundation of Q-Learning, Deep Q-Networks (DQN), and RLHF (how ChatGPT is trained).

The Training Loop

Instead of one human playing, we run thousands of games in a loop. Each game updates the Q-table. After enough games, the agent learns to play well.

import pygame
from collections import defaultdict

# (All class definitions from Step 8 go here)

q_table = defaultdict(float)
ALPHA = 0.1; GAMMA = 0.9; EPSILON = 0.1

# Map relative action (0,1,2) to absolute direction
TURNS = {
    "RIGHT": ["RIGHT","DOWN","UP"],
    "LEFT":  ["LEFT","UP","DOWN"],
    "UP":    ["UP","RIGHT","LEFT"],
    "DOWN":  ["DOWN","LEFT","RIGHT"],
}

def train(n_episodes=10000, visual=False):
    """Train the Q-learner over n_episodes games."""
    pygame.init()
    screen = pygame.display.set_mode(
        (GRID_COLS*CELL_SIZE, GRID_ROWS*CELL_SIZE)) if visual else None
    clock = pygame.time.Clock()
    scores = []

    for episode in range(n_episodes):
        game = Game()
        state = get_state(game.snake, game.food, game.snake.direction)
        steps = 0

        while game.running and steps < 1000:   # cap steps to avoid loops
            action = choose_action(state)
            new_dir = TURNS[game.snake.direction][action]
            game.snake.direction = new_dir

            old_score = game.score
            game.update()
            steps += 1

            if game.score > old_score:
                reward = 10   # ate food
            elif not game.running:
                reward = -10  # died
            else:
                reward = -0.1  # still alive, small penalty

            next_state = get_state(game.snake, game.food,
                                   game.snake.direction)
            update_q(state, action, reward, next_state)
            state = next_state

            if visual:
                game.draw(); clock.tick(60)

        scores.append(game.score)
        if episode % 1000 == 0:
            avg = sum(scores[-100:])/min(100, len(scores))
            print(f"Episode {episode:5d} | Avg score: {avg:.2f}"
                  f" | Q-table size: {len(q_table)}")

    pygame.quit()
    return scores

if __name__ == "__main__":
    train(n_episodes=5000, visual=True)

What Happens During Training?

Early in training the snake dies quickly (Q-table is all zeros, actions are random). After a few thousand games, patterns emerge.

Episode rangeAvg scoreWhat's happening
0 – 100≈ 0.5Mostly random — dies in a few steps
100 – 1000≈ 2–5Learns to avoid immediate walls
1000 – 5000≈ 5–15Learns to navigate toward food
5000 – 20000≈ 15–30Starts to avoid self-collisions
20000+≈ 30+Sophisticated strategy emerges
Limitations — our simple 11-bit state can't represent the full body shape. A snake that spirals inward will die even though the 11 sensors show no immediate danger. This is why state design is crucial in RL: a better state representation (e.g. distance to nearest body segment in each direction) would learn a better policy. DeepMind's DQN uses the full pixel image as state — 84×84 grayscale frames.

The Big Picture — Where This Leads

What you've learned

Q-Learning, Bellman equation, state representation, epsilon-greedy exploration, reward engineering, and training loops — the same concepts used in professional AI research.

Next steps in AI

Replace the Q-table with a neural network → Deep Q-Network (DQN). Use a CNN on raw pixels → matches DeepMind's Atari paper. Add experience replay and target networks → modern stable RL training.

🔄 Pattern Recognition — CT all the way down

Look back at every step. Decomposition (Step 5: functions), Pattern Recognition (modulo, RL loops), Abstraction (OOP, feature engineering), Algorithm Design (Bellman equation, collision detection) — these four CT skills are exactly the skills used by AI researchers, game developers, and software engineers. You didn't just learn to code a Snake game. You learned to think computationally.

Try It — Exercises

1

Print Q-table stats 📋 Algorithm

After training, print the 5 (state, action) pairs with the highest Q-values: sorted(q_table.items(), key=lambda x: x[1], reverse=True)[:5]. What do the top states have in common?

2

Plot the learning curve 🔄 Pattern

Save episode scores to a list. Use matplotlib.pyplot.plot(scores) after training to visualise how the agent improves over time. Install matplotlib: pip install matplotlib.

3

Save and load the Q-table 💡 Abstraction

Use the json module from Step 7 to save q_table as a JSON file after training, and load it before playing. You'll need to convert tuple keys to strings: str(key).

🎉 You've completed the Snake Game Tutorial!

You went from print("Hello, World!") to building and training an AI agent — in 9 steps. That's remarkable.

Skills you've gained

Variables, loops, conditionals, lists, functions, file I/O, OOP, and machine learning — the full stack of programming knowledge, applied to a real game.

What to build next

Tetris, Pac-Man, a platformer — you have all the tools. Or explore pygame's network module for multiplayer, or PyTorch for a real neural network agent.

📋
Research Study — Post-Survey
You’ve finished all 10 steps — complete the post-survey to close the study loop.
Take Post-Survey →

← Back to the Tutorial Index

⚠️ Common Mistakes — Spot These Before You Start

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

✗ Mistake 1: Setting the learning rate too high
LEARNING_RATE = 1.0   # replaces Q-value completely each update
🐞 A learning rate of 1.0 discards all past experience — the agent forgets what it learned the moment new feedback arrives. Q-values oscillate wildly and the agent never improves.
LEARNING_RATE = 0.1   # blend 10% new info with 90% existing knowledge
✗ Mistake 2: Not decaying epsilon
EPSILON = 0.3   # fixed: agent always explores 30% of the time
🐞 The agent keeps making random moves even after thousands of training games. It learned a strategy but keeps ignoring it 30% of the time — performance never stabilises.
epsilon = max(0.01, epsilon * 0.995)  # decay: explore less as agent learns
⚡ Mistake 3: Confusing state with action
# Trying to store the state IN the action variable
⚡ The state describes the world (where is the snake? where is food? are there walls ahead?). The action is what the agent does next (go UP, DOWN, LEFT, RIGHT). The Q-table maps state → action → expected reward.
state = get_state(snake, food)   # observe the world
action = choose_action(state)    # decide what to do
q_table[state][action] += ...    # update the value of that choice

✅ Quick Check — Are You Ready?

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

1 What does the Q-table store?
2 What is epsilon used for in epsilon-greedy learning?
3 Why should epsilon decrease over training time?

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

Start Activities → Tier 1 → 2 → 3 → 4
Previous← OOP Refactor