今天来分享一下写的扫雷小游戏,其中包括了 展开一片的递归操作,标记,和取消标记。
除了操作界面不一样之外,逻辑和平时玩的扫雷基本是一样的。
游戏规则
给定一个棋盘,期棋盘中有若干个雷,玩家需要通过分析,把所有不是雷的格子给排查,也就是翻开。
操作
1)点击格子进行排雷
排雷后的格子有两种状态
该位置是雷,游戏结束。
该位置不是雷,显示该位置周围八个坐标中雷的个数,如果周围八个坐标没有雷,则这里显示空格。
2)右击格子进行标记
标记分为两种
玩家确定这个位置是雷,给上小红旗。
玩家不确定这个位置是雷,给上一个问号。
胜利条件
玩家通过排雷和标记的配合,结合排雷后的格子上提示的信息,将棋盘中所有不是雷的格子都排查完毕。
游戏实现
棋盘
棋牌作用
埋雷
显示排雷情况
看一下实际的效果:
![]() 游戏中的扫雷棋盘 |
![]() 模拟的扫雷棋盘 |
![]() 埋雷的棋盘 |
扫雷棋盘,就是可以看成一个矩阵,这里面的是9*9的一个矩阵,所以我们要用二维数组来模拟。
规定:
棋盘可操作的区域是9*9的二维字符数组
棋盘有两个,一个用来埋雷,一个用来显示排查雷的情况
埋雷的棋盘,‘1’代表该位置有雷,‘0’代表该位置没雷
实际的棋盘要多出两行两列
解释
首先,我们先回忆一下,排雷的规则,给出一个坐标,如果该位置不是雷,对其周围八个格子雷的数量进行统计,显示在该位置。
但问题是,一定是8个吗,显然是不一定的,对于边上的坐标,理应排查五个位置,角上的坐标,理应排查三个位置,但是这样考虑,是不是显得很麻烦,因为大部分坐标排查的都是8个位置,所以我们做一下处理,增加两行两列,这些位置不埋雷,也就是雷的个数是 0,但这些位置需要可以访问,所以实际数组的大小为,11*11,如图所示:

