☰
C++控制台迷宫小游戏实战:DFS生成与BFS寻路全解析
2026/10/25 11:39:34 网站建设 项目流程

简介:一份用C++实现的迷宫小游戏源码包,适合算法初学者和编程实践者,完整演示了经典的“小老鼠走迷宫”过程。代码以二维数组表示迷宫,用0和1标记通道与墙壁;采用广度优先搜索BFS寻找出口,并通过队列管理待扩展节点,能够避免重复访问。游戏循环中处理了上下左右移动、撞墙提示、到达终点等交互逻辑,用户可直接运行生成的可执行文件,也可阅读源码学习项目组织方式。压缩包共16个文件,涵盖Maze.h、Maze.cpp、main.cpp等源代码,以及exe、obj、pdb、dsp、dsw等构建与调试配套文件,整体体积仅1.17MB。目前已有1172人浏览学习,借助该代码包可快速理解BFS算法的实际应用,并掌握二维数组建图、队列搜索、用户输入处理等技能,还能进一步扩展SDL2/SFML图形界面或随机迷宫生成,是课程设计与算法实验的良好参考。

1. Maze迷宫游戏与C++:一个控制台程序为什么值得认真做

如果你搜过“Maze迷宫游戏 源码”或“迷宫小游戏C++”,大概率见过两种东西:一张控制台截图配几百行代码,复制进Visual Studio却跑不起来;或者一篇只讲生成算法的片段,没有输入、没有渲染、没有游戏循环。迷宫小游戏这个标题看着简单,实际拆开有四条线:随机生成算法、地图建模、控制台交互、自动寻路。哪条线没闭环,代码就只能停在“能生成一张迷宫图”的阶段。这篇文章按一条完整可复现的路来走——用C++写一个能在控制台里用方向键走迷宫的版本,顺便把生成、寻路、参数验证一次讲透。适合想练C++项目、做课程设计,或者纯粹想搞明白迷宫算法背后逻辑的人。

2. 迷宫生成算法选型:DFS回溯与递归分割,先写哪个

2.1 完美迷宫与非完美迷宫:先定验收标准

写生成算法前先定一个标准:你要的迷宫是“完美迷宫”还是“非完美迷宫”。完美迷宫指任意两个格子之间只有唯一一条路径,没有环路,也没有不可达区域。DFS回溯、递归分割、Prim、Kruskal这几种主流算法生成的默认都是完美迷宫;如果你在生成后手动多凿开几堵墙,迷宫就会出现环路,变成非完美迷宫。

从游戏角度看,完美迷宫已经足够好玩,路径长、岔路多、有挑战性,而且它自带一个极好的验收特性:用BFS从起点搜到终点,如果能搜到,说明迷宫没有把路堵死。这个特性后面会被拿来做自动回归验证。所以我建议第一版就锁定“生成完美迷宫”这个目标,不搞复杂的地形装饰。

另一个需要提前定的参数是迷宫尺寸的表示方式。常见做法是用“房间格”加“墙位”两层模型:玩家实际站在房间格里,两个相邻房间之间隔着一格宽的墙。也就是说,如果房间格是9行9列,地图字符数组的尺寸是(29+1)×(29+1)=19×19。这个映射关系第一次写很容易算错,建议你先在纸上画一个3×3的房间格,再把墙位补上,确认坐标公式之后再写代码。

2.2 DFS回溯生成:迭代栈版本与随机数参数

DFS回溯的思路一句话就能说清:从起点房间出发,随机挑一个没去过的相邻房间,打通中间的墙,走过去,把当前房间压栈;如果当前房间四周都去过了,就弹栈回退。栈空时所有房间都被访问过,迷宫生成完毕。

这里有一个值得注意的工程决策:生成用迭代栈而不是递归。递归实现看起来更短,但递归深度等于探索路径的最长长度,在几十乘几十的房间格下很容易爆栈,这在后面避坑章会展开。下面是我常用的生成函数,直接返回地图字符串数组:

