宝剑方从磨砺出,梅花香自苦寒来
目录
扫雷简介
实现思路
代码说明
具体代码实现
1.数据定义
2.初始化
3.棋盘打印
4.设置地雷
5.排查地雷
6.递归展开Pull函数详解
7.计数Count函数详解
8.标记Tip函数详解
完整代码
⛵️1.Game.h
2.Game.c
✈️3.test.c
游戏整体预览
总结
《扫雷》是一款大众类的益智小游戏,于1992年发行。 游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。 扫雷在科技历史上也扮演了相似的角色。
在了解了游戏的规则后,我们就要想办法大概构造出扫雷小游戏得基本构架。要下棋我们就得有棋盘,首先第一步就是打印棋盘,这时候二维数组就派上用场了。在打印完棋盘后,我们就要用电脑随机设置地雷,但是因为我们又要打印棋盘出来让玩家下棋,又不能让玩家看见雷的位置,要解决这个问题,那我们就可以设置两个相同的二维数组,也就是两个相同的棋盘,一个用来存放雷,一个用来下棋展示给玩家看。
在我们完成打印棋盘和布置地雷后,接下来就是开始排雷。我们知道,当我们排雷的时候,如果我们当前排雷的位置是地雷的话,那么地雷爆炸然后游戏结束,如果不是雷,那就需要统计该位置旁边的八个位置的地雷数,并将该数放在我们当前排查的位置上,如果周围八个位置都没有雷的话,那就继续向外扩散,直到最后只剩下雷没排查,那么排雷成功,游戏结束。
图示如下:
统计周围八个位置。
当前位置及周围八个位置都没雷,则继续向外扩散统计直到碰到周围有雷的地方就停下,并将该雷数展示出来。
总结:我们主要实现的功能就如下
1.打印棋盘
2.设置地雷
3.排除地雷(重难点)
本次代码编程使用的是vs2022,代码分3个文件,如下图
Game.h:存放头文件,宏定义及函数声明
Game.c:存放各模块函数的具体实现代码
test.c:存放主函数及菜单,并在编写代码时测试各模块代码功能。
二维数组的行高列宽以及地雷的数量我们都用宏定义(Row代表行,Col代表列,bowcount代表地雷数量),需要改变行高列宽或者地雷的数量的时候我们就只需要在Game.h文件中的宏定义更改即可,不需要去代码中一一去找,增加代码的可维护性。
代码:
#define Row 9 #define Col 9 #define Rows Row + 2 #define Cols Col + 2 #define bowcount 10
值得一提的是,我们在定义了Row和Col后又定义了一个Rows和Cols,为什么这样定义呢?你想,当我们在排雷的时候,我们需要排查该位置的周围八个位置是否有雷,但是当我们排查的位置位于边缘位置的时侯,那如果我们还继续排查这个位置的周围那不就发生越界访问了吗,如图:
所以,针对这个问题,我们就把棋盘故意设置的大一圈,但是我们在操作的时候只操作其中我们设定的大小的棋盘,这样就可以完美避免越界问题。如下图:
行高列宽和地雷定义完后,在实现思路那我们知道,我们还需要两个二维数组来完成我们的游戏设计,一个二维数组用于存放地雷,一个二维数组用于玩家下棋及展示。
代码:
char arr[Rows][Cols] = { 0 }; char arr_new[Rows][Cols] = { 0 };
这里我们用arr数组用于存放地雷,arr_new数组用于玩家下棋及打印。
在我们完成了二维数组的定义后,接下来就是对我们的arr和arr_new两个二维数组进行初始化
代码:
void Init(char arr[Rows][Cols]) { int i = 0; int j = 0; for (i = 0; i < Rows; i++) { for (j = 0; j < Cols; j++) { arr[i][j] = '*'; } } }
二维数组的初始化我们是用两个for循环嵌套遍历将二维数组中的元素都初始化为 '*' 。
为了更贴近我们编程时候的想法,我们写完一段代码就进行一段代码的测试,这样做的好处在于我们写完一段代码后进行测试,可以及时发现代码的Bug然后进行修改,观察我们的代码是否达到了我们的预期想要的结果,如哦我们不是写一段测一段,而是一股脑的疯狂写完,在最后运行的时候失败了,一测试发现这么多的错误眼花缭乱,直接心态就裂开了,所以说写一段测一段的好习惯对我们编程有非常大的好处。所以说接下来我们就是实现棋盘打印的代码,方便我们观察初始化后的结果。
代码:
void Print(char arr[Rows][Cols]) { int i = 0; int j = 0; for (i = 1; i < Row + 1; i++) { for (j = 1; j < Col + 1; j++) { printf("%c ", arr[i][j]); } printf("\n"); } }
在打印棋盘模块里,我们还是用两个for循环嵌套依次遍历二维数组的每一个元素并将其打印出来,打印效果如下图:
在看见棋盘后我们发现,当我们排雷时需要输入坐标,这时候就麻烦了,我们需要一行一行,一列一列的去数,那不如我们直接给他加上行列编号。
优化代码:
void Print(char arr[Rows][Cols]) { int i = 0; int j = 0; for (i = 0; i < Row + 1; i++) { printf("%d ", i); } printf("\n"); for (i = 1; i < Row + 1; i++) { printf("%d ", i); for (j = 1; j < Col + 1; j++) { printf("%c ", arr[i][j]); } printf("\n"); } }
效果如下图:
在棋盘准备好之后,我们就要开始着手地雷的设置了,设置地雷我们需要用到rand()函数,而使用rand()函数有一点我们需要注意的是我们需要设置时间戳来保证我们每次运行时生成的地雷足够随机,所以我们在主函数调用一次srand()函数,如图:
设置地雷代码:
void Put_bow(char arr[Rows][Cols], int count) { int x = 0; int y = 0; int i = 0; for (i = 0; i < count; i++) { while (1) { x = rand() % Row + 1; y = rand() % Col + 1; if (arr[x][y] == '*') { arr[x][y] = '1'; break; } } } }
我们把arr数组和地雷数传给Put_bow函数,利用for循环放置地雷,代码中有两个点需要注意一下,一个是给x和y的赋值,我们利用rand()函数生成随机数然后分别模上行高和列宽,此时生成的数就是大于等于0并且小于行高或者列宽的数,但是我们前面说过棋盘的整个外围一圈是留出来防止越界的,不对其进行操作,因此我们在后面加上一个1就满足了我们的要求。还有一个是后面的if判断语句,当随机生成的坐标在二维数组中存放的是 '*' 时,那么我们就将其置为字符1,我们用字符1代表当前位置为地雷,如果随机生成的坐标在二维数组中存放的不是 ‘*’ ,那么就代表着该位置已经被设置为地雷了,就需要重新生成,所以是一个循环,直到生成一个雷后才break跳出,接下来设置下一个雷,当地雷设置到规定数量时,跳出for循环,程序结束。
代码写完后,继续我们的写一段测一段工程,因为我们是把地雷设置到arr数组中的,所以我们只需要把arr传给Print函数即可。效果如下:
一切准备完毕后,就到了我们本次工程的重难点了,实现我们的排雷函数,排雷时,我们首先要输入我们要排查的坐标,如果是雷的话那么游戏结束,如果不是雷就统计他周围八个位置的地雷数量,如果还不是就继续统计更外层地雷数量,递归展开。代码如下:
void Push(char arr[Rows][Cols], char arr_new[Rows][Cols]) { int x = 0; int y = 0; int win = 0; int* p = &win; int a = 0; while (win < Row * Col - bowcount) { printf("请输入排雷的坐标(行 列):>\n"); scanf("%d %d", &x, &y); if (x > Row || x < 1 || y > Col || y < 1) { printf("坐标输入不合法,请重新输入\n"); } else if (arr_new[x][y] != ' ') { printf("该地点已经排查过,请重新输入\n"); } else { system("cls"); if (arr[x][y] == '1') { printf("踩雷,游戏结束\n"); Print(arr); system("pause"); system("cls"); break; } else { Pull(arr, arr_new, x, y, p); Print(arr_new); printf("是否需要标记雷的位置?\n"); printf("1.是 0.否\n"); scanf("%d", &a); if (a == 1) { Tip(arr_new); Print(arr_new); } } } } if (win == Row * Col - bowcount) { system("cls"); printf("排雷成功!\n"); Print(arr_new); system("pause"); system("cls"); } }
值得一提的是,代码中我们设置了一个win变量和一个指针p变量,指针p指向win的地址,我们设置win变量是帮助我们统计我们排雷的次数,方便在后面判断是否完成了排雷,有因为涉及递归调用,所以我们用p传址调用。
图解如下:
先行放上代码:
void Pull(char arr[Rows][Cols], char arr_new[Rows][Cols], int x, int y, int* p) { if (x >= 1 && x <= Row && y >= 1 && y <= Col) { int sum = Count(arr, x, y); int i = 0; int j = 0; if (sum == 0) { (*p)++; arr_new[x][y] = ' '; for (i = x - 1; i <= x + 1; i++) { for (j = y - 1; j <= y + 1; j++) { if (arr_new[i][j] == '*') { Pull(arr, arr_new, i, j, p); } } } } else { (*p)++; arr_new[x][y] = sum + '0'; } } }
图解如下:
值得一提的是对p解引用win加1代表排除了一个非雷位置,在排除一个非雷位置后,我们将这个非雷位置置为空格代表排查过,也是递归的条件,防止死递归。最后一条语句可能咋一看看不懂,但是当我们分析一下就很好理解了
因为我们数组是char类型,不能直接将整形sum赋值给我们的数组,因此我们查阅ASCII码表发现,字符0的ASCII码值为48,字符1的ASCII码值为49,依次往上ASCII码值依次加1,我们发现(字符5 - 字符0 = 5),而(5 + 字符0 = 字符5),同理(字符6 - 字符0 = 6)推出(6 + 字符0 = 字符6)不难推出(数字a + 字符0 = 字符a),所以我们的sum加上字符0得到字符sum并将其赋值给我们的arr_new数组。
Count函数代码:
int Count(char arr[Rows][Cols], int x, int y) { int i = 0; int j = 0; int sum = 0; for (i = x - 1; i <= x + 1; i++) { for (j = y - 1; j <= y + 1; j++) { if (arr[i][j] == '1') { sum++; } } } return sum; }
这个代码结构非常简单,定义一个sum变量用于计数,两个for循环遍历坐标周围八个区域,如果等于字符1就代表该坐标为地雷,那么sum就加1,最后返回sum。
标记功能是用于在还没有排查过的地方但是我们已经确定该地方为雷时,我们就可以用一个符号标记起来,代码中我们用$表示标记符。
Tip函数代码:
void Tip(char arr_new[Rows][Cols]) { int x = 0; int y = 0; while (1) { printf("请输入标记的坐标(行 列):>\n"); scanf("%d %d", &x, &y); if (arr_new[x][y] == '*') { arr_new[x][y] = '$'; break; } else { printf("这个地方已经排查过了,请重新标记\n"); } } }
代码图解:
#pragma once #include
#include #define Row 9 #define Col 9 #define Rows Row + 2 #define Cols Col + 2 #define bowcount 10 void Init(char arr[Rows][Cols]); //初始化 void Print(char arr[Rows][Cols]); //打印函数 void Put_bow(char arr[Rows][Cols], int count); //埋雷函数 void Push(char arr[Rows][Cols], char arr_new[Rows][Cols]); //排雷函数 void Pull(char arr[Rows][Cols], char arr_new[Rows][Cols], int x, int y, int* p); int Count(char arr[Rows][Cols], int x, int y); void Tip(char arr_new[Rows][Cols]);
#define _CRT_SECURE_NO_WARNINGS 1 #include "Game.h" void Init(char arr[Rows][Cols]) { int i = 0; int j = 0; for (i = 0; i < Rows; i++) { for (j = 0; j < Cols; j++) { arr[i][j] = '*'; } } } void Print(char arr[Rows][Cols]) { int i = 0; int j = 0; for (i = 0; i < Row + 1; i++) { printf("%d ", i); } printf("\n"); for (i = 1; i < Row + 1; i++) { printf("%d ", i); for (j = 1; j < Col + 1; j++) { printf("%c ", arr[i][j]); } printf("\n"); } } void Put_bow(char arr[Rows][Cols], int count) { int x = 0; int y = 0; int i = 0; for (i = 0; i < count; i++) { while (1) { x = rand() % Row + 1; y = rand() % Col + 1; if (arr[x][y] == '*') { arr[x][y] = '1'; break; } } } } void Push(char arr[Rows][Cols], char arr_new[Rows][Cols]) { int x = 0; int y = 0; int win = 0; int* p = &win; int a = 0; while (win < Row * Col - bowcount) { printf("请输入排雷的坐标(行 列):>\n"); scanf("%d %d", &x, &y); if (x > Row || x < 1 || y > Col || y < 1) { printf("坐标输入不合法,请重新输入\n"); } else if (arr_new[x][y] == ' ') { printf("该地点已经排查过,请重新输入\n"); } else { system("cls"); if (arr[x][y] == '1') { printf("踩雷,游戏结束\n"); Print(arr); system("pause"); system("cls"); break; } else { Pull(arr, arr_new, x, y, p); Print(arr_new); printf("是否需要标记雷的位置?\n"); printf("1.是 0.否\n"); scanf("%d", &a); if (a == 1) { Tip(arr_new); Print(arr_new); } } } } if (win == Row * Col - bowcount) { system("cls"); printf("排雷成功!\n"); Print(arr_new); system("pause"); system("cls"); } } void Tip(char arr_new[Rows][Cols]) { int x = 0; int y = 0; while (1) { printf("请输入标记的坐标(行 列):>\n"); scanf("%d %d", &x, &y); if (arr_new[x][y] == '*') { arr_new[x][y] = '$'; break; } else { printf("这个地方已经排查过了,请重新标记\n"); } } } void Pull(char arr[Rows][Cols], char arr_new[Rows][Cols], int x, int y, int* p) { if (x >= 1 && x <= Row && y >= 1 && y <= Col) { int sum = Count(arr, x, y); int i = 0; int j = 0; if (sum == 0) { (*p)++; arr_new[x][y] = ' '; for (i = x - 1; i <= x + 1; i++) { for (j = y - 1; j <= y + 1; j++) { if (arr_new[i][j] == '*') { Pull(arr, arr_new, i, j, p); } } } } else { (*p)++; arr_new[x][y] = sum + '0'; } } } int Count(char arr[Rows][Cols], int x, int y) { int i = 0; int j = 0; int sum = 0; for (i = x - 1; i <= x + 1; i++) { for (j = y - 1; j <= y + 1; j++) { if (arr[i][j] == '1') { sum++; } } } return sum; }
#define _CRT_SECURE_NO_WARNINGS 1 #include "Game.h" void game() { //int n = 0; int i = 1; int x = 1; int y = 0; int sum = 0; char arr[Rows][Cols] = { 0 }; char arr_new[Rows][Cols] = { 0 }; Init(arr_new); Init(arr); //初始化 Print(arr_new); //打印 Put_bow(arr, bowcount); //放置雷 Push(arr, arr_new); } void menu() { printf("************************\n"); printf("********1.start*********\n"); printf("********0.exit *********\n"); printf("************************\n"); } int main() { int i = 1; srand((unsigned int)time(NULL)); while (i) { menu(); printf("请输入你的选择:>\n"); scanf("%d", &i); switch (i) { case 1: system("cls"); game(); break; case 0: system("cls"); printf("安全退出\n"); break; default: printf("输入错误,请重新输入\n"); system("pause"); system("cls"); break; } } return 0; }
玩的有点菜.........
本次的扫雷小游戏到这里也就告一段落了,作者水平和精力有限,第一次写8000字博客,头昏眼胀,如果发现代码有什么错误或者有哪里值得优化的地方希望大家指正,有什么好的意见或者建议也希望大家多多评论,本次代码也同步到了作者gitee上,如果有不方便复制代码的也可以到gitee上自行下载,链接附在下方。
Game_8_4 · 牛爷爷爱写代码/study-code - 码云 - 开源中国 (gitee.com)