各位大佬好!!!以下是我这个小萌新改进的扫雷代码(可炸开一片和可标记)
游戏介绍
在命令窗口实现扫雷游戏:
1、玩家可自己选择排雷的数量,根据排雷数生成清除次数
2、输入方式:玩家选择0.排查雷,1.清除雷,2.游戏结束
3、排雷的判定:如果排雷是该坐标周边有雷,则显示周围雷的信息;如果该坐标周围没有雷,则一次展开多项,直到展开到雷的附近
4、清除雷:输入坐标,若为雷,则清除,并显示"#"
5、游戏结束判定:当玩家触碰到雷,或者棋盘上雷的被玩家全部清除,或者玩家清除次数用尽
6、游戏中输入非法字符能保证游戏继续进行,不陷入死循环
首先我们分文件设计游戏,分一个test.c来管理游戏的执行流程,一个game.c来实现游戏需要的自定义函数,一个game.h来封装函数声明,常数的定义,头文件的包含等。
一、头文件部分(头文件的包含和一些函数接口)
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> #include<time.h> #define ROW 9 #define COL 9 #define ROWS ROW+2 #define COLS COL+2 //初始化棋盘 void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set); //打印棋盘 void DisplayBoard(char arr[ROWS][COLS],int row,int col); //随机布置雷 void SetMine(char arr[ROWS][COLS], int row, int col, int N_COUNT); void StartBoard(char arr[ROWS][COLS], int row, int col, int N_COUNT); //查找雷或清除雷 void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int N_COUNT); //拓展功能(一次性扫一大片) void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y); //判断合法输入 int judge(int n);
头文件就不过多赘述,其用途为:包含了C函数声明和宏定义,被多个源文件中引用共享。
包括:我们编写的头文件和C标准库自带的头文件
二、主函数部分
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> #include"game.h" void menu() { printf("********************\n"); printf("**** 1.play ****\n"); printf("**** 0.exit ****\n"); printf("********************\n"); } /* 1.存放数据的类型太多,容易产生歧义 1.创建两个数组, 2.一个雷的信息,一个存放排查雷的信息 2.在统计一个坐标周围雷的个数的时候,可能会越界 */ void game(int N_COUNT) { char mine[ROWS][COLS]; char show[ROWS][COLS]; //初始化棋盘 InitBoard(mine, ROWS, COLS, '0');// '0' InitBoard(show, ROWS, COLS, '*');// '*' //打印棋盘 //DisplayBoard(show,ROW,COL); //DisplayBoard(mine,ROW,COL); //1.布置雷 SetMine(mine,ROW,COL,N_COUNT); StartBoard(mine, ROW, COL, N_COUNT); //DisplayBoard(mine, ROW, COL); //2.排查雷 FindMine(mine, show, ROW, COL, N_COUNT); } int main() { int N_COUNT = 0; int input = 0; srand((unsigned int)time(NULL)); do { menu(); printf("请输入:"); input = judge(input); switch (input) { case 1: printf("请输入你要扫雷的数量(不要太多哦!!!)\n"); N_COUNT = judge(N_COUNT); int i = 5 + N_COUNT; printf("你有%d的次数清除雷\n", i); if (N_COUNT > 50) { printf("你的雷有点多啊!\n"); printf("请重新开始吧!\n"); break; }else if(N_COUNT <= 0) { printf("你的雷不能是非正数啊!\n"); printf("请重新开始吧!\n"); break; } game(N_COUNT); break; case 0: printf("退出游戏\n"); break; default: printf("选择错误,重新选择\n"); break; } } while (input); return 0; }
声明一下
show是玩家可以看到的数组
mine是用于埋雷、排雷和清除雷的数组
首先说下为什么使用9x9的雷盘要初始化化成11x11呢?
1、找周围的八个雷会简单很多。因为只在需要的9x9的二维数组里进行埋雷,所以多出来的这两行两列(雷盘上下,左右各多一行)是没有雷的,可以方便统计周围八个雷的数量。
2、方便打印棋盘的行号和列号,也方便用户确定雷的坐标
没有judge函数的后果(T⌓T) (T⌓T) (T⌓T)
1、先解释一下void menu(),这是一个很简单的函数,里面是打印菜单的操作
2、对于game中的函数,解释请移步到game.c中观看详解
3、再解释一下main函数的部分
(一)对于judge的使用:限定输入scanf的为整型(整数),防止scanf是读取要求的类型与输入的类型不符合,然而又被留在scanf的缓存区中了,故一直循环读取scanf缓存区的内容,形成了死循环! 具体可看拙作:
C语言中限定输入scanf的为整型(整数),浮点型-CSDN博客
(二)do - while与switch函数的使用
输入的第一个数据是菜单的选择
输入0时,直接执行case 0;while(0)游戏结束
输入1时,游戏开始,程序继续运行
输入其他数据时,执行default,可以再次输入,有容错
(三)if语句的使用
防止用户输入的数字过大或过小,雷的数量不合理
(有注释的就不一一解释了!!!)
三、game.c
非常长的一个文件,我决定分为两个部分解释
1、雷的布置与打印棋盘
#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<string.h> #include"game.h" #include<time.h> #include<stdlib.h> void InitBoard(char arr[ROWS][COLS], int rows, int cols,char set) { int i = 0; for (i = 0; i < rows; i++) { int j = 0; for (j = 0; j < cols; j++) { arr[i][j] = set; } } } void StartBoard(char arr[ROWS][COLS], int row, int col, int N_COUNT) { int i = 1; printf("*********扫雷*********\n"); int count = N_COUNT; printf("***你需要找出%d个雷***\n",N_COUNT); //先打印列号 for (i = 0; i <= row; i++) { printf("%d ", i); }printf("\n"); for (i = 1; i <= row; i++) { int j = 0; printf("%d ", i);//再打印行号 for (j = 1; j <= col; j++) { printf("* "); } printf("\n"); } } void DisplayBoard(char arr[ROWS][COLS], int row, int col) { int i = 1; printf("*********扫雷*********\n"); //先打印列号 for (i = 0; i <= row; i++) { printf("%d ", i); }printf("\n"); for (i = 1; i <= row; i++) { int j = 0; printf("%d ", i);//再打印行号 for (j = 1; j <= col; j++) { printf("%c ", arr[i][j]); } printf("\n"); } } void SetMine(char arr[ROWS][COLS], int row, int col, int N_COUNT) { //布置1-10个雷 int count = N_COUNT;//用count接收雷的数量 while (count) { //布置雷 int x = rand() % row + 1;//1-9的值 int y = rand() % col + 1;//1-9的值 //布置成功一个雷count--,还需要布置雷的数量 if (arr[x][y] = '0') { arr[x][y] = '1'; count--; } } }
(一)InitBoard函数:利用for循环遍历二维数组的每一个元素,将棋盘初始化。
初始化mine棋盘:布雷的棋盘初始化全为’0‘。
初始化show棋盘:显示的棋盘初始化全为’*‘。
(二)StartBoard和DisplayBoard:两个函数非常相似,不同的是StartBoard打印的就是初始棋盘,而DisplayBoard可以根据输入的参数而打印出对应的棋盘。
打印棋盘这里与定义初始化的方法一致,都是用for循环遍历每个元素。唯一不同的地方在于为了方便输入坐标,这里将行号和列号在第一行和第一列打印。(打印完行号后需要换行,打印列号则不用)
(三)SetMine函数:此处用rand()库函数对坐标X和Y分别设计随机数,%row+1和%col+1确保X和Y在 1~row 范围内,rand()%9+1的范围为[1,9],符合坐标的要求
每次生成坐标时需要判断该坐标是否已经布置过雷。并且注意,坐标的范围是棋盘游戏的范围,而不是最初设定时的范围。并把雷设置成'1',不是雷则为‘0’。
2、雷的判断、雷数量的统计、雷的查找与雷的清除(此处重点)
judge函数已经在前面谈过,不多赘述!
static GetMineCount(char mine[ROWS][COLS], int x, int y) { return mine[x - 1][y] + mine[x - 1][y - 1] + mine[x][y - 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x][y + 1] + mine[x + 1][y + 1] + mine[x - 1][y + 1] - 8*'0'; } void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y) { int temp = 0; int j = 0, i = 0; int n = GetMineCount(mine, x, y);//统计坐标周围有几个雷 if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (n == 0 && mine[x][y] == '0') { show[x][y] = '0'; for (j = x - 1; j <= x + 1; j++) { for (i = y - 1; i <= y + 1; i++) { if (show[i][j] == '*' && i > 0 && i <= col && j > 0 && j <= row) { Expand(mine, show, row, col, i, j); } } } } else if (mine[x][y] == '1' && mine[x][y] != '#') { show[x][y] = '*'; } else { show[x][y] = n + '0'; } } } int judge(int n) { while (scanf("%d", &n) == 0 || getchar() != '\n') { printf("输入了非法字符,请重新输入:"); while (getchar() != '\n'); // 清除缓存区 } return n; } void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int N_COUNT) { int t = 0, x = 0, y = 0, i = 5 + N_COUNT; int arr1[10][10] = { 0 }; while (1) { int reinput = 0; printf("0.排查雷,1.清除雷,2.游戏结束\n"); printf("请输入2或1或0:"); reinput = judge(reinput); switch (reinput) { case 2: { goto h; } case 1: printf("请输入要消除的坐标(请输入数字):\n"); printf("请输入纵坐标:");x = judge(x); printf("请输入横坐标:");y = judge(y); if (x >= 1 && x <= row && y >= 1 && y <= col) { if (arr1[x][y] == 0) { arr1[x][y] = 1; if (mine[x][y] == '1') { i--; t = t + 1; show[x][y] = '#'; mine[x][y] = '#'; DisplayBoard(show, ROW, COL); printf("清除成功,请再次选择\n"); if (t == N_COUNT) { printf("恭喜你扫雷成功,游戏结束\n"); goto h; } } else { i--; if (i == 0) { printf("你的次数用尽了!!!"); goto h; } printf("清除失败,你还有%d次机会,请重新选择\n", i); } } else { printf("你已经尝试过清除了,请重新选择\n"); } } break; case 0: printf("请输入要排查的坐标(请输入数字):\n"); printf("请输入纵坐标:"); x = judge(x); printf("请输入横坐标:"); y = judge(y); if (x >= 1 && x <= row && y >= 1 && y <= col) { if (show[x][y] == '#') { printf("你已经尝试过清除了,请重新选择\n"); } else if (mine[x][y] == '1') { printf("很遗憾,你被炸死了\n"); DisplayBoard(mine, row, col); goto h; } else { Expand(mine, show, row, col, x, y); DisplayBoard(show, row, col); } } else { printf("坐标非法请重新输入\n"); break; } } } h: printf("游戏结束"); system("pause"); }
(一)GetMineCount函数:统计周围八格雷的数量,字符串相加就是对应的ASCII码值相加,要得到雷的个数需减去8个‘0’的值
(二)FindMine和Expand(此处重点)
首先解释一下FindMine的作用:雷的判断、雷数量的统计、雷的查找与雷的清除。
对!没错,他有着以上四个作用,我们来一一解析。
首先,在用户输入完需要扫雷的个数后会出现以下选项,这也说明进入了FindMine函数中
当用户输入时有三个选项,与之对应的,代码中也有switch为代表的三个case
在用户输入其他字符时,因为judge和if (x >= 1 && x <= row && y >= 1 && y <= col)的存在,并不会出现报错和死循环的结果,给予用户再次输入的机会。
在输入1后,直接跳转到此处代码,进入case 1(排查) 的语句中
case 1: printf("请输入要消除的坐标(请输入数字):\n"); printf("请输入纵坐标:");x = judge(x); printf("请输入横坐标:");y = judge(y); if (x >= 1 && x <= row && y >= 1 && y <= col) { if (arr1[x][y] == 0) { arr1[x][y] = 1; if (mine[x][y] == '1') { i--; t = t + 1; show[x][y] = '#'; mine[x][y] = '#'; DisplayBoard(show, ROW, COL); printf("清除成功,请再次选择\n"); if (t == N_COUNT) { printf("恭喜你扫雷成功,游戏结束\n"); goto h; } } else { i--; if (i == 0) { printf("你的次数用尽了!!!"); goto h; } printf("清除失败,你还有%d次机会,请重新选择\n", i); } } else { printf("你已经尝试过清除了,请重新选择\n"); } } break;
arr[x][y]是用来判断是否清除的数组,其初始的值为0,若为1,则打印已清除过。
当多次清除后仍然没有把所有的雷清除完,则打印次数用尽,直接游戏结束。
当有雷被清除后,打印棋盘并给雷标上“#”,以此表示已经清除过了
当所有雷被清除完后,扫雷成功,游戏结束。
在输入0后,直接跳转到此处代码,进入case 0(排查) 的语句中
case 0: printf("请输入要排查的坐标(请输入数字):\n"); printf("请输入纵坐标:"); x = judge(x); printf("请输入横坐标:"); y = judge(y); if (x >= 1 && x <= row && y >= 1 && y <= col) { if (show[x][y] == '#') { printf("你已经尝试过清除了,请重新选择\n"); } else if (mine[x][y] == '1') { printf("很遗憾,你被炸死了\n"); DisplayBoard(mine, row, col); goto h; } else { Expand(mine, show, row, col, x, y); DisplayBoard(show, row, col); } }
在用户输入坐标后嵌套在里面的if语句会判断是否选到雷,如果是没清除的雷,直接炸死,执行goto语句,游戏结束,如果是清除了的雷,会打印,然后再次循环后重新选择,若都不是,则执行Expand函数
现在来解释一下Expand函数:
void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y) { int temp = 0; int j = 0, i = 0; int n = GetMineCount(mine, x, y);//统计坐标周围有几个雷 if (x >= 1 && x <= ROW && y >= 1 && y <= COL) { if (n == 0 && mine[x][y] == '0') { show[x][y] = '0'; for (j = x - 1; j <= x + 1; j++) { for (i = y - 1; i <= y + 1; i++) { if (show[i][j] == '*' && i > 0 && i <= col && j > 0 && j <= row) { Expand(mine, show, row, col, i, j); } } } } else if (mine[x][y] == '1' && mine[x][y] != '#') { show[x][y] = '*'; } else { show[x][y] = n + '0'; } } }
expand()是一个扩展函数,功能是: (n = GetMineCount(mine, x, y) )
当n = 0且mine[x][y] !='1'时候,将该位置的值改为GetMineCount(mine, x, y)的返回值。
当排查的坐标位置周围为0个雷的时候,把该位置置为0,并检查周围8个位置是否它的周围也是0个雷,如果周围坐标位置有满足条件n == 0 ,这将这个位置也置为空,
如果周围周围的位置也满足条件n == 0,这也将该位置置为0,
如果周围的周围的周围也满足条件n == 0.........。就这么循环下去
通俗一点就是:以你输入的位置为起点,只要该位置n == 0,就把它置空,同时把周围满足 n == 0也置空,同时也把周围这个位置也看做起点。满足递归思想,用递归能够很舒服的解决,但是也不要忘记,我们的雷不能给置空啊!!!所以我多加了一条语句判断是否为雷。
以上便是所有函数的详解了
试玩效果如下:
整体代码
整体代码在这里C语言实现扫雷(递归实现一扫一片,附源码)-CSDN博客
当然还有很多值得优化的地方,
(甚至还有一两个小bug,技术力不够啊(T⌓T) ),
大家有什么改进的办法可以交流一下。