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()andgame.cell() - Store a snake as a list of positions and
letit 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
% 5instead 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)- Something to think about
Get the head with column, row
=snake[0]. Add at the front withsnake.insert(0, [column + 1, row])and remove at the back withsnake.pop(). - Which command helps?
Up means row − 1 (y points down!), down means row
+1.Only remove the last piece if eats is
False–ifnot eats:snake.pop()Valid columns go from 0 to columns − 1. So check: column
<0 or column>=columns – the same for the row.snake[1:]is the snake without its head.snake[0]insnake[1:]checks whether the head appears there. - Gap code – fill in the ___
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 ___: column = ___ elif ___: column = ___ elif ___: row = ___ elif ___: row = ___ snake.insert(___) if ___: snake.pop() def is_over(snake, columns, rows): column, row = snake[0] if ___: return ___ return ___ 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🔒 Unlocks after all hints or 2 checks
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)