对扫雷游戏的C语言实现和拓展(详解)

今天给大家带来的是关于扫雷游戏的C语言代码和拓展.**和之前一样,如果代码中有不妥或错误之处烦请指正不胜感激.(提醒一下,我的代码必然有错误但应该只有一处,希望有高人相助)**首先在文章但是之前我想声明一下这篇文章涉及不到动态内存管理,因为本人还没学到但是又由于有的编译器不支持变长数组,所以本人如下的c语言代码无法改变行数和列数烦请告知
首先我认为一个程序的实现有赖于算法,所以得先思考再设计.
下面是实现它的思考

一级标题1.创建二维数组和初始化

首先既然要实现扫雷的话他必定有一些格子那既有函
行数又有列数(99),所以我们要创建二维数组
但是如果只创建一个二维数组是不够的因为一个用来给用户看,还有一个是用来排查雷的信息如果只用一个二维数组那显然会混起来所以应该创建两个二维数组
然后二维数组我们要动脑筋思考一下需要什么类型的?
如果这个雷和不是雷用数字零和一的话,可能是大家的首选,但是如果一开始给用户的界面我们采用星号代表还没有被点击的部分但是由于这个数组必须是相同类型的,所以我们可以把这个其他的全部变成字符,因为在c语言中这个字符和数字是等价的可以用ascii码来转化
其次由于这个矩形,它有边有角落而在边上他只有以他周围虽然也是八个,但是有一部分在这个边框之外应该懂我的意思吧?我不太会搞图啊还请大家谅解然后因此我们就上下分别拓宽一行,左右分别拓宽一列这样的话我们就不需要再花额外的心思来区分中间的点和边上的点
然后设置完了这个数组的话,我们可以对这个数组进行初始化由于这个初始化是默认反斜杠零他跟字符零显然不一样所以我们需要用一个初始化数组的函数,这个由我们自己定义,应该用循环吧
然后呢由于我们这里是有两个数组,两个都需要初始化那一个就是我们用户的初始化成
还有一个我们可以初始化成零,就是一开始是不设置雷的情况但是呢我们还不想设置两个函数那样太麻烦了而两个这个初始化的字符不一样,所以可以再给函数增加一个参数这样的好处就是可以用同一个函数给不同的这个数组初始化
然后接下来就是我们的打印数组为了之后可以更好的确定坐标,因为我们的目的是用户输入坐标排雷。所以此时最好确定一下这个每一行每一列
此时二维数组原本是9×9的就是说真正有效的打印出来的只有9×9但是我们定义的时候应该是按照11×11的定义要不然的话,后面就要区分边上的点和中间的点比较麻烦9×9显然下标应该是010,但是我们去掉零,去掉10下标正好是19。而这个列下标显然是在最上面,因此我们先行打印但是呢由于这个第一列的位置它是行下标的位置所以说我们从零开始,正好就遍历了一到九,对应的是呃数组的19的下标然后这个行行下边应该是在循环的时候是在外循环开始之后打印,也就是说外循环指的是行循循环指的是列就是说嗯先是是第一行内循环做完然后这个外循环和内循环,他的那个便历都是19

一级标题2.打印数组并布置雷区

打印完之后就要开始布雷了由于布雷具有随机性,我们此时需要用到rand的函数但由于它生成的是伪随机数,所以我们需要用到srand 这个函数它返回的由于它跟时间有关,时间肯定是正数,它没有负数这个说法因此需要的是无符号整型具体的可以看我接下来的代码
这里要注意啊,这个srand((unsigned int)time(NULL))需要包含对应的头文件具体看接下来的代码然后呢由于它是随机的呀,所以它可能两次布的是同一个位置所以在布置雷的时候必须要先检查一下这个点我之前有没有布置雷过然后呢为了让用户输入坐标并且知道用户什么时候胜利。必须要用一个变量跟踪然后这个变量如果已经大到等于这个整个整个矩形的个数,再减去雷的个数,显然它就已经排雷成功了
因此这里我们用循环语句

一级标题3.排雷

然后呢判断这个输入的坐标是否合理?
如果第一次就踩中雷了,游戏结束并且打印这个有雷的这个数组,那如果没有踩雷的话,这里需要知道这个字符和数字如何转换任何一个数字加上零字符就等于这个数字所对应的字符由于这个数字和零的距离是固定的而这个数字正好是在ascii码值上是递增的所以一个数字加上0的ASCII码值,相当于将这个零的位置往上抬了几位,正好就达到了这个数字所对应的ascii码值,而数字转化成字符也是类似的然后每次一旦没有点中雷,我就让这个计数增加一如果正好等于这个格子数减去雷的个数,那说明挑战成功这里我们如果没有踩中雷的话,需要提示一下我们所选的坐标周围有几个雷?如果用for循环,如果把这个点看成00的话,那么显然是从-1行,负一列到一行一列所以由于一个字符减去对应的零的字符正好就等于这个字符所对应的数字,所以九个正好减去九为什么中间那个不加以考虑呢?由于中间那个肯定是零啊,因为它不是雷嘛所有对结果没有影响所有直接相加即可至此我们的扫雷游戏基本上就完成了