#include <vector> #include <stack> #include <random> #include <string> // 生成 rows*cols 房间格的完美迷宫 // 返回地图尺寸: (2*rows+1) x (2*cols+1), '#' 为墙, ' ' 为路 std::vector<std::string> generateMaze(int rows, int cols, unsigned seed) { int h = 2 * rows + 1; int w = 2 * cols + 1; std::vector<std::string> maze(h, std::string(w, '#')); std::vector<std::vector<bool>> visited(rows, std::vector<bool>(cols, false)); int dr[4] = {-1, 0, 1, 0}; // 上 右 下 左 int dc[4] = {0, 1, 0, -1}; std::mt19937 gen(seed); std::uniform_int_distribution<int> pick(0, 3); std::stack<std::pair<int, int>> st; visited[0][0] = true; st.push({0, 0}); while (!st.empty()) { int r = st.top().first; int c = st.top().second; std::vector<int> candidates; for (int d = 0; d < 4; ++d) { int nr = r + dr[d]; int nc = c + dc[d]; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc]) { candidates.push_back(d); } } if (candidates.empty()) { st.pop(); // 四周都访问过,回溯 continue; } int d = candidates[pick(gen) % candidates.size()]; int nr = r + dr[d]; int nc = c + dc[d]; // 房间格在地图坐标是 (2r+1, 2c+1) // 两个相邻房间的墙位正好在它们坐标的中点 maze[2 * r + 1 + dr[d]][2 * c + 1 + dc[d]] = ' '; maze[2 * nr + 1][2 * nc + 1] = ' '; visited[nr][nc] = true; st.push({nr, nc}); } return maze; }

逻辑说明:每个房间格四周各有四个墙位,墙位坐标就是两个相邻房间地图坐标的中间点,比如房间(0,0)和(0,1)之间的墙位是(1,1)。代码里maze[2 * r + 1 + dr[d]][2 * c + 1 + dc[d]] = ' '实际上是从当前房间跨半步到墙位,然后再maze[2 * nr + 1][2 * nc + 1] = ' '把目标房间打通,两步合起来完成了“破墙”。生成结束后,起点房间(0,0)和终点房间(rows-1, cols-1)一定是空格,但还没有标记出入口,那是渲染时的事。

参数说明:rows和cols是房间格数量,不是地图字符尺寸;9×9房间对应19×19字符地图。seed是随机种子,这里用C++11的std::mt19937,不要用srand+rand,具体原因在避坑章会讲。pick(gen) % candidates.size()是因为当某个房间只剩两个未访问邻居时,方向取值范围必须收缩,不能直接用4取余,否则会选中已访问邻居导致逻辑错误。

2.3 递归分割与Prim的对比:什么时候换算法

DFS回溯不是唯一选择,至少有三种常见算法值得知道,选型的依据是你要的迷宫质感。

算法空间结构路径走向实现复杂度适用场景
DFS回溯单通道深、分支少长而曲折低游戏关卡、新手练习
递归分割规则分块、中轴对称偏长且规律低快速生成、程序化纹理
Prim / Kruskal分支多、通道短随机感强中探索型游戏、视觉自然

递归分割的思路是:把整个区域用一堵带缺口的墙分成两半,然后对两个子区域递归执行同样的操作。它生成的迷宫非常快,但墙的位置天然对齐在二分坐标上,玩起来能明显感觉到“块状”规律,缺少DFS回溯那种一走进死胡同再绕出来的拉扯感。Prim则从起点开始维护一个候选墙集合,每次随机挑一面墙打通并把新墙加入集合,生成结果分支多、路径段,但代码需要额外维护集合,第一版不必上。

我自己的建议是:第一版只用DFS回溯,跑通完整游戏循环后再考虑换算法。原因很简单,DFS回溯的代码与“房间格+墙位”模型绑定得最紧,换算法时你只需要替换generateMaze这一个函数,游戏逻辑完全不用动。这个替换成本很低,等你看腻了单通道迷宫再换不迟。

3. 从地图到可玩的C++小游戏:渲染、方向键与游戏主循环

3.1 地图数据结构:用vector 而不是裸数组

地图数据我在生成函数里已经用了std::vector<std::string>,这不是随手选的。如果用char maze[H][W],H和W必须是编译期常量,那命令行传参控制迷宫尺寸就做不到了。std::vector<std::string>可以在运行时决定尺寸,每行是一个独立的std::string,下标访问maze[r][c]和裸数组手感一致,但拷贝、传参、改行长度都安全得多。

初始化也省事:std::vector<std::string> maze(h, std::string(w, '#'))一行就建好全墙地图。这句代码值得记一下,它其实是“用同一个字符串初始化vector里的每一行”的标准写法。如果你项目要求老标准不支持std::vector<std::string>,退一步用vector<vector<char>>也能工作,但每行的字符串操作(比如输出整行时直接puts(maze[r].c_str()))就没那么顺了。

