如何用C语言实现扫雷游戏代码(保姆级别讲解,与你共勉)

一,编写代码先问自己几个问题?

1.你玩过扫雷游戏吗?

如果你没有玩过扫雷游戏的,可以不用往下看了,先去玩一下游戏再回来,因为讲了你也听不懂,看了你也看不懂。

在线扫雷游戏:https://www.msn.cn/zh-cn/play/microsoft-minesweeper/cg-msminesweeper

2.你玩过扫雷游戏,并且你知道扫雷上的数字,1,2,3代表的意义是什么?

在这里我还是需要普及一下扫雷游戏规则,照顾一些可能真没有玩过扫雷这种单机的低端游戏的童鞋们!

2.1 数字1:意味着1的四周除1意外的8个空格内有1个地雷。

2.2 数字2:意味着2的四周除2意外的8个空格内有2个地雷。

2.3 数字3:意味着3的四周除3意外的8个空格内有3个地雷。

如果你确认这个区域是雷区,那么就给他一个插上胜利的红旗!

今天你就是旗手!

OK,讲的有点跑偏了,今天我们重点是来写代码的,不是来玩游戏的。

讲重点!

二,如何实现9*9的棋盘格子,进行扫雷。

1.这是一张需要进行排雷的9x9的棋盘

默认在棋盘上布置10个雷!

2.排雷规则

2.1 如果排查位置不是雷,就显示数字(1或2或3),数字代表其周围有多少雷,作为提示。

2.2 如果排查位置是雷,则就炸死结束游戏。

2.3 如果将布置的10个雷全部排查出来,那么则排雷成功,结束游戏。

3.设计你专属的游戏界面

这种?

还是这种? 

4.排雷游戏的分析和设计

4.1 问题1:如何布置雷,布置的雷如何存储?

4.2 问题2:如何排查雷,排查的雷如何存储?

因此,我们需要设计数据结构来进行存储,那么我会想到设计一个9x9的数组来存放雷的位置。 

如果这个位置布置一个雷,那么我们就放置一个1,否则我们就放置0,现布置10个1,代表1个雷

4.3 布置雷完成之后,假设我们开始排雷的坐标时候。

4.3.1 排查坐标(2,2)

当我们排查(2,2)这个坐标的时候,我们访问周围的的8个黄色位置,统计周围的雷的个数是1个。(为什么我们访问的是8个黄色位置,因为根据扫雷游戏的规则,我们查看雷的个数,我们会在9个格子里面放置雷,放置雷是随机的,现在如果以中间绿色的区域向外排雷,那么右下角的有一个雷,也就是在8个黄色的区域中有一个雷)

4.3.2排查坐标(5,8)

当我们排查(5,8)这个坐标的时候,我们访问周围的一圈8个黄色位置,当我们统计雷的个数时候,我们发现最下面的三个坐标就会出现越出边界,也就是设计的数组长度不够,那么为了防止查询越界,我们在设计的时候就给数组增加一圈。

4.3.3 雷和非雷的坐标地址

现在,我已经可以知道雷和非雷的坐标地址,这个地址就算他们的一个身份信息。可是,我们排查了一个位置之后,就会知道聪这个位置(例如(2,2)这个坐标的周围的8个坐标有一个雷)。那么我们需要将雷的信息进行存储起来,并且也需要打印在显示屏上面共玩家查看,因为这个信息是供玩家参考的。

有同学说,排查出来雷的数据存储在原来布置雷的数组里面,原来的数组存储数据本身没有问题,但是这样雷的信息和雷的个数就会产生信息的重叠和阅读的困难。 

那有没有更好的办法呢?

这里我想到使用两个数组,一个数组用于存储布置好的雷的信息mine[] ,另一个数组用来存储排查出来的雷的信息print[]。 

有了mine[]和print[]两个数组,那么存放的雷的信息和排查出来的雷的信息就不会互相干扰了,如下图示意。

由于我们加了边界,那么实际数组的长度是

 4.3.4 如何增加代码可读性

为了游戏可读性,我们定义两个数组用于存储信息:

char mine[11][11]={0}; //用来存放需要排查的雷的信息

char display[11][11]={0};//用来存放已经排查出来的雷的个数信息

5.如何进行程序框架设计

为了将不同功能存放到对应的文件中,我们可以设计多个文件来进行

test.c //用来存放扫雷游戏的逻辑部分功能的处理

game.c  //用来存放游戏中功能函数

game.h //用来存放游戏中需要使用的数据类型和函数声明

5 如何编写代码的棋盘和游戏规则

5.1 定义一个菜单,玩家可以选择1或者0

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

5.2 为了更方便移动值,初始化棋盘的值,我们会进行宏定义声明

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

然后对数组mine和display进行初始化

