Python迷宫项目:递归回溯生成与BFS最短路径求解源码解析
2026/9/12 14:51:06 网站建设 项目流程

简介:这是一份基于Python实现的迷宫求解小游戏工程包,适合高校学生完成课程设计、期末大作业或入门图形化编程实践。项目包含完整的源码、依赖说明与可执行文件,下载后按说明配置即可直接运行,适合需要快速交付高分项目的学习者。压缩包共17个文件,核心为两个Python源文件及运行入口,另附游戏运行所需的图片、音频素材,以及环境安装脚本、使用说明和打包好的exe程序,整体约26.89MB,结构清晰、便于二次修改。目前已有348人学习下载,资源实用性得到一定验证。通过该工程,读者可了解迷宫生成与自动求解的常用算法思路,学习如何将Python逻辑与简单界面、音效结合成完整小游戏;同时包内保留了第三方包安装命令和可执行程序,既能帮助理解项目搭建流程,也能直接用于演示或答辩,整体完成度较高,适合作为参考模板快速改造和扩展。

1. 压缩包里的迷宫求解是否真的“下载即用”

真正让迷宫项目值得复用的,不是 output 目录里的 exe,而是它把生成迷宫和求解路径的 Python 源码完整保留下来。很多课程设计只丢一个打包好的黑盒,验收能跑,答辩却讲不清算法流程;这个压缩包把 src、asset、main.py 按常规结构铺开,正好补上那块短板。适合两类人:一是期末需要快速过审的学生,二是想找递归回溯和 BFS 完整样例做课设底座的开发者。项目把迷宫的生成端和搜索端分开,运行时由 main.py 组装,output 目录下则有 PyInstaller 打好的免环境 exe。下面按生成、搜索、运行、打包四个环节拆开,讲清楚各自该看什么、改什么、易踩什么坑。

2. 迷宫生成与路径搜索:递归回溯与BFS的选型逻辑

先用一句话概括这个项目的核心:生成段用深度优先的随机回溯,求解段用 BFS。这不是两套算法里最极致的选择,却是最容易在答辩现场把来龙去脉讲清楚的一组组合。下面先解释为什么是它,再给出源码层面的完整代码和参数说明。

2.1 生成端为什么选中递归回溯

递归回溯生成迷宫的过程可以理解为“一个会回头的随机深度优先搜索”:从起点格开始,每次向前跳两格并打通中间的墙,走到死胡同就沿栈回退,直到所有格子都被访问过。这样生成的迷宫是一棵满二叉树状的树结构,任意两点之间有且仅有一条通路,也就是通常说的“完美迷宫”。

之所以课程设计项目普遍用它,而不是 Prim 或 Kruskal,原因很实际:代码量小,只要一个栈加一个随机数;生成的迷宫视觉上分支明确,适合游戏界面按格子渲染;证明生成正确性时只需要说明“每个偶数索引格子都被访问过”,答辩逻辑非常短。

生成算法迷宫结构典型代码量答辩解释成本
递归回溯单路径树、死胡同较长小,单栈实现低,过程直观
Prim分支均匀、连通性好中,需维护候选集中,理解边界条件
Kruskal随机性强偏大,需并查集高,得先解释集合合并
递归分割走廊以直线为主高,反复切分循环难讲清

对课程设计来讲,代码量直接决定你提交的报告里能把多少篇幅留给运行效果和测试数据。递归回溯在这一点上性价比最高。

2.2 BFS与DFS在求解阶段的取舍

迷宫生成结束后,求解端要处理的是一个无环的树形网格。BFS 按层向外扩散,第一次到达终点时得到的路径就是最短路径;DFS 则沿一条分支走到黑,找到哪条算哪条,路径长度完全不可控。这也是项目最终选择 BFS 而不是 DFS 的根本原因:演示时路径更短、更直,界面观感更好,也方便和“最短路径”这个卖点对应上。

求解算法最短路径主要开销迷宫场景表现
BFS保证O(H*W) 空间存前驱和队列路径规整,逐层可视
DFS不保证O(H*W) 递归栈路径可能绕远,意外性强
A*保证额外堆结构效果好,但需设计启发函数

下面是最短路径求解的 BFS 实现,它同时承担了去重和路径重建两个任务:

from collections import deque def bfs_shortest_path(maze, start, end): """ 求解迷宫最短路径。 maze: 二维列表,0表示可通行,1表示墙体 start/end: (row, col) 坐标元组 返回路径坐标列表;无解时返回空列表 """ rows, cols = len(maze), len(maze[0]) prev = {start: None} # 记录每个格子的前驱节点,用于回溯路径 queue = deque([start]) directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] while queue: r, c = queue.popleft() if (r, c) == end: # 从终点沿着 prev 回退到起点 path = [] cur = end while cur is not None: path.append(cur) cur = prev[cur] path.reverse() return path for dr, dc in directions: nr, nc = r + dr, c + dc if (0 <= nr < rows and 0 <= nc < cols and (nr, nc) not in prev and maze[nr][nc] == 0): prev[(nr, nc)] = (r, c) queue.append((nr, nc)) return []