迷宫尺寸的换算要在一开始就封装成函数,别在main里裸算。地图高度是2 * rows + 1,宽度是2 * cols + 1;玩家起点在左上角房间,地图坐标永远是(1, 1);终点在右下角房间,地图坐标是(2 * rows - 1, 2 * cols - 1)。这三个坐标在游戏循环、寻路、渲染里到处要用,写死数字是翻车的开始。

3.2 用windows.h做控制台渲染:光标定位与防闪烁

控制台渲染最常见的错误是每次用system("cls")清屏。这个函数在Windows控制台上有肉眼可见的闪烁,因为清屏外加光标回到左上角不是原子操作,屏幕会闪一下。更好的做法是把整帧迷宫拼成一个字符串,用SetConsoleCursorPosition把光标定位到(0,0),再一次性printf输出。

#include <windows.h> #include <cstdio> #include <string> #include <vector> void renderMaze(const std::vector<std::string>& maze, int playerR, int playerC) { static std::string buffer; buffer.clear(); for (int r = 0; r < (int)maze.size(); ++r) { for (int c = 0; c < (int)maze[r].size(); ++c) { char ch = maze[r][c]; if (r == playerR && c == playerC) { ch = '@'; // 玩家位置在渲染层覆盖 } buffer += ch; } buffer += '\n'; } HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos = {0, 0}; SetConsoleCursorPosition(hOut, pos); printf("%s", buffer.c_str()); }

逻辑说明:玩家位置不写进maze,而是在渲染时才用@覆盖到缓冲区对应字符上。这样移动玩家只需要改playerR和playerC两个变量,不需要在地图数组里来回擦写旧位置。static std::string buffer避免每帧重新分配堆内存,清空后逐字符追加,输出内容固定时也不会有残影。

参数说明:COORD是Windows API的结构体,两个字段分别表示列和行,注意别把{0, 0}写成{0},少一个字段会编译不过。printf("%s", ...)比cout输出一整块字符串快一个量级,游戏循环里每帧一次输出完全够用。不喜欢整帧重绘的话,也可以只重绘玩家旧位置和新位置两个字符,但迷宫本身很小,整帧重绘反而代码更简单。

3.3 方向键输入与游戏主循环:_getch()的两种返回值

方向键在Windows控制台里不是普通字符,_getch()第一次会返回0xE0(有些键盘驱动返回0),第二次才返回真正的扫描码:上72、下80、左75、右77。需要conio.h头文件,这是Windows专属API,跨平台项目可以封装一层接口。

#include <conio.h> // 处理一次方向键移动, 返回是否成功移动 bool movePlayer(std::vector<std::string>& maze, int& pr, int& pc, int key) { int nr = pr, nc = pc; if (key == 72) nr--; // 上 else if (key == 80) nr++; // 下 else if (key == 75) nc--; // 左 else if (key == 77) nc++; // 右 else return false; if (maze[nr][nc] == ' ') { // 只允许走到路格 maze[pr][pc] = ' '; // 旧位置还原成路 pr = nr; pc = nc; return true; } return false; } int main() { int rows = 9, cols = 9; auto maze = generateMaze(rows, cols, 20240301); int pr = 1, pc = 1; // 起点房间 int er = 2 * rows - 1, ec = 2 * cols - 1; // 终点房间 maze[er][ec] = 'E'; for (;;) { renderMaze(maze, pr, pc); int ch = _getch(); if (ch == 0xE0 || ch == 0) { ch = _getch(); // 读真正方向码 } if (ch == 'q') break; if (ch == 72 || ch == 75 || ch == 80 || ch == 77) { movePlayer(maze, pr, pc, ch); } } return 0; }

逻辑说明:_getch()是阻塞式读取,玩家按一下键才跑一圈循环,所以不用额外加Sleep限帧。首字节判断是关键——如果你只判断第二字节,方向键第一次返回的0xE0会进入你的分支条件,结果什么也不匹配,游戏表现为“按键没反应”;如果你完全不判断首字节,方向码读取就会错位。移动后maze[pr][pc] = ' '是把旧的玩家位置还原,因为上次渲染时旧位置在屏幕上显示为@,但地图数组里存的始终是空格,不还原的话下一帧会把@写进地图。

参数说明:主循环里没有做“到达终点”判定,那是留给读者扩展的。实际做的时候可以在movePlayer之后判断pr == er && pc == ec,打印胜利信息后break。20240301是一个固定的随机种子,调试时固定种子能让你每次跑出同一个迷宫,这是后面自动化验证的基础。

