扫雷游戏简易版——控制台实现

Hello~我是芝士,好久没有更新啦,那就浅浅写一篇博客记录一下最近所学的内容吧!今天要分享的是扫雷游戏,这里涉及的内容比较综合、包括函数、各种语句等等,虽然前面复习的模块没有分享,但是没关系,那就跟着小芝士我一起来看看如何从零实现所有的代码吧!

1. 游戏规则

在对整个简易版的扫雷游戏实现之前,我先来简单介绍一下具体的游戏的具体情况和规则吧。本次简易版的扫雷游戏主要通过我们电脑端的控制台实现,实现版的游戏界面如下图所示:
在这里插入图片描述
根据上图可知扫雷的棋盘大小是9*9的格子。我们通过选择输入0或者1进行选择是否开始游戏,选择1开始游戏之后输入对应的坐标来对雷进行排查。比如输入坐标:2 5
在这里插入图片描述
在(2,5)中就出现了一个数字0代表这个位置周围坐标雷的个数为0。再次输入一个新的坐标:(2 , 8)
在这里插入图片描述
发现这里触发了雷,程序显示被炸死了,并且将全部雷的布置情况打印了出来,因此游戏结束。如果输入的坐标都是非雷,那么排除所有坐标后就成功通过游戏。因此排查雷的整个逻辑规则可以总结如下:

  • 如果位置不是雷,会显示该位置周围有几个雷;
  • 如果该位置是雷,那么就会被炸死游戏结束;
  • 将所有雷(本文是10个)之外的所有非雷都找出,那么排雷成功游戏结束。
    以上就是扫雷游戏的一个规则。

2. 思路解析

为了实现这个简易版的扫雷游戏,我们先进行一个思路的拆解——需要什么,我将其分成以下两部分。

2.1 前期分析

我们知道要在控制台实现这个简易版的扫雷游戏,并且显示的棋盘大小是9*9大小,因此先预设一下首先我们需要第1个部件是9*9大小的棋盘,并且显示的界面全部是*,我们假设用一个大小为9\*9的数组存放
其次再来思考一下,我们要实现的场景是选择对应位置棋盘上的*号之后可以判断该位置是否有雷,如果没有雷则显示周围雷的个数,因此可以可以很容易想到这实际上是一个统计计算的过程。那么就要思考了,一个棋盘可以满足我们的需求吗?根据数组的特点,我们可以明显得出一个数组必然是不能满足我们的需求的,因此这里我们需要第2个部件与第1个部件相同大小数组。我们用一个数组存放我们雷的信息,另一个数组用来存放实际展示的信息,这样就可以实现我们想要的效果。
在这里插入图片描述
思考到这里,我们需要再重新考虑一下第3个部件:数组的大小。根据上图我们可以知道,我们在进行游戏的时候,需要统计周围雷的个数,这里也就涉及到了具体的计算了。具体什么意思可以如下图所示:
在这里插入图片描述
假设我们要统计1号位置周围雷的个数,我们需要计算1号位置周围8个位置是否存在雷就可以了,但是对于2号和3号位置这些边界位置在这个9*9的棋盘上就不容易计算,这就容易出现不统一的情况。那么为了统一计算的方式,我们可以尝试扩大棋盘的大小,扩展成11\*11,这样计算的方式就统一了。扩展后的棋盘情况如下所示:
在这里插入图片描述
蓝色部分表示扩展的部分,那么相应的,我们具体展示的棋盘大小也要相应的扩大,只是我们在具体展示的时候只是展示9*9大小。
最后,分析完成以上三个部件之后,我们还需要考虑第4个部件:数组的类型。我们知道数组的类型有多种有char、int、float……等类型,那么我们应当选择哪一种类型呢。或许很容易想到在计算过程中可以用int类型的数组来计算,这确实是一个不错的想法,但是我们的另一个数组存放的全部都是字符*号,因此如果另一个数组存放的是int类型,那么后面将计算的结果存放在一个char类型的数组中就会出现数据类型不匹配的情况。
根据我们前面学习到的知识点ASCII码值,我们可以将数组创建为char类型,因为字符型的数组中的值也可以实现计算,这就解决了类型不匹配的情况。因此,决定创建char类型的二维数组,大小是11\*11。同时,由于字符1和字符0的ASCII值得分别是49和48,它们相减的值为1,能够为我们后面的转换提供非常巧妙的帮助,因此这里我们将在放雷的数组中存放字符1和字符0,1表示雷,0表示非雷。(当然,这里也可以用其他字符进行表示,只要它们的ASCII码相邻即可)

