有趣的代码——扫雷游戏的代码实现

2023-12-18 16:35:29

之前给大家讲过两个用C语言实现的小游戏——猜数字游戏和井字棋游戏,两个小游戏都不是特别的难,可以说是用简单的所学知识就把童年的经典游戏复刻了,让我们既熟悉了编程知识,提高了对于编程学习的兴趣,又重温了童年的那些小小的美好。而提到童年经典游戏,扫雷绝对是初遇电脑的我们绕不开的典中典游戏。今天,就在这里和大家一起实现一个简易版的扫雷吧。

目录

1.扫雷游戏的简介

2.游戏的实现思路

3.game函数的具体实现

3.1棋盘设计——雷的信息存储

3.2棋盘初始化

3.3打印棋盘

?3.4布置地雷

3.5排查地雷

4.扫雷的代码实现

5.扫雷游戏的待完善点


1.扫雷游戏的简介

扫雷游戏是一款经典的小游戏,网页版或者是windows系统自带的扫雷游戏有分难度,每个难度有不一样的雷的个数以及棋盘大小,比如简单难度:9*9的棋盘里面包含了10个雷。扫雷游戏的规则是在尽量短的时间内依照所点击格子的数字提示,点开所有没有布置过雷的格子,同时避免点到存在雷的格子,如果踩到雷游戏就结束。

如果点击的格子周围8个格子没有雷,则就会向周围展开,如果有雷,就会在格子上显示数字:

同时我们可以标记或者清除标记,标记的位置不能被探查:

?可见,扫雷游戏可实现的功能很多,今天我们要实现的扫雷游戏是9*9大小棋盘含有10个地雷的简单难度的扫雷。

2.游戏的实现思路

还记得我在猜数字游戏的实现中说过的一句话吗?没错,我们在写编程题或者小游戏代码时,最重要的就是理清实现思路——主体是什么?为了实现目的要创建哪些函数?函数的功能都是什么?只有当我们心中有了一个大体的框架,知道该做些什么时,我们才能更高效地编写代码,完成程序设计。

扫雷游戏的算法实现如下:

1.menu(提供游戏菜单,由玩家选择是否进行游戏:按“1”开始游戏,按“0”退出游戏,按其他则显示“选择错误,请重新选择”。)

2.game( )(进行游戏)

3.本轮游戏结束,打印游戏菜单并再次询问玩家选择。

在上面的算法实现框里面没有写game函数的具体算法实现过程,因为对于扫雷游戏来说,game函数是相当的复杂,写在里面比较麻烦。

我们在思考大体框架时不用过多在意具体函数的实现方法,可以先起个能表达其功能的函数名并把它放在需要的位置。(说白了就是先搞一个空壳函数占位置,等到大体框架调试完毕不再有问题后,再去实现这些空壳函数)大体框架代码如下:

int main()
{
	int input=0;
	srand((unsigned)time(NULL));//因为雷的位置是随机的,所以大家应该明白这个函数是干嘛的吧?
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d",&input);
		switch(input)
		{
			case 1:
				printf("扫雷!\n");
				game();
				break;
			case 0:
				printf("退出游戏\n");
				break;
			default:
				printf("选择错误,重新选择!\n");
				break;
		}
	}while(input);
	return 0;
}

?我们定义了一个菜单函数,同时在主函数里开始运行,运用switch语句,输入1则进入游戏,输入0则退出游戏,输入其他数字则重新输入,同时我们想:玩一把不过瘾想要继续玩怎么办?用循环结构就好了,我们选择do-while语句先执行后判断,由input控制循环,如果选择了0那正好不符合while的条件退出循环。(注:大部分游戏等都是采用do……while语句,因为大都是先执行后判断)
menu函数代码如下:

void menu()
{
	printf("*********************************\n");
	printf("**********   1.play   ***********\n");
	printf("**********   0.exit   ***********\n");
	printf("*********************************\n");
}
3.game函数的具体实现

因为game函数比较复杂,所以我们把它提出来单独讲解,并且为了让代码的可读性更好,我们要使用之前在井字棋中讲到的知识点——多文件程序。(有对于多文件程序不了解的,可以看下面文章中的第三部分知识)有趣的代码——井字棋游戏的实现-CSDN博客