4. BFS自动寻路:用一条最短路径验证迷宫连通性

4.1 为什么最短路径用BFS而不是DFS

迷宫生成完,第一个需要验证的问题是:从起点到终点真的连通吗?在“找一条路”这个语义下,DFS也能找到路,但它不保证最短。DFS回溯生成迷宫时,迷宫本身是一棵树,任意两点之间只有唯一路径,所以理论上这条路就是唯一的,不存在“最短”问题,DFS找出来的路也就是那条唯一的路径。

但这里有个微妙之处:一旦你以后换用Prim或递归分割之外的算法,或者手动加了额外通道,迷宫出现环路,DFS找出的路就不再可靠——它会受递归顺序影响,可能绕远路。BFS按层扩展,首次到达终点时就保证是最短路径。这个性质让BFS成为验证连通性的正确工具:如果BFS能到达终点,迷宫连通;如果BFS路径长度异常偏长,说明地图建模有问题。

复杂度上也不用担心。迷宫地图是格子图,每个格子的边数最多4条,BFS的复杂度是O(V+E),在100×100房间格也就是4万个格子量级,控制台上肉眼可见的“瞬间完成”。这也是为什么我给迷宫项目配自动寻路时,首先考虑BFS而不是更花哨的A*——A*多出来的启发式函数在完美迷宫这种小图上几乎没有收益。

4.2 BFS寻路实现:队列、前驱数组与路径回溯

实现BFS需要两样东西:一个队列用于按层遍历,一个parent数组记录每个格子的前驱节点。前者用std::queue,后者用vector<vector<pair<int,int>>>,尺寸和地图一致。

#include <queue> #include <algorithm> // 返回起点到终点的最短路径, 不可达时返回空数组 std::vector<std::pair<int,int>> bfsShortestPath( const std::vector<std::string>& maze, std::pair<int,int> start, std::pair<int,int> end) { int h = (int)maze.size(); int w = (int)maze[0].size(); std::vector<std::vector<std::pair<int,int>>> parent( h, std::vector<std::pair<int,int>>(w, {-1, -1})); std::vector<std::vector<bool>> visited(h, std::vector<bool>(w, false)); std::queue<std::pair<int,int>> q; q.push(start); visited[start.first][start.second] = true; int dr[4] = {-1, 1, 0, 0}; int dc[4] = {0, 0, -1, 1}; while (!q.empty()) { auto cur = q.front(); q.pop(); if (cur == end) break; for (int d = 0; d < 4; ++d) { int nr = cur.first + dr[d]; int nc = cur.second + dc[d]; if (nr < 0 || nr >= h || nc < 0 || nc >= w) continue; if (visited[nr][nc] || maze[nr][nc] == '#') continue; visited[nr][nc] = true; parent[nr][nc] = cur; q.push({nr, nc}); } } // 回溯路径: 从终点倒着找前驱直到起点 std::vector<std::pair<int,int>> path; for (auto cur = end; cur != start; cur = parent[cur.first][cur.second]) { path.push_back(cur); if (cur == std::make_pair(-1, -1)) break; // 不连通 } std::reverse(path.begin(), path.end()); return path; }

逻辑说明:parent里存的是“我从哪里来”,不只是布尔标记。BFS逐层向外扩展时,第一次遇到某个格子时的前驱一定来自最近的层,因此最终回溯出来的路径就是最短路径。回溯时从end出发,每次跳到parent[cur],直到回到start。如果中途遇到{-1,-1},说明生成地图时出问题,终点不在起点所在的连通分量里,此时返回空路径。

参数说明:start和end是地图坐标,不是房间格坐标。调用时传{1,1}和{2*rows-1, 2*cols-1},和主循环里的玩家起点、终点保持一致。跳过条件maze[nr][nc] == '#'是BFS能跑在迷宫地图上的关键,墙格永远不进队列;但要注意出口格E和玩家占位格@在寻路时不会被搜索到——寻路应该跑在原始地图上,而不是渲染后的地图。如果map里已经写了@,BFS会把@当成墙跳过,路径会绕开玩家当前位置,这个细节后面避坑章还会提到。

4.3 把路径标到地图上:一键“作弊”的展示逻辑

拿到路径数组后,把路径上的每个格子标成*,再渲染一帧,就能在控制台上肉眼确认迷宫生成得对不对。这个功能也是游戏里“自动寻路”或“作弊模式”的雏形。

