扫雷游戏C语言代码实现——万字长文手把手教你实现,新手也能学会

300 阅读15分钟

文章目录

一、问题描述

游戏说明

二、思路分析

1. 数据结构

mine数组

show数组

2. 文件结构

三、分步实现过程

1. menu函数——游戏菜单

2. main函数的实现框架

3. game函数——游戏过程函数

3.1 数组的创建和初始化

3.2 initarr函数——数组的初始化

3.3 printarr函数——数组的打印

3.4 setmine()函数——随机布置10个雷

3.5 findmine()函数——扫雷过程实现的函数

3.6 getmine()函数——判断位置周围3*3范围内雷的数量

3.7 findmine()函数代码的完善

四、源代码

game.h文件 ——项目头文件

game.c文件——项目函数封装

test.c文件——主函数

效果展示

一、问题描述

游戏说明

  1. 使⽤控制台实现经典的扫雷游戏
  2. 游戏可以通过菜单实现继续玩或者退出游戏
  3. 扫雷的棋盘是9*9的格⼦
  4. 默认随机布置10个雷
  5. 可以排查雷——通过输入坐标
  • 如果位置不是雷,该坐标就会显示周围的雷的数量
  • 如果位置是雷,就炸死游戏结束
  • 把除10个雷之外的所有非雷坐标都找出来,排雷成功,游戏结束

二、思路分析

1. 数据结构

通过两个数组来实现,

一个数组用来存储棋盘本身的数据,这个数组暂且称为mine

一个用来对外展示,这个数组暂且称为show

mine数组

我们需要在9*9的棋盘上布置雷的信息和排查雷,所以需要创建⼀个9*9的数组来存放 信息。如果这个位置布置雷,我们就存放1,没有布置雷就存放0.

​编辑

但是这个时候也存在一个问题,当我们要排查的坐标处于数组的边缘时,计算周围雷的数量就可能产生越界和出错,为了防止这种行为发生,我们创建一个11*11的数组并初始化,但是实际使用中依然使用9*9的范围

​编辑

show数组

在游戏排雷的过程中对外展示,初始界面暂且全部设置为'*',每次排查一个坐标时,就将该位置改为周围一圈雷的数量

至于为什么要创建两个数组来分别存储数据和对外展示,

假设我们排查了某 ⼀个位置后,这个坐标处不是雷,这个坐标的周围有1个雷,那我们需要将排查出的雷的数量信息记录 存储,并打印出来,作为排雷的重要参考信息的。那这个雷的个数信息存放在哪⾥呢?如果存放在布 置雷的数组中,这样雷的信息和雷的个数信息就可能或产⽣混淆和打印上的困难,这个时候就产生了歧义——到底1表示该位置是雷呢,还是说该位置的周围有一个雷的?

所以为了防止这种情况发生,我们将两组数据分开来单独存放

2. 文件结构

通过三个文件来实现(为了方便后期的修改和调试)

这里

我们使用game.h来存放函数的声明

使用game.c来实现函数完整的定义

使用test.c包含main函数和函数的调用

三、分步实现过程

游戏的框架——主函数的实现

进入程序首先要出现一个菜单以及对游戏的说明,这里我们用一个函数封装来实现,暂且称为

1. menu函数——游戏菜单

游戏菜单说明开始游戏和结束游戏的方式,进入程序的第一步就要展现出来,这里我们把它放在game.c文件,并在game.h文件声明,test.c的main函数中调用,(以下皆同,不再重复)

使用printf函数不要忘记包含stdio.h头文件

为了代码更加简便,我们将所有头文件都放在game.h头文件中,再在主程序test.c中包含game.h头文件

包含自己创建的头文件的方式是 #include"game.h" 放在双引号内部

#include"game.h"
void menu()
{
	printf("*********扫雷游戏***********\n");
	printf("*****输入数字1 开始游戏*****\n");
	printf("*****输入数字0 结束游戏*****\n");
	printf("*********游戏说明***********\n");
	printf("*开始游戏后,输入两位数坐标*\n");
	printf("*并按回车确认您要排雷的位置*\n");
}