2.2 子程序拆解

对前期的一些逻辑进行梳理之后,我们需要思考需要哪些子程序——函数,来实现我们需要的一些功能。这里主要可以总结为以下几个部分:

1. 菜单模块

在我们进行游戏的时候需要有一个界面,假设这个界面就是称之为菜单,具体的界面样式如下所示:
在这里插入图片描述
这个界面可以方便我们进行选择,其中0表示退出游戏,1表示开始游戏。这里将其分装到Menu()子程序中。这个子程序主要是为了实现相应界面的打印,因此我们不需要传参也不需要任何返回值

2. 棋盘初始化

根据我们前面分析的,为了实现这个游戏,我们需要对棋盘上的雷进行相应的布置前需要对棋盘进行一个初始化,因此需要一个棋盘初始化函数,这里将其命名为InitBoard()。这个函数需要实现的是布置两个棋盘即展示棋盘以及存放雷的棋盘。那么对于初始化棋盘这个子程序的参数如何思考呢?我们知道,需要对两个棋盘初始化,因此一定需要传的参数有数组、数组的行、数组的列。为了用一个函数实现两个数组的初始化,我们可以将初始化的值也作为其中一个参数,并且是不需要返回类型的。

3. 棋盘打印

在进行代码的测试过程中,我们需要多次打印出相应的棋盘来观察我们设置的棋盘的大小,并且刚开始的时候我们就要显示以下界面具体如下所示在这里插入图片描述
因此棋盘的打印是必不可少的,这里我们假设分装到PrintBoard()子程序中实习。再对其传参以及返回类型进行分析,显然地,我们不需要任何返回值,传入的参数有数组,对应的行以及列

4. 布置雷

有了棋盘打印函数可以显示对应棋盘中存放的数值之后,接下来比较关键的是需要对雷进行布置,并且我们只需要对其中一个存放雷的数组的雷进行布置即可,我们假设有这样一个名为SetBoard()的函数对雷的位置进行设置,对于其参数分析可知一定需要传入数组、对应的行和列,并且不需要返回值。对于雷的的布置需要注意的点是,我们是随机布置雷的,因此需要用到rand()函数、srand()函数、以及time()函数辅助,这样就可以保证每次游戏开始后布置雷的位置具有随机性了.

5. 排查雷

对雷进行布置完成后,要真正地开始玩游戏了即排查雷,我们假设有这样一个函数FineBoard(),能够对雷进行排查。对于这个函数的参数一定是有数组、对应的行和列,并且我们不需要任何的返回值。那么在排查雷的过程中,我们根据前面的分析可知需要进行相应雷的个数的计算,因此这个函数内部会嵌套一个计算的过程,我们也假设有一个函数能够实现相应的计算,命名为ComputeBoard()。这个函数是要进行相应计算的,并且我们需要根据其返回的值进行相应的判断,因此需要一个返回值,而对于该子程序的参数的思考可以见下图。
在这里插入图片描述
有上图可知,我们要计算(x,y)坐标的值时需要根据周围的坐标进行计算,因此对于这个ComputeBoard()函数的参数,需要判断坐标的行号和列号

3. 代码实现

对于这个游戏的具体代码主要分成以下几个方面进行详细介绍:

3.1 框架逻辑

1. 文件个数及命名
在开始正式书写代码时,我们需要思考一个重要的问题——需要创建那些个文件?根据目前所学知识,我们在创建对应的项目之后需要创建3个文件:两个源文件和一个头文件,我将他命名为game_1.c,main_1.c以及game.h。其中第一个源文件用来存放函数的定义,第二个源文件用来测试游戏,第三个头文件用来存放函数的声明。具体见下图所示:在这里插入图片描述
2. 初始基本逻辑
我们知道对于这个扫雷游戏,我们应该一开始就进入了就会打印对于的菜单栏目,然后再输入对于的数字开始选择是否开始玩游戏。因此这里可以借助do while循环语法,并且用switch语句进行相应的判断,输入对应的数后开始玩游戏或者不玩游戏,这里可以增加一个Game()函数表示进入游戏,通过这个函数可以调用前面分析的一些函数,并且我们可以通过这个函数创立我们需要的两个数组。对于数组的创建,我们知道实际上我们需要显示的是99大小的数组,而实际创建是1111的数据,为了使代码更加具有通用性,我们可以在头文件中定义Row,Col表示9,Rows的值为Row+2,Cols的值为Col+2,这样我们后续要更不同的大小时候直接对Row和Col进行修改即可。具体的代码如下所示(game.h):

