【c语言】扫雷游戏的实现

 1.项目创建

        首先在写代码之前可以先整理思路,将程序的各个功能部分抽象分离,再分别实现代码细节,使得各部分条例清晰,同时方便调试。

        在开始之前,先创建项目。

大概像这些区分头文件,源文件即可。

其中头文件中存放函数声明,宏定义,头文件包含等代码。

源文件中的game.c存放具体的函数实现,test.c则负责测试和整理各个组分的功能。

2.扫雷的基础功能

大概有如上几种功能需要分别实现。

从上图也可看出,我使用了二维数组,这是因为二维数组的二维展示和扫雷游戏的棋盘很相似,

使用二维数组可以很自然的实现扫雷游戏的棋盘。

大概就像这样,二维数组的下标可以和坐标对应,使我们对相应的棋盘位置进行操作。

所以最开始我们可以写初始化棋盘的函数。

2.1 初始化棋盘

        考虑到程序的可维护性,首先我们可以进行宏定义

#define ROW 9//棋盘长
#define COL 9//棋盘宽

这样在所有使用到棋盘长宽相关数据的地方,通通可以用ROW,和COL,之后如果要更改棋盘的大小,只需要将宏定义后的数字更改掉,而不需要在每个地方都改,这就增强了程序的可维护性。

可以想到扫雷分为两层棋盘,一层是展示给玩家的,一层是存放地雷信息的。

但是我们真的可以直接就把数组初始化为9*9吗,其实并不是。

其实在扫雷游戏的实现中,最重要的功能就是“提示雷数”的功能,为这个功能做考虑,我们先把存放地雷信息的棋盘变为

#define ROWS ROW+2
#define COLS COL+2

也就是11*11,至于为何要这么做,在实现“提示雷数”功能的时候我会进行解释。

void Initbroad(char show[ROWS][COLS], char mine[ROWS][COLS])//初始化棋盘
{
	for (int i = 0; i < ROWS; ++i)
	{
		for (int j = 0; j < COLS; ++j)
		{
			mine[i][j] = '0';
		}
	}

	for (int i = 1; i < ROW + 1; ++i)
	{
		for (int j = 1; j < ROW + 1; ++j)
		{
			show[i][j] = '*';
		}
	}
}

“初始化棋盘”的函数可以这样实现。

当然,光初始化还不行,还得打印棋盘。

2.2 打印棋盘

        打印棋盘的代码实现很简单

   

void Printbroad(char broad[ROWS][COLS])//打印棋盘
{
	for (int i = 0; i < ROW + 1; ++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 ", broad[i][j]);
		}
		printf("\n");
	}
}

效果:

分别打印横坐标,并在每行前加纵坐标进行打印。

主要可以留意一下变量i和j的起始值,这个写法的优点在于可以既可以打印存放雷的棋盘,又可以打印展示用的棋盘,但缺点是浪费了一定的数组空间,可以看到,存放字符'*'的数组show只打印了81个'*',但是它的实际大小确实121个int,如果我不把show和mine的数组大小统一,就不能统一用这个函数打印棋盘了。至于为何要打印存放地雷信息的棋盘,是为了我们观察地雷随机生成的情况,也能方便测试。

在test.c中,我们可以这样进行测试。

void game(void)
{
	char show[ROWS][COLS];
	char mine[ROWS][COLS];
	Initbroad(show, mine);
	Printbroad(show);
    Printbroad(mine);
}

int main()
{
    game();
    return 0;
}

既然我们可以观察到地雷的存放情况了,就可以开始实现“随机生成地雷”的功能了。

2.3 随机生成地雷

        随机生成就需要用到这几个头文件,


#include<stdlib.h>
#include<time.h>

我们需要用srand函数和time函数将rand函数的种子进行改变,再用rand函数随机生成地雷的坐标,然后不难想到,我们需要记录地雷成功生成的个数。

#define difficulty 10//雷数
void Setmines(char mine[ROWS][COLS])//随机生成雷
{
	int minenum = 0;
	while (minenum < difficulty)
	{
		int i = rand() % ROW + 1;
		int j = rand() % COL + 1;
		if (mine[i][j] == '0')
		{
			mine[i][j] = '1';
			++minenum;
		}
	}
}

        其中minenum记录了成功生成的地雷个数,为避免地雷重复生成在同一位置导致计数错误,我们增加了一个判断该位置是否为'0'的条件,若满足,才能使minenum计数加一,同时将该位置更改为'1'。

​
void game(void)
{
	char show[ROWS][COLS];
	char mine[ROWS][COLS];
	Initbroad(show, mine);
	Printbroad(show);
    Printbroad(mine);
    Setmines(mine);//随机生成雷
    Printbroad(mine);
    
}

int main()
{
    srand(time(NULL));
    game();
    return 0;
}

​

srand函数可以在主函数中进行,这样就srand函数就不会被白白执行多次,之后再多运行几次观察是雷数是否正确,多运行几次观察是否随机。

既然地雷也有了,就可以开始实现最关键的功能,“扫雷”功能了。

2.4扫雷

        可以想到,扫雷的逻辑为,扫到雷就结束游戏,没扫到就现实周围雷的数量,

