资讯动态

Python迷宫项目:递归回溯生成与BFS最短路径求解源码解析

发布时间:2026/9/12 14:51:22 来源:尧图企业网站定制
简介这是一份基于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 mazemaze[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_maze、from 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 安装完成 pausechcp 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根目录下执行命令而不是进入src或asset子目录再执行cd /d maze-games-主master python main.pymain.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 行列尺寸必须是奇数且最小值为3generate_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*N1比如 31x31 对应 15 条走廊。这样把“生成后自动修正”变成“传入前主动校验”答辩时更好交代边界条件。5.2 先画字符迷宫再进 pygame 界面调算法时每次弹窗口很浪费时间而且难以肉眼确认路径是否绕路。更好的方式是先用字符画把迷宫和路径打印到控制台def debug_maze(maze, pathNone): 调试用把 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* 访问节点更少就可以很自然地把话题引向启发函数的有效性——这是迷宫求解项目里性价比最高的一个扩展点。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价