一级标题4.代码准备

这里我们分三个文件来写这个代码一个是实现游戏的主体的源文件还有一个是实现游戏本身的源文件,还有一个是实现游戏的头文件这里我们需要知道函数可以在头文件中声明而实现可以由它的源文件实现这样可以模块化更加的清晰而由于这个函数和全局变量都具有外部链接属性,所以在使用的时候需要包含外部文件由于这个外部文件是我们自己创建的,它不属于标准库文件,所以需要用引号加起来为了简洁,由于两个源文件都包含这个头文件因此可以在头文件中包含标准库函数,只需要使用一次就即可

一级标题5.代码实现

二级标题1.主文件

#include "game.h"

void menu() {
	printf("\n***********************\n");
	printf("***********************\n");
	printf("******  1.play  *******\n");
	printf("******  0.exit  *******\n");
	printf("***********************\n");
	printf("***********************\n");
}
void game()
{
	char mine[ROWS][COLS];//雷的信息
	char show[ROWS][COLS];//排雷的信息
	//初始化两个二维数组
	InitBoard(mine,ROWS,COLS,'0');
	InitBoard(show, ROWS, COLS, '*');
	DisplayBoard(show, ROW, COL);//打印棋盘
	//布置雷
	MineSet(mine,ROW,COL);
	//排查雷
	FindMine(mine,show, ROW, COL);
}

int main()
{
	int input;
	srand((unsigned int)time(NULL));
	do{
		menu();
		printf("请输入你的选择:");
		scanf("%d", &input);
		switch (input) {
		case 1:
			printf("请开始扫雷\n");
			time_t start = time(NULL);
			game();
			break;
		case 0:
			printf("已退出游戏\n");
			break;
		default:
			printf("选择错误,请重新输入\n");
			break;
		}
	} while (input);

	return 0;
}

二级标题2.头文件

#include <time.h>
#include <stdlib.h>
#include <string.h>
#define Mine_difficulty 10
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2

void InitBoard(char Board[ROWS][COLS], int rows, int cols, char set);

void DisplayBoard(char Board[ROWS][COLS], int row, int col);

void MineSet(char Board[ROWS][COLS], int row, int col);

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

void MarkMine(char show[ROWS][COLS], int x, int y);

int Get_Mine_Count(char mine[ROWS][COLS], int x, int y);

void MarkMine(char show[ROWS][COLS], int x, int y);

void clear_stdin_buffer();

void ExpandBlank(char mine[ROWS][COLS], char show[ROWS][COLS],int x,int y);

二级标题3.游戏实现

#include "game.h"

time_t start;

int Get_Mine_Count(char mine[ROWS][COLS], int x, int y);

void MarkMine(char show[ROWS][COLS], int x, int y);

void clear_stdin_buffer()
{
	int c;
	while ((c = getchar()) != '\n' && c != EOF);
}


void InitBoard(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[i][j] = set;
		}
	}
}
void DisplayBoard(char Board[ROWS][COLS], int row, int col)
{
	int i = 0;
	for (i = 0; i <= col; i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		int j = 0;
		printf("%d ", i);
		for (j =1; j <= col; j++)
		{
			printf("%c ", Board[i][j]);
		}
		printf("\n");
	}
}
void MineSet(char mine[ROWS][COLS], int row, int col)
{
	int Mine_number = 10;
	int x = 0;
	int y = 0;//设置坐标
	while (Mine_number)
	{
		x = rand()%row + 1;
		y = rand()%col + 1;
		if (mine[x][y] != '1')
		{
			mine[x][y] = '1';
			Mine_number--;
		}
	}
}

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	char input[100];
	while (win <ROW*COL - Mine_difficulty)
	{ 
		printf("请输入想要排查的雷的坐标,或者标记雷用F标记(如 3 5 F):");
		fgets(input, sizeof(input), stdin);
		char action = '\0';
		int count = sscanf(input, "%d %d %c", &x, &y, &action);
		//int result = scanf("%d %d %c", &x, &y,&action);
		if (count == 2) {
			if (x >= 1 && x <= row && y >= 1 && y <= col)
			{
				if (mine[x][y] == '1')
				{
					printf("很遗憾,你踩雷了,游戏结束");
					time_t end = time(NULL);
					double diff = difftime(end, start);
					printf("游戏用时:%.2lf秒\n", diff);
					DisplayBoard(mine, ROW, COL);
					break;
				}
				else
				{
					int mine_number = Get_Mine_Count(mine, x, y);
					ExpandBlank(mine, show, x, y);
					DisplayBoard(show, ROW, COL);
					win++;
				}
			}
			else
				printf("请重新选择(1到9)");
		}
		else if (count == 3 && (action == 'F')) {
			MarkMine(show, x, y);
			DisplayBoard(show, ROW, COL);
		}
		else {
			printf("不合理的输入,请重新输入");
			clear_stdin_buffer();
		}
	}
	if (win == ROW * COL - Mine_difficulty)
	{
		printf("恭喜你,游戏成功");
		time_t end = time(NULL);
		double diff = difftime(end, start);
		printf("游戏用时:%.2lf秒\n", diff);
		DisplayBoard(mine, ROW, COL);
	}
}

