【C语言游戏】扫雷游戏(含递归展开,难度选择,雷位标记等功能)
一、项目介绍
1. 游戏功能
1. 难度选择:
提供了easy,normal,hard三种难度。棋盘大小,雷数随难度增加而增大。
2. 为棋盘动态分配内存,模拟实现二维数组。
3. 自动调整窗口大小:
使窗口容纳下相应难度的棋盘
4. 标记雷位:
玩家可以对自己确定的雷位进行标记,已经标记的雷位不能翻开。防止遗忘和误输造成的错误
5. 递归展开
1>> 检查选定的位置周围有没有雷
2>> 如果没有雷自动翻开周围的位置
3>> 重复递归进行以上步骤,就可以造成翻开一片的效果
6. 优化游戏界面,提高游戏体验
二、设计思路
1. 准备工作
1>> 两个棋盘:一个棋盘负责向玩家展示,另一个棋盘放置炸弹。
1>> 分配内存:难度不同,棋盘的大小也不同,所以应该根据需要动态分配棋盘的内存空间
2>> 初始化:分配好内存空间后,初始化两个棋盘的内容
3>> 放置炸弹:开始游戏前,在随机位置安放预定数量的炸弹。
4>> 释放内存:不同于一维动态内存空间,二维的动态内存应先释放低维空间,再释放高维空间
2. 游戏主体流程图