#include <stdio.h>
#define Row 9
#define Col 9
#define Rows Row+2
#define Cols Col+2

根据以上逻辑的梳理书写代码程序如下:

#include "game.h"
void Game()
{
	char Mine[Rows][Cols];
	char Show[Rows][Cols];
	//初始化棋盘

	//棋盘打印

	//布置雷

	//排查雷

}
int main()
{
	int input = 0;
	do
	{
		Menu();//菜单模块
		printf("请输入:\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			Game();
			break;

		case 0:
			printf("退出游戏\n");
			break;

		default:
			printf("请输入正确的数字\n");
			break;
		}

	} while (input);
	return 0;
}

这里有个需要注意的点:
头文件的包含:我们将函数的声明以及所需要的头文件统一放在game.h文件中,然后这里直接包含即可。

3.2 函数实现

对于我们需要分装的函数以下几块:

1.菜单模块

我们前面分析了,这个函数主要功能是打印,不需要任何返回值,因此对于该函数的代码实习如下。

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

需要注意的是,该代码存放在game_1.c文件中,还需要再game.h文件中进行相应的声明。

2. 棋盘初始化

对雷进行布置之前,我们需要对棋盘进行相应的初始化,这里很容易想到借助循环来初始化,具体的代码如下:

//初始化棋盘
void InitBoard(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;
		}
	}
}

这里需要注意的是for循环中i 和j 是小于rows和cols的,因为这里的实参中rows和cols的值是11,而数组是从0开始的,因此0~10已经有11个索引了。

3. 棋盘打印

有了初始化棋盘的函数之后,我们就可以看看它能否实现我们想要的效果。因此这里可以用棋盘打印函数来进行一个可视化操作,函数的代码如下:

//棋盘打印
void PrintBoard(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");
	}
	printf("----------扫雷----------\n");

}

这里将实际存放雷的数组和展示的数组都打印,呈现出的效果如下所示:
在这里插入图片描述

4. 布置雷

在可以实现对棋盘的打印之后,我们就要开始布置雷了,这里取名SetBoard函数,根据前面的分析,我们要在存放雷的数组中放雷,因此参数有字符型数组,而实际存放雷的位置是在9*9的区域中,因此我们这里实际传参的行列数应该是Row和Col,while循环if语句对雷(个数是10)进行布置,具体代码实现如下:

//布置雷
void SetBoard(char arr[Rows][Cols], int row, int col)
{
	int count = 10;//布置10个雷
	while (count) 
	{
		//限制数组中x和y的取值为1~row(col)这里是9,因为实参Row和Col是9
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (arr[x][y] == '0')
		{
			arr[x][y] = '1';
			count--;
		}
	}
}

使用之前棋盘打印函数,对布置雷的棋盘进行打印,得到的布置雷的效果如下:
在这里插入图片描述
由于这里使用了系统运行的时间作为随机种子,因此实现了每次调用程序的时候都有不同的布置效果。

5. 排查雷

对雷进行相应的布置之后,这是我们就需要排查雷了。再次分析一下排查雷函数FindBoard的参数。由于我们要对存放雷的数组进行排查,并且对排查的结果放到实际展示的棋盘数组中,因此参数存放雷的数组实际展示数组。还要思考的一个问题,我们在排查过程中是通过坐标进行排查和计算,因此这里的参数包括输入的横坐标以及纵坐标
再进一步梳理一下,由于我们需要根据坐标来进行排雷,因此这个过程中会用到scanf函数来从我们键盘获取相应的坐标,然后我们根据坐标来计算该坐标周围雷的个数,对于具体计算雷的个数,我们假设分装到ComputeBoard()函数中,对于该函数的具体计算逻辑我们后面进行梳理。
此外,我们还需要考虑的一个问题是我们排查雷的过程并不是一次性的,因此这里需要结束while循环进行多次排查,如果整个排查过程中我们都没有触碰到雷,那么我们最后也应当停止下来,因此对于while循环的终止条件我们可以利用没有雷位置的个数来进行限制,每次排查出一个没有雷的坐标我们就对其进行减少,知道把所有坐标找出循环也就停止了。因此我们可以写出排查雷的一个大致函数框架如下:

