A minimalist Snake clone: move the snake with the arrow keys/WASD, eat food to grow, avoid biting your tail. It features a clean grid look, score display, pause/resume, and instant restart.
pygame
(pip install pygame
)venv
)# create & activate a venv
python -m venv venv # macOS/Linux: python3 -m venv venv
venv\Scripts\activate # macOS/Linux: source venv/bin/activate
# install pygame
pip install pygame
# run
python snake.py
#!/usr/bin/env python3
"""
Snake — a simple pygame clone
--------------------------------
Controls:
Arrow keys / WASD : move
P or Space : pause/resume
R : restart after game over
ESC : quit
Tweaks:
Change GRID_SIZE, CELL, and FPS to adjust difficulty and look.
"""
import pygame
import random
from dataclasses import dataclass
# ---------- Settings ----------
GRID_SIZE = (30, 20) # width, height in cells
CELL = 24 # pixel size of a cell
MARGIN = 2 # cell padding (for a grid effect)
FPS = 12 # frames per second
START_LENGTH = 4
# Colors
BG = (18, 18, 18)
SNAKE = (60, 220, 120)
SNAKE_HEAD = (80, 255, 160)
FOOD = (255, 90, 110)
GRID_COLOR = (28, 28, 28)
TEXT = (230, 230, 230)
SHADOW = (0, 0, 0)
# --------------------------------
DIRS = {
pygame.K_UP: (0, -1), pygame.K_w: (0, -1),
pygame.K_DOWN: (0, 1), pygame.K_s: (0, 1),
pygame.K_LEFT: (-1, 0), pygame.K_a: (-1, 0),
pygame.K_RIGHT: (1, 0), pygame.K_d: (1, 0),
}
@dataclass
class GameState:
snake: list
direction: tuple
food: tuple
score: int
paused: bool
game_over: bool
def random_empty_cell(occupied, grid_w, grid_h):
# Avoid endless loop by sampling from all cells minus occupied
all_cells = {(x, y) for x in range(grid_w) for y in range(grid_h)}
choices = list(all_cells - set(occupied))
return random.choice(choices) if choices else None
def new_game():
grid_w, grid_h = GRID_SIZE
cx, cy = grid_w // 2, grid_h // 2
snake = [(cx - i, cy) for i in range(START_LENGTH)]
direction = (1, 0)
food = random_empty_cell(snake, grid_w, grid_h)
return GameState(snake=snake, direction=direction, food=food, score=0, paused=False, game_over=False)
def draw_cell(surf, x, y, color):
rect = pygame.Rect(x*CELL, y*CELL, CELL, CELL)
if MARGIN:
rect.inflate_ip(-MARGIN, -MARGIN)
pygame.draw.rect(surf, color, rect, border_radius=4)
def draw_grid(surf):
if MARGIN == 0:
return
surf_w, surf_h = surf.get_size()
for x in range(0, surf_w, CELL):
pygame.draw.line(surf, GRID_COLOR, (x, 0), (x, surf_h))
for y in range(0, surf_h, CELL):
pygame.draw.line(surf, GRID_COLOR, (0, y), (surf_w, y))
def step(state: GameState):
if state.paused or state.game_over:
return state
head_x, head_y = state.snake[0]
dx, dy = state.direction
nx, ny = head_x + dx, head_y + dy
# Wrap around edges (toggle this behavior by commenting next two lines and uncommenting the wall check below)
nx %= GRID_SIZE[0]
ny %= GRID_SIZE[1]
new_head = (nx, ny)
# Wall collision alternative:
# if not (0 <= nx < GRID_SIZE[0] and 0 <= ny < GRID_SIZE[1]):
# state.game_over = True
# return state
# Self collision
if new_head in state.snake:
state.game_over = True
return state
state.snake.insert(0, new_head)
# Eat or move
if new_head == state.food:
state.score += 1
state.food = random_empty_cell(state.snake, *GRID_SIZE)
else:
state.snake.pop()
return state
def main():
pygame.init()
pygame.display.set_caption("Snake (pygame)")
screen = pygame.display.set_mode((GRID_SIZE[0]*CELL, GRID_SIZE[1]*CELL))
clock = pygame.time.Clock()
font = pygame.font.SysFont("consolas,menlo,monospace", 20)
big_font = pygame.font.SysFont("consolas,menlo,monospace", 40)
state = new_game()
queued_dir = state.direction
running = True
while running:
# --- INPUT ---
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
elif event.key in DIRS:
ndx, ndy = DIRS[event.key]
# prevent reversing directly
if (ndx, ndy) != (-state.direction[0], -state.direction[1]):
queued_dir = (ndx, ndy)
elif event.key in (pygame.K_SPACE, pygame.K_p):
state.paused = not state.paused and not state.game_over
elif event.key == pygame.K_r:
if state.game_over:
state = new_game()
queued_dir = state.direction
# Apply queued direction at most once per frame
state.direction = queued_dir
# --- UPDATE ---
step(state)
# --- DRAW ---
screen.fill(BG)
draw_grid(screen)
# Food
if state.food:
draw_cell(screen, *state.food, FOOD)
# Snake
for i, (x, y) in enumerate(state.snake):
color = SNAKE_HEAD if i == 0 else SNAKE
draw_cell(screen, x, y, color)
# UI overlay
score_surf = font.render(f"Score: {state.score}", True, TEXT)
screen.blit(score_surf, (8, 6))
if state.paused and not state.game_over:
label = big_font.render("PAUSED", True, TEXT)
screen.blit(label, label.get_rect(center=screen.get_rect().center))
if state.game_over:
over = big_font.render("GAME OVER", True, TEXT)
hint = font.render("Press R to restart", True, TEXT)
rect = screen.get_rect()
screen.blit(over, over.get_rect(center=(rect.centerx, rect.centery - 18)))
screen.blit(hint, hint.get_rect(center=(rect.centerx, rect.centery + 18)))
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
if __name__ == "__main__":
main()
1) Create and activate a virtual environment (recommended)
Windows:
python -m venv venv
venv\Scripts\activate
macOS/Linux:
python3 -m venv venv
source venv/bin/activate
2) Install dependency:
pip install pygame
3) Run:
python snake.py
Open snake.py
and adjust:
GRID_SIZE
(cells wide, high)CELL
(pixel size per cell)FPS
(speed)