char mine[ROWS][COLS] = {0};//'0'
char display[ROWS][COLS] = { 0 };//'*'

初始化数组之后,那我们应该怎么样才能将字符’0‘和’*‘存储到棋盘中

这个时候,我们就想到我们需要定义一个函数来将mine数组和display数组进行初始化,给每一个行列上代表的坐标存储一个数值,如下图是使用函数进行存储的字符。 

要存储两种字符,我们只需要用一个函数就可以实现

因此,我们是怎么只使用一个函数来进行初始化实现呢?

void board_init(char board[ROWS][COLS], int rows, int cols,char set)
{
	int i = 0;
	for (i = 0; i < rows; i++)
	{
		int j = 0;
		for (j = 0; j < cols; j++)
		{
			board[rows][cols] = set;
		}
	}
}

因此,我们定义了字符:char set

然后将字符可以传递过去,虽然使用的是同一个函数,但是我们只要遵循谁先调用,那谁来决定初始化mine[][] 或者是display[][]。

具体想初始化什么内容,是’0‘还是’*‘可以由你来决定。 

	board_init(mine, ROWS, COLS,'0');
	board_init(display, ROWS, COLS,'*');

5.3 棋盘的打印

那我们需要写一个打印的函数,不过由于我们这边设置了边界增加了ROW+2,COL+2

因此,我们在这里打印输出就不需要打印边界,只需要将中间的9*9打印出来就可以。 

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