这里的关键变量是prev字典。它记录的是“当前格子是从哪个格子走过来的”,既替代了 visited 集合完成去重,又在找到终点时提供了完整的路径回溯链路。maze[nr][nc] == 0是墙体判断,值 0 表示可通行;坐标用(row, col)的顺序,和后续 pygame 绘制时的行、列索引保持一致,避免横纵坐标写反。

2.3 生成器的坐标步进与奇偶约束

迷宫网格的行列必须保持奇数,比如 31x31、41x41。原因在于递归回溯的步长是 2:假设起点在 (1, 1),下一次要跳到 (3, 1) 或 (1, 3),中间格子 (2, 1) 或 (1, 2) 是待打通的隔墙。如果传入的是偶数尺寸,最后一行或最后一列会残留无法访问的墙体外围。

import random def generate_maze(height, width): """递归回溯生成迷宫,返回二维列表 maze。""" # 自动把偶数修正为奇数,保证边界闭合 height = height if height % 2 == 1 else height + 1 width = width if width % 2 == 1 else width + 1 maze = [[1] * width for _ in range(height)] stack = [(1, 1)] # 起点固定在内围第二行第二列 maze[1][1] = 0 directions = [(2, 0), (-2, 0), (0, 2), (0, -2)] while stack: r, c = stack[-1] candidates = [] for dr, dc in directions: nr, nc = r + dr, c + dc # 目标格未访问过,且位于边界内 if 0 <= nr < height and 0 <= nc < width and maze[nr][nc] == 1: candidates.append((nr, nc, dr, dc)) if not candidates: stack.pop() continue nr, nc, dr, dc = random.choice(candidates) # 打通当前格与目标格之间的隔墙 maze[r + dr // 2][c + dc // 2] = 0 maze[nr][nc] = 0 stack.append((nr, nc)) return maze

