"""
PROJECT 7: Snake
=================
Concepts: pygame game loop, event handling, grid-based movement,
deque (efficient queue), collision detection, rendering
"""
import pygame
import random
from collections import deque
# Config
CELL = 20
COLS = 30
ROWS = 25
WIDTH = COLS * CELL
HEIGHT = ROWS * CELL
FPS = 10
# Colors (R, G, B)
BLACK = (10, 10, 10)
GREEN = (80, 200, 80)
DARK_GREEN = (40, 140, 40)
RED = (220, 60, 60)
WHITE = (240, 240, 240)
UP = (0, -1)
DOWN = (0, 1)
LEFT = (-1, 0)
RIGHT = (1, 0)
def draw_cell(surface, col, row, color, shrink=2):
rect = pygame.Rect(col * CELL + shrink, row * CELL + shrink,
CELL - shrink * 2, CELL - shrink * 2)
pygame.draw.rect(surface, color, rect, border_radius=4)
def main():
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
def new_game():
start = (COLS // 2, ROWS // 2)
snake = deque([start, (start[0]-1, start[1]), (start[0]-2, start[1])])
direction = RIGHT
food = spawn_food(snake)
return snake, direction, food, 0
def spawn_food(snake):
snake_set = set(snake)
while True:
pos = (random.randint(0, COLS-1), random.randint(0, ROWS-1))
if pos not in snake_set:
return pos
snake, direction, food, score = new_game()
game_over = False
pending_dir = direction
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit(); return
if event.type == pygame.KEYDOWN:
# prevent reversing into yourself
if event.key == pygame.K_UP and direction != DOWN: pending_dir = UP
elif event.key == pygame.K_DOWN and direction != UP: pending_dir = DOWN
elif event.key == pygame.K_LEFT and direction != RIGHT: pending_dir = LEFT
elif event.key == pygame.K_RIGHT and direction != LEFT: pending_dir = RIGHT
if not game_over:
direction = pending_dir
head = snake[0]
new_head = (head[0] + direction[0], head[1] + direction[1])
hit_wall = not (0 <= new_head[0] < COLS and 0 <= new_head[1] < ROWS)
hit_self = new_head in set(snake)
if hit_wall or hit_self:
game_over = True
else:
snake.appendleft(new_head)
if new_head == food:
score += 1
food = spawn_food(snake)
else:
snake.pop() # remove tail — normal move
screen.fill(BLACK)
draw_cell(screen, food[0], food[1], RED, shrink=3)
for i, (col, row) in enumerate(snake):
draw_cell(screen, col, row, GREEN if i > 0 else DARK_GREEN)
pygame.display.flip()
clock.tick(FPS)
if __name__ == "__main__":
main()
Concepts practiced
pygamegame loop + events
dequeO(1) head/tail ops
Collisionwall + self check
Gridpixel ↔ cell mapping
Setsfast food spawn check
Closuresnew_game, spawn_food
Reflection
I built a classic Snake game using pygame, representing the snake's body as a collections.deque so I could efficiently add to the head and remove from the tail in O(1) time without shifting a full list. I learned how a pygame game loop works — polling events, updating state, and rendering every frame at a fixed FPS; and how grid-based movement maps pixel coordinates to cell positions. The bug that stumped me most was the snake occasionally reversing into itself when keys were pressed quickly: the fix was introducing a pending_dir variable that buffers the next direction and only applies it at the start of each tick, preventing a mid-frame direction reversal.