函数的实现如下:

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	for (i = 1; i<=row; i++)
	{
		int j = 0;
		for (j = 1; j<=col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

然后调试打印一下输出结果,现在mine数组初始化为0,display数组初始化为*

5.4 但是从这个棋盘我们感觉,似乎这个不直观,我们是不是加上行和列

那我们的函数稍作修改,就能够实现行,列数字的一一对应

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	for (i = 0; i <= row; i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i<=row; i++)
	{
		printf("%d ", i);
		int j = 0;
		for (j = 1; j<=col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

运行一下

5.5 抑制雷的坐标屏蔽

从软件开发的角度来说,我们正常是不会将雷的这个棋盘告诉玩家的,因此,我们可以选择将雷的打印这个屏蔽。

	//DisplayBoard(mine, ROW, COL);
	DisplayBoard(display, ROW, COL);

5.6 布置雷 ,怎么布置雷呢?

5.6.1 雷的存放

我们需要把布置的雷布置在中间的9*9的格子里面,也就是在mine[][]的数组里面。 

5.6.2 如何随机布置雷

我们需要随机布置雷,就需要用到随机数,要求只在9*9的格子里面进行布置雷。由于我们需要的随机数字行列是在1到9之间,因此,我们对生成的随机数进行模row,如rang()%row+1  ,rang 模上row=9之后的数字是0~8,那么加1之后就算1~9
 5.6.3 布置雷,需要考虑该坐标是否布置过雷

拿到随机数之后,我们还需要关注一个地方就是,要对未知坐标进行布置雷,那么首先要判断改坐标没有布置过雷。
而且我们已知的是mine[][]的坐标,一开始初始化为0,因此如果查询到mine[x][y]='0',那就可以布置一个雷。 

函数实现如下:

void Setmine(char mine[ROWS][COLS], int row, int col)
{
	int count = EASY_COUNT;
	while (count) //布置一个雷之后,conut就会--,直到布置完
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

运行数一下,布置雷布置成功,只有10个雷

5.7 排查雷 

如何排查?

需要设计一个排查雷的函数:包含如下两部分

5.7.1 找出排查坐标周围的8个坐标雷的个数

其中这个函数需要包含雷的个数,那就需要调用mine数组内已经布置好的雷,输入一个坐标将自己认为是雷的坐标进行排查。那就需要调用mine[][]数组。

5.7.2 找出排查坐标周围的8个坐标雷的个数并展示出来

通过5.7.1 选中一个排查的坐标之后,如果排查的是雷,那么就需要展现打印出来,

我们设计一个函数:FindMineCount(char mini)

那么首先你需要知道,你排查的做标中心点四周的8个坐标,然后再去判定这8个坐标里雷的个数有几个,然后将雷的个数返回出来,并记录到排查的坐标内。

5.7.3 那么我们的程序如何实现呢?

以X,Y坐标为中心点往四周查看,那么我们需要查看,

第一列:(x-1,y-1),(x-1,y),(x-1,y+1)

第二列:(x,y-1),(x,y+1)

第三列:(x+1,y-1),(x+1,y),(x+1,y+1)

既然已经把需要排查的坐标穷举出来,那么我们需要对这么坐标进行判断其是否是雷。

5.7.4 如何判断该坐标是雷?

-->因为我们已经知道,当我们在布置雷的时候,我们回顾一下,我们布置雷的坐标放置的字符‘1’,不布置雷的坐标放置‘0’,下图就是我们一开始设计程序的时候进行的一个布置。

因此,我们判断一个做板是否为雷。

首先我们来了解一下:字符‘1’的ASCLL码值是49,字符‘2’的ASCLL码值是50,字符‘3’的ASCLL码值是51。

那么我们来看字符‘0’的ASCLL码值是48

好了,字符‘1’减去字符‘0’就相当于49减去48得到的数值。

‘1’-‘0’=49-48 = 1

‘2’-‘0’=50-48 =2‘3’-‘0’=51-48=3

那么只需要取出改坐标的字符与字符‘0’ 比较大小,将待比较坐标与字符‘0’相减,

例如将做板坐标(2,1)内放置的字符是‘0’减去字符‘0’,那么结果是0,也就说明该坐标非雷。

例如将做板坐标(3,3)内放置的字符是‘1’减去字符‘0’,那么结果是1,也就说明该坐标是雷。

-->通过以上的分析,我们只需要将5.7.2图所示的其他8个坐标分别与字符‘0’进行相减,那么就可以得知排查的做板(x,y)周围的八个坐标有多少个雷了。

 在代码实现如下,8个坐标减去8个字符'0'后的数值作为排查雷的返回个数:

//函数作用,用来计算排查的坐标的四周除自己坐标之外的8个坐标内有几个是雷
//函数返回值,返回雷的个数
int GetMineCount(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');

}

int count = GetmineCount(mine, x, y);
				display[x][y] = count + '0';

这个int count是我们用来存放有多少个雷的个数,这个个数是int 类型的,而要将这个int 类型转成字符,那么我们只需要对该数字加上字符‘0’就可以转换成对应的字符了。 

同时我们排查这个坐标的周围有几个雷进行打印出来:

DisplayBoard(display, ROW, COL);

那个真个查找雷的函数代码如下: 

void Findmine(char mine[ROWS][COLS], char display[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("很遗憾,被炸死,游戏结束!");
				DisplayBoard(mine, ROW, COL);//打印雷的位置,让玩家死得其所
				break;
			}
			else
			{
				int count = GetmineCount(mine, x, y);
				display[x][y] = count + '0';//数字加上字符’0‘之后就可以转成字符count对应的数字字符了
				DisplayBoard(display, ROW, COL); //排查完之后,再次打印Display的棋盘供玩家参考
				//获取雷的个数,在X,Y周围有几个雷,如果X,Y附件有几个雷,那就放1,2,3放到则个排查的坐标里面
					//’0‘的asc值是48
					//’1‘的ass是49
					//’2‘的ass是50
					//’1‘-’0‘ =1  字符转成数字
					//’2‘-’0‘ =2
					//字符1需要放到display数组里面,就算计算出来的雷的个数,放到display数组里面去。
			}
		}
		else
		{
			printf("非法输入,重新输入!\n");
		}
	} 
	//思路,上来之后就排查,使用printf,请输入要排查的坐标,如坐标(2,5)
	//输入的值的坐标需要在9*9范围内
	//如果输入值的坐标不在范围内,提升输入非法,重新输入
	//因为要多次输入,因此这是一个循环,使用while()
	//如果坐标合法的情况下,那就要判断这个坐标是不是雷
	//如果这个坐标是雷,也就是坐标是’1‘,那就被炸死,打印雷的坐标,游戏结束,
	//如果不是雷,则继续游戏
}

接下来,我们进行调试排查雷的个数,并输入一个坐标2 7:

查看坐标2 7 周围有几个雷:

输入坐标2 7 之后,我们可以看到程序排查出了2个雷:

继续排查一下坐标2 4,看他周围有几个雷,排查结果是0个雷。

程序到这里,还没有写完,我们玩游戏,以玩家的角度,我们总是想要赢的对吧!

5.7.5 如何实现全部雷的排查

那么我们在排查雷的时候,如果while(1)里面放置的一直是1,那么就是在死循环的排雷。

因此,我们需要需要把所有的雷排查出来之后,才认为游戏结束。 

那么,我们需要统计一下雷的个数,并觉得游戏结束的条件:

那么我们定义一个整数来存放剩余雷的个数,int win =0;

如果win < ROW*COL-EASY_COUNT

在game.h的头文件里面,我们进行了宏定义EASY_COUNT=10;

ROW=9,COL=9

因此win只要小于<9*9-10=71,那么就算排雷成功

#pragma once
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2

#define EASY_COUNT 10

void board_init(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 display[ROWS][COLS], int row,int col);
int GetMineCount(char mine[ROWS][COLS], int row, int col);
int GetMineCount(char mine[ROWS][COLS], int row, int col);

但是考虑到排查雷的时间会很久,我们这里延时的时候,就一开始将雷布置79个,这个只剩下来个地方待待排查。

#define EASY_COUNT 79

放置79个雷,可以知道,现在只有2个坐标3 8以及8 2需要排查。 

代码这部分我们增加win的判断:

void Findmine(char mine[ROWS][COLS], char display[ROWS][COLS], int row, int col)
{

	int x = 0;
	int y = 0;
	int win = 0;
	while (win<row*col-EASY_COUNT)   //EASY_COUNT=10,EASY_COUNT是我们一开始布置雷的个数
	{
		printf("请输入要排查的坐标!");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,被炸死,游戏结束!");
				DisplayBoard(mine, ROW, COL);//打印雷的位置,让玩家死得其所
				break;
			}
			else
			{
				int count = GetmineCount(mine, x, y);
				display[x][y] = count + '0';//数字加上字符’0‘之后就可以转成字符count对应的数字字符了
				DisplayBoard(display, ROW, COL); //排查完之后,再次打印Display的棋盘供玩家参考
				//获取雷的个数,在X,Y周围有几个雷,如果X,Y附件有几个雷,那就放1,2,3放到则个排查的坐标里面
					//’0‘的asc值是48
					//’1‘的ass是49
					//’2‘的ass是50
					//’1‘-’0‘ =1  字符转成数字
					//’2‘-’0‘ =2
					//字符1需要放到display数组里面,就算计算出来的雷的个数,放到display数组里面去。
				win++;//排查成功一个雷,win加上一个
			}
		}
		else
		{
			printf("非法输入,重新输入!\n");
		}
		if (win == row * col - EASY_COUNT) //当win等于71的时候,也就是所有的雷都排查出来,排雷成功
		{
			printf("恭喜你,排雷成功!");
			DisplayBoard(mine, ROW, COL); //展示那个做板放置了雷
			//为了演示需求,我们只放置一个两个雷来进行排查,这样会快一点
			//因此,我们可以将EASY_COUNT宏定义声明为79
		}
	}

输入待排查的坐标3 8 ,8 2 

当排查完这两个坐标之后,在游戏的最后,显示排雷成功!!!

6 玩游戏

6.1.玩家规则

玩家肯定是不知道一开始有多少雷,因此,我们需要将棋盘进行隐藏,我们只需要将DisplayBoard(mine, ROW, COL); 这个函数进行隐藏注释掉即可。

void game()
{
	char mine[ROWS][COLS];//'0'
	char display[ROWS][COLS];//'*'
	board_init(mine, ROWS, COLS,'0');
	board_init(display, ROWS, COLS,'*');
	//谁来调用,就谁来初始化,根据想初始化什么就初始化什么

	//打印一下棋盘,只打印中间的9个就可以了,从1到9行
	//DisplayBoard(mine, ROW, COL);
	DisplayBoard(display, ROW, COL);
	//布置雷

	Setmine(mine,ROW,COL);
	//DisplayBoard(mine, ROW, COL); //棋盘雷的个数展示
	// 
	//排查雷
	Findmine(mine, display, ROW, COL);
		//从mine数组里面找雷,找到之后放到display数组里面去

}

隐藏之后,我们看到的棋盘是这样子的

6.2 那么开始玩游戏吧!

随机输入一个坐标2 3,很幸运,你竟然没有被炸死。

再输入个3 4 ,很幸运还是没死!!

再输入个4 6 

很遗憾被炸死了

为了让你死得不冤,所以这里我们再打印一下你输入排查的坐标到底是不是雷:

至此,整个扫雷游戏的分析结束!

7 讨论区

7.1 大家觉得扫雷游戏应该怎么写!

不知道各位看懂了没有,如果有不懂的地方,可以问我哦!

我一定知无不言,言无不尽!!!

需要源代码的可以私信关注我们之后,联系我拿!

最后,请各位发财的金手指,帮忙点点赞和关注!

💁‍♂️💁‍♀️🙋🙋‍♂️🙋‍♀️🧏🧏‍♂️一赞三连🧏‍♀️🙇🙇‍♂️🙇‍♀️🤦🤦‍♂️🤦‍♀️🤷🤷‍♂️🤷‍♀️

💁‍♂️💁‍♀️🙋🙋‍♂️🙋‍♀️🧏🧏‍♂️一赞三连🧏‍♀️🙇🙇‍♂️🙇‍♀️🤦🤦‍♂️🤦‍♀️🤷🤷‍♂️🤷‍♀️

💁‍♂️💁‍♀️🙋🙋‍♂️🙋‍♀️🧏🧏‍♂️一赞三连🧏‍♀️🙇🙇‍♂️🙇‍♀️🤦🤦‍♂️🤦‍♀️🤷🤷‍♂️🤷‍♀️

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值