Editor

Snake in Python

Pygame Online · 142 lines · Arrow keys

Steer the snake to the food without hitting a wall or your own tail.

How it works

The snake is a list of cells

The board is a grid of 24 by 16 cells. The snake is a list of (column, row) pairs with its head first, so drawing it is a loop over the list and each cell becomes a rectangle with cell_rect.

def cell_rect(x, y):
    return pygame.Rect(x * CELL, TOP + y * CELL, CELL, CELL)

Food goes on an empty cell

place_food lists every cell the snake is not on and picks one at random. When none are left, the snake fills the board and you have won.

def place_food():
    """A random empty cell, or None when the snake fills the whole board."""
    empty = [(x, y) for x in range(COLS) for y in range(ROWS) if (x, y) not in snake]
    if not empty:
        return None
    return random.choice(empty)

Moving is adding a head and dropping the tail

Each move puts a new head in front. If the snake is eating, the tail stays and the snake grows; otherwise the last cell is removed. Hitting a wall or the body ends the game.

head = (snake[0][0] + direction[0], snake[0][1] + direction[1])
eating = head == food
# The tail moves out of the way unless the snake is growing.
body = snake if eating else snake[:-1]
if not (0 <= head[0] < COLS and 0 <= head[1] < ROWS) or head in body:
    state = "over"
else:
    snake.insert(0, head)
    if eating:
        score += 1
        food = place_food()
        if food is None:
            state = "won"
    else:
        snake.pop()
if state != "playing":
    best = max(best, score)
    wait = 30

Turns wait in a short queue

Key presses go into turns, and the snake takes one turn per move. That way two quick presses (up, then left) both count, and a turn straight back into the body is skipped.

# Use the next turn, unless it would reverse the snake into itself.
while turns:
    turn = turns.pop(0)
    if turn != (-direction[0], -direction[1]):
        direction = turn
        break

Make it yours

Change the code above and press Run again. Some ideas:

  • Make the board bigger: change COLS and ROWS.
  • Let the snake wrap around: when the head leaves one side, bring it in on the other with % COLS and % ROWS.
  • Add a golden food worth 5 points that disappears after a few seconds.
  • Start slower: change the 10 in max(5, 10 - score // 5).

The whole program

Plain pygame: it runs the same on a computer with Python and pygame installed.

import random
import pygame

pygame.init()
CELL = 20
COLS, ROWS = 24, 16
TOP = 40  # room for the score above the board
WIDTH, HEIGHT = COLS * CELL, TOP + ROWS * CELL
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Snake")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 30)
big_font = pygame.font.Font(None, 64)

BG = (16, 18, 22)
BOARD = (24, 27, 33)
YELLOW = (255, 212, 59)
BLUE = (48, 105, 152)
RED = (235, 95, 85)
TEXT = (230, 232, 235)

ARROWS = {
    pygame.K_UP: (0, -1),
    pygame.K_DOWN: (0, 1),
    pygame.K_LEFT: (-1, 0),
    pygame.K_RIGHT: (1, 0),
}
START_KEYS = (pygame.K_UP, pygame.K_SPACE, pygame.K_RETURN)


def new_game():
    global snake, direction, turns, food, score, move_timer
    snake = [(6, 8), (5, 8), (4, 8)]  # head first
    direction = (1, 0)
    turns = []  # arrow presses waiting for the next move
    food = place_food()
    score = 0
    move_timer = 0


def place_food():
    """A random empty cell, or None when the snake fills the whole board."""
    empty = [(x, y) for x in range(COLS) for y in range(ROWS) if (x, y) not in snake]
    if not empty:
        return None
    return random.choice(empty)


def cell_rect(x, y):
    return pygame.Rect(x * CELL, TOP + y * CELL, CELL, CELL)


def draw_text(text, text_font, color, center):
    image = text_font.render(text, True, color)
    screen.blit(image, image.get_rect(center=center))


new_game()
best = 0
state = "ready"  # "ready", "playing", "over" or "won"
wait = 0  # frames until a new game may start
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            if state == "playing":
                if event.key in ARROWS and len(turns) < 3:
                    turns.append(ARROWS[event.key])
            elif state == "ready" and (event.key in ARROWS or event.key in START_KEYS):
                state = "playing"
                if event.key in ARROWS:
                    turns.append(ARROWS[event.key])
            elif wait == 0 and event.key in START_KEYS:
                new_game()
                state = "playing"

    if wait > 0:
        wait -= 1

    if state == "playing":
        move_timer += 1
        # The snake moves every few frames, a little faster as it grows.
        if move_timer >= max(5, 10 - score // 5):
            move_timer = 0
            # Use the next turn, unless it would reverse the snake into itself.
            while turns:
                turn = turns.pop(0)
                if turn != (-direction[0], -direction[1]):
                    direction = turn
                    break
            head = (snake[0][0] + direction[0], snake[0][1] + direction[1])
            eating = head == food
            # The tail moves out of the way unless the snake is growing.
            body = snake if eating else snake[:-1]
            if not (0 <= head[0] < COLS and 0 <= head[1] < ROWS) or head in body:
                state = "over"
            else:
                snake.insert(0, head)
                if eating:
                    score += 1
                    food = place_food()
                    if food is None:
                        state = "won"
                else:
                    snake.pop()
            if state != "playing":
                best = max(best, score)
                wait = 30

    screen.fill(BG)
    pygame.draw.rect(screen, BOARD, (0, TOP, WIDTH, ROWS * CELL))
    if food:
        pygame.draw.circle(screen, RED, cell_rect(*food).center, CELL // 2 - 3)
    for i, (x, y) in enumerate(snake):
        color = BLUE
        if i == 0:
            color = RED if state == "over" else YELLOW
        pygame.draw.rect(screen, color, cell_rect(x, y).inflate(-2, -2), border_radius=5)

    screen.blit(font.render(f"Score {score}", True, TEXT), (12, 11))
    best_text = font.render(f"Best {best}", True, YELLOW)
    screen.blit(best_text, (WIDTH - best_text.get_width() - 12, 11))

    if state != "playing":
        panel = pygame.Rect(0, 0, 360, 130)
        panel.center = (WIDTH // 2, TOP + ROWS * CELL // 2)
        pygame.draw.rect(screen, BG, panel, border_radius=12)
        pygame.draw.rect(screen, BLUE, panel, 2, border_radius=12)
        titles = {"ready": "Snake", "over": "Game over", "won": "You win!"}
        draw_text(titles[state], big_font, YELLOW, (panel.centerx, panel.y + 45))
        if state == "ready":
            hint = "Press an arrow key to start"
        else:
            hint = "Press Up or Space to play again"
        draw_text(hint, font, TEXT, (panel.centerx, panel.y + 98))

    pygame.display.flip()
    clock.tick(60)

pygame.quit()