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
COLSandROWS. - Let the snake wrap around: when the head leaves one side, bring it in on the other with
% COLSand% ROWS. - Add a golden food worth 5 points that disappears after a few seconds.
- Start slower: change the
10inmax(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()