void markPath(std::vector<std::string>& maze, const std::vector<std::pair<int,int>>& path, char marker = '*') { for (auto& p : path) { int r = p.first; int c = p.second; if (maze[r][c] == ' ' || maze[r][c] == '*') { maze[r][c] = marker; } } }

逻辑说明:标路径时要跳过E和玩家位置,否则会把出口字符覆盖掉。marker参数默认*,你也可以标成.来区分“路径”和“玩家能走的区域”。隐藏路径时遍历同样的路径数组把*重新还原成' '即可,不需要再跑一次BFS。

参数说明:maze以非常量引用传入,说明这个函数会直接改地图。一个工程上的好习惯是:寻路结果只用于展示,不要在修改后的地图上再次寻路。如果你按一次键就显示路径,再按一次隐藏,地图数据会在原始状态和标记状态之间来回切换,保证隐藏时全部还原成空格就不会出错。我习惯把这个功能绑定在Tab键上,结合方向键玩家可以边看边学路线。

5. C++迷宫小游戏避坑指南:随机数、乱码与爆栈的排查

5.1 同一个迷宫重复出现:srand(time(0))的秒级陷阱

现象:程序每次启动生成的迷宫一模一样,或者在同一秒内反复启动结果完全相同,玩家炒几次迷宫发现摆烂都一样。

原因:rand()配合srand(time(0))初始化随机数时,time(0)精确到秒。同一秒内启动的所有进程种子相同,rand()产生的序列也完全一致。更隐蔽的是,如果你用rand() % 4选方向,在迷宫本来就需要收缩取值范围时,取模偏差也会让某些迷宫形态出现概率异常。

解决:用C++11的<random>头文件。std::mt19937 gen(seed)配合std::uniform_int_distribution<int> pick(0, 3),质量远超rand()。调试期固定传入一个常数种子(比如20240301),发布时用std::random_device{}()生成熵。我在生成函数里保留seed参数,就是为了让迷宫可复现。注意mt19937的种子类型是unsigned,从命令行读字符串时要std::stoul转换,不是stoi。

5.2 控制台中文乱码:字符集与代码页

现象:用中文给游戏加提示文字,输出变成一片乱码,迷宫地图本身也被挤得对不齐。走迷宫的时候墙体字符错位,玩家位置看起来跟实际坐标不一致。

原因:Windows控制台默认代码页是936(GBK),而用VSCode配置C/C++环境时源文件默认以UTF-8保存。当源文件里的中文字符串以UTF-8编码存储、但控制台按GBK解码时,多字节序列就被拆成了乱码。迷宫游戏里如果连地图字符都用中文,后果更严重——一个中文字符的UTF-8是3字节,在控制台上占2列,地图行宽直接错乱。

解决:最省心的方案是迷宫本体只用#、空格、@、E这些单字节ASCII字符。游戏提示信息如果要中文,在main开头调用SetConsoleOutputCP(65001)把控制台代码页切成UTF-8,或者外部执行chcp 65001。我自己的血泪经验是:迷宫生成和渲染的代码不要依赖任何中文字符,这是这类小游戏最不容易踩坑的写法。

5.3 方向键读取翻车:_getch()的扫描码边界

现象:按方向键游戏没反应,或者按一次动两格,甚至按q退出时被识别成上方向。

原因:方向键在Windows控制台是两字节序列:第一个字节0xE0(或0x00),第二个字节才是真正的扫描码。代码里如果只调用一次_getch()然后直接和72比较,读到的是0xE0,当然匹配不上。如果完全忽略首字节、直接拿第二次的扫描码当普通键,那q的正常单字节读取也会被误判。

解决:每次读取先判断首字节。int ch = _getch(); if (ch == 0xE0 || ch == 0) ch = _getch();这行是标准做法。之后判断72/75/80/77四个方向码,q的ASCII是113,走else分支就不会冲突。写测试的时候注意:方向键在有的远程终端或IDE集成终端里行为不一致,我一般先在原生控制台里验证输入,再挪到VSCode的集成终端里调试。

5.4 大尺寸迷宫爆栈:递归版DFS的深度问题

现象:把rows和cols调到50以上,程序运行几秒后直接闪退,或报出栈溢出错误。30以下没事,60以上必现。

原因:递归实现的DFS回溯深度等于当前探索路径的深度,在迷宫这种可以一条路走到黑的结构里,最坏情况深度接近房间格总数。每层递归占用栈帧,Windows默认栈大小约1MB,几千上万层递归就会溢出。迭代版用std::stack(注意是容器适配器,内部在堆上分配)替代函数调用栈,深度不再受线程栈限制。