maze[r + dr // 2][c + dc // 2]是这段代码里最需要解释的一行。当步长是 2 时,隔墙坐标恰好落在当前坐标和目标坐标的中点,例如从 (1, 1) 跳到 (3, 1),中间格是 (2, 1),dr // 2等于 1,因此没问题。每次随机选择会优先从未访问的墙体格中挑选,所以迷宫不会出现闭合回路。你也可以刻意把步长改成 4 来做“宽走廊迷宫”,但那样画出来的格子会明显变稀疏,不太适合小窗口展示。

3. 源码结构:main.py、src与asset的分层解析

压缩包的目录结构看起来简单,实际复用时信息量不小。先看清每个文件属于哪一层,再决定改哪里、别动哪里。

3.1 压缩包的目录全景与职责划分

项目的顶层结构如下:

maze-games-主master/ ├── .vscode/ │ └── settings.json ├── src/ # 生成器与求解器所在目录 ├── asset/ # 图片、图标等素材 ├── main.py # 程序入口 ├── tempCodeRunnerFile.py # VS Code插件产生的临时文件 ├── output/ │ └── 迷宫小游戏.exe ├── 安装第三方包.cmd └── 使用说明.txt

每个文件对应一个明确的职责:

文件/目录典型作用修改建议
.vscode/settings.json指定解释器路径、文件编码、运行参数按本机环境调整
src/存放迷宫生成和路径求解模块核心算法都在这里改
asset/存放窗口图标、路径贴图等静态资源一般不动
main.py组装算法、窗口、事件循环改入口逻辑时动
tempCodeRunnerFile.pyCode Runner 插件留下的临时执行脚本忽略或删除
output/已打包好的 exe 输出位置重新打包时覆盖
安装第三方包.cmd一键安装依赖按 Python 版本微调

这里特别想提醒一点:tempCodeRunnerFile.py不是项目入口。它是 VS Code 的 Code Runner 插件在“右键运行”时临时生成的脚本,经常被误当成main.py的直接替代品。直接运行它常常会因为工作目录不对而报ModuleNotFoundError: No module named 'src',实际上和代码本身无关,是入口选错了。

3.2 src模块:把算法从界面里剥离出来

src目录的价值在于将“纯算法”和“界面渲染”分离。按照常见的分层方式,里面至少有两个模块:一个负责迷宫生成,一个负责路径求解。外部通过from src.maze_generator import generate_mazefrom src.maze_solver import bfs_shortest_path这种方式引用。

这种写法在课程设计里容易被忽略,但答辩时是加分项:你可以明确说“算法模块不依赖 pygame,可以单独做单元测试”。如果要进一步规范,给src目录补一个空的__init__.py,把目录变成标准包。这样后续 PyInstaller 打包时,对import src.xxx这类语句的识别也更稳定,不容易出现“源码能跑、打包后找不到模块”的情况。

# src/__init__.py # 空文件即可,声明 src 是一个 Python 包

3.3 main.py 的游戏循环与资源加载边界

main.py负责把算法结果变成可交互的游戏窗口。它的工作流是固定的:创建 pygame 窗口 → 调用generate_maze生成迷宫 → 调用bfs_shortest_path求路径 → 循环处理键盘事件并重绘画面。

import sys import pygame from src.maze_generator import generate_maze from src.maze_solver import bfs_shortest_path def draw_maze(screen, maze, path, cell_size): """把 0/1 二维数组绘制成具体的方块。""" for r, row in enumerate(maze): for c, cell in enumerate(row): color = (250, 250, 250) if cell == 0 else (30, 30, 30) pygame.draw.rect( screen, color, (c * cell_size, r * cell_size, cell_size, cell_size) ) def main(): pygame.init() screen = pygame.display.set_mode((800, 800)) clock = pygame.time.Clock() maze = generate_maze(41, 41) start, end = (1, 1), (len(maze) - 2, len(maze[0]) - 2) path = bfs_shortest_path(maze, start, end) while True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit() sys.exit() draw_maze(screen, maze, path, 20) pygame.display.flip() clock.tick(60) if __name__ == "__main__": main()

入口坐标的选取有讲究:起点固定在(1, 1),因为生成器保证该位置一定是通路;终点取(len(maze)-2, len(maze[0])-2),即右下角内侧一格,保证出口落在迷宫内部而不是边界墙里。cell_size决定每格像素,窗口 800x800 配合 41 行迷宫,单格像素约 19 像素,视觉效果比较合适。

4. 一键安装依赖、源码运行与PyInstaller打包链路

这部分是“下载即用”和“重新打包”两条路径的分水岭。前者只需要安装第三方包.cmd和 exe,后者需要自己走一遍完整的 Python 打包流程。

4.1 安装第三方包.cmd 的批处理逻辑

安装第三方包.cmd的作用是减少手动敲pip install的步骤。在课程设计中它通常是下面这样一组命令的组合:

@echo off chcp 65001 >nul echo 正在安装所需第三方库... python -m pip install pygame pyinstaller if errorlevel 1 ( echo 安装失败,请检查网络或pip源 pause exit /b 1 ) echo 安装完成 pause

chcp 65001把控制台代码页切到 UTF-8,避免中文路径或提示出现乱码。使用python -m pip install而不是直接写pip install,是为了避免系统里同时存在 Python 2/3 或多个虚拟环境时装错解释器。如果当前网络环境连 PyPI 较慢,可以给命令追加镜像源:

python -m pip install pygame pyinstaller -i https://pypi.tuna.tsinghua.edu.cn/simple

镜像地址只影响下载效率,不影响代码运行结果,但能在机房这种出口带宽受限的环境里显著减少等待时间。

4.2 源码运行时的两条标准路径

拿到压缩包后,最常见的两个运行场景是:直接双击output/迷宫小游戏.exe,或在源码目录下启动main.py。前者不依赖 Python 环境,后者用来调试和改造。

直接运行 exe 没有太多可说,重点是源码运行时的目录位置。必须在maze-games-主master根目录下执行命令,而不是进入srcasset子目录再执行:

cd /d maze-games-主master python main.py

main.py里的from src.maze_generator import ...是相对项目根目录的包导入。如果在其他目录执行,解释器的sys.path无法定位到src,立刻就会报ModuleNotFoundError。如果你的机器上有多个 Python 版本,建议先把入口封装进虚拟环境:

py -m venv venv venv\Scripts\activate python -m pip install pygame python main.py

注意安装第三方包.cmd里装的是全局环境,而这里装进的是venv虚拟环境,二者不冲突。虚拟环境的优势是干净可删,做完项目直接把venv目录删除就能清理所有依赖。

4.3 PyInstaller 打包参数与资源路径修正

如果要把main.py重新打包成迷宫小游戏.exe,标准命令是:

pyinstaller -F -w \ --add-data "asset;asset" \ --icon asset/maze.ico \ main.py

各参数含义如下:

参数作用建议
-F打包成单个 exe 文件课程设计交付用
-w运行时不弹出控制台窗口图形界面程序必加
--add-data把 asset 目录压缩进 exe缺了会找不到图标和素材
--icon给 exe 换图标视觉加分项

Windows 下--add-data的源路径和目标路径用分号分隔,例如asset;asset表示把本地asset目录映射到解包目录下的asset路径。Linux/macOS 下要用冒号:,跨平台时容易踩坑。

打包后还不能直接结束,必须处理资源路径。PyInstaller 会把 exe 解压到临时目录sys._MEIPASS,源码里写的os.path.join("asset", "xxx.png")在打包环境下会失效。需要在main.py里加一个兼容函数:

import sys import os def resource_path(relative): """同时兼容源码运行和 PyInstaller 打包后的资源定位。""" if hasattr(sys, "_MEIPASS"): return os.path.join(sys._MEIPASS, relative) return os.path.join(os.path.abspath("."), relative)

之后所有读取 asset 的路径都写成resource_path(os.path.join("asset", "icon.png"))。这个函数是 exe 能否离开项目目录独立运行的开关。

5. 迷宫尺寸边界、字符调试与A*改造验证

最后这部分是实际调试中最常碰到的三个场景,也是把课程设计从“能跑”推到“好讲”的关键。

5.1 行列尺寸必须是奇数,且最小值为3

generate_maze(41, 41)没问题,但有人在改成generate_maze(30, 30)后突然报错,或者界面右侧冒出半列墙。原因在 2.3 节提到过:递归回溯的步长为 2,尺寸一旦为偶数,最后一行或最后一列就无法被访问边界覆盖。更隐蔽的是30会被代码偷偷修正成31,而使用方不知道,导致期望的 30x30 和实际生成的 31x31 对不上。

稳妥做法是在调用前显式校验:

def guard_maze_size(height, width): if height % 2 == 0 or width % 2 == 0: raise ValueError("迷宫行数与列数必须为奇数") if height < 3 or width < 3: raise ValueError("迷宫最小尺寸为 3x3")

翻译成直观结论:想生成 N 条走廊的迷宫,传入的尺寸应该写成2*N+1,比如 31x31 对应 15 条走廊。这样把“生成后自动修正”变成“传入前主动校验”,答辩时更好交代边界条件。

5.2 先画字符迷宫,再进 pygame 界面

调算法时每次弹窗口很浪费时间,而且难以肉眼确认路径是否绕路。更好的方式是先用字符画把迷宫和路径打印到控制台:

def debug_maze(maze, path=None): """调试用:把 0/1 迷宫渲染成可读文本。""" path_set = set(path) if path else set() for r, row in enumerate(maze): line = "" for c, cell in enumerate(row): if (r, c) in path_set: line += "·" else: line += " " if cell == 0 else "#" print(line)

路径上的格子用·标记,墙体用#,通路留空。打印结果能直接验证三件事:起点和终点确实可通行;BFS 路径不会穿过墙体;迷宫没有出现宽度超过一个格子的走廊。这一步过了再进 pygame,界面表现基本不会有大问题。

5.3 用 A* 替换 BFS,作为答辩的对比实验

BFS 是最短路径的兜底解法,但答辩时如果只讲一个队列解法,深度不够。把 BFS 替换成带曼哈顿距离的 A*,难度不大,却可以立刻引出一个对比点:搜索扩展的格子数明显减少。核心改动如下:

import heapq def a_star(maze, start, end): """A* 求解迷宫最短路径,启发函数取曼哈顿距离。""" rows, cols = len(maze), len(maze[0]) def h(cur): return abs(cur[0] - end[0]) + abs(cur[1] - end[1]) open_heap = [(h(start), 0, start)] # (估计总代价, 实际代价, 坐标) g_score = {start: 0} prev = {start: None} directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] while open_heap: _, cur_g, cur = heapq.heappop(open_heap) if cur == end: path = [] while cur is not None: path.append(cur) cur = prev[cur] return path[::-1] for dr, dc in directions: nr, nc = cur[0] + dr, cur[1] + dc if (0 <= nr < rows and 0 <= nc < cols and maze[nr][nc] == 0): tentative_g = cur_g + 1 if tentative_g < g_score.get((nr, nc), float("inf")): g_score[(nr, nc)] = tentative_g prev[(nr, nc)] = cur heapq.heappush(open_heap, (tentative_g + h((nr, nc)), tentative_g, (nr, nc))) return []

改造后的a_star返回值格式和bfs_shortest_path完全一致,main.py里只需要替换一行导入即可无缝切换。对比时记录两个指标:len(path)是否相等,以及prev字典被写入的次数。前者验证最短路径长度是否一致,后者反映搜索量差异。如果二者长度一样但 A* 访问节点更少,就可以很自然地把话题引向启发函数的有效性——这是迷宫求解项目里性价比最高的一个扩展点。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询