?今天我们的代码就分别放在三个文件:test.c、game.c、game.h中。test.c源文件主要用来测试整个小游戏;game.c源文件主要用来完成小游戏各功能的实现;game.h头文件中则用来包含所有的库函数并声明game.c中的函数。这样分文件书写代码段的好处在于:结构化清晰,便于团队合作;易于维护、修改和功能扩张;代码的可读性高。

3.1棋盘设计——雷的信息存储

扫雷游戏的棋盘我们很容易想到用二维数组来建立,但考虑到我们在玩网页版扫雷时,棋盘会根据我们的操作产生变化(如提示周围多少雷和标记可能是雷等等),所以,我们在建立二维数组,应该建立两个二维数组,一个用于存放雷的情况,另一个用于我们进行操作和标记。

注意:二维数组的行列大小确定,玩家点击格子,系统会对该格子的周围8个格子进行探查是否存在雷,如果我们点击的四个边的格子,可能会出现越界的现象,如下图:

为避免越界情况出现(可能会造成错误),我们在建立数组时除了考虑本身要使用的9*9棋盘之余?,还要把四周考虑到,故最终应该建立11*11大小的数组,如下图所示:

    //雷的信息存储
	//1.布置好的雷的信息
	char mine[ROWS][COLS]={0};//11*11
	//2.排查出的雷的信息 
	char show[ROWS][COLS]={0};

这里我们使用宏定义定义ROW、COL为9,ROWS和COLS为ROW+2,这样方便棋盘的扩大和游戏的升级。

3.2棋盘初始化

上面我们已经建立了两个二维数组,分别是存放布置好的雷的信息的主体数组mine和存放排查出的雷的信息的展示数组show。接下来我们要把两个数组分别进行初始化,在最初的考虑中是想分别建立两个函数进行初始化,但后来想到两个数组本质上一样的,只是我们初始化时赋的初值不同,那么只要我们把相应欲赋的字符也传进去,即可满足两个数组初始化的需求。

代码如下图所示:

/*    game.c中的内容    */
void Initboard(char mine[ROWS][COLS], int rows, int cols,char set)
{
	
	for (int i = 0; i < rows; i++)
	{
		for (int j = 0; j < cols; j++)
		{
			mine[i][j] = set;
		}
	}
}
/*    tect.c中的内容    */ 
Initboard(mine, ROWS, COLS,'0');//初始化存放地雷的数组
Initboard(show, ROWS, COLS,'*');//初始化显示信息的数组
3.3打印棋盘

我们在玩扫雷游戏过程中,游戏是会及时给我们反馈的。(当我们没有扫到雷,它会提醒我们周围的雷数;当我们扫到雷时,会打印布置的雷的信息)换句话说,就是无论我们扫雷的情况是啥,程序应该都会给我们打印操作后的棋盘,只不过区别在于踩雷时,打印的时布置雷信息的棋盘,让我们看到本次游戏中哪里有雷,而未踩雷时,打印的是我们排查出的雷的信息。所以,我们建立的打印棋盘函数理应打印两个数组,代码如下图所示:

/*    game.c中的打印函数实现    */
void DisplayBoard(char board[ROWS][COLS],int row,int col)//传入的数组该是几行就是几行,不能改变数组本身,但是row这种参数,则是根据需要传入 
{
	int i=0;
	int j=0;
	//打印列号
	for(i=0;i<=col;i++) 
	{
		printf("%d ",i);
	}
	printf("\n");
	for(i=1;i<=row;i++)
	{
		printf("%d ",i);
		for(j=1;j<=col;j++)
		{
			printf("%c ",board[i][j]);
		}
		printf("\n");
	}
}
/*    tect.c中的内容    */
    printf("初始棋盘如下:\n");
	DisplayBoard(mine,ROW,COL);
	printf("\n");
	printf("覆盖,埋雷\n");
	DisplayBoard(show,ROW,COL);

具体效果如下图所示(左图存放为布置的雷的信息-暂时未放雷,右图为排查的雷的信息-未排查啥信息也没有)?

?3.4布置地雷

就像上面图展示,到目前为止棋盘中是没有雷的,那我们就需要布置雷。