接下里是

2. main函数的实现框架

刚刚我们完成了菜单的打印,接下来就要创建一个整型变量来存储用户输入的值,并对此做出反应,这里用一个switch语句来实现——分别在输入1的时候开始游戏,输入0的时候结束游戏,在输入其他值的时候做出 出错提醒

#include"game.h"
int main()
{
	menu();
	int a = 0;
	scanf("%d", &a);
	switch (a)
	{
	case 1:
		//游戏过程
		break;
	case 0:
		printf("结束游戏\n");
		break;
	default:
		printf("输入的值错误,请重新输入\n");

	}
	return 0;
}

但是如果要想要多次游戏的话,就需要将switch分支语句和menu函数放在一个循环里实现,但是第一次进入程序的时候,我们必须保证能至少进行一次判断,这里使用do while循环就比较合适

而且将输入的值作为循环是否执行的条件恰到好处

#include"game.h"
int main()
{
	int a = 0;
	do
	{
		menu();
		scanf("%d", &a);
		switch (a)
		{
		case 1:
			//游戏过程
			break;
		case 0:
			printf("结束游戏\n");
			break;
		default:
			printf("输入的值错误,请重新输入\n");

		}
	} while (a);
	return 0;
}

来看一下效果

​编辑

现在,游戏的整体框架在main函数的部分就已经完成了,接下来我们将游戏过程封装在一个函数game()函数中来实现,来逐步完成细节

3. game函数——游戏过程函数

3.1 数组的创建和初始化

首先,进入游戏内部,我们需要先完成数据结构的构建,即上面我们提到了两个数组

分别是对内存储棋盘数据的mine数组和对外展示的show数组

出于对安全的考虑,我们创建11*11的数组,实际使用的范围是9*9的数组

并且,为了方便以后修改代码不需要修改太多的地方,我们在创建数组的时候不直接使用常量,而是在game.h中使用define定义常量,这样以后修改棋盘大小的时候只需要改动此处即可

#define ROW 9  //实际使用的变量大小
#define COL 9

#define ROWS 11  //创建数组的变量大小
#define COLS 11

接下来 ,创建数组

需要注意的是,因为我们初始的时候想要用'*'来对外展示棋盘,所以数组类型是char类型,为了分别操作,最好使两个数组的类型相同

void game() //游戏过程实现函数
{
	char mine[ROWS][COLS]; 
	char show[ROWS][COLS]; 


}

并且字符数组没办法使用一个值进行初始化,所以我们还需要一个函数对数组进行初始化

3.2 initarr函数——数组的初始化

要实现数组的初始化,最简单的办法就是使用两个for循环嵌套,依次对每一行每一列进行赋值

下面我们来实现这个函数

void initarr(char arr[ROWS][COLS],int rows, int cols, char set)//棋盘初始化函数
{
	for (int i = 0; i < rows; i++)
	{
		for (int j = 0; j < cols; j++)
		{
			arr[i][j] = set;
		}
	}
}

关于参数的说明——

  1. 第一个参数为数组,因为函数传参如果是数组的话,传过去的是一个地址,所以在函数内可以真实改变参数的值
  2. 关于二维数组形式的参数的写法,声明和定义时可以省略行数,但是不可以省略列数。不过为了保险起见,我们在进行数组传参的时候还是写上完整的行数和列数最好
  3. 二维数组在函数调用作为参数时,只需要写上数组名字即可
  4. 第二个参数和第三个参数,分别是我们创建的数组的行数和列数
  5. 第四个参数——char类型的set需要着重强调一下。这个参数是作为我们的数组的每一行每一列的初始化的内容来赋值的,如果这里不是使用的char类型变量,而是直接使用了字符'*'或者字符'0',那就需要写两个基本完全一样的函数来做相同的事情,造成了极大的浪费。所以多传递一个字符参数,可以极大提高代码的复用程度

记得别忘记在头文件中声明这个函数

​编辑

在game函数中的调用