int Get_Mine_Count(char mine[ROWS][COLS],int x,int y)
{
	int i = 0;
	int count = 0;
	for (i = -1; i <= 1; i++)
	{
		int j = 0;
		for (j = -1; j <= 1; j++)
		{
			count += (mine[x + i][y + j] - '0');
		}
	}
	return count;
}

void MarkMine(char show[ROWS][COLS], int x, int y)
{
	if (show[x][y] == 'F') {
		show[x][y] = '*';
	}
	else if (show[x][y] == '*') {
		show[x][y] = 'F';
	}
}

void ExpandBlank(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
	if (x < 0 || x >= ROWS || y < 0 || y >= COLS)
		return;
	if (show[x][y] != '*')
		return;
	int num = Get_Mine_Count(mine, x, y);
	if (num != 0)
	{
		show[x][y] = num + '0';
	}
	else {
		show[x][y] = ' ';
		for (int i = -1; i <= 1; i++) {
			for (int j = -1; j <= 1; j++) {
				if(i == 0 && j == 0)
					continue;
				ExpandBlank(mine, show, x + i, y + j);
			}
			
			}
	}

}

大家可能发现了这里的代码可能跟上面的有所不同,因为我把它稍微拓展了一下。其一是计算这个游戏的时间。其二,我实现了可以标注雷的过程其三它可以拓宽就像在电脑上玩的一样,如果他周围都没有雷的话,全部是空白一点一大片,这种我们用递归算法,而且循环算法暂时有点儿超纲了关键是如何判断这个用户是指出了坐标还是既输的坐标又输了这个大f?这显然是可变的,

三级标题标注雷

如果单纯的用scanf,它是无法实现的这里需要用到f_gets 他用于fgets() 是一个标准C库函数,用于从给定的文件流中读取一行文本。在此情境中,它用于从标准输入(通常是键盘)读取一行。其原型如下:

char *fgets(char *str, int n, FILE *stream);

str:是一个指针,指向用于存储读取的数据的字符数组。
n:是一个整数,表示要读取的最大字符数(包括最后的空字符\0)。一般设为字符数组的大小以避免缓冲溢出。
stream:是文件流的指针,在此情况下使用 stdin 指的是标准输入。
使用 fgets() 的原因是它能够读取一行直到新行(’\n’)或数组满为止,而不会发生缓冲溢出,这使得它比如 gets() 这样的函数要安全得多。

sscanf() 是 scanf() 函数的字符串版本,从字符串而不是标准输入(或其他文件流)读取输入。它的原型如下:

char input[100];
fgets(input, sizeof(input), stdin);//stdin表示标准输入

int sscanf(const char *str, const char *format, …);
str:是包含待解析数据的字符串。
format:是一个格式化字符串,指示如何解析 str 中的数据。
…:这是一系列指针,存储根据 format 解析出的数据。
sscanf() 返回成功读取并赋值的输入项个数。

下面是一个简单的使用 sscanf() 的示例,试图解析两个整数和一个字符:

int x, y;
char action;
char input[] = "10 20 F";
int count = sscanf(input, "%d %d %c", &x, &y, &action);
/* count 将会是3,如果 input 中包含了10、20和'F' */