存放地雷的二维数组之前被我们全部初始化为'0',那么我们就让电脑在9*9的范围内随机更改二维数组的值为'1',表示放置地雷。

设置地雷就需要用到随机数的三个函数:

int rand(void)? ? ? ? ? ?void srand(unsigned int seed)? ? ? ? ?time_t time(time_t* timer)

如果对三个函数的用法不太了解,可以访问我的文章:有趣的代码——猜数字游戏的实现-CSDN博客里面的第三部分有相关知识的详细介绍。

void SetMine(char board[ROWS][COLS],int row,int col)
{
	int count=EASY_COUNT;
	while(count)
	{
		int x=rand()%row+1;
		int y=rand()%col+1;
		if(board[x][y]=='0')
		{
			board[x][y]='1';
			count--; 
		}
	}
 } 

EASY_COUNT这里我们用符号常量表示雷的个数,方便我们进行调试和修改,同时使代码的可读性更好,便于拓展新功能和新模式。

3.5排查地雷

游戏既然叫扫雷,那么排查地雷很显然是最重要的一环了,在我们今天的简易版扫雷中排查雷只需要能够解决三种情况即可——①失败时:踩雷了,判定游戏失败;②游戏继续时:未踩雷,但也没有把雷排完,需显示周围雷的情况;③获胜时:雷排完了,判定游戏成功。

上面我们已经把排查雷的思路梳理完毕,代码如下图所示:

void FindMine(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-EASY_COUNT)
	{
		printf("请输入排查雷的坐标:>\n");
		scanf("%d%d",&x,&y);
		if(x>=1&&x<=row&&y>=1&&y<=col)
		{
			//坐标的合法性
			//1.踩雷
			if(mine[x][y]=='1')
			{
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine,row,col);
				break;
			} 
			if(mine[x][y]!='0')
			{
				printf("您已标记过该坐标,请重新输入\n");
				continue; 
			}
			//2.不是雷 
			else//计算坐标周围几个雷 
			{
				int count=get_mine_count(mine,x,y);
				show[x][y]=count+'0';//count本来是个整型+'0'变成char 
				DisplayBoard(show,row,col);//这里实际上应该有扩展式扫雷,但目前程度不够解决该问题。 
				win++;	
			}
	    }
		else
		{
			printf("输入坐标非法,请重新输入!\n");
		}
	}
	if(win==row*col-EASY_COUNT)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine,row,col);//获胜了,打印一下布置雷的信息,看看都是哪些地方有雷,也符合原版扫雷 
	}
}

上面FindMine函数里面还嵌套了一个未讲解的函数get_mine_count,其功能是当游戏处于②继续进行时用于判断周围雷的情况,之所以在定义一个函数,是为了避免函数冗长。

get_mine_count函数的代码如下所示:

int get_mine_count(char mine[ROWS][COLS],int x,int y)
{
	return mine[x-1][y]+mine[x-1][y-1]+mine[x][y-1]+mine[x+1][y-1]+mine[x+1][y]+mine[x+1][y+1]+mine[x][y+1]
	+mine[x-1][y+1]-8*'0';
}

大家也可以用for循环来解决,当然直接写时间效率高一些,不用嵌套循环。

4.扫雷的代码实现

tect.c的代码如下图所示:

/*    tect.c    */
#include"game.h"
void menu(); 
void game();
 
int main()
{
	int input=0;
	srand((unsigned)time(NULL));
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d",&input);
		switch(input)
		{
			case 1:
				printf("扫雷!\n");
				game();
				break;
			case 0:
				printf("退出游戏\n");
				break;
			default:
				printf("选择错误,重新选择!\n");
				break;
		}
	}while(input);
	return 0;
}

void menu()
{
	printf("*********************************\n");
	printf("**********   1.play   ***********\n");
	printf("**********   0.exit   ***********\n");
	printf("*********************************\n");
}