//排查雷
void FindBoard(char Mine[Rows][Cols], char Show[Rows][Cols],int x, int y)
{
	//总共需要排查9*9-10个位置
	//Row*Col -10
	x = 0;
	y = 0;
	int Count = Row * Col - 10;
	while (Count)
	{
		printf("请输入排查坐标: ");
		scanf("%d %d", &x, &y);
		if (Mine[x][y] == '0')
		{
			//计算该位置周围雷的坐标
			int value=ComeputeBoard(Mine, x, y);
			//将该坐标的值放到Show数组中
			Show[x][y] = value + '0';
			PrintBoard(Show, Row, Col);
			Count--;
		}
		else
		{
			printf("游戏结束,被炸死了\n");
			PrintBoard(Mine, Row, Col);
			break;//跳出循环
		}
		

	}
	if (Count == 0)
	{
		printf("恭喜你,排雷成功\n");
		PrintBoard(Mine, Row, Col);
	}
}

对于这里的ComeputeBoard函数,我们根据前面的分析,得到其计算逻辑代码如下:

int ComeputeBoard(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');
}

这里我们可以实验一下,得到以下结果
在这里插入图片描述

3.4 优化及完整代码

对于以上写的代码,可以进一步优化,为了增加代码的通用性,外面这里可以将雷的个数做一个定义。这里用COUNT表示,定义在game.h文件中 并对之前对对应的count进行替换。最终每个文件的代码如下:
game.h

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define Row 9
#define Col 9
#define Rows Row+2
#define Cols Col+2
#define COUNT 10
//菜单打印
 void Menu();
 //初始化棋盘
 void InitBoard(char arr[Rows][Cols], int rows, int cols, char set);
//棋盘打印
 void PrintBoard(char arr[Rows][Cols], int row, int col);
 //布置雷
 void SetBoard(char arr[Rows][Cols], int row, int col);
 //计算
 int ComeputeBoard(char Mine[Rows][Cols], int x, int y);
 //排查雷
 void FindBoard(char Mine[Rows][Cols], char Show[Rows][Cols], int x, int y);

game_1.c

#include "game.h"
//菜单模块
void Menu()
{
	printf("******************************\n");
	printf("************0 exit************\n");
	printf("************1 exit************\n");
	printf("******************************\n");
}

//初始化棋盘
void InitBoard(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 PrintBoard(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");
	}
	printf("----------扫雷----------\n");

}

//布置雷
void SetBoard(char arr[Rows][Cols], int row, int col)
{
	int count = COUNT;//布置10个雷
	while (count) 
	{
		//限制数组中x和y的取值为1~row(col)这里是9,因为实参Row和Col是9
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (arr[x][y] == '0')
		{
			arr[x][y] = '1';
			count--;
		}
	}
}

//计算数值
int ComeputeBoard(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');
}

//排查雷
void FindBoard(char Mine[Rows][Cols], char Show[Rows][Cols],int x, int y)
{
	//总共需要排查9*9-10个位置
	//Row*Col -10
	x = 0;
	y = 0;
	int Count = Row * Col - COUNT;
	while (Count)
	{
		printf("请输入排查坐标: ");
		scanf("%d %d", &x, &y);
		if (Mine[x][y] == '0')
		{
			//计算该位置周围雷的坐标
			int value=ComeputeBoard(Mine, x, y);
			//将该坐标的值放到Show数组中
			Show[x][y] = value + '0';
			PrintBoard(Show, Row, Col);
			Count--;
		}
		else
		{
			printf("游戏结束,被炸死了\n");
			PrintBoard(Mine, Row, Col);
			break;//跳出循环
		}
	}
	if (Count == 0)
	{
		printf("恭喜你,排雷成功\n");
		PrintBoard(Mine, Row, Col);
	}
}

main_1.c

#include "game.h"
void Game()
{
	char Mine[Rows][Cols];
	char Show[Rows][Cols];
	//初始化棋盘
	InitBoard(Mine, Rows, Cols, '0');
	InitBoard(Show, Rows, Cols, '*');
	//棋盘打印
	PrintBoard(Mine, Row, Col);
	PrintBoard(Show, Row, Col);
	//布置雷
	//为了使得每次生成的坐标不一样,这里调用srand函数
	srand((unsigned int)time(NULL));
	//srand参数类型是unsigned int因此这里对time函数使用强制类型转换
	//time(NULL)表示获取当前时间作为srand的随机种子
	SetBoard(Mine, Row, Col);
	//PrintBoard(Mine, Row, Col);
	//排查雷
	FindBoard(Mine, Show, Row, Col);

}
int main()
{
	int input = 0;
	do
	{
		Menu();//菜单模块
		printf("请输入:\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			Game();
			break;

		case 0:
			printf("退出游戏\n");
			break;

		default:
			printf("请输入正确的数字\n");
			break;
		}

	} while (input);
	return 0;
}

好了以上就是本次扫雷游戏简易版的分享,有不足的地方请批评指正。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值