Create a Simple Game in Python with pygame (Snake Clone)

andreasGames3 weeks ago29 Views

What you’ll build

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.

Prerequisites

  • Python 3.8+
  • pygame (pip install pygame)

Quick start (recommended with 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

Controls

  • Arrow keys / WASD: move
  • Space / P: pause or resume
  • R: restart after game over
  • ESC: quit
#!/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()

Snake — pygame clone

Quick start

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

Controls

  • Arrow keys / WASD : move
  • Space or P : pause/resume
  • R : restart after game over
  • ESC : quit

Tuning

Open snake.py and adjust:

  • GRID_SIZE (cells wide, high)
  • CELL (pixel size per cell)
  • FPS (speed)
  • change wrap-around to wall-collisions (see comment in code)

1 Votes: 1 Upvotes, 0 Downvotes (1 Points)

Leave a reply

Loading Next Post...
Loading

Signing-in 3 seconds...