void game()
{
	//雷的信息存储
	//1.布置好的雷的信息
	char mine[ROWS][COLS]={0};//11*11
	//2.排查出的雷的信息 
	char show[ROWS][COLS]={0};
	//初始化
	InitBoard(mine,ROWS,COLS,'0');
	InitBoard(show,ROWS,COLS,'*');//保持神秘感,看不到是否是雷 
	//打印棋盘
	printf("初始棋盘如下:\n");
	DisplayBoard(mine,ROW,COL);
	printf("\n");
	printf("覆盖,埋雷\n");
	DisplayBoard(show,ROW,COL);
	//布置雷
	SetMine(mine,ROW,COL); 
	//扫雷
	FindMine(mine,show,ROW,COL); 
} 

game.h的代码如下图所示:

/*    game.h    */
#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 InitBoard(char board[ROWS][COLS],int rows,int cols,char set);//把对应的符号放入
void DisplayBoard(char board[ROWS][COLS],int row,int col);
void SetMine(char board[ROWS][COLS],int row,int col);
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);//函数声明后面有分号 

game.c的代码如下图所示:

/*    game.c    */
#include"game.h"
void InitBoard(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 DisplayBoard(char board[ROWS][COLS],int row,int col)//传入的数组该是几行就是几行,不能改变数组本身,但是row这种参数,则是根据需要传入 
{
	int i=0;
	int j=0;
	//打印列号
	for(i=0;i<=col;i++) 
	{
		printf("%d ",i);
	}
	printf("\n");
	for(i=1;i<=row;i++)
	{
		printf("%d ",i);
		for(j=1;j<=col;j++)
		{
			printf("%c ",board[i][j]);
		}
		printf("\n");
	}
}
void SetMine(char board[ROWS][COLS],int row,int col)
{
	int count=EASY_COUNT;
	while(count)
	{
		int x=rand()%row+1;
		int y=rand()%col+1;
		if(board[x][y]=='0')
		{
			board[x][y]='1';
			count--; 
		}
	}
 } 
int get_mine_count(char mine[ROWS][COLS],int x,int y)
{
	return mine[x-1][y]+mine[x-1][y-1]+mine[x][y-1]+mine[x+1][y-1]+mine[x+1][y]+mine[x+1][y+1]+mine[x][y+1]
	+mine[x-1][y+1]-8*'0';
}
void FindMine(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-EASY_COUNT)
	{
		printf("请输入排查雷的坐标:>\n");
		scanf("%d%d",&x,&y);
		if(x>=1&&x<=row&&y>=1&&y<=col)
		{
			//坐标的合法性
			//1.踩雷
			if(mine[x][y]=='1')
			{
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine,row,col);
				break;
			} 
			if(mine[x][y]!='0')
			{
				printf("您已标记过该坐标,请重新输入\n");
				continue; 
			}
			//2.不是雷 
			else//计算坐标周围几个雷 
			{
				int count=get_mine_count(mine,x,y);
				show[x][y]=count+'0';//count本来是个整型+'0'变成char 
				DisplayBoard(show,row,col);//这里实际上应该有扩展式扫雷,但目前程度不够解决该问题。 
				win++;	
			}
	    }
		else
		{
			printf("输入坐标非法,请重新输入!\n");
		}
	}
	if(win==row*col-EASY_COUNT)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine,row,col);//获胜了,打印一下布置雷的信息,看看都是哪些地方有雷,也符合原版扫雷 
	}
}

上面有一个地方值得大家注意,game.h里面的函数声明没有get_mine_count,这样对吗?其实,没什么问题的,因为我们在主函数main里面并没有用到get_mine_count函数,而get_mine_count函数唯一使用的地方是game.c文件中,但是get_mine_count函数在game.c中有定义,所以就不存在什么问题。

5.扫雷游戏的待完善点

①没有实现展开功能,即不能点一个位置,清理附近无雷的地方。这样的话,我们想要获得游戏胜利的难度就比较大,因为我们需要一个一个点,最多可能要点9*9-雷的个数次,这样游戏的可玩性比较低。

②没有标志和取消标志功能,即对于存疑地方进行标注。同样,这样功能的缺失使游戏的可玩性比较低,影响游戏的进行。

总之,本次扫雷游戏的代码实现分享就到此结束了,希望大家能从中收获乐趣和知识。另外,大家感兴趣的话,也可以自行对游戏代码进行完善,当然,后面应该也会出一篇完善版,大家可以期待一下。

文章来源:https://blog.csdn.net/m0_74950144/article/details/135061087
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。