void game() //游戏过程实现函数
{
	char mine[ROWS][COLS]; 
	char show[ROWS][COLS]; 
	initarr(mine, ROWS, COLS, '0');
	initarr(show, ROWS, COLS, '*');
}

实现了数组的初始化函数之后,接下来就是数组的打印函数了

3.3 printarr函数——数组的打印

这个函数的时候和上面初始化函数的实现类似,同样也是用一个函数来实现两个数组的打印

代码如下

void printarr(char arr[ROWS][COLS], int row, int col)//棋盘打印函数
{

	for (int i = 1; i <= row; i++)
	{
		for (int j = 1; j <= col; j++)
		{
			printf("%c ", arr[i][j]);
		}
		printf("\n");
	}
}

关于参数的说明——

这里第一个参数数组参数和上面初始化函数的功能和规则都是相同的,第二个和第三个参数与初始化函数中使用的参数是不同的,这是因为初始化的时候,为了方便,我们是直接将申请的所有数组空间都初始化了的。而在打印的时候,则完全不同,因为我们想对外展示的只是9*9范围的数组,所以打印的时候也是9*9范围的数组,这一点是需要注意的 这样在for循环中控制变量的循环范围就是1-9这个中间范围

别忘记在打印完每一行的时候换行哦

完成了初始化函数和打印函数之后,就可以先把打印函数放在game函数里测试一下看看效果了

​编辑

这里呢,可以看到已经达到我们想要的效果了,但是多考虑一点的话就会发现,游戏实现之后我们需要输入棋盘的坐标来排雷,如果这样每个都要数的话岂不是太麻烦了,不妨打印的时候我们在每一行每一列都加上对应的行号和列号

代码修改如下

void printarr(char arr[ROWS][COLS], int row, int col)//棋盘打印函数
{
	for (int i = 0; i <= row; 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 ", arr[i][j]);
		}
		printf("\n");
	}
}

修改的思路——

  1. 首先在每一行打印开始的时候,加上行号的打印
  2. 列号的打印需要额外增加一个循环在打印内容开始之前,而循环范围内是从0到9,是个数字,比打印的内容从1到9正好多一个,这是因为左边多出了一列行号,所以左边多加上一位数字才能让列号和内容对齐
  3. 还有一点小小的可以改进的地方——在每一次排雷重新打印时,前后两次的结果是紧挨在一起的,为了视觉上更好看一点,可以在打印内容开始之前,加上一条分界线

效果如下——

​编辑

这一步完成之后呢,接下来回想一下主题,下一步来完成布置10个雷,并且是随机的10个雷,这里呢我们将雷表示为字符'1',非雷表示为字符'0',这个设置保存在存放后台数据的数组mine中

3.4 setmine()函数——随机布置10个雷

关于随机数生成的实现在这篇文章有详细的介绍,这里介于篇幅所限,就不再展开长篇大论了

猜数字游戏C语言代码实现游戏可以连续进行,每次游戏有五次猜测机会。描述——随机产生一个100到200的整数,猜测产生的数 - 掘金(文章中关于rand函数 srand函数 和time函数都有详细介绍)

先上代码再解释每一步细节

void setmine(char arr[ROWS][COLS], int row, int col)//布置雷
{
	int count = SET_COUNT;
	while (count)
	{
		int x = rand() % ROW + 1;
		int y = rand() % COL + 1;
		if (arr[x][y] =='0')
		{
			arr[x][y] = '1';
			count--;
		}
	}
}

代码解释说明——

  1. count——在这一版本中雷的数量是10个,所以设置一个整型变量,来记录雷的数量。同时为了方便后期的更改,这次也使用了define定义常量,所以在头文件中加上一句#define SET_COUNT 10 当然常量的名字由你自己决定
  2. 为了完成随机数的生成——需要在main函数加上一句 srand((unsigned int)time(NULL)); 并在头文件包含time.h 和 stdlib.h 头文件 这个原理可以参考上述文章详细介绍
  3. 接下来使用一个while循环——控制变量为count,初始值为10,每次成功布置一个雷,count--,直到count减为0 循环结束
  4. 进入循环内部——创建两个整型变量x和y来存储和表示数组的下标,同时为了实现随机的效果,x和y的取值是调用rand函数的结果。而rand函数的返回结果是一个较大的整型值,为了使其取值范围在我们想要的范围,将rand函数的返回结果%9范围在0到8,再+1,范围就是1到9,正好是棋盘的数据范围
  5. 判断随机生成的下标是否已经被布置过雷——使用一个if语句判断,只有当该下标不是雷的时候,再布置雷,改为字符'1',并且count--