其中提示雷的数量的功能先用Hint函数来先搭建,等会再实现其具体代码,首先我们知道Hint函数一定会返回一个值,用来提示周围的雷数,所以返回值可以为char,将会被赋值该扫除的棋盘坐标

#define makeit ROW * COL - difficulty//需要清理的数量
void Findmines(char mine[ROWS][COLS],char show[ROWS][COLS])//扫雷
{
	int win = 0;
	while (win < makeit)
	{
		int i;
		int j;
		scanf("%d", &i);
		scanf("%d", &j);
		if (i > 0 && i < ROW + 1 && j>0 && j < COL + 1)
		{
			if (mine[i][j] == '0'&&show[i][j] == '*')
			{
				show[i][j] = Hint(mine,show,i,j);
				Printbroad(show);
				++win;
			}
			else
			{
				if (mine[i][j] == '0' && show[i][j] != '*')
				{
					printf("重复扫除,请重新输入\n");
				}
				else
				{
					printf("你被炸死了\n");
					break;
				}
			}
		}
		else
		{
			printf("错误输入,请重新输入\n");
		}
	}
	if (win == makeit)
	{
		printf("芜湖通关!\n");
	}

        注意观察第一层if-else条件语句是为了将排雷的坐标限制再棋盘之内,同时win变量来记录成功清除的次数,用来判断是否通关。

2.5提示地雷数

        首先,想要知道周围的地雷数量,就需要对周围的地雷数量进行排查,有几种思路,

第一个思路较为常规,可以分别判断周围是否为地雷,然后计数,

第二个思路更加巧妙,这里只讲第二种思路的具体实现,

        通过ASCII标准我们知道

           '1' - '0' = 1    

        所以我们可以把周围数组的字符直接相加,再整体减去8*'0'这样就可以得到周围的雷数

        

        如上的情况中,会有'1' + '0' + '1' + '0' + '0' + '1' + '1' + '0' -8* '0' = 4的运算,4就是对应的雷数。具体实现如下。这时你应该也可以理解了为何一开始要把数组大小比实际棋盘大小设置的大一圈,就是防止在检测雷数时越界访问产生不可预期的错误。

char Hint(char mine[ROWS][COLS],char show[ROWS][COLS], int x, int y)//提示雷数
{
	int sum = 0;
	for (int i = x - 1; i < x + 2; ++i)
	{
		for (int j = y - 1; j < y + 2; ++j)
		{
			sum += mine[i][j];
		}
	}
	return sum - '0' * 8;
}

可以看到在for循环中我实际将九个位置的字符相加了,最后只减去了8个'0',得到的结果刚好是雷数对应的字符,例如:2个雷对应  '2',此时return返回值给show数组,再打印

这之后就可以将各个功能做个整理使得游戏开始正常运行了!

2.6菜单以及功能整理

具体的代码实现比较简单,不再赘述,以下为具体实现

void game(void)
{
	char show[ROWS][COLS];
	char mine[ROWS][COLS];
	Initbroad(show, mine);//初始化棋盘
	Printbroad(show);//打印棋盘
	Setmines(mine);//随机生成雷
	//Printbroad(mine);
	Findmines(mine,show);//扫雷


}

void menu()
{
	printf("***********************\n");
	printf("***1.play      0.quit**\n");
	printf("***********************\n");
	printf("输入1或0继续\n");

}



int main()
{
	int input = 1;
	srand(time(NULL));
	while (input)
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			break;
		default:
			printf("输入错误,重新输入\n");
			break;
		}
	}

	return 0;

3.自动扩展棋盘

        自动扩展功能:

        

大概就是上图那样,在扫除周围雷数为零的区域时,会连带着把周围雷数同样不为零和为零的位置扫开,扫到周围有雷的区域停止。

        这里通过函数递归比较容易,先看看具体实现:

        

void Autofind(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y,int* win)//自动开辟
{
	if (show[x][y] == '0'&& x >= 1 && x <= ROW && y >= 1 && y <= COL)
	{
		for (int i = x - 1; i < x + 2; ++i)
		{
			for (int j = y - 1; j < y + 2; ++j)
			{
				if (show[i][j] == '*'&& mine[i][j] == '0'&&i >= 1 && i <= ROW && j >= 1 && j <= COL)
				{
					show[i][j] = Hint(mine, show, i, j);
					++*win;
					Autofind(mine, show, i, j,win);
				}
			}
		}
	}

可以注意到,两层if语句都包含了对坐标的限制,防止数组越界访问,在函数起始位置判断该位置是否无雷,第二个if再判断是否为已经扫开的区域,仔细品味第一个if语句和第二个if语句的区别,你就能理解这样写的原因了。

同时可以注意到,我还引入了一个指针,其实这是'扫雷'功能函数中的win的地址,win是用来记录成功扫除的区域个数的变量。

这个写法的思路大概就是检测四周区域是否符合自动扫除的条件,再在扫除  附近有雷的区域  后停止递归,将自动开出一片区域的效果,分解为一次开或不开周围八个的的小效果,通过函数递归来实现这一功能。相信理解了原理,具体的代码实现含义便一目了然。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值