公司动态
扫雷游戏项目
扫雷游戏 —— 项目设计报告一、游戏简介扫雷是一款经典的单人益智游戏最早在 Windows 3.1 时代随系统附带而广为人知。游戏的目标很简单在不踩到地雷的前提下翻开棋盘上所有安全格子。基本规则如下棋盘由若干格子组成其中随机分布着一定数量的地雷。玩家每次点击输入一个坐标翻开该格子。如果翻开的是地雷游戏结束。如果翻开的不是地雷格子上会显示该位置周围 8 个相邻格子中地雷的总数帮助玩家推断哪些格子是安全的。当所有非雷格子都被翻开时玩家获胜。本项目实现的是一个基于控制台的 9×9 扫雷共 10 颗地雷玩家通过输入坐标来进行游戏。二、项目文件结构本项目采用 C 语言编写源文件一共三个文件名职责game.h头文件定义宏常量、全局变量声明、函数声明game.c核心逻辑实现棋盘初始化、布雷、翻开格子、游戏主循环test.c测试与入口包含main函数、菜单界面、游戏启动流程三个文件各司其职test.c负责程序入口和交互入口game.c负责游戏算法和核心逻辑game.h则把二者连接起来。三、数据结构设计3.1 双棋盘设计扫雷的核心设计思路是“双棋盘”——程序内部维护两块同样大小的二维数组charmine_board[SIZE][SIZE];// 底层地雷盘存放真实地雷分布charshow_board[SIZE][SIZE];// 玩家显示盘存放玩家能看到的信息mine_board是上帝视角的地雷分布图只有程序自己读写玩家看不到。初始化时所有格子设为0布雷时将地雷位置设为*。show_board是玩家实际看到的界面初始化时所有格子设为*代表未翻开随着游戏推进逐步被替换为具体数字或空格。这种分离有两个好处一是保证玩家无法通过查看内存作弊二是逻辑上把真实状态和显示状态解耦代码更清晰。3.2 为什么用char而不是int棋盘元素使用char类型而不是int原因是棋盘需要显示字符未翻开用*地雷用*翻开后用数字字符0~8。使用char可以直接存字符打印时无需额外转换代码更简洁。数字的转换利用了 ASCII 码的特性show[x][y]cnt0;// cnt 是 0~8 的整数加上 0 后变成对应字符这是一个很常用的技巧——字符0的 ASCII 码是 481是 49以此类推所以整数加0就能得到对应字符。四、核心功能实现4.1 棋盘初始化voidinit_board(charboard[SIZE][SIZE],charval){memset(board,val,SIZE*SIZE);}使用memset将整个棋盘填充为指定字符。mine_board初始化为0无雷show_board初始化为*全未翻开。memset按字节操作对于char数组来说刚好适用。4.2 随机布雷voidset_mine(charmine[SIZE][SIZE]){srand((unsignedint)time(NULL));intcount0;while(countMINE_NUM){intxrand()%SIZE;intyrand()%SIZE;if(mine[x][y]!*){mine[x][y]*;count;}}}这段代码的逻辑很直观用srand(time(NULL))以当前时间作为随机种子保证每次运行游戏地雷分布不同。循环生成随机坐标(x, y)范围是0到SIZE-1。如果该位置还没有布雷不是*就放置一颗地雷然后count加一。重复直到布够MINE_NUM10 颗地雷。这里有一个细节如果随机到的位置已经有雷就跳过不计入 count所以虽然rand()可能重复但最终一定会布够 10 颗雷。不过在雷数接近格子总数时效率会变低但对于 9×9 只有 10 颗雷的场景来说完全不是问题。4.3 翻开格子周围雷数计算voidopen_cell(charmine[SIZE][SIZE],charshow[SIZE][SIZE],intx,inty){if(x0||xSIZE||y0||ySIZE)return;if(show[x][y]!*)return;intcnt0;for(intix-1;ix1;i){for(intjy-1;jy1;j){if(i0iSIZEj0jSIZE){if(mine[i][j]*)cnt;}}}show[x][y]cnt0;}这是扫雷中最关键的算法之一。它的任务是给定一个坐标(x, y)统计它周围 3×3 范围内含自身有多少颗地雷然后把结果显示到show_board上。具体步骤边界检查如果坐标越界直接返回。重复检查如果该格子已经翻开不是*也不需要处理。遍历相邻格子用两层循环遍历(x-1, y-1)到(x1, y1)的 9 个位置跳过越界的位置统计其中地雷的数量。写入结果cnt 0将数字转为字符写入show_board。需要指出的是当前实现只翻开单个格子。标准扫雷中如果翻开的格子周围雷数为 0应该自动展开周围所有相邻格子递归展开 / 泛洪填充。本项目暂未实现这个功能后续可以在open_cell中加入递归调用来完善。4.4 游戏主循环voidgame_main(charmine[SIZE][SIZE],charshow[SIZE][SIZE],intx,inty){intnum0;while(SIZE*SIZE-MINE_NUM-num){printf(请输入坐标);scanf(%d %d,x,y);if(show[x][y]!*){printf(已经验证过了,试试别的值吧\n);}if(mine[x][y]*){printf(踩雷游戏结束\n);print_board(mine);exit(1);}else{open_cell(mine_board,show,x,y);print_board(show);num;}}printf(------恭喜你过关-----\n);}游戏主循环的逻辑循环条件是SIZE * SIZE - MINE_NUM - num即总格子数减去雷数再减去已翻开数只要不为 0 就继续。每次循环接收玩家输入的坐标。如果该格子已经翻开提示玩家重新选择。如果踩到雷打印地雷盘并退出程序。如果是安全格子调用open_cell处理并打印显示盘已翻开数加一。当所有安全格子都被翻开打印过关信息。4.5 菜单与入口intmain(){intnum;do{menu();printf(请输入你的选择);scanf(%d,num);switch(num){case1:game();break;case0:break;}}while(num);return0;}main函数通过do-while循环实现了一个简单的菜单系统显示菜单 → 接收选择 → 执行对应操作 → 循环直到选择退出。进入游戏时调用game()函数完成棋盘初始化、布雷和游戏循环的全部流程。五、程序运行流程整体运行流程可以用下面的流程图来概括程序启动 │ ▼ 显示菜单1. 开始游戏 / 0. 退出 │ ▼ 玩家选择 │ ├── 选择 0 → 退出程序 │ └── 选择 1 → 进入 game() │ ▼ 初始化两个棋盘 mine_board 全 0show_board 全 * │ ▼ 打印初始 show_board │ ▼ 随机布雷set_mine │ ▼ 进入游戏循环game_main │ ▼ 玩家输入坐标 │ ├── 踩雷 → 打印 mine_board游戏结束 │ └── 安全 → open_cell 计算周围雷数 更新 show_board 并打印 检查是否全部翻开 │ ├── 未完成 → 继续输入 └── 全部翻开 → 恭喜过关六、代码中的不足与改进方向在阅读和分析代码的过程中也发现了一些可以改进的地方6.1 全局变量的使用game.h中声明了全局变量intx;inty;charmine_board[SIZE][SIZE];charshow_board[SIZE][SIZE];全局变量会导致多个函数共享同一份数据虽然在这个小程序中问题不大但从工程规范上来说建议把这些变量移到main或game函数内部通过参数传递给各子函数这样代码的可维护性和可测试性会更好。6.2 缺少坐标合法性校验玩家输入坐标后代码没有检查x和y是否在合法范围内0到SIZE-1。如果输入了超出范围的值直接访问数组会导致越界可能产生不可预期的行为。建议在接收输入后加入范围校验。6.3 翻开逻辑的局限当前open_cell只处理单个格子没有实现零值自动展开。标准扫雷中当翻开的格子周围雷数为 0 时应该自动递归翻开所有相邻格子直到遇到雷数不为 0 的边界。这个功能可以用递归或 BFS 实现能大幅提升游戏体验。6.4 缺少插旗功能标准扫雷允许玩家标记疑似地雷的位置插旗帮助记忆和推理。当前版本没有这个功能可以通过在show_board中使用特殊字符如F来表示旗帜标记。6.5 胜利判定的潜在问题胜利条件while (SIZE * SIZE - MINE_NUM - num)依赖num的计数。但当前代码中如果玩家输入了已经翻开的坐标num不会增加循环条件仍然正确。不过更健壮的做法是直接统计show_board中未翻开且非雷的格子数而不是依赖计数器。七、总结本项目实现了一个基础但功能完整的控制台扫雷游戏。通过双棋盘设计将底层数据与显示分离利用memset初始化、rand随机布雷、双重循环统计周围雷数等手段用不到 200 行代码完成了扫雷的核心逻辑。