完成setmine函数的定义之后,在头文件加上函数声明

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

#define ROW 9  //实际使用的变量大小
#define COL 9

#define ROWS 11  //创建数组的变量大小
#define COLS 11

#define SET_COUNT 10

void menu();//游戏菜单

void game();//游戏控制函数

void initarr(char arr[ROWS][COLS], int rows, int cols, char set);//初始化函数

void printarr(char arr[ROWS][COLS], int row, int col);//棋盘打印函数

void setmine(char arr[ROWS][COLS], int row, int col);//随机布置雷

然后在game函数中加上setmine函数的调用

这里我们先在setmine函数后面加上一条打印mine数组的数据

void game() //游戏过程实现函数
{
	char mine[ROWS][COLS]; //存放棋盘雷的数据
	char show[ROWS][COLS]; //对外展示的扫雷界面
	initarr(mine, ROWS, COLS, '0');//初始化mine数组
	initarr(show, ROWS, COLS, '*');//初始化show数组
	printarr(mine, ROW, COL); //打印mine数组
	//printarr(show, ROW, COL); //打印show数组
	setmine(mine, ROW, COL);  //布置雷
	printarr(mine, ROW, COL); //打印mine数组

}

来测试几次看看

​编辑​编辑​编辑

可以看到每次布置雷的结果都是不同的,这样我们想要达到的随机效果就实现了

接下来,最后一步,也是最关键的一步,就是实现扫雷过程中输入下标排查雷并获得反馈的过程

3.5 findmine()函数——扫雷过程实现的函数

回到最初,在这个阶段我们想要实现的效果是

  • 如果位置不是雷,该坐标就会显示周围的雷的数量
  • 如果位置是雷,就炸死游戏结束
  • 把除10个雷之外的所有非雷坐标都找出来,排雷成功,游戏结束

现在,我们来一步一步完善findmine函数的框架

第一,这个函数的参数——这次需要把两个数组都作为参数传进去,因为这里需要对mine内的数据进行判断是不是雷,并在show数组改变数据进行反馈打印出来,然后我们再需要两个参数,分别是我们需要操作的9*9的数组的行数和列数

第二,屏幕打印一句提示,提醒输入两个坐标,并读取两个输入的值

第三,得到输入的两个值之后作为数组下标去进行判断——首先再最外层判断输入的值是不是合法的值,有没有越界,如果不是合法的值,提醒重新输入;否则,进行下一步判断。

第四,如果输入的是合法的值——判断该位置是否是雷,如果是雷的话,屏幕打印游戏失败(此时最好能把答案打印出来了结玩家的疑惑),并退出;如果该位置不是雷,根据游戏的要求,需要打印出来该位置周围3*3范围内存在的雷数量

第五,判断位置周围存在的雷数量——为了防止该函数内部变得过于繁琐,最好是把判断和计算数量的部分单独拿出来作为一个函数实现。

到这一步的代码实现如下

void findmine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	printf("请输入要排查的位置,按回车键确认\n");
	scanf("%d %d", &x, &y);
	if ((x >= 1) && (x <= ROW) && (y >= 1) && (y <= COL))
	{
		if (arr1[x][y] == '1')
		{
			printf("很遗憾,你被炸死,游戏失败\n");
			printf("正确位置如下\n");
			printarr(arr1, ROW, COL); 
            break;
		}
		else
		{
			//计算周围雷的数量,并修改show数组输出
		}
	}
	else
	{
		printf("输入的值不正确,请重新输入\n");
	}

}