三、完整代码详解
//文件包含
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include //定义标识符,统一控制空格
#define MENUBLANK "\t\t"
#define BOARDBLANK "\t"
1. main函数
处理玩家选项,决定游戏难度或是退出程序。
enum option{EXIT,EASY,NORMAL,HARD
};int main(){enum option input;srand((unsigned)time(NULL));do{//**修改窗口颜色,大小**GameInterface(0);//打印菜单Menu();//处理玩家选项printf("请选择:");fflush(stdin);scanf("%d", &input);switch (input){case EASY://根据玩家所选难度调整窗口大小,以容纳整个棋盘GameInterface(EASY);//Playgame函数的三个参数:行,列,雷数Playgame(9, 9, 5);system("pause");break;case NORMAL:GameInterface(NORMAL);Playgame(16, 16, 30);system("pause");break;case HARD:GameInterface(HARD);Playgame(16, 30, 3);system("pause");break;case EXIT:break;default:printf("输入错误,请重新输入!\n");system("pause");break;}} while (input);
}
2. GameInterface函数
根据游戏难度调整窗口大小,修改窗口背景色
void GameInterface(int cmd){switch (cmd){//难度不同,棋盘大小也不同case EASY:system("mode con cols=55 lines=30");break;case NORMAL:system("mode con cols=85 lines=45");break;case HARD:system("mode con cols=140 lines=45");break;default:system("color B0");system("mode con cols=55 lines=25");break;}
}
3. Menu函数
打印菜单以供玩家选择
void Menu(){system("cls");printf("\n\n\n\n\n");printf(MENUBLANK" Mine Sweeper\n");printf("\n\n");printf(MENUBLANK"*************************\n");printf(MENUBLANK"****** 1.Easy *****\n");printf(MENUBLANK"*************************\n");printf(MENUBLANK"****** 2.Normal *****\n");printf(MENUBLANK"*************************\n");printf(MENUBLANK"****** 3.Hard *****\n");printf(MENUBLANK"*************************\n");printf(MENUBLANK"****** 0.Exit *****\n");printf(MENUBLANK"*************************\n");printf("\n");
}
4. PlayGame函数
动态内存分配,初始化棋盘,调用函数开始游戏,释放内存空间
void Playgame(int row, int col, int minenum){char** mineboard;//雷区char** showboard;//展示区//为“二维数组”动态分配内存//多分配了两行两列是为了方便后续排雷//边上的一圈和中间的位置就可以统一处理了mineboard = MallocArr2d(row + 2, col + 2);if (mineboard == NULL){exit(EXIT_FAILURE);}showboard = MallocArr2d(row + 2, col + 2);if (showboard == NULL){exit(EXIT_FAILURE);}//初始化雷区和展示区Initboard(mineboard, row + 2, col + 2, ' ');Initboard(showboard, row + 2, col + 2, '#');//为雷区安放炸弹Setmine(mineboard, row, col, minenum);//排雷Findmine(showboard, mineboard, row, col, minenum);//释放动态分配的内存FreeArr2d(mineboard, row + 2);mineboard = NULL;FreeArr2d(showboard, row + 2);showboard = NULL;
}
5. MallocArr2d函数
分配二维动态内存,模拟实现二维数组
char** MallocArr2d(int x, int y){char** arr2d;//先为第一维(行)分配内存arr2d = malloc(x * sizeof(char*));//第一维的每个元素都为指针类型if (arr2d == NULL){perror("Arrmalloc2d");return NULL;}//再为第二维(列)分配内存int i = 0;for ( i = 0; i < x; i++){arr2d[i] = malloc(y*sizeof(char));//第二维的元素才是char类型if (arr2d[i] == NULL){perror("Arrmalloc2d");return NULL;}}return arr2d;
}
6. FreeArr2d函数
释放二维动态内存
void FreeArr2d(char** arr2d, int x){int i = 0;//释放内存时,由内而外。//先释放第二维内存空间for ( i = 0; i < x; i++){free(arr2d[i]);}//再释放第一维内存空间free(arr2d);}
7. Displayboard函数
打印棋盘:包括行号,列号,分割线
void Displayboard(char** board, int row, int col){system("cls");printf("\n\n");int i, j;//打印列号 printf(BOARDBLANK" ");for ( i = 1; i <= col; i++){printf("| %-2d", i);if (i == col){printf("|");}}printf("\n");for ( i = 1; i <= row; i++){//打印横线printf(BOARDBLANK"---");for ( j = 1; j <= col; j++){printf("|---");if (j == col){printf("|");}}printf("\n");//打印行号printf(BOARDBLANK"%2d ", i);//打印棋盘内容//因为棋盘多分配了两行两列的内存//而打印,安放炸弹,排雷等操作的范围不包括外围的一圈//所以行列坐标都应该是从1开始的闭区间for (j = 1; j <= col; j++){printf("| %c ",board[i][j]);if (j == col){printf("|");}}printf("\n");//最后一行打印最后一条横线if (i == row){printf(BOARDBLANK"---");for (j = 1; j <= col; j++){printf("|---");if (j == col){printf("|");}}}}printf("\n\n");}
8. Initboard函数
初始化棋盘
void Initboard(char** board, int rows, int cols, char ch){int i, j;for (i = 0; i < rows; i++){for (j = 0; j < cols; j++){board[i][j] = ch;}}}
9. Setmine函数
为雷区安放炸弹
void Setmine(char** mineboard, int row, int col, int minenum){int i;int x, y;for (i = 0; i < minenum;){x = rand() % row+1;y = rand() % col+1;if (mineboard[x][y] != '*'){mineboard[x][y] = '*';i++;}}
}
10. Findmine函数
体现游戏设计思路的主体函数,通过调用游戏具体实现的函数,控制游戏的整个流程。分标记雷位;排雷;判断输赢三大块内容
void Findmine(char** showboard, char** mineboard, int row, int col, int minenum){int x, y;while (1){ //打印展示区Displayboard(showboard, row, col);//接受玩家输入printf("请输入排雷坐标:");fflush(stdin);scanf("%d %d", &x, &y);// # 标记雷位 #if (x == 0 && y == 0){SIGN:printf("请输入标记坐标:");fflush(stdin);scanf("%d %d", &x, &y);if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标是否在范围内{if (showboard[x][y] == '#' || showboard[x][y] == '!')//判断坐标是否被排除或已经被标记{showboard[x][y] = showboard[x][y]=='!'?'#':'!';//如果未被标记就标记,反之取消标记continue;}else{printf("此坐标已排除!\n");goto SIGN;}}else{printf("坐标非法!\n");goto SIGN;}}// # 排雷 #if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标是否在范围内{if (showboard[x][y] == '#')//判断坐标是否被排除{if (mineboard[x][y] != '*')//判断是否是雷{Chain_reflect(showboard, mineboard, row, col, x, y);//触发链式反应}else{//排雷失败打印棋盘Displayboard(showboard, row, col);printf("Bang!\n");printf("排雷失败!\n");break;}}else{printf("此坐标已排除!\n");system("pause");}}else{printf("坐标非法!\n");system("pause");}// # 判断是否完成排雷 #if (Iswin(showboard, row, col, minenum) == 1){Displayboard(showboard, row, col);printf("恭喜你,排雷成功!\n");break;} }
}
11. get_minenum函数
检查给定坐标周围位置的雷数,并将雷数转换为char型返回
char get_minenum(char** mineboard, int x, int y){int i, j;int count = 0;for (i = -1; i <= 1; i++){for (j = -1; j <= 1; j++){if (mineboard[x + i][y + j] == '*')count++;}}return count + '0';//count + '0'将int转换为char型返回
}
12. Chain_reflect函数
连锁反应,实现递归展开的功能
void Chain_reflect(char** showboard, char** mineboard, int row, int col, int x, int y){int i, j;//检查坐标是否在范围内if (x >= 1 && x <= row && y >= 1 && y <= col){//判断坐标是否被排除if (showboard[x][y] == '#'){//判断周围是否有雷if (get_minenum(mineboard, x, y) == '0'){//将雷位置为空格showboard[x][y] = ' ';//传递周围雷位for (i = x - 1; i <= x + 1; i++){for (j = y - 1; j <= y + 1; j++){Chain_reflect(showboard, mineboard, row, col, i, j);}}}else{showboard[x][y] = get_minenum(mineboard, x, y);}} } return;
}
13. Iswin函数
判断是否排雷成功
int Iswin(char** showboard, int row, int col, int minenum){//如何判断输赢:遍历展示区统计'#'(未排)和'!'(标记)的数量//如果总数等于雷数,证明玩家已经找出所有的雷位,排雷成功int i, j;int count = 0;for (i = 1; i <= row; i++){for (j = 1; j <= col; j++){if (showboard[i][j] == '#' || showboard[i][j] == '!'){count++;}}}if (count == minenum){return 1;}else{return 0;}
}
四、重难点讲解
1. 二维动态内存空间的分配和释放
1. 我们知道二维数组名是其元素一维数组的地址,即二维数组名的数据类型为 char(*arr)[10](数组指针)。
由于我们并不能确定到底有多少列,也就是说不能确定一维数组的元素个数。所以不能将mineboard和showboard设置成数组指针的类型。
2. 但是我们可以模仿二维数组的实现方式,即二维数组名是一维数组的地址(char(*arr)[10])
而一维数组的地址就是数组首元素的地址(char*),虽然数据类型不同,但两个地址完全相同。
3. 因此我们可以将mineboard和showboard设置为 char** (二级指针),以showboard为例(假设m行n列):
先为showboard分配m个char*类型的元素空间,再为每个char*元素分配n个char类型的元素空间

2.system执行系统命令
| system("cls"); | 清屏 |
| system("pause"); | 程序暂停 |
| system("color 5e"); | 修改背景和字体颜色 |
| system("mode con cols=60 lines=30"); | 调整控制台大小 |
| system("Shutdown -s -t 60"); | 设置60秒后自动关机 |
| system("Shutdown -a"); | 取消关机计划 |
颜色属性由两个十六进制数字指定
第一个:对应于背景,第二个:对应于前景。
每个数字可以为以下任何值:
| 0 = 黑色 | 1 = 蓝色 | 2 = 绿色 | 3 = 浅绿色 | 4 = 红色 | 5 = 紫色 | 6 = 黄色 | 7 = 白色 |
| 8 = 灰色 | 9 = 淡蓝色 | A = 淡绿色 | B = 淡浅绿色 | C = 淡红色 | D = 淡紫色 | E = 淡黄色 | F = 亮白色 |
3.关于递归展开的一些问题
- 问题一:为什么要多分配两行两列的内存空间?
答:调用get_minenum函数统计坐标周围雷数时,如果位置恰好在边脚就会造成越界访问。(Chain_reflect函数也会有类似情况)
与其分各种情况讨论,不如在最外层多分配一圈内存空间,这样就统一了边角和中间位置的访问方法。
在下面的问题中,我把最外层多分配的一圈元素简称为“外层元素”。
- 问题二:在Findmine函数中已经判断过坐标的合法性了,为什么在Chain_reflect函数中又要判断一次?
答:在递归展开的过程中,可能外层元素的坐标传递给下一层函数,如果不加判断直接调用get_minenum函数会造成越界访问。
- 问题三:为什么要在Chain_reflect函数中判断坐标是否被排除(showboard[x][y] == '#')?
答:在递归展开的过程中,可能会将其他递归线路已经展开的坐标再次传递给下一层函数,如果不进行判断的话就会出现栈溢出的问题(stack overflow)
五、游戏效果

六、分享交流
最后将完整源码分享给大家,希望大家能与我在交流中共同学习,共同进步:
https://gitee.com/zty857016148/C_OS_Project/tree/master/Mine_Sweeper
https://gitee.com/zty857016148/C_OS_Project/tree/master/Mine_Sweeper
如果大家还有什么问题欢迎提问,我抽空一定回答!
若是各位大佬发现了代码中的错误或是遗漏之处还请海涵,最好在评论区与我交流助我一臂之力!

上一篇:【初级C语言】详解井字棋游戏(电脑下棋算法优化)
https://blog.csdn.net/zty857016148/article/details/126721652
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
