@TOC
开篇语
关于这篇扫雷,我也是拖了一周了,实在是有点忙的找不到时间,编程一边要学的知识很多,另一边学校也各种乱七八糟,而且快要期末考试了,由于平时精力都花在了编程上,学校课程有很多都还没怎么复习,所以准备放慢写博客的速度,最近应该不会再经常发了,甚至如果实在情况不太妙的话,也有可能停更一段时间,当然这也是最不想看到的情况。在文章末尾我会附上一整段原码,即使一遍没有学会的也可以去玩一下游戏,提高一下兴趣。再来多学几遍一定可以!
在开始之前,先鼓励一下自己吧!一匹真正的好马,即使在鞭子的影子下,也能飞奔!
扫雷游戏的规则介绍
虽然我感觉看这篇文章的人可能都是会玩扫雷的,但是秉持着严谨的态度我还是决定简单介绍一下扫雷的玩法,玩法呢其实也很简单,给你一个棋盘,棋盘每一个格子都可以点,当你选择一个格子时候呢,就可以排查这个格子周围八个格子中的雷的数量,如图中所示。这个格子周围八个格子中就有一个雷。当你排查完棋盘上所有除了雷以外的格子的时候,你就获胜了。若中途点到了一个含有雷的格子,你就被炸死了,游戏也结束。
游戏的整体逻辑
我们在完成一项比较长的工程之前,理清整体的逻辑是必不可少的,由于我们前面已经知道了模块化开发的概念,所以我们今天就用游戏模块和测试模块来完成。先创建好我们的test.c/game.c/game.h三个文件。
我们目前来想一下要实现的逻辑大概有:
1.我们的整体框架
2.然后实现游戏内部:
3.初始化棋盘
4.打印棋盘
5.布置雷
6.排查雷
7.游戏判断是否胜利
目前大致就能想到这些,至于细节方面就要在实现的过程中再细说了,毕竟罗马不是一天建成的。
整体框架
我们要做一个游戏的整体框架我们前面已经写过两次,一次是猜数字小游戏,另一次是三子棋。感兴趣的可以翻看我之前的博客。整体框架其实也非常简单,只要打印一个简易菜单,让玩家能够做出选择即可。
话不多说,我们直接上代码,
#include<stdio.h>
void menu()
{
printf("****************************\n");
printf("******** 1.play *********\n");
printf("******** 0.exit *********\n");
printf("****************************\n");
}
void game()
{
printf("扫雷\n");
}
void test()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
再再再强调一次!代码一定要写一部分测试一部分,千万千万不要一拍脑门写到最后,一编译一堆bug,到时候你连想改的心思都没有了。
我们来测试一下我们的整体逻辑是否有问题:
可以看到达到了我们想到的效果,这部分没问题了,进行下一步。
设计棋盘(格局打开)
我们想要做扫雷,至少要先有一个棋盘才行,这个棋盘我们就暂时先决定做一个99的棋盘吧,其实我们要的就是一个99的数组,但是问题来了,字符数组,数字数组?但是这时候问题来了,其实都可以,但是我这里还是坚持用字符数组来解决,至于原因到后面会有解释。我们假设有一个99的棋盘,我们默认初始化全部都是字符0(注意是字符0),我们要往里面放的雷是字符1,但是当我们最后要去统计这个坐标周围八个坐标中的时候,又出问题了,如果是最边缘一排的话,是不是会造成数组越界呢,考虑到这一点我们可以两边各自加上一行或一列,这时候我们的数组就是一个1111的棋盘,还有一个需要注意的点是,一个棋盘可以吗?我们是要把雷和不是雷的信息都放到这个棋盘中,如果直接向玩家打印出来还玩什么呢?所以我们需要两个相同的棋盘,一个用来存储我的信息,一个用来向玩家展示才可以。好了,分析到这里,我们对于棋盘的思考就差不多了,具体怎么来实现呢?我们开始动手吧。
初始化棋盘
初始化棋盘这步就比较简单了,我们只需要创建两个1111的棋盘,将每一个元素初始化为字符0即可。但是考虑到代码的通用性问题,若以后我们想改为1616的棋盘呢?
所以不要直接把数组就定义成11*11的,比较好的方式如下:
#include"game.h"
void game()
{
char mine_board[ROWS][COLS] = {
0 };
char show_board[ROWS][COLS] = {
0 };
}
这里引用game.h的原因就是我把定义的函数和宏都放到game.h文件里面:
这里我们的两个棋盘就有了,一个是最初的棋盘,一个是要展示的棋盘。
我们接下来要写个函数来初始化一下两个棋盘:注意这里不要直接写两个初始化函数,一个初始化为字符0,一个初始化为*,我们只要给函数多传一个参数即可直接实现。代码如下:
#include"game.h"
//初始化棋盘
void init_board(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
void game()
{
//创建棋盘
char mine_board[ROWS][COLS] = {
0 };
char show_board[ROWS][COLS] = {
0 };
//初始化棋盘
init_board(mine_board, ROWS, COLS, '0');
init_board(show_board, ROWS, COLS, '*');
}
当然了,其实我们是要分开写的,函数部分要写到game..c里面,并且要在game.h里包含一下头文件。实际情况是这样的:
不要再问为什么模块化编程这么麻烦还非得这样做了,前面的文章中已经专门解释过模块化编程的重要性了,这种习惯就是越早培养越好。至于模块化编程的好处传送门在这里:里面有我对于模块化开发的好处详细解释
打印棋盘
接下来就是我们的打印棋盘部分了,打印棋盘我们只需要打印9*9的棋盘即可。所以我们的代码:
void print_board(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
}
这里我们就只写函数主体部分的代码,下面是game.h和test.c中的部分
我们写好后可以打印出棋盘来看一下效果,
诶?好像感觉哪里不太对,如果我们要选择一个坐标来排一下雷,是不是还得自己数一下坐标,所以我们打印部分是可以再多添加一些修改的:
void print_board(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
printf("-------扫雷--------\n");
for (j = 0; j <= col; j++)
{
printf("%d ",j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ",i);
for (j = 1; j <= col; j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
printf("-------扫雷--------\n");
}
当我们这样去修改时,我们再来看一下效果:
这样是不是就好看多了呢?
布置雷
假设我要布置10个雷,其实就是在9*9的棋盘中任意10个位置的字符0改为字符1即可。
关于生成随机数的问题在猜数字小游戏中已经有过详细解释,在三子棋中也有应用,这里就不再多啰嗦,需要的自行从传送门去复习一下。
猜数字小游戏传送门、三子棋小游戏传送门
另外还值得注意的是,不是随便生成一个随机数都可以用这个下标上去就布置雷的,如果生成的随机数出现相同,就会出现布置的雷数量不够的情况,所以还要判断一下该坐标是否是字符0。关于布置雷的数量问题,我们最好也考虑到通用性,定义一个标识符常量来确定雷的数量,代码如下:
void set_mine(char board[ROWS][COLS], int row, int col)
{
int count = Easycount;
while (count)
{
//生成随机下标
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
写到这时候我们就可以把我们的打印棋盘的函数拿下来,打印一下我们的mine棋盘看一下,雷是否已经布置下去,并且看一下雷的数量是不是够10个,
可以看到我们的代码是没有问题的,成功的布置了10个雷。
排查雷
当我们的雷布置下去以后,这时候就到了排查雷的步骤了,排查雷就是让玩家输入要排查的坐标,然后看一下这个位置是不是雷,如果是雷就直接游戏结束,如果不是雷,就要排查一下周围一圈八个格子中雷的数量,并用显示出来作为提示。
值得注意的点就是首先要判断输入坐标的合法性,一定要记住只要是人为输入的,就有错的可能,另外还有一处就是我们要统计一个坐标周围8个坐标的雷的个数,我们这里的方法就非常巧妙了,原理如下图:
我们这里使用的方法统计雷的个数也就是我一开始坚持让棋盘里放成字符0和字符1的原因,否则你可能需要将周围8个坐标一个一个地去判断,然后再统计一下。实现的代码呢,就如下:
//计算雷的个数
static int get_mine_count(char mine[ROWS][COLS],int x,int y)
{
return mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] +
mine[x][y - 1] + mine[x][y+1] + mine[x + 1][y - 1] + mine[x + 1][y]
+ mine[x + 1][y + 1] - 8 * '0';
}
//排查雷
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
print_board(mine, ROW, COL);
break;
}
else
{
int count = get_mine_count(mine, x, y);
show[x][y] = count + '0';//这里是字符数组,一定要转换成字符再放进去
print_board(show, ROW, COL);
}
}
else
{
printf("坐标非法,请重新输入:>");
}
}
}
好了,关于排查雷的部分其实是思考过程是比较复杂的,一定要自己亲手试一下才能更好的感受到。
判断游戏是否胜利
这个就比较简单了,我们只需要定义一个变量,来统计一下我们排查过的坐标数量,到达所有坐标个数减去雷的个数的时候就胜利了。
代码如下:
//排查雷
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
while (win<row*col-Easycount)
{
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
print_board(mine, ROW, COL);
printf("很遗憾,你被炸死了\n");
printf("很遗憾,你被炸死了\n");
printf("很遗憾,你被炸死了\n");
Sleep(3000);
system("cls");
break;
}
else
{
int count = get_mine_count(mine, x, y);
show[x][y] = count + '0';//这里是字符数组,一定要转换成字符再放进去
print_board(show, ROW, COL);
win++;
}
}
else
{
printf("坐标非法,请重新输入:>");
}
}
if (win == (row * col - Easycount))
{
printf("恭喜你,排雷成功!!\n");
printf("恭喜你,排雷成功!!\n");
printf("恭喜你,排雷成功!!\n");
Sleep(2000);
system("cls");
}
}
好了,到这里我们的扫雷基本就完成了,但是还是有很多需要优化的地方,如果你玩过扫雷的话,就知道点下去一个格子他是会展开一片的,这个优化部分就推荐大家自己尝试了,关于优化后期有时间我也会发出来。提示一下:(要用递归哦!),另外还有在控制台玩游戏也是比较挫的,但是受知识程度的限制,等后面可以学一下easyX的图形化界面就可以真正意义上的做出属于自己的游戏了。
好了,最后将所有代码整到一个文件中附上,感兴趣但自己没做出来的可以去尝试一下哦:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<windows.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define Easycount 10
void init_board(char board[ROWS][COLS], int rows, int cols, char set);
void print_board(char board[ROWS][COLS], int rows, int cols);
void set_mine(char board[ROWS][COLS], int row, int col);
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//初始化棋盘
void init_board(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
void print_board(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
printf("-------扫雷--------\n");
for (j = 0; j <= col; j++)
{
printf("%d ", j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("-------扫雷--------\n");
}
//布置雷
void set_mine(char board[ROWS][COLS], int row, int col)
{
int count = Easycount;
while (count)
{
//生成随机下标
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
//计算雷的个数
static int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] +
mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] + mine[x + 1][y]
+ mine[x + 1][y + 1] - 8 * '0';
}
//排查雷
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
while (win < row * col - Easycount)
{
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
print_board(mine, ROW, COL);
printf("很遗憾,你被炸死了\n");
printf("很遗憾,你被炸死了\n");
printf("很遗憾,你被炸死了\n");
Sleep(3000);
system("cls");
break;
}
else
{
int count = get_mine_count(mine, x, y);
show[x][y] = count + '0';//这里是字符数组,一定要转换成字符再放进去
print_board(show, ROW, COL);
win++;
}
}
else
{
printf("坐标非法,请重新输入:>");
}
}
if (win == (row * col - Easycount))
{
printf("恭喜你,排雷成功!!\n");
printf("恭喜你,排雷成功!!\n");
printf("恭喜你,排雷成功!!\n");
Sleep(2000);
system("cls");
}
}
#include<stdio.h>
#include"game.h"
void menu()
{
printf("****************************\n");
printf("******** 1.play *********\n");
printf("******** 0.exit *********\n");
printf("****************************\n");
}
void game()
{
//创建棋盘
char mine_board[ROWS][COLS] = {
0 };
char show_board[ROWS][COLS] = {
0 };
//初始化棋盘
init_board(mine_board, ROWS, COLS, '0');
init_board(show_board, ROWS, COLS, '*');
//打印棋盘
//print_board(mine_board, ROW, COL);
print_board(show_board, ROW, COL);
//布置雷
set_mine(mine_board, ROW, COL);
//print_board(mine_board, ROW, COL);
//排查雷
find_mine(mine_board, show_board, ROW, COL);
}
void test()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}