3.6 getmine()函数——判断位置周围3*3范围内雷的数量

为了实现该函数,最简单的办法就是用一个for循环来实现——因为该位置是一个3*3的范围,行号是从x-1到x+1,列号是从y-1到y+1,只要创建一个变量来记录,每次判断该位置是不是雷,如果是雷的话,该值+1,最终就可以得到雷的数量

该代码实现如下

int getmine(char arr[ROWS][COLS], int x, int y)//计算周围雷的数量
{
	int count = 0;
	for (int i = x - 1; i <= x + 1; i++)
	{
		for (int j = y - 1; j <= y + 1; j++)
		{
			if (arr[i][j] == '1')
			{
				count++;
			}
		}
	}
	return count;
}

现在,我们再回过头来补充findmine函数内的代码

3.7 findmine()函数代码的完善

上面我们用getmine函数得到了排查位置周围雷的数量,但是,不要忘记了,该值是一个整型值,而我们mine数组和show数组都是char类型的数组。

初始的时候,我们使用的是字符'0',所以在此处得到数量的一个整型值之后,也应该变为一个字符类型表示。比如getmine函数计算后得到一个数字3,如何才能转换为字符'3'呢

参照ASCII表中,字符0的ASCII值是48,而想要获得后面的数字的ASCII值,比如3,只要在字符0的基础上+3就可以了

再将得到的这个值赋值给show数组对应位置下标的值,并打印在屏幕上

​编辑

findmine函数完善如下

void findmine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	printf("请输入要排查的位置,按回车键确认\n");
	scanf("%d %d", &x, &y);
	if ((x >= 1) && (x <= ROW) && (y >= 1) && (y <= COL))
	{
		if (arr1[x][y] == '1')
		{
			printf("很遗憾,你被炸死,游戏失败\n");
			printf("正确位置如下\n");
			printarr(arr1, ROW, COL); 
            break;
		}
		else
		{
			int count = getmine(arr1, x, y);
			arr2[x][y] = count + '0';
			printarr(arr2, ROW, COL);

		}
	}
	else
	{
		printf("输入的值不正确,请重新输入\n");
	}

}

然后在game()内加上findmine函数的调用

但是还有一个问题,上面的代码实现只是一次,但在实际过程中,不可能一次就能猜中结果,所以排雷的过程中应该放在一个循环中来实现

那循环结束的条件呢,到此为止,还有一个非常重要的事情没有完成,那就是排雷的结束条件

仔细想想,在9*9的棋盘中,布置10个雷,那么剩余的71个数量就是安全的,如此,只要把所有安全的位置都排查出来,那就算游戏胜利

这里,我们再用一个变量win来记录并作为循环结束的条件

并且当循环结束退出的时候,再使用一个if语句判断循环是否是因为游戏胜利而结束的

findmine函数完整代码如下

void findmine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col)
{
	int win = row * col - SET_COUNT;
	while (win)
	{
		int x = 0;
		int y = 0;
		printf("请输入要排查的位置,按回车键确认\n");
		scanf("%d %d", &x, &y);
		if ((x >= 1) && (x <= ROW) && (y >= 1) && (y <= COL))
		{
			if (arr1[x][y] == '1')
			{
				printf("很遗憾,你被炸死,游戏失败\n");
				printf("正确位置如下\n");
				printarr(arr1, ROW, COL);
                break;
			}
			else
			{
				int count = getmine(arr1, x, y);
				arr2[x][y] = count + '0';
				printarr(arr2, ROW, COL);
				win--;
			}
		}
		else
		{
			printf("输入的值不正确,请重新输入\n");
		}

	}
	if (win == 0)
	{
		printf("恭喜您,排雷成功,游戏胜利\n");
		printarr(arr1, ROW, COL);
	}
}

至此,整个游戏的代码已经全部完成,快去运行来尝试一下吧

四、源代码

game.h文件 ——项目头文件

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

#define ROW 9  //实际使用的变量大小
#define COL 9

#define ROWS 11  //创建数组的变量大小
#define COLS 11

#define SET_COUNT 10

void menu();//游戏菜单

