这个代码还是比较长的,为了增加可读性,我们还是把他的功能分装到了test.c,game.c,game.h里面。
扫雷游戏的规则相信大家来阅读本文之前已经知晓了,如果点到雷就输了,如果不是雷,点到的格子会显示周围雷的个数,如果这个格子不是雷,他周围也没有雷,就再展开,如果周围八个格子里面又有符合该条件的,则又会重复这个操作,效果上就是展开了一大片。这个功能我们是用函数递归来实现的,扫雷游戏还可以标记,我们可以标记某个格子。由于本文的写法还是比较简易的,没有贴素材什么的,就是能先让玩家判断是要排雷还是要标记。本代码的循环特别多,这也是难点所在。话不多说,直接上game.h文件,通过里面的函数声明,大家应该就能对游戏大致的思路有一个理解。
代码中使用了三处宏定义,这是为了方便日后的修改。
test.c
从主函数开始看,首先看到的是一个srand((unsigned)time(NULL)),看过前面三子棋和猜数字游戏的老铁应该马上就懂了,这是我们产生随机数的固定套路,说明后面有用到随机数的地方,这里就提前告诉大家是布置雷的时候我们是随机布置的。由于我们想要游戏可以重复玩,玩一把之后不过瘾可以接着玩,我们就采用了我们熟悉的do..while循环,这个写法在猜数字游戏和三子棋游戏中也同样采用,用于游戏的重复玩。(提一嘴与这个代码无关的,如果多组输入应该怎么写?答案是while(ch=getchar()!EOF))。
如果我们输入1就开始游戏了,首先我们要创建一个数组作为棋盘,创建的时候为了防止边缘以及四个角上的元素在判断周围是否有雷的时候越界访问,我们故意创建了一个更大的棋盘,虽然我们需要的是一个9*9的棋盘,但我们直接创建一个11*11的棋盘,当然这里的9和11都是用的宏定义代替,防止以后如果想要更改棋盘大小得把所有代码全改一遍。到时候我们玩游戏打印棋盘就从这个大棋盘里面扣一个小棋盘出来打印即可。
创建了数组之后应该先初始化,这里初始化成什么字符,就很有讲究了。假如我们要在放雷的棋盘上,用0代表没有雷,用1代表有雷,这时候老铁们可能有疑问了,用数字代表有没有雷,那玩的时候点了一下出现了一个1,这个1是代表此处是雷啊,还是他周围有一个雷啊,这就有歧义了,但是我就是要坚持用0和1来初始化,这是因为后面统计周围雷的个数的时候比较方便,那这样我们就要重新创建一个相同的新数组用来存放排查的雷的信息,并使得这个新数组show和我们放雷的数组mine建立起来联系,这里刚上来我们就把mine数组全部初始化为0,表示还没有放雷,show数组初始化成什么可以根据大家的喜好,我这里把show数组全部初始化为*,接下来就要开始布置雷了,然后就是排查雷。游戏的大题思路就是这样。
接下来就是挨个实现函数的功能
首先是初始化棋盘的函数
这个函数还是比较好理解的,注意把整个大棋盘初始化而不是只初始化那个小棋盘就行。
接下来是打印棋盘的函数,我们不管是初始化棋盘还是布置雷,还是排查雷,都需要打印一下棋盘展示给玩家
打印棋盘本质上就是打印一个二维数组,注意换行就行。打印棋盘的时候为了方便玩家知道排查的格子是第几行第几列,我们还把行号和列号打印出来了。
接下来就要布置雷
我们要求是随机放雷,这里是放10个,因为不是说一次就能放完十个的,因此显然要循环,而且要求放过雷的位置就不要再放雷了。rand()会产生一个随机数,我们让他%9就能产生0~8的随机数,再+1就产生了1~9的随机数。因为我们要在9*9的棋盘里面布置雷。
统计周围雷的个数的函数
到这里老铁们明白为什么我当初为什么非要用0和1初始化mine数组了吧,这样直接return周围八个格子的和,再减去八倍的字符0的ASCII码值(因为我们存的是字符0和字符1)就是周围雷的个数了。
接下来是一次性扩展的函数
这个函数我们是使用递归的方式来写的,这个win就是我们胜利的标准,每走一个格子且不是雷他就+1,他最高加到row*col-雷的个数,这时候我们就赢了。我们传递的是win的地址,也就是进行了传址调用,这是为了能够在函数内部改变外部的变量(关于传值调用和传址调用的问题可以去看我函数的那篇文章),扩展的条件有三个,第一条是我们选择的格子本身不是雷,这个非常好办,,第二条是选择的格子周围没有雷,这个可以通过前面的统计周围雷的个数的函数来判断,第三就是我们所选的格子不能被重复遍历,不然就会陷入死递归。这个我们就直接在递归调用的时候去除x,y这个坐标就行。
接下来是最关键的函数,排查和标记雷的函数
上来先提示玩家选择排查还是标记,我这里用的是1和2,大家也可以根据自己的喜好选择别的,而且不管是排查雷还是标记,都应该能够进行多次,因此都应是循环。如果我们选择排查雷,在输入坐标之后,就有以下情况,第一种就是踩到雷了,直接就打印你寄了,然后打印一下mine棋盘让玩家输的明白,然后直接goto END即可,END可以看到是我再代码最后面放的一个空语句,因为这里循环嵌套的实在是太多了,我觉得goto语句的作用在这种情况下就很好的体现了。第二种情况就是没有踩到雷,没有踩到雷就要看看这个格子周围有几个雷,让他进入expend_board函数,出来之后连用两个break跳出switch语句,第一个break跳出的是里面的while循环,第二个break跳出的才是switch语句,当然我知道这肯定不是一种很优的解法,但是作为一个初学者我也是改了半天才写出来的,我感觉还是比较好理解的。而且我们发现每次第一个break跳出while循环的时候都会判断一下是不是赢了,如果赢了,也是直接goto END。
最后介绍一下几个可能出错的点:
1.如果你的代码每一次都会打印一下别的东西,可以看看是不是switch语句里面忘记了写break
2.expen_board函数不仅承载了周围没有雷就递归延展的功能,还要执行周围有雷要打印出雷的个数的功能,我刚上来就这么写的
这样写的问题就是,如果一个格子周围没有雷,我们本来是想打印空格,而进入expend_board函数之后也确实把这个格子改成了空格,但是执行完之后出了函数,这个空格就立马被改成了0,而且win的值也多加了一次,就像下面这样,因此我就把周围有雷的情况也弄到了expend_board函数内部去
3.在find_mine函数中,由于循环嵌套巨多,少写了break,导致死循环
4.在expend_board函数中,又遍历了x,y坐标的格子,导致死递归,判断格子是不是已经被处理过了,只需要看看show[横坐标][纵坐标]处是不是*即可
最后奉上完整源代码
game.h
#pragma once
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
void menu();//打印菜单的函数
void initboard(char arr, int rows, int cols, char a);//初始化两个大棋盘的函数
void display_board(char arr[ROWS][COLS], int row, int col);//打印棋盘的函数
void set_mine(char arr[ROWS][COLS], int row, int col);//布置雷的函数
int count_mine(char arr[ROWS][COLS], int x, int y);//统计周围雷个数的函数
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//排查雷的函数
test.c
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void game() {
//为了防止边缘以及四个角在判断周围有几个雷的时候越界,我们把棋盘加大
char mine[ROWS][COLS] = { 0 };//用来存放雷的棋盘
char show[ROWS][COLS] = { 0 };//用来展示周围有几个雷的棋盘
//这是因为我们用1表示有雷,0表示无雷
//如果把放雷和展示有几个雷用同一个棋盘,就会造成歧义
//因为不知道1代表是此处是雷还是周围有一个雷
initboard(mine, ROWS, COLS, '0');//用来放雷的棋盘刚上来全放上0,表示还没放雷,一会再放雷
initboard(show, ROWS, COLS, '*');//用来展示周围有几个雷的棋盘,刚上来全放上*
display_board(show, ROW, COL);
set_mine(mine, ROW, COL);//布置雷
find_mine(mine, show, ROW, COL);//排查雷
}
int main() {
int input = 0;
srand((unsigned)time(NULL));
do {
menu();
scanf("%d", &input);
switch (input) {
case 1: {
game();
break;
}
case 0: {
printf("退出游戏\n");
break;
}
default: {
printf("输入错误,请重新输入\n");
break;
}
}
} while (input);
return 0;
}
game.c
#define _CRT_SECURE_NO_WARNINGS
#include "game.h"
void menu() {
printf("****************************\n");
printf("*******1.play***************\n");
printf("*******0.exit***************\n");
printf("****************************\n");
}
//初始化棋盘的函数
void initboard(char arr[ROWS][COLS], int rows, int cols, char a) {
int i = 0;
for (i = 0; i < rows; i++) {
int j = 0;
for (j = 0; j < cols; j++) {
arr[i][j] = a;
}
}
}
//打印棋盘的函数
//打印棋盘只需要打印9*9的即可,因为展示给玩家的就是9*9的
//但是在操作的时候要对11*11的数组操作,因为我们并没有创建所谓9*9的数组
void display_board(char arr[ROWS][COLS], int row, int col) {
int i = 0;
//从第二行也就是i=1开始打印,从第二列也就是j=1开始打印
//打印列的标识
for (i = 0; i <= col; 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 set_mine(char arr[ROWS][COLS], int row, int col) {
int count = EASY_COUNT;
while (count) {
int x = rand() % row + 1;
int y = rand() % col + 1;
if (arr[x][y] == '0') {
arr[x][y] = '1';
count--;
}
}
}
//统计周围雷的个数的函数
//自始至终我们都在操作mine或者show数组,这两个数组都是ROWS*COLS的数组
int count_mine(char arr[ROWS][COLS], int x, int y) {
return(arr[x - 1][y - 1] + arr[x - 1][y] + arr[x - 1][y + 1]
+ arr[x][y - 1] + arr[x][y + 1] + arr[x + 1][y - 1]
+ arr[x + 1][y] + arr[x + 1][y + 1]-8*'0');
//这就是为什么我们当初布置雷的时候为什么要放字符1和字符0
}
//一次性扩展的函数 递归实现
//该坐标不是雷,该坐标周围没有雷,坐标未被排查过
void expend_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y,int* win) {
int ret = count_mine(mine, x, y);
if (ret != 0) {
show[x][y] = ret + '0';//显示周围雷的个数
*win++;
return;
}
else {
show[x][y] = ' ';
*win++;//传址调用
//递归周围八个格子
if (show[x - 1][y - 1] == '*' && (x - 1) >= 1 && (x - 1) <= ROW
&& (y - 1) >= 1 && (y - 1) <= COL) {
expend_mine(mine, show, x - 1, y - 1, win);
}
if (show[x - 1][y] == '*' && (x - 1) >= 1 && (x - 1) <= ROW
&& (y) >= 1 && (y) <= COL) {
expend_mine(mine, show, x - 1, y , win);
}
if (show[x - 1][y + 1] == '*' && (x - 1) >= 1 && (x - 1) <= ROW
&& (y + 1) >= 1 && (y + 1) <= COL) {
expend_mine(mine, show, x - 1, y + 1, win);
}
if (show[x][y - 1] == '*' && (x ) >= 1 && (x ) <= ROW
&& (y - 1) >= 1 && (y - 1) <= COL) {
expend_mine(mine, show, x, y - 1, win);
}
if (show[x][y + 1] == '*' && (x) >= 1 && (x) <= ROW
&& (y + 1) >= 1 && (y + 1) <= COL) {
expend_mine(mine, show, x , y + 1, win);
}
if (show[x + 1][y - 1] == '*' && (x + 1) >= 1 && (x + 1) <= ROW
&& (y - 1) >= 1 && (y - 1) <= COL) {
expend_mine(mine, show, x + 1, y - 1, win);
}
if (show[x + 1][y] == '*' && (x + 1) >= 1 && (x + 1) <= ROW
&& (y) >= 1 && (y) <= COL) {
expend_mine(mine, show, x + 1, y, win);
}
if (show[x + 1][y + 1] == '*' && (x + 1) >= 1 && (x + 1) <= ROW
&& (y + 1) >= 1 && (y + 1) <= COL) {
expend_mine(mine, show, x + 1, y + 1, win);
}
}
}
//排查和标记雷的函数
//标记用#
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
int x = 0;
int y = 0;
int win = 0;
int input = 0;
while (1) {
printf("排雷请按1,标记请按2,再次标记将会取消标记\n");
scanf("%d", &input);
switch (input) {
case 1: {
while (win < row * col - EASY_COUNT) {
printf("请输入要排查的坐标\n");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col) {
if (mine[x][y] == '1') {
printf("你寄了\n");
display_board(mine, row, col);
goto END;
}
else {
expend_mine(mine, show, x, y, &win);
display_board(show, row, col);
break;
}
}
else {
printf("坐标非法,请重新输入\n");
}
break;
}
if (win == row * col - EASY_COUNT) {
printf("恭喜你,排雷成功\n");
display_board(mine, row, col);
goto END;
}
break;//跳出switch语句
}
case 2: {
while (1) {
printf("请输入要标记的坐标\n");
scanf("%d %d", &x, &y);
if (show[x][y] == '*') {
show[x][y] = '#';
display_board(show, ROW, COL);
break;//跳出case 2里面的while循环
}
else if(show[x][y]=='#') {
show[x][y] = '*';
display_board(show, ROW, COL);
break;//跳出case 2里面的while循环
}
else {
printf("坐标已经被占用,无法标记\n");
break;//跳出case 2里面的while循环
}
}
break;//跳出switch语句,如果没有这个break,不管输入什么坐标都会走一遍下面的default
}
default:printf("输入错误,请重新输入\n");
break;
}
}
END:;
}