如图,中间蓝色9*9的区域,是我们要排雷的区域,这些区域埋的有雷,而外层橙色的区域是多加的两行两列,这些区域没有雷,方便我们对特殊位置的排雷,保证每个位置都是对周围8个位置进行统计雷的个数。
为何用两个棋盘
你想,按照目前的规定,假如只用一个棋盘,我们对一个位置排雷了,并将这个位置周围雷的信息显示到该位置,比如是3个雷,那么如果,我们排查其它位置的时候,如果也需要排查这个位置,但我们用的是‘0’或‘1’进行判断是否为雷,但这个‘3’是不是就影响我们的判断了,而且处理起来也比较麻烦。
所以,我们干脆使用两个棋盘,它们的大小是一模一样的,排雷使用的是埋的有雷的棋盘,之后,我们将信息显示到另一个棋盘对应的位置,之后只显示该棋盘,这个问题就解决了。
![]() 埋雷的棋盘 |
![]() 显示信息的棋盘 |
辅助函数
接下来给出一些变量和常量的定义
#define rows 9//可操作的行
#define cols 9//可操作的列
#define ROWS rows+2//实际的行
#define COLS cols+2//实际的列
#define MINES 10//雷的个数
char board[ROWS][COLS] = { 0 };//埋雷 排雷的棋盘
char show[ROWS][COLS] = { 0 };//打印效果的棋盘
上面这些变量,是在函数里面要传的参数。
下面三个函数是这个游戏中要一直用的函数,先介绍一下:
- 初始化函数
两个棋盘一个埋雷,一个显示,规定,埋雷的棋盘全部初始化为字符‘0’,而显示的棋盘,全部初始化为‘*’。
注意,这里需要传实际的行和列,尤其是对于埋雷的棋盘,那多加的两行两列要初始化为‘0’。
void InitBoard(char board[ROWS][COLS], int row, int col,char cur)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
board[i][j] = cur;
}
}
}
这个函数比较简单,比较巧的一点是,两个棋盘大小一样,但需要初始化的值不一样,那么就把需要的初始化值作为参数 cur传进来,初始化就好了。
- 打印棋盘
我们打印棋盘肯定要打印的是中间那块能操作的区域,也就是9*9那块,所以干脆就传这个行和列。
对于这个打印我们提供了两个函数,其实也就是加上了边框,看一下效果
![]() 没加边框打印 |
![]() 加边框打印 |
代码如下,自取哦~
没加边框打印:
void ShowBoard(char board[ROWS][COLS], int row, int col)
{
printf("----------扫雷游戏----------\n");
for (int i = 0; i <= col; i++)
{
printf(" %d ", i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf(" %d ", i);
for (int j = 1; j <= col; j++)
{
printf(" %c ", board[i][j]);
}
printf("\n");
}
}
加边框打印:
void ShowBoard(char board[ROWS][COLS], int row, int col)
{
printf("----------扫雷游戏----------\n");
for (int i = 0; i <= col; i++)
{
if (i == 0)
printf(" %d ",i);
else
{
printf(" %d ", i);
}
}
printf("\n");
for (int i = 0; i <= col; i++)
{
if (i == 0)
{
printf(" |");
}
else
{
printf("---|");
}
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf(" %d|", i);
for (int j = 1; j <= col; j++)
{
printf(" %c |", board[i][j]);
}
printf("\n");
for (int k = 1; k <= row; k++)
{
if (k == 1)
{
printf(" ");
}
if(k==1)
printf("|---|");
else
{
printf("---|");
}
}
printf("\n");
}
}
- 埋雷
埋雷就是生成一个坐标,只要这个位置还没雷,就在这个位置埋一个雷。
void Setmine(char board[ROWS][COLS], int row, int col)
{
//坐标
int x = 0;
int y = 0;
int mines = MINES;//10个雷
while (mines)
{
//生成1到9的坐标
x = rand() % 9 + 1;
y = rand() % 9 + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
mines--;
}
}
}
- 统计雷
写统计雷之前,我们先复习一个小的知识点,就是0到9的数字它对应的字符的ASCII码值和这个数字的关系。
字符零‘0’它的ASCII值是48,‘1’的ASCII值是49,‘2’的ASCII值是50,后边的依次类推。
那么通过字符‘1’如何得到整数1,是不是就用49减去48,就得到整数1了,其它的数字也是同样的道理,因为对字符进行算数运算,操作的还是它的ASCII值。
代码如下:
int MineCount(char board[ROWS][COLS], int x, int y)
{
return 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';
}
看这个函数,传过来了我们埋雷的数组,和一个坐标,对于埋雷的数组,它的每个位置不是‘0’就是‘1’,我们把它的周围八个位置上字符加起来,其实加的是ASCII值,然后根据上面的小知识点,再减去8*‘0’,其实是8*48,得到的整数不就是周围雷的个数吗,所以这个代码就是这个原理。
再举个实例,比如一个坐标周围有5个雷,也就是 5个‘1’和3个‘0’,加起来是
3*48+5*49=389,8*48=384,389-384=5,5不就是雷的个数吗,5再加上48,对应的字符不就是‘5’吗。
看一下排雷的效果:
![]() 显示的棋盘 |
![]() 埋雷的棋盘 |
如图所示,显示棋盘中,左上角的 2,就代表周围红方框内有两个雷,中间的2 就代表,中间红方框内有两个雷,对应到埋雷的棋盘,确实是如此。
标记和取消标记
这两个操作相对简单,先介绍这两个操作,最后再介绍排雷的操作。
标记
1.注意
坐标输入要合理
操作的是显示的棋盘
已标记的位置不重复标记
已排查的位置不能标记
标记分为两种,‘!’和'?'
被标记的位置不能排查
前面四种比较好理解,重点解释一下最后两种。
标记分为两种:
'!'代表玩家确定这个位置是雷,
'?'代表玩家不确定这个位置是否有雷,可能是有的。
被标记的位置不能排查:
就是说这个位置玩家认为它是雷或者可能为雷,在排雷的过程中,这些位置是不能被排查,在递归的过程中,也要考虑这种情况,因为递归的过程也是在排查雷。
2.代码
void PosMark(char show[ROWS][COLS], int row, int col)
{
int x = 0, y = 0;
printf("请输入标记的坐标:>");
scanf("%d%d", &x, &y);
system("cls");
if (x >= 1 && x <= row && y >= 1 && y <= col)//输入的坐标数值合法
{
if (show[x][y] == '*')
{
show[x][y] = '!';
}
else
{
if (show[x][y] == '!'||show[x][y]=='?')
{
printf("该位置,已标记不可重复标记!!\n");
}
else
{
printf("该位置已经排查过,不能标记!!\n");
}
}
}
else
{
printf("输入坐标不合法,请重新输入!!\n");
}
}
对于正确的标记,只需把原来位置上的'*'改为!即可,其它错误的标记,给出合理的提示即可。
取消标记
- 注意
坐标输入要合理
操作的是显示的棋盘
未标记的、已排查的位置不能取消标记
- 正确的取消标记
第一次,先把!改为?
第二次,再把?改为*
- 代码
void EraMark(char show[ROWS][COLS], int row, int col)
{
int x = 0, y = 0;
printf("请输入取消标记的坐标:>");
scanf("%d%d", &x, &y);
system("cls");
if (x >= 1 && x <= row && y >= 1 && y <= 9)//坐标合理
{
if (show[x][y] == '!')//标记为 '!'
{
show[x][y] = '?';//标记改为 '?'
}
else if (show[x][y]=='?')//标记为'?'
{
show[x][y] = '*';//改为初始值
}
else
{
if (show[x][y] == '*')//对未标记的坐标进行取消标记
{
printf("该位置未被标记,不可取消标记!!\n");
}
else//对排查过的位置进行取消标记
{
printf("该位置已排查,不可取消标记!!\n");
}
}
}
else//坐标输入错误
{
printf("坐标输入错误!!\n");
}
}
只有当坐标输入正确且该位置已经标记,才能取消标记,其它位置给出合理提示即可。
排雷
排雷中,难点在于那种一大片的效果,那是采用的递归算法。
- 注意
该位置有雷,游戏结束
该位置没雷,进行排雷
坐标不越界、不重复排雷
- 代码
int Findmine(char board[ROWS][COLS], char show[ROWS][COLS], int row, int col) {
int x = 0, y = 0;
//system("cls");
printf("请输入排雷坐标:>");
scanf("%d%d", &x, &y);
system("cls");
if (x >= 1 && x <= row && y >= 1 && y <= col) {//坐标合理
if (show[x][y] != '*') {//该位置不是初始值,不能排雷
if (show[x][y] == '!' || show[x][y] == '?') {//该位置被标记,不能排雷
printf("该位置已被标记,暂时不可排雷,请取消标记后,再进行排雷!!\n");
return 1;
} else {//该位置已经排雷,不能排雷
printf("该位置已排雷,不可重复排雷!!\n");
return 1;
}
} else { //统计雷的个数
if (board[x][y] == '1') { //该位置是雷,游戏结束
return 0;
} else {//该位置需要排雷
RecuMine(board, show, x, y);//递归排雷
return 1;
}
}
} else {//坐标输入不合法
printf("坐标输入错误!!!\n");
return 1;
}
}
排雷本身的逻辑并不复杂,只要在正确的位置排雷即可,对于不同排雷的情况,我们给出不同的提示。
里面返回值: 1 代表本次排雷后,游戏继续(输赢是在这个函数外面判断的),0 代表排到雷了,游戏结束。
- 递归排雷
1)条件:
该位置周围没有雷(该位置显示的是空格)
该位置没有被排查过(不重复递归)
坐标不越界
2)递归的代码
void RecuMine(char board[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
if (x < 1 || x>9 || y < 1 || y>9)//坐标不合理不递归
return;
if (show[x][y] == '!'||show[x][y]!='*'||show[x][y]=='?')
// 该位置被标记(!或?)、该位置被排查过 不递归
//您想被排查的坐标只要不是雷,那肯定有信息,一定不会是初始值 ‘*’
//那么就可确定只要不等于 * 的位置,就排查过了
return;
int count = 0;
count = MineCount(board,x, y);//先统计改坐标周围雷的个数
if (count == 0)//雷的个数为 0,即周围 8个位置都不是雷,那么这个八个位置就放心排雷了
{
show[x][y] = ' ';//先把该位置置为空格
//递归它周围 8个位置的坐标,进行排雷
RecuMine(board, show, x - 1, y - 1);
RecuMine(board, show, x - 1, y );
RecuMine(board, show, x - 1, y + 1);
RecuMine(board, show, x, y - 1);
RecuMine(board, show, x, y + 1);
RecuMine(board, show,x+1, y - 1);
RecuMine(board, show, x+1, y);
RecuMine(board, show, x+1, y +1);
}
else//这里代表这个位置周围是有雷的,那么把的个数显示上去,程序走到尾部,递归也就返回了
{
show[x][y] = count + '0';
}
}
递归的代码如上,接下来看一下递归产生的效果。
![]() 排查的是(4,4)这个位置 |
![]() 排查的是(3,6)这个位置 |
以上就是对这个递归排雷的介绍,我们尤其要注意的是递归进行的条件!!
判断输赢
这个比较简单,只要所有的非雷格子都被翻开,那么就赢了,我们只需要统计这些格子的数量就好了。
代码
//判断输赢
int IsWin(char show[ROWS][COLS], int row, int col)
{
//所有不是雷的格子都被掀开了,就赢了
int count = 0;
for (int i = 1; i <= row; i++)
{
for (int j = 1; j <= col; j++)
{
if (show[i][j]!='*'&&show[i][j]!='!'&&show[i][j]!='?')
//这个地方不是雷 他被掀开了
count++;
}
}
if (count == rows*cols-MINES)
return 1;
else
return 0;
}
这个的功能其实是判断是否赢了,输了我们是在排雷那里根据返回值判断的,这里返回值为1 代表赢了,返回值为 0代表没有赢,游戏继续。
if (show[i][j]!='*'&&show[i][j]!='!'&&show[i][j]!='?')
//这个地方不是雷 他被掀开了
count++;
这个语句的意思是,该位置已经被排查过了(同时不能被标记),那么这个格子就是我们要统计的。
再来
if (count == rows*cols-MINES)
return 1;
else
return 0;
总的格子数减去埋有雷的格子数,就是我们要统计的个数,只要是这个数,那么就赢了。
总结
- 主函数
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void menu() {
printf("*************************\n");
printf("********* 1.play *******\n");
printf("********* 0.exit *******\n");
printf("*************************\n");
}
void menu2() {
printf("1.排雷 2.标记 3.取消标记\n");
}
void game() {
system("cls");
char board[ROWS][COLS] = { 0 };//埋雷 排雷的棋盘
char show[ROWS][COLS] = { 0 };//打印效果的棋盘
//初始化棋盘
InitBoard(board, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//ShowBoard(show, rows, cols);
//埋雷
Setmine(board, rows, cols);
ShowBoard(board, rows, cols);
int cur = 1;
//这里初始值设为1,是为了防止第一次没有排雷,直接判断cur等于0就结束了
//所以这个初始值 要非零
int input = 0;
while (1) {
ShowBoard(show, rows, cols);
menu2();
printf("请选择操作:>");
scanf("%d", &input);
switch (input) {
case 1:
//排雷
cur = Findmine(board, show, rows, cols);
break;
case 2:
//标记
PosMark(show, rows, cols);
break;
case 3:
//取消标记
EraMark(show, rows, cols);
default:
printf("输入错误,请重新输入!!\n");
break;
}
if (cur == 0) {
printf("很遗憾,你被炸死了,游戏结束!!!\n");
ShowBoard(board, rows, cols);
break;
}
if (IsWin(show, rows, cols)) {
printf("恭喜你,你赢了!!!\n");
ShowBoard(show, rows, cols);
ShowBoard(board, rows, cols);
break;
}
}
}
int main() {
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:
break;
}
} while (input);
return 0;
}
- 注意
把游戏各个功能分成一个个模块,逐个去写,并且写一部分调试一部分
扫雷游戏难点在棋盘的设计和操作的实现
以上就是本次分享的全部内容啦
如果对您有帮助,希望给博主点点赞,收藏,分享一下,感谢您的支持!!
源代码和往常一下,放在gitee仓库里面啦,有需自取哦~~~
我们下期,再见~
链接如下~~
扫雷完整版_1_2_25 · 琦琦爱敲代码/琦琦的日常代码 - 码云 - 开源中国 (gitee.com)
老铁们,可以玩一玩这个扫雷哦,评论区可以留下你成功了多少次,用时多少哦~~