简介:这是一个面向C语言与数据结构课程设计场景的老鼠走迷宫游戏升级版方案,定位于帮助计算机专业学生完成迷宫类项目的设计与实现。程序运行后展示迷宫地图,玩家可用键盘方向键操控老鼠在限定时间内抵达粮仓,并具备结果判定、编辑迷宫(墙变路、路变墙)以及找出全部路径和最短路径等核心功能。资源包共5个文件,以C++源文件、可直接运行的exe程序和3个迷宫文本数据文件构成,压缩后仅50KB,文件结构紧凑,便于对照源码与迷宫数据进行学习。该资源已有3889人学习下载,适合作为数据结构课程设计、迷宫算法练习或C语言项目实战的参考。通过学习可掌握迷宫建模、深度/广度遍历搜索、最短路径求解以及交互式地图编辑等关键技能,直接复用或扩展为完整课程设计成果。
1. 老鼠走迷宫游戏课程设计:为什么值得把这份C语言源码留下来
很多人在期末翻车,不是卡在“找一条路”,而是卡在“换了个迷宫文件就完全跑不动”。这份老鼠走迷宫游戏升级版课程设计,正好是那种能直接跑、能换地图、能讲清楚栈和回溯的C语言数据结构项目。它用二维数组模拟迷宫,用栈实现深度优先搜索,每一步都有对应的代码和注释,适合大二做课程设计的学生,也适合想把栈、文件操作、二维数组一次性串起来的C语言自学者。下载后拿到的不只是一段源码,还有迷宫文件和一个可以对着讲满十分钟答辩的完整逻辑链路。
2. 源码结构和运行入口:从下载到跑通只花三分钟
拿到压缩包先别急着双击打开 main.c,这种课程设计源码包通常已经按“迷宫模块 + 栈模块 + 主流程”拆好了文件。先花三分钟看懂文件安排,后面改代码时就不会迷路。
2.1 文件清单:maze.h、stack.h、main.c 各自负责什么
一份能通过课程设计答辩的C语言迷宫项目,至少要包含以下文件:
main.c:入口文件,负责接收迷宫文件路径,调用加载函数,执行寻路,最后打印结果。maze.h/maze.c:定义迷宫二维数组、行列数、文件读取函数、坐标定位函数。stack.h/stack.c:用数组模拟栈,提供push、pop、isEmpty操作。maze1.txt、maze2.txt:两个迷宫地图文件,通常一个是简单迷宫,一个是带环路的地图。
这个分层方式对课程设计特别有利。答辩时老师问“你的栈在哪实现”,你直接把stack.c打开就行,而不是在一千行里翻找。如果你打算加链表或者队列,也只需要改stack.h对应的接口,main.c 不用动。
这里有个选型细节:很多同学喜欢直接调系统栈写递归版的 DFS,代码确实短,但课程设计通常要求体现“数据结构”的知识点,老师更希望看到显式的栈结构。显式栈还有个好处,就是可以用打印栈顶坐标的方式实时观察回溯过程,现场演示比口头解释有说服力。
2.2 编译与运行:gcc 一条命令跑通
假设所有文件都解压到了同一个目录,打开终端或命令行窗口,进入该目录后执行:
gcc -std=c11 -Wall main.c maze.c stack.c -o maze_game ./maze_game maze1.txt-std=c11指定使用 C11 标准,-Wall可以显示所有警告,这个参数强烈建议保留。我第一次读这个资源时,就是靠-Wall发现一个函数里定义了变量但没用,编译器报告后才避免答辩时被问“这段代码里那个变量是干啥的”。编译生成了maze_game可执行文件,运行时把迷宫文件名作为参数传进去。
如果你用的是 Dev-C++ 或者 Visual Studio,就别用命令行编译了,直接建一个空项目,把三个.c文件和两个头文件拖进工程,在运行时参数里填maze1.txt即可。注意 Dev-C++ 默认使用的 GCC 版本较老,-std=c11在旧版本上不识别,这时可以去掉这个参数,只保留-Wall。
2.3 迷宫文件的格式约定
迷宫文件本质上就是一个纯文本地图,资源包里的 txt 文件遵守一套约定:第一行是两个整数,代表行数和列数;从第二行开始,每行是一个字符串,字符含义如下:
#:墙,老鼠不能走- 空格或者
.:通道 S:老鼠的起点E:出口
一个典型的迷宫文件长这样:
8 10 ########## # S # # # ## # ## # # E# ##########这里容易忽略的是坐标约定。我解析文件时统一把x当作行号,y当作列号,那么上面这个地图里S的坐标是(1, 2)。如果你换成别的课设代码,可能有人把方向数组写成[2][1],表示第 2 行第 1 列,这就是行列序不一致。拿到资源后第一步建议先打印一遍start_x、start_y、end_x、end_y,确认坐标系符合预期,再谈寻路正确性。
3. 核心算法:栈模拟回溯,把“撞墙后退换方向”翻译成C语言
迷宫寻路的本质是深度优先搜索。老鼠走到岔路口,随便选一个方向走,撞墙就退回岔路口换一个方向。这个“退回岔路口”的动作,在数据结构里就是栈的弹出操作。如果把这段逻辑讲清楚,课程设计的核心分基本就拿到了。
3.1 方向数组与二维数组建模
迷宫整体用一个二维字符数组保存,方向用二维数组表示。代码定义如下:
#define MAX_ROWS 100 #define MAX_COLS 100 char maze[MAX_ROWS][MAX_COLS]; int rows, cols; int start_x, start_y, end_x, end_y; // 四个方向:上、下、左、右 int dir[4][2] = { {-1, 0}, {1, 0}, {0, -1}, {0, 1} };方向数组不是随便写的。dir[0]表示行坐标减 1,也就是往地图上方移动一格;dir[1]表示行坐标加 1,往下方移动;dir[2]和dir[3]分别表示左和右。搜索时用循环依次尝试这四个方向,而不是手动写四段 if 分支。这样代码短,也方便调整方向优先级。如果希望老鼠优先往右走,就把{0, 1}提到数组最前面。
3.2 用数组手写栈,记录“从哪里来、试到哪个方向”
栈元素不能只存坐标,还要存一个方向下标。这一步是很多简化版代码容易偷懒的地方。如果只存坐标,回溯时会忘记哪个方向已经试过,结果原地重复试同一格,死循环。合理定义如下:
typedef struct { int x; int y; int dir; // 下一次准备尝试方向的下标,0~3 } StackItem; StackItem stack[MAX_ROWS * MAX_COLS]; int top = -1; void push(int x, int y, int d) { top++; stack[top].x = x; stack[top].y = y; stack[top].dir = d; } StackItem pop(void) { StackItem item = stack[top]; top--; return item; } int isEmpty(void) { return top == -1; }栈容量直接设为MAX_ROWS * MAX_COLS,意思是每个格子最多入栈一次,不会溢出。dir在入栈时初始化为 0,表示接下来从方向 0 开始试探。这个字段会在后面的回溯循环中被不断递增,直到 4 就说明四个方向都试完了。
3.3 回溯搜索主流程:尝试、入栈、出栈、标记
核心搜索函数是一个 while 循环,处理栈顶元素,按方向依次试探邻居。
int findPath(void) { top = -1; push(start_x, start_y, 0); maze[start_x][start_y] = '.'; // 标记起点已走过 while (!isEmpty()) { StackItem *cur = &stack[top]; // 只读栈顶,不着急弹 if (cur->x == end_x && cur->y == end_y) { return 1; // 已经到出口 } int found = 0; while (cur->dir < 4) { int nx = cur->x + dir[cur->dir][0]; int ny = cur->y + dir[cur->dir][1]; cur->dir++; // 方向指针后移 if (nx < 0 || nx >= rows || ny < 0 || ny >= cols) continue; if (maze[nx][ny] == '#' || maze[nx][ny] == '.') continue; // 空格或者 E 都可以走 if (maze[nx][ny] == 'E' || maze[nx][ny] == ' ') { push(nx, ny, 0); maze[nx][ny] = '.'; found = 1; break; } } if (!found) { // 四个方向都不通,回溯 pop(); } } return 0; }这段代码的难点在于什么时候弹出栈。当cur->dir递增到 4 时,说明当前格子四个方向都已经尝试过,found仍为 0,于是pop()回退到上一个岔路口。注意cur是指向stack[top]的指针,cur->dir++会真正修改栈顶元素里的方向值,这也是它能持续推进的原因。把新位置压栈前立刻标记.,这一步非常重要,标注“已经访问过”,否则两个相邻格子会互相入栈,直接死循环。
如果你正在看严蔚敏《数据结构(C语言版)》第三章,会发现这个迷宫寻路就是典型栈应用的变体。基础版的课程设计到这里已经可以交差。升级版常见做法是:找到出口后不急着 return,而是用一个计数器累加,同时把路径信息保存下来,这样就能统计“一共有几条通路”。我在自己做的升级版里就是加了一个pathCount,每次到出口加一,继续回溯搜索下一分支。
3.4 为什么强调“入栈时标记”而不是“出栈时标记”
很多初学版本会在进while循环后再给maze[nx][ny] = '.',甚至等到pop时才标记。这样会有个隐蔽问题:A 格和 B 格相邻,A 把 B 压入栈后,还没轮到 B 执行,A 又从另一个方向看到了 B 没有被标记,于是再次压入。最终结果就是栈里全是重复坐标,路径变成一圈圈原地绕。正确做法是,在邻居满足可走条件的那一刻立刻标记,这样其他格子不会再试探它。
4. 迷宫文件与地图设计:拿到迷宫文件后怎么改、怎么造新地图
跑通一遍之后,大多数人要做的第一件事是换自己的地图。迷宫文件不是随便改改就能用,有几个约束条件需要先明白。
4.1 字符地图的约束条件
一张合法的迷宫地图,首先最外层建议全部用#封闭,否则老鼠可以直接沿着地图边界走到终点,完全体现不了回溯过程。其次是S和E各只能出现一个,程序一般用遍历找坐标,如果地图里有两个E,循环结束后end_x只会保存最后一个,结果不可预测。
行列数也不能超过源码里的MAX_ROWS和MAX_COLS。资源包自带的地图通常在 100 格以内,课程设计演示完全足够,但你自己用 Excel 拖一张 200 列的地图,加载程序就会报告“迷宫大小超出限制”。
手工设计迷宫时,我习惯先把S和E之间的主线道路画出来,保证至少有一条通路,然后再用#把其余区域填充成墙。先有主线、后有墙体,这样不会出现一上来就把路堵死的情况。
4.2 解析函数:fgets 读到换行符别让坐标错乱
迷宫文件读取是另一个高频翻车点。最常见的解析方式是先用fscanf读第一行的行列数,再用fgets逐行读地图。代码示例如下:
int loadMaze(const char *filename) { FILE *fp = fopen(filename, "r"); if (!fp) { printf("无法打开文件 %s\n", filename); return -1; } if (fscanf(fp, "%d %d\n", &rows, &cols) != 2) { printf("文件头格式错误\n"); fclose(fp); return -1; } if (rows > MAX_ROWS || cols > MAX_COLS) { printf("迷宫大小超出限制\n"); fclose(fp); return -1; } for (int i = 0; i < rows; i++) { if (fgets(maze[i], MAX_COLS, fp) == NULL) { printf("读取第 %d 行失败\n", i); fclose(fp); return -1; } // 去掉行尾换行符,防止边界判断出错 size_t len = strlen(maze[i]); if (len > 0 && maze[i][len - 1] == '\n') maze[i][len - 1] = '\0'; // Windows 文件里常常还带 \r if (len > 1 && maze[i][len - 2] == '\r') maze[i][len - 2] = '\0'; for (int j = 0; j < cols; j++) { if (maze[i][j] == 'S') { start_x = i; start_y = j; } if (maze[i][j] == 'E') { end_x = i; end_y = j; } } } fclose(fp); return 0; }这里有两个关键参数。第一个是fscanf(fp, "%d %d\n", &rows, &cols)末尾的\n,它会读掉第一行末尾的换行符,但不会读掉\r。所以我在fgets之后连续两次截断,先删\n再删\r。第二个参数是fgets的第三个参数MAX_COLS,它代表最多读MAX_COLS - 1个字符。如果地图列数刚好等于MAX_COLS,一行会被拆成两行读取,后续行全部错位。用自己的地图时,务必保证cols < MAX_COLS。
4.3 迷宫的合法性检测:不能只靠人眼看
在进入寻路前加一个checkMaze函数,是课程设计加分项。它负责检查字符是否合法、起点终点数量是否正确:
int checkMaze(void) { int startCount = 0, endCount = 0; for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { if (maze[i][j] == 'S') startCount++; if (maze[i][j] == 'E') endCount++; if (maze[i][j] != '#' && maze[i][j] != ' ' && maze[i][j] != '.' && maze[i][j] != 'S' && maze[i][j] != 'E') { printf("第%d行第%d列存在非法字符\n", i, j); return -1; } } } if (startCount != 1 || endCount != 1) { printf("起点或终点数量不正确\n"); return -1; } return 0; }这个函数的作用是让错误暴露在寻路之前。答辩现场老师经常直接修改 txt 文件、故意放一个非法字符,程序报错信息是否清晰,直接决定老师对你的项目完成度的判断。
5. 常见问题与避坑指南:这份C语言迷宫源码最容易翻车的4个地方
5.1 只找到一条路径就返回,完全没体现回溯过程
现象:程序能跑通,但路线是直线,看不到任何“退回去换条路”的过程,答辩效果很差。
原因:代码里一遇到出口就return 1,而且已经用.标记了路径,其他分支永远不会被探索。
解决:把出口处的return改成nearExit = 1;并继续循环,或者额外用一个pathCount变量记录出口次数。每次到出口时,先把当前栈里的坐标打印出来,再继续回溯寻找下一条路。这样既能演示回溯,又能展示多条路径。
5.2 路径穿墙或数组越界崩溃
现象:打印地图时,老鼠走过的路径直接穿过了#墙,或者程序运行一会儿后异常退出。
原因:方向数组计算出的新坐标没有同时判断上界和下界。很多代码只写了if (nx < 0) continue;,却漏了if (nx >= rows) continue;,当老鼠走到最下边时就越界了。
解决:每个邻居坐标都统一做四界判断:
if (nx < 0 || nx >= rows || ny < 0 || ny >= cols) continue;而且要在数组下标maze[nx][ny]之前做判断,不要先访问再判断。
5.3 栈不断增长,程序卡死
现象:迷宫很小,但程序运行几十秒都没有结果,像陷入死循环。
原因:最常见的是漏了“已访问”标记,导致两个相邻空格互相入栈。另一个原因是栈元素里的dir字段没有初始化为 0,回溯时从随机值开始试探,方向判断永远不对,栈无法回退。
解决:在push(nx, ny, 0)入栈的一瞬间就写maze[nx][ny] = '.'。同时检查push函数里是否把dir赋值为 0。我用调试手段是在每次push后打印一次stack[top].x, stack[top].y, stack[top].dir,肉眼跟踪几轮就能看出问题。
5.4 迷宫文件第一行读到乱码,起点坐标错乱
现象:同样的 txt 文件在记事本里看是正常的,程序加载后起点跑到奇怪的位置,地图第一行看起来有不可见字符。
原因:文件被保存成了 UTF-8 with BOM 编码,文件头多出EF BB BF三个字节,fscanf读第一行行列数时解析失败。另一个常见原因是 Windows 下换行符是\r\n,如果加载函数只处理了\n,每行地图末尾会残留一个\r,最后一列全部判断失败。
解决:用 Notepad++ 或 VS Code 把地图文件另存为 “UTF-8 without BOM” 或 “ANSI” 编码。代码里在fgets后处理\r,具体写法在 4.2 节已经给出。课程设计交作业时,我一般习惯把所有地图文件统一转成 ANSI 保存,兼容性最好,也免得在老师机器上乱码。
6. 进阶:把“找一条路”升级成“找最短路”,并验证算法够不够稳
资源包里的源码默认是深度优先搜索,适合演示回溯。但实际游戏里的老鼠走迷宫,有时候想要的是“最短路径”。这个进阶功能不需要改迷宫结构,只需要把栈换成队列。
6.1 BFS 改造思路
广度优先搜索用队列,每一步扩展四个方向的邻居,先到达出口的那一次就是最短步数。队列可以继续用数组模拟:
typedef struct { int x, y, step; } Node; Node queue[MAX_ROWS * MAX_COLS]; int head = 0, tail = 0; void enqueue(int x, int y, int step) { queue[tail].x = x; queue[tail].y = y; queue[tail].step = step; tail++; } Node dequeue(void) { return queue[head++]; }搜索时从起点出发,把四个方向的可走点全部入队,依次处理。遇到E就输出当前的step + 1,因为从当前格子再走一步就到出口。要把路径画出来,还需要一个prev数组记录每个格子的前一个坐标,类似“指路牌”,最后从出口倒推到起点。
BFS 的坑在于队列容量。同一层的节点会同时入队,容量必须大于地图总格子数。我习惯在enqueue时加一个判断,如果tail >= MAX_ROWS * MAX_COLS就退出并提示,免得tail越界把别的内容覆盖了。
6.2 验证算法正确性的固定套路
我写完任何一份迷宫搜索代码,都会用三张固定地图做回归测试。
第一张是简单直线地图,从S到E中间没有岔路,结果必须只有一条路径。第二张是无解地图,出口周围全部用#围死,程序要正常输出“找不到路径”而不是崩溃。第三张是带环地图,故意让路径中有一段环形通道,检验maze[nx][ny] = '.'的标记是否挡住回头路。
验证路径长度时,我会手工数一遍打印出来的坐标点个数,再和 BFS 输出的step对比。如果对不上,大概率是出口位置的步数加一减一没算对。换地图跑的时候,优先看起点和终点坐标的打印结果,如果和 txt 文件里的位置不一致,先回看文件头的fscanf格式,再查坐标原点是否被约定成左上角。多数迷宫源码的坐标原点都约定为左上角,也就是第一行第一列是(0,0),这点不要乱改。
还有一个演示技巧:在做课程设计时,把深度优先和广度优先都实现出来,用同一张地图跑两个结果。深度优先路径通常不走运地绕一大圈,广度优先则直接输出最短路,两个结果一对比,老师立刻知道你没背答案。
从那以后我每次交迷宫题作业,都会强制走一遍“检查 BOM、清理换行符、四界判断、栈顶方向初始化”这四件事,减少很多不必要的返工。希望帮到你。
本文还有配套的精品资源,点击获取