Building a Maze Generator in Python with Turtle and DFS (Fixed & Explained)
Why Does the Turtle Overlap in Maze Generation?
When implementing a maze generator using Depth-First Search (DFS) and Python's turtle module, a frequent bug is the turtle drawing over already visited paths or looping endlessly. In the original snippet, this happens because of two primary architectural issues:
- Floating-Point Precision Issues with
turtle.pos(): Python's turtle library tracks coordinates as floating-point numbers (e.g.,(-100.00, -80.00000000000001)). Comparing float tuples directly usingpos in visitedorset()often fails due to tiny rounding errors. - Flawed DFS / Backtracking Logic: Picking a random direction in a basic
whileloop without an explicit stack or recursion isn't true Depth-First Search. Without proper backtracking, when the turtle encounters dead ends, it gets trapped or repeatedly bumps into visited boundaries.
The Correct Approach: Decouple the Grid Logic from Drawing
The cleanest, most robust way to generate a maze in Python is to separate the grid data structure from the turtle rendering logic. By working with discrete integer coordinates on a grid (such as (0, 0), (0, 1)), we eliminate floating-point coordinate bugs entirely.
Complete Working DFS Maze Generator
Here is a complete, working solution implementing randomized Depth-First Search with backtracking (using an explicit stack) and visualizing it smoothly using Python's turtle library.
import turtle as t
import random
# Configuration
GRID_SIZE = 10 # 10x10 cells
CELL_SIZE = 24 # Pixel width of each cell
START_X = -120
START_Y = -120
def cell_to_coords(x, y):
"""Converts grid (x, y) coordinates to turtle canvas (X, Y) coordinates."""
return START_X + x * CELL_SIZE, START_Y + y * CELL_SIZE
def get_unvisited_neighbors(x, y, visited):
"""Finds adjacent grid cells that haven't been visited yet."""
neighbors = []
directions = [((0, 1), (x, y + 1)), # Up
((0, -1), (x, y - 1)), # Down
((-1, 0), (x - 1, y)), # Left
((1, 0), (x + 1, y))] # Right
for _, (nx, ny) in directions:
if 0 <= nx < GRID_SIZE and 0 <= ny < GRID_SIZE:
if (nx, ny) not in visited:
neighbors.append((nx, ny))
return neighbors
def generate_maze():
# Setup turtle screen
screen = t.Screen()
screen.setup(width=600, height=600)
screen.bgcolor("black")
screen.tracer(0) # Turn off auto-animation for speed control
# Setup maze drawer
maze = t.Turtle()
maze.color("white")
maze.shape("square")
maze.pensize(CELL_SIZE - 4)
maze.speed(0)
# DFS State tracking
stack = [(0, 0)]
visited = {(0, 0)}
# Move turtle to start
sx, sy = cell_to_coords(0, 0)
maze.penup()
maze.goto(sx, sy)
maze.pendown()
while stack:
current_cell = stack[-1]
cx, cy = current_cell
neighbors = get_unvisited_neighbors(cx, cy, visited)
if neighbors:
# Pick a random unvisited neighbor
next_cell = random.choice(neighbors)
nx, ny = next_cell
# Mark visited and push to stack
visited.add(next_cell)
stack.append(next_cell)
# Draw path to neighbor
tx, ty = cell_to_coords(nx, ny)
maze.goto(tx, ty)
else:
# Backtrack
stack.pop()
if stack:
bx, by = stack[-1]
maze.penup()
maze.goto(cell_to_coords(bx, by))
maze.pendown()
screen.update()
maze.hideturtle()
screen.update()
t.done()
if __name__ == "__main__":
generate_maze()
Key Improvements in This Implementation
- Discrete Grid System: Using integer grid tuples
(x, y)ensuresin visitedlookups areO(1)and completely immune to floating-point rounding mismatches. - Stack-Based Backtracking: When the turtle runs into a dead end,
stack.pop()safely unwinds the path back to a node that still has open neighbors. - Fast & Smooth Rendering: Using
screen.tracer(0)paired withscreen.update()eliminates slow movement delays and allows smooth visual feedback. - Set-Based Visited Tracking: Using a Python
setforvisitedprovides significantly faster membership testing than scanning through a standardlist.