解决:生成迷宫一律用迭代栈版本,就是我第2章给的代码。std::stack内部容器默认是deque,节点分配在堆上,只受内存总量约束。像50×50房间格,栈深最大2500层,对迭代版本来说完全无感。寻路的BFS本来就是队列迭代实现,不存在爆栈问题;真正需要检查的是递归版的生成函数、以及如果你把DFS写成递归版来做连通性测试时的隐患。

5.5 终点设在墙格里

现象:生成的迷宫能看到E标记,但玩家走到E旁边时被弹回来,永远到不了终点,BFS寻路也返回空路径。

原因:有人图省事直接在右下角maze[2*rows-1][2*cols-1] = 'E',但这个坐标在边界上,恰好是墙格#的位置。移动判定maze[nr][nc] == ' '会拦住玩家,因为E不是空格。入口和出口必须设置在房间格上,也就是坐标必须同时满足行和列都是奇数的格子。

解决:起点固定{1, 1},终点固定{2*rows-1, 2*cols-1},这两个坐标永远是房间格。如果你玩“随机出口”,就不要在房间里硬放E,而是先确认目标格是' '再覆盖。我一般把出口标记放在渲染层处理,和玩家@一样,不给地图数据里混入不可通行的特殊字符,这样移动逻辑和寻路逻辑都不用关心出口字符。

6. 参数化尺寸与自动验证:把迷宫项目收尾成可维护的代码

6.1 用命令行参数控制行数、列数与随机种子

迷宫生成函数已经接受rows、cols、seed三个参数,main函数把它们暴露成命令行参数就够用了。--rows 15 --cols 20 --seed 42这种风格解析起来最简单,不需要依赖第三方库。

#include <string> int main(int argc, char* argv[]) { int rows = 9, cols = 9; unsigned seed = std::random_device{}(); for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if (arg == "--rows" && i + 1 < argc) { rows = std::stoi(argv[++i]); } else if (arg == "--cols" && i + 1 < argc) { cols = std::stoi(argv[++i]); } else if (arg == "--seed" && i + 1 < argc) { seed = static_cast<unsigned>(std::stoul(argv[++i])); } } // 解析后用 rows/cols/seed 生成迷宫并进入主循环 }

逻辑说明:参数在循环里挨个扫,遇到已知参数名就消费后面的值。std::stoi会自动跳过前导空格,但要注意它碰到非法输入会抛异常,这里没有包try-catch,实际使用时建议包一层,或者用std::from_chars做更严格的解析。seed用std::stoul而不是stoi,因为种子是无符号整数。

6.2 批量生成与连通性回归:让寻路失败无处可藏

参数化带来的最大收益是自动化测试。我每次改完生成算法,都会跑一遍批量验证:循环100个种子,每个种子生成一张迷宫,然后跑BFS检查起点到终点是否连通。只要有一个种子生成的地图断的,就能立刻定位到算法改动引入的问题。

void runRegression(int rows, int cols, int samples) { std::random_device rd; for (int i = 0; i < samples; ++i) { unsigned seed = rd(); auto maze = generateMaze(rows, cols, seed); auto path = bfsShortestPath(maze, {1, 1}, {2 * rows - 1, 2 * cols - 1}); if (path.empty()) { printf("seed %u: 起点到终点不可达\n", seed); } } printf("回归完成: 共 %d 个样本\n", samples); }

逻辑说明:连通性靠BFS的结果判断,比人眼盯屏幕可靠得多。100个种子跑完只需要几秒钟,代价可以忽略。这个函数独立于游戏入口存在,我在main里加一个--check分支,解析到该参数时就跑回归测试然后直接退出,不进游戏循环。这样每次改完代码只需要mazeGame --check --rows 30 --cols 30 --samples 200一条命令。

6.3 一个值得养成的习惯

我自己现在写迷宫类程序,第一步永远是把命令行参数和--check分支加好,再动生成算法。先让种子可复现,再让验证可自动,最后才谈新功能。没有这两道保险就改算法,出问题只能靠肉眼盯屏幕猜,那才是真的黑匣子。迷宫小游戏这个方向值不值得投入,我的答案是值得——它把STL容器、图遍历、控制台API、参数化设计四个C++高频考点串成一条线,而且每步都有肉眼可见的反馈。希望帮到你。

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

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

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

立即咨询