学习了二维数组以后,不仅可以写个三子棋,我们也可以更近一步,来写个扫雷玩玩?
编写程序的时候,一定要先做好构思与大体思路步骤,扫雷的基本运行逻辑其实并不算非常复杂,我们只需要把每一步捋顺即可。
本人能力有限,难免有叙述错误或者不详细之处!希望读者在阅读时可以反馈一下错误以及不够好的地方!感激不尽!
目录
菜单的创建与变量的创建
雷区的打印与初始化
游戏主体逻辑
插旗效果实现
展开的实现
插旗排雷
1.做一个简单可互动的菜单,准备好需要用到的变量。
2.创建两个二维数组,一个用于存放真实的雷区,另一个存放显示用的雷区,其中真实的雷区用随机数随机填入地雷,真实雷区范围设置 要设置成11 11,显示雷区范围在9 9内,否则数组将会越界。
3.也是打印雷区地图,雷子用1与0,外面用于遮盖的用*。
4.编写可以随机在雷区内埋雷的函数与对雷区初始化的函数。
5.玩家行动的编写,失败的触发条件,玩家行动时的选择,是排查还是插旗子。
6.游戏主体逻辑的编写:包涵检测胜利,以及最终难点,扫雷的展开。
菜单的创建与变量的创建
变量的创建
我们先在头文件内创建一个新文件,在其中定义我们所需要的变量以及所需库函数的调用。
我们的扫雷棋盘设置为9X9,所以我们定义两个变量:ROW,COL分别为其赋值为9,但是由于涉及到需要遍历格子旁边8格的地雷数量函数的编写,如果选择计算最左上角的格子附近8格的地雷数量,9X9的数组元素个数完全不够,会出现数组越界的情况,我们不妨为其新建另外两个真实的控制二维数组大小变量的值,让ROW与COL只显示9X9的雷区,而真实的二维数组大小应是11X11,我们创建两个变量ROWS,ROLS为其赋值为11。
一旦设置成9X9,我们编写的寻找附近地雷的函数就会越界,因为它找不着附近的元素了。
#include <stdio.h>
#include <stdlib.h>
#include <time.h >
#define ROW 9
#define COL 9
#define ROWS 11
#define COLS 11
接下来我们在main函数里创建二维数组,一个用于存放真实的雷区,另一个用于显示外面的覆盖雷区。
int main()
{char mine[ROWS][COLS] = { 0 };//真实雷区char showmine[ROWS][COLS] = { 0 };//显示雷区return 0;
}
目标实现的效果如下:
真雷区 覆盖后雷区
menu()
这个并不算困难,首先是打印基本的UI界面,只需要创建一个函数放入几个printf即可,个人代码如下,想创建成其他样式请随意。
void menu()
{printf("\n");printf("======| 扫雷 |======\n");printf("======| 1.play |======\n");printf("======| 0.exit |======\n");
}
菜单的互动
我们使用case语句来编写菜单的互动,非常简单,玩家输入1进入游戏,输入0退出,输入错误重试,代码如下:
int main()
{char mine[ROWS][COLS] = { 0 };char showmine[ROWS][COLS] = { 0 };int c = 1;menu();do{scanf("%d", &c);switch (c){case 1:printf("游戏开始!祝你好运!\n");game();break;case 0:printf("退出游戏!\n");break;default:printf("输入错误,请重新选择输入!\n");}} while (c);return 0;
}
雷区的打印与初始化
雷区初始化
这一部分的代码请注意作用于真实雷区,即mine而非showmine,我们创建另一个源文件function来实现这个函数
由于我们有两个雷区都涉及到初始化以及打印的问题,那么我们不妨集成一下功能,让这个函数不仅可以打印还可以初始化雷区,设置形参的时候我们多创建一个char类型的变量n,当我们需要初始化真实雷区的时候n传参0,反之,显示雷区则传递*,若是直接打印当前雷区情况则输入其他字符都可以。
9X9的二维数组,两个for循环为其赋值字符0即可完成初始化
//头文件内的设置:void IntMineField(char mine[ROWS][COLS], int row, int col);void DrawMineField(char mine[ROWS][COLS], int row, int col, char n)
{int i = 0;int j = 0;printf("________OwO________\n");for (j = 0; j < col+1; j++)//纵向打印坐标数字,方便知道坐标{printf("%d ", j);} printf("\n");for (i = 1; i <=row; i++){printf("%d ", i); //横向打印坐标数字for (j = 1; j <= col; j++){if (n == '0' || n == '*')//如果输入字符0或者*,则初始化,不输入就纯打印{mine[i][j] = n;//之后若是需要让其发挥初始化功能从这下手}printf("%c ", mine[i][j]);}printf("\n");}printf("===================\n");}
DrawMineField(showmine, ROW, COL, '*');初始化显示雷区
DrawMineField(showmine, ROW, COL, 'k');打印雷区,打印哪一个雷区只需要换传入的参数即可
DrawMineField(mine, ROW, COL, '0');初始化并显示真实雷区
在这里我们可以发现,虽然它可以实现初始化的功能,但是会打印出来,更改其中的逻辑会使得这个函数变得过于冗长,不妨直接再编写一个初始化的函数方便我们初始化。当然想要更正实现这一功能也很简单,在这里就不做优化了。
单纯的初始化真实雷区,不打印
void IntMineField(char mine[ROWS][COLS], int row, int col)
{int i = 0;int j = 0;for (i = 0; i < row; i++){for (j = 0; j < col; j++){mine[i][j] = '0';}}
}
坐标数字的打印已经在代码段内用注释标出。
随机埋雷:
而为了达成随机条件,则需要生成随机数。随机数的生成我们可以借助rand函数来进行生成。
rand函数在生成随机数的时候,会随机生成一个种子值,然后rand函数会借由这个种子值生成随机数,但是每一次只产生一次种子值rand函数所产生的随机数不是完全随机的,所以我们在使用rand函数之前还需要借助srand函数来使得rand函数的种子值不断变化以达成真正的随机数生成。而一直不断变化的值,那就是系统时间了。
srand(unsigned seed)通过参数seed改变系统提供的种子值,从而可以使得每次调用rand函数生成的伪随机数序列不同,从而实现真正意义上的“随机”。通常可以利用系统时间来改变系统的种子值,即srand(time(NULL)),可以为rand函数提供不同的种子值,进而产生不同的随机数序列。
rand函数位于#include <stdlib.h>的头文件内。
为了使用time函数,也需要使用<time.h>的头文件。
我们先在main函数中先实现随机数
int main()
{srand((unsigned int)time(NULL));return 0;
}
然后编写一个函数,用于随机埋雷,代码如下:
void ReadyForMine(char mine[ROWS][COLS], int row, int col)
{int x = 0;int y = 0;int i = 0;int count = 0;while (count < 10){x = rand() % row+1;//控制随机数的范围y = rand() % col+1;if (mine[x][y] != '1'){mine[x][y] = '1';count++;}}}
效果如下:
游戏主体逻辑
以下为逻辑概述:
首先是提醒玩家行动,分为:1.插旗排雷 2.排查地雷
进入插旗排雷行动后,每一次插旗进行一次判断,当成功插在地雷上则雷的总数-1,当雷的总数计数为0时,跳出循环胜利。
排查时,获取玩家输入的坐标,排查过的区域不能反复排查,将获得的坐标与真实雷区的坐标比对,等于字符1则游戏失败,等于字符0则继续游戏并且进行展开的判断
插旗效果实现
首先我们先创建一个用于检测是否胜利的全局变量地雷数量minenumber,当minenumber等于0的时候游戏胜利。
我们需要实时更新被排掉的雷,若是在函数内进行创建每一次函数在被调用的时候minenumber会被重新赋值一次,所以创建全局变量。
int minenumber = 10;//地雷数量int Flag(char mine[ROWS][COLS], char showmine[ROWS][COLS], int x, int y)
{if (showmine[x][y] == '*'){showmine[x][y] = '@';//插上旗帜的坐标位置用@替代DrawMineField(showmine, ROW, COL, 'k');if (mine[x][y] == '1'){minenumber--;printf("%d\n", minenumber);if (minenumber == 0){return 2;}return 1;}return 1;}else{printf("坐标非法,请重新输入!");}
展开的实现
展开的实现需要先满足以下3种情况:
该坐标不是雷,且没有被排查过
该坐标周围没有雷
该坐标的附近一旦有雷,则不要进入展开函数,遍历附近格子数得到附近8格地雷数量
首先则是实现对输入坐标附近8格进行地雷查找
思路是将当前坐标附近8格真实雷区里的元素值相加再减去字符0的值,可以得到附近8格里有几个1了。简单来说,收集附近8格的值,有雷就+1。
代码如下:
//获得坐标处旁的地雷数量
int countmine(char board[ROWS][COLS], int x,int y)
{int count = board[x - 1][y - 1] + board[x - 1][y] + board[x - 1][y + 1] +board[ x ] [y - 1] + board[x][y + 1] +board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1] - 8 * '0';return count;
}
接下来则是展开,代码如下:
void Spread(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{int offset_x = 0;int offset_y = 0;int count = 0;//坐标合法if (x >= 1 && x <= 9 && y >= 1 && y <= 9){//遍历周围坐标for (offset_x = -1; offset_x <= 1; offset_x++){for (offset_y = -1; offset_y <= 1; offset_y++){//如果这个坐标不是雷if (mine[x + offset_x][y + offset_y] == '0'){//统计周围雷的个数count = countmine(mine, x + offset_x, y + offset_y);if (count == 0){if (show[x + offset_x][y + offset_y] == '*'){show[x + offset_x][y + offset_y] = ' ';Spread(mine, show, x + offset_x, y + offset_y);}}else{show[x + offset_x][y + offset_y] = count + '0';}}}}}
}
展开的代码的逻辑是有点绕的,为了更好的方便我们去理解这一串套娃,我决定画图来分析。
效果如下:
插旗排雷
插旗排雷的代码逻辑并不困难,需要注意的是为了在函数里成功判断的地雷数量是否达成胜利条件,地雷数量的变量我在声明时使用了全局变量。
代码如下:
//插旗子:int Flag(char mine[ROWS][COLS], char showmine[ROWS][COLS], int x, int y)
{if (showmine[x][y] == '*'){showmine[x][y] = '@';//为插上棋子的地方放上一个@当旗子DrawMineField(showmine, ROW, COL, 'k');//插完旗子后显示雷区,让玩家知道自己的棋子插在了哪里if (mine[x][y] == '1'){minenumber--;//如果插下的棋子底下真的是雷,地雷数量减一printf("%d\n", minenumber);//调试时用于检测函数是否生效if (minenumber == 0)//每一次插完旗子检测一次是否胜利,胜利返回2{return 2;}return 1;//还有地雷就返回1,继续}return 1;}else{printf("坐标非法,请重新输入!");}
}
至此,我们大部分所需要的函数都写好了,把它们放到game函数里整理逻辑即可。
代码如下:
//游戏主体逻辑
void game(char mine[ROWS][COLS], char showmine[ROWS][COLS], int row, int col, int rows, int cols)
{int x = 0;int y = 0;int minenumber = 0;int ret = 1;int ret2 = 1;int c = 0;IntMineField(mine, ROWS, COLS);//初始化雷区DrawMineField(showmine, ROW, COL, '*');//初始化雷区后打印外面的隐藏雷区ReadyForMine(mine, ROW, COL);//埋雷//DrawMineField(mine, ROW, COL, 'k');//此处解除注释即可在初始化的时候看见雷的部署情况do{printf("请选择接下来的动作:\n1.探查地雷 2.为确认的地雷插下旗帜\n");scanf("%d", &c);if (2 == c){printf("请输入需要 插下旗帜 的坐标->.");scanf("%d %d", &x, &y);ret2 = Flag(mine, showmine, x, y);if (ret2 == 2)//当Flag函数返回2的时候跳出循环,进行胜利的判断break;}else if (1 == c){printf("请输入需要 排查 的坐标->.");scanf("%d %d", &x, &y);ret = Check(mine, x, y);//判断玩家是否踩到了雷if (showmine[x][y] != '*'){printf("该坐标被排查过了,不能重复排查\n");}else if (ret == 1){Spread(mine, showmine, x, y);//展开函数}DrawMineField(showmine, ROW, COL, 'k');//打印排查完毕后的雷区状况}else{printf("输入错误,请重新输入!\n");}} while (ret);if (ret == 0){printf("寄!你被炸死了,下辈子小心!XD\n");DrawMineField(mine, ROW, COL, 'k');//玩家失败,展示真实雷区情况}if (ret2 == 2){printf("NICE!你排完了所有的地雷,高手!");}//DrawMineField(showmine, ROW, COL, 'k');
}
当然main函数里也要加上一些其他的逻辑:
int main()
{srand((unsigned int)time(NULL));char mine[ROWS][COLS] = { 0 };char showmine[ROWS][COLS] = { 0 };int c = 1;menu();do{scanf("%d", &c);switch (c){case 1:printf("游戏开始!祝你好运!\n");game(mine, showmine, ROW, COL, ROWS, COLS);menu();IntMineField(mine, ROWS, COLS);break;case 0:printf("退出游戏!\n");break;default:printf("输入错误,请重新选择输入!\n");}} while (c);return 0;
}
完整代码地址:C语言刷题仓库: 装刷过的题目代码 - Gitee.com
感谢阅读!希望能对你有一些帮助!