在扫雷游戏情景中,使用 fgets() 和 sscanf() 的结合允许用户输入不定量数据,而不担心程序崩溃或不正确的行为,因为 fgets() 读取全部的行输入,包括空格和换行符,而 sscanf() 然后可以按照提供的格式从这行文本中抽取数据。
但这里还有一个问题即缓冲区问题在命令行界面,当你按下回车键时,即使你没有键入任何其他字符,fgets也会读取一个换行符。这将导致sscanf返回的count值为0,因此程序会立即指出输入不合理。该函数如下

// 清空stdin缓冲区的函数
void flush_stdin() {
    int c;
    while ((c = getchar()) != '\n' && c != EOF); // 读取字符直到文件结尾
}

// 清空stdin缓冲区后再读取
flush_stdin();

这里的关键是如果读到换行符只是第一个条件为真,然后他还需要判断第二个条件如果第二个条件为假那继续判断之后的这里声明了一个名为c的变量,它的类型是int。在C语言中,int类型不仅可以存储整数,也经常用来存储字符的ASCII码值。在这个情况下,c将被用来暂存从标准输入读取的单个字符。

这行代码实际上是一个while循环,它持续执行,直到没有字符可读(即读取到了文件结束符EOF

getchar()是一个标准库函数,每次调用它会从stdin读取下一个可用的字符,并返回它的ASCII码值,如果到达了输入流的末尾,则返回EOF。
c = getchar()将getchar()的返回值赋给变量c。
(c != ‘\n’ && c != EOF)是循环继续的条件。只要c不是EOF,循环就会继续,这样就可以从输入缓冲区中删除任何剩余的字符。
循环体是空的;循环做的唯一工作是在条件中调用getchar()并评估结果。这就足以清空缓冲区,因为每次循环迭代只是为了读取并丢弃一个字符。
整个函数没有大括号{}包围循环体,因为循环体为空。这是C语言中表示不需要执行任何操作的快捷方式。
getchar 是 C 语言中的一个标准库函数,定义在 <stdio.h> 头文件中。它用于从标准输入(通常是键盘)读取下一个可用的字符,并返回它。这个函数一次只读取一个字符,如果没有可用的字符,它会阻塞程序执行,直到用户输入一些文字并按下回车键。

当调用 getchar 时,它会检查标准输入流的缓冲区。如果缓冲区是空的,如前所述,它会等待用户输入;如果缓冲区中有字符,getchar 会一个接一个地返回这些字符,直至缓冲区为空。

函数的返回类型是 int,这可能令人费解,因为它实际读取的是字符。原因是 EOF(文件结束符)也需要表示,而 EOF 在多数系统中是定义为 -1 的宏,超出了 unsigned char 类型所能表示的范围。因此,返回 int 使得 getchar 能够返回任何可能的字符和 EOF 信号。
实际上这里的调用清除缓冲区函数应该有问题不过我还没找到,希望高手相助.
说了这么多迭代就不详细说了
迭代第一个要确定什么时候结束迭代
第二个什么时候返回到他自己?迭代其实我现在也没完全懂.

一级标题6.总结

今天我们一起学习了扫雷游戏的C语言实现和拓展相信大家或多或少都有收获.所有多思考我觉得下次遇到此类问题总能相处办法,相信总能解决.

  • 29
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
C语言扫雷游戏排行榜是一个用于记录玩家在扫雷游戏中的得分和排名的榜单。在C语言中,我们可以使用数组和结构体来实现这个排行榜。 首先,我们可以创建一个结构体来表示每个玩家的信息,如玩家的姓名和得分。结构体的定义可以像这样: ``` struct Player { char name[20]; int score; }; ``` 然后,我们创建一个数组来存储多个玩家的信息,数组的大小可以根据需要进行调整。例如: ``` struct Player leaderboard[10]; ``` 接下来,我们可以编写函数来实现对排行榜的操作,如添加玩家、更新得分和显示排行榜等。 添加玩家的函数可以接受玩家的姓名和得分,并将其添加到排行榜中。例如: ``` void addPlayer(char name[20], int score) { // 找到排行榜中得分低于当前得分的位置,并将其后的玩家信息依次后移一位 // 将当前玩家信息插入到空出的位置 } ``` 更新得分的函数可以接受玩家的姓名和新得分,并根据姓名找到对应的玩家并更新其得分。例如: ``` void updateScore(char name[20], int newScore) { // 根据姓名找到对应的玩家,并更新其得分 } ``` 最后,我们可以编写显示排行榜的函数,按照得分从高到低的顺序显示玩家信息。例如: ``` void showLeaderboard() { // 对排行榜中的玩家根据得分进行排序,并输出玩家信息 } ``` 以上是用C语言实现扫雷游戏排行榜的基本思路,具体的实现细节可以根据实际需求进行调整和完善。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值