void game();//游戏控制函数

void initarr(char arr[ROWS][COLS], int rows, int cols, char set);//初始化函数

void printarr(char arr[ROWS][COLS], int row, int col);//棋盘打印函数

void setmine(char arr[ROWS][COLS], int row, int col);//随机布置雷

void findmine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col);//排雷函数

int getmine(char arr[ROWS][COLS], int x, int y);//计算周围的雷的数量

game.c文件——项目函数封装

#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"

void menu()
{
	printf("*********扫雷游戏***********\n");
	printf("*****输入数字1 开始游戏*****\n");
	printf("*****输入数字0 结束游戏*****\n");
	printf("*********游戏说明***********\n");
	printf("*开始游戏后,输入两位数坐标*\n");
	printf("*并按回车确认您要排雷的位置*\n");
}

void game() //游戏过程实现函数
{
	char mine[ROWS][COLS]; //存放棋盘雷的数据
	char show[ROWS][COLS]; //对外展示的扫雷界面
	initarr(mine, ROWS, COLS, '0');//初始化mine数组
	initarr(show, ROWS, COLS, '*');//初始化show数组
	//printarr(mine, ROW, COL); //打印mine数组
	printarr(show, ROW, COL); //打印show数组
	setmine(mine, ROW, COL);  //布置雷
	findmine(mine, show, ROW, COL);//排查雷

}

void initarr(char arr[ROWS][COLS],int rows, int cols, char set)//棋盘初始化函数
{
	for (int i = 0; i < rows; i++)
	{
		for (int j = 0; j < cols; j++)
		{
			arr[i][j] = set;
		}
	}
}

void printarr(char arr[ROWS][COLS], int row, int col)//棋盘打印函数
{
	printf("-————扫雷游戏————-\n");
	for (int i = 0; i <= row; 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 ", arr[i][j]);
		}
		printf("\n");
	}
}

void setmine(char arr[ROWS][COLS], int row, int col)//布置雷
{
	int count = SET_COUNT;
	while (count)
	{
		int x = rand() % ROW + 1;
		int y = rand() % COL + 1;
		if (arr[x][y] == '0')
		{
			arr[x][y] = '1';
			count--;
		}
	}
}

void findmine(char arr1[ROWS][COLS], char arr2[ROWS][COLS], int row, int col)
{
	int win = row * col - SET_COUNT;
	while (win)
	{
		int x = 0;
		int y = 0;
		printf("请输入要排查的位置,按回车键确认\n");
		scanf("%d %d", &x, &y);
		if ((x >= 1) && (x <= ROW) && (y >= 1) && (y <= COL))
		{
			if (arr1[x][y] == '1')
			{
				printf("很遗憾,你被炸死,游戏失败\n");
				printf("正确位置如下\n");
				printarr(arr1, ROW, COL);
				break;
			}
			else
			{
				int count = getmine(arr1, x, y);
				arr2[x][y] = count + '0';
				printarr(arr2, ROW, COL);
				win--;
			}
		}
		else
		{
			printf("输入的值不正确,请重新输入\n");
		}

	}
	if (win == 0)
	{
		printf("恭喜您,排雷成功,游戏胜利\n");
		printarr(arr1, ROW, COL);
	}
}

int getmine(char arr[ROWS][COLS], int x, int y)//计算周围雷的数量
{
	int count = 0;
	for (int i = x - 1; i <= x + 1; i++)
	{
		for (int j = y - 1; j <= y + 1; j++)
		{
			if (arr[i][j] == '1')
			{
				count++;
			}
		}
	}
	return count;
}

test.c文件——主函数

#include"game.h"
int main()
{
	srand((unsigned int)time(NULL));
	int a = 0;
	do
	{
		menu();
		scanf("%d", &a);
		switch (a)
		{
		case 1:
			game();
			break;
		case 0:
			printf("结束游戏\n");
			break;
		default:
			printf("输入的值错误,请重新输入\n");
			break;
		}
	} while (a);
	return 0;
}

效果展示

​编辑

​编辑

​编辑