Skip to content
BetaDE
Lesson 3 of 4Programming games

Programming games · Lesson 3 · 60 minutes

Project: snake game

What you'll learn

  • Build a game area made of squares with game.grid() and game.cell()
  • Store a snake as a list of positions and let it grow
  • Change direction with game.pressed() and detect the end of the game

A snake made of squares

In the snake game, a snake crawls across a game area made of squares. When it eats food, it gets longer. If it hits the wall or itself, the game is over. It’s a classic – and now you build it yourself.

The grid

game.grid(20, 15, 20) makes a game area with 20 columns and 15 rows. Every square is 20 pixels big. game.cell(column, row, color) fills one square. Counting starts at 0:

from codelix import game

game.grid(20, 15, 20)
game.cell(0, 0, "white")      # top left
game.cell(19, 14, "white")    # bottom right
game.cell(10, 7, "red")       # roughly in the middle
print("Columns:", game.columns(), "Rows:", game.rows())

The snake is a list

Every piece of the snake is a position [column, row]. The whole snake is a list of them. The first element is the head:

from codelix import game

game.grid(20, 15, 20)
snake = [[5, 7], [4, 7], [3, 7]]

for piece in snake:
    game.cell(piece[0], piece[1], "lime")

print("Head:", snake[0])
print("Length:", len(snake))

How a snake crawls

The trick: you don’t push every piece forward one by one. You put a new head at the front and take one piece away at the back. The middle stays as it is:

snake = [[5, 7], [4, 7], [3, 7]]
print("before:", snake)

column, row = snake[0]
snake.insert(0, [column + 1, row])   # new head to the right
snake.pop()                           # last piece gone
print("after: ", snake)

And when the snake eats? Then you simply leave out pop(). Now it’s one piece longer.

Changing direction

The snake shouldn’t jump one square every frame – that would be far too fast. With game.frame() % 6 == 0 it only moves in every sixth frame.

For the direction you use game.pressed(). It is only True in the frame right after you pressed the key. That way even a very short tap doesn’t get lost:

from codelix import game

game.grid(20, 15, 20)
head = [10, 7]
state = {"direction": "right"}


def update():
    for key in ["left", "right", "up", "down"]:
        if game.pressed(key):
            state["direction"] = key
    if game.frame() % 6 == 0:
        if state["direction"] == "right":
            head[0] = (head[0] + 1) % 20
        elif state["direction"] == "left":
            head[0] = (head[0] - 1) % 20
        elif state["direction"] == "up":
            head[1] = (head[1] - 1) % 15
        else:
            head[1] = (head[1] + 1) % 15
    game.cell(head[0], head[1], "lime")
    game.text(8, 6, state["direction"], "white", 16)


game.run(update)

Do you see the % 20? It makes the head come back in on the other side. In the real game, though, the wall is deadly.

Keep building: When your task works: stop the snake from turning around directly (from right straight to left). Make it faster with every point – for example with % 5 instead of % 6. And make sure the food never appears on the snake.

Your task

Your turn

The game is almost finished – the two most important functions are missing. 1. move(snake, direction, eats): Put a new head at the front, one square further in direction ("right", "left", "up" or "down"). If the snake isn't eating right now, remove the last piece at the back. 2. is_over(snake, columns, rows): Return True if the head is outside the game area or on another part of the snake. Otherwise False.

import random
from codelix import game

game.grid(20, 15, 20)
snake = [[5, 7], [4, 7], [3, 7]]
state = {"direction": "right", "food": [12, 7], "points": 0}


def move(snake, direction, eats):
    pass


def is_over(snake, columns, rows):
    return False


def update():
    for key in ["left", "right", "up", "down"]:
        if game.pressed(key):
            state["direction"] = key

    if game.frame() % 6 == 0:
        column, row = snake[0]
        target = {"right": [column + 1, row], "left": [column - 1, row],
                  "up": [column, row - 1], "down": [column, row + 1]}[state["direction"]]
        eats = target == state["food"]
        move(snake, state["direction"], eats)
        if eats:
            state["points"] = state["points"] + 1
            state["food"] = [random.randint(0, 19), random.randint(0, 14)]
        if is_over(snake, game.columns(), game.rows()):
            game.text(120, 130, "Game over!", "tomato", 30)
            game.stop()

    game.cell(state["food"][0], state["food"][1], "red")
    for piece in snake:
        game.cell(piece[0], piece[1], "lime")
    game.text(8, 6, f"Points: {state['points']}", "white", 16)


game.run(update)

Solve it without hints to earn a ⭐ on your progress page.

Show solution

Try it yourself first – mistakes teach you the most. Here is one possible solution:

import random
from codelix import game

game.grid(20, 15, 20)
snake = [[5, 7], [4, 7], [3, 7]]
state = {"direction": "right", "food": [12, 7], "points": 0}


def move(snake, direction, eats):
    column, row = snake[0]
    if direction == "right":
        column = column + 1
    elif direction == "left":
        column = column - 1
    elif direction == "up":
        row = row - 1
    elif direction == "down":
        row = row + 1
    snake.insert(0, [column, row])
    if not eats:
        snake.pop()


def is_over(snake, columns, rows):
    column, row = snake[0]
    if column < 0 or column >= columns or row < 0 or row >= rows:
        return True
    return snake[0] in snake[1:]


def update():
    for key in ["left", "right", "up", "down"]:
        if game.pressed(key):
            state["direction"] = key

    if game.frame() % 6 == 0:
        column, row = snake[0]
        target = {"right": [column + 1, row], "left": [column - 1, row],
                  "up": [column, row - 1], "down": [column, row + 1]}[state["direction"]]
        eats = target == state["food"]
        move(snake, state["direction"], eats)
        if eats:
            state["points"] = state["points"] + 1
            state["food"] = [random.randint(0, 19), random.randint(0, 14)]
        if is_over(snake, game.columns(), game.rows()):
            game.text(120, 130, "Game over!", "tomato", 30)
            game.stop()

    game.cell(state["food"][0], state["food"][1], "red")
    for piece in snake:
        game.cell(piece[0], piece[1], "lime")
    game.text(8, 6, f"Points: {state['points']}", "white", 16)


game.run(update)