用C语言实现基础版扫雷游戏

一、前言

扫雷小游戏是C语言数组、函数应用的典型案例。本文将以通俗易懂的语言,讲清楚如何用C语言从0到1实现基础版的扫雷游戏。

二、准备工作

2.1 创建工程文件

以VS2022为例,我们创建一个头文件game.h,两个源文件game.c和test.c。

三个文件各司其职:

game.h是游戏的“使用说明书”。我们在game.h中声明函数但不写函数具体怎么实现,只写“目录”,让test.c知道game.c里有什么功能可用。

game.c是游戏的“发动机”,包含了函数的具体实现。在这个文件里只需专注实现各个函数的功能,不需要管玩家怎么操作菜单。

test.c是游戏的“外壳”,这个文件包含main函数,是程序的入口。它一方面组织流程,根据用户的输入去调用各个功能;另一方面与用户的交互,完成一些打印菜单(欢迎界面)、游戏循环(玩完一局是否再来一局)之类的事情。

注意,要在game.c和test.c中都包含game.h这个头文件。

#include"game.h"

顺带说一嘴,这样把一个工程分成多个文件,不仅逻辑清晰,而且方便多人协作,想象一下,把一个工程的各个功能交给不同程序员实现,每个人负责一个功能模块(一个头文件和一个源文件),然后再统一放在一个源文件中整合,效率是不是提高了?

另外,这样也有利于适当隐藏代码:以这个扫雷游戏为例,可以只把game.c编译产生的一些文件(乱码,可以用来调用函数功能,但用户无法解码成看得懂的代码)以及game.h(只告诉用户函数的使用方式)交给用户,就把game.c里的具体代码保护起来了~

2.2 在test.c中构建整体逻辑

我们想要实现的是:一上来先打印个菜单,让用户选择开始玩游戏还是退出。选择开始玩游戏后进入游戏,玩完一轮后再选择继续玩还是退出。从刚才的描述中可以看出,我们需要一个循环结构来构建游戏逻辑。由于我们需要用户一上来就看到菜单,不需要判断条件再打印菜单,所以选择do while循环,其中需要具体功能的部分先用空的函数临时代替,我们后面再来实现这些函数的功能:

//test.c
int main()
{
	int input = 0;
	do
	{
		menu(); //临时代替一下打印菜单的代码,一会儿再来填充
		scanf("%d", &input);//让用户输入1/0来选择是否进入游戏
		switch (input)
		{
		case 1:
			game();//临时代替一下游戏中的所有功能,一会儿再来填充
			break;
		case 0:
			printf("已选择退出,游戏结束\n");
			break;
		default://防止用户输入其他数字,设置default分支
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input);//用户输入非零数字,继续循环;用户输入0,退出游戏
	return 0;
}

填充一下menu函数,打印一个简单菜单:

//test.c
void menu()
{
	printf("********************\n");
	printf("*******1.play*******\n");
	printf("*******2.exit*******\n");
	printf("********************\n");
	printf("请输入1或0:>");
}

对了,VS2022中,为了防止使用scanf函数时报错,可以在文件开头加上这一句:

//test.c
#define _CRT_SECURE_NO_WARNINGS

到这里,游戏的整体逻辑就构建完成了,多简单。

接着就要开始实现具体的功能模块了。

三、具体功能模块

在test.c中写上game函数,准备往里填充具体功能模块:

//test.c
void game()
{

}
 3.1 创建数组作为游戏棋盘

根据游戏经验我们知道需要两个数组,一个是储存地雷位置信息的“雷区”,一个是玩游戏时展示给用户看某坐标周围有几个雷的“展示板”。我们在game.h函数中定义一下行数和列数这两个常量,先以9*9棋盘为例:

//game.h
#define ROW 9
#define COL 9

用#define来定义常量,而不是把行数列数作为数字直接写进代码里,有以下三个好处:

① 一次修改,全局生效
如果想把棋盘从  9×9  改成  16×16 ,只需要在game.h里修改这两行:

//game.h
#define ROW 16
#define COL 16


而不用在代码里找遍所有写着  9  的地方去改。
 
② 让代码更“自解释”
 char mine[ROW][COL]  一看就知道是“行×列”的棋盘。
如果写成  char mine[9][9] ,别人可能要猜很久这个  9  代表什么。
 
③ 避免“魔法数字”
直接在代码里写  9  这种没有上下文的数字,被称为“魔法数字”,会让代码难以维护。
用宏定义给数字赋予有意义的名字( ROW 、 COL ),能让代码更专业。

需要注意的是,当我们需要判断边界位置处的坐标周围的地雷数量时,会超出数组的边界。如果要判断边界,需要额外写一些代码。为了避免这种问题,让代码更简洁,我们为这两个数组设置个“虚拟边框”:

//game.h
#define ROWS ROW+2
#define COLS COL+2
3.2 初始化数组

我们定义一个函数用于初始化数组。

//game.c
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;
		}
	}
}

利用这个函数,把“雷区”数组中的元素全都设为'0',“展示板”数组中的元素全都设为'*':

//test.c
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
3.3 布置地雷

我们定义一个函数用于布置地雷。

我们将地雷数量也进行宏定义。

//game.h
#define EASY_COUNT 10

要游戏玩得起来,雷的位置需要随机生成。此时就需要用到rand函数(需要包含头文件<stdlib.h>)。然而rand函数生成的数是从一个“种子”出发生成的伪随机数,默认的种子为1。如果不做更改,每次调用都会生成同一序列的伪随机数。要得到变化的随机数,关键在于使“种子”发生变化。因此我们使用srand函数和time函数(使用time函数需包含头文件time.h),将种子设置为程序运行时的时间,这样每次运行,种子都会变化,我们就可以每次获得不同的随机地雷坐标了。

//test.c
srand((unsigned int)time(NULL));

接着我们就可以完成布置地雷的函数了。

//game.c
void Setmine(char arr[ROWS][COLS], int row, int col)
{
	int count = EASY_COUNT;
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (arr[x][y] == '0')
		{
			arr[x][y] = '1';
			count--;
		}
	}
}
3.4 打印展示板

我们定义一个函数用于打印展示板,打印时加上行号列号。

//game.c
void ShowBoard(char board[ROWS][COLS], int row, int col)
{
	printf("------扫雷游戏------\n");
	for (int k = 0; k <= ROW; k++)
	{
		printf("%2d", k);
	}//这个循环用于打印行号
	printf("\n");
	for (int i = 1; i <= ROW; i++)
	{
		printf("%2d", i);//打印行号
		int j = 1;
		for (j; j <= COL; j++)
		{
			printf("%2c", board[i][j]);
		}//这个循环用于打印每一行行号之后的数组内容
		printf("\n");
	}
}
3.5 排查地雷

在这一部分,我们希望实现这样的功能:用户输入要排查的坐标,程序判断坐标是否是地雷。如果是雷,显示踩到地雷的提示语,显示出雷区数组,结束游戏。如果不是地雷,在展示板上显示此坐标周围有几个地雷。当用户排查完所有不是雷的坐标时,显示游戏胜利的提示语并结束游戏。

需要注意,判断是否是雷前要判断用户输入坐标的合法性(是否在棋盘范围之内)。

此外注意我们创建的数组中放的数据是char类型的,转换成整数类型需要进行一些运算。

‘0’对应的ASCII码为48,‘1’对应的ASCII码为49,由此可以推断出字符类型的数据减去字符‘0’即可转换成整数类型,反之也一样。

由此我们写出这样的循环,统计周围地雷数的过程有点复杂,我们先用一个函数GetMineCount代替,一会儿再来填充:

//game.c
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	while (win < ROW * COL - EASY_COUNT)
	{
		printf("请输入要排查的坐标:>");
		scanf("%d %d", &x, &y);
		//printf("%c\n", mine[x][y]);
		//判断坐标合法性
		if (x >= 1 && x <= row && y >= 1 && y <= row)
		{
			if (show[x][y] == '*')
			{
				if (mine[x][y] == '1')
				{
					printf("BOMB!!!!!踩到地雷啦!\n");
					ShowBoard(mine, row, col);
					break;
				}
				else
				{
					//该坐标不是雷,需要排查周围有几个雷
					win++;
					int count = GetMineCount(mine, x, y);
					show[x][y] = count + '0';
					ShowBoard(show, ROWS, COLS);
				}
			}
			else
				printf("该坐标已经排查过,重新输入");
		}
		else
			printf("您输入的坐标无效,请重新输入\n");
	}
	if (win == ROW * COL - EASY_COUNT)
		printf("恭喜你,排雷成功!!!\n");
}

现在我们来填充统计地雷数的函数。由于进入统计周围地雷数这一步的坐标一定是‘0’,我们只需遍历这一坐标周围的三行三列共 9 个坐标,就可以统计出地雷数量了。

game.c
int GetMineCount(char arr[ROWS][COLS], int x, int y)
{
	int count = 0;
	int i = 0;
	for (i = x - 1; i <= x + 1; i++)
	{
		int j = 0;
		for (j = y - 1; j <= y + 1; j++)
		{
			count += (arr[i][j] - '0');
		}
	}
	return count;
}

至此,所有的功能模块都完成了。

四、整合

把所有模块按顺序排列,现在我们的game函数是这样的:

//test.c
void game()
{
	//创建数组
	char mine[ROWS][COLS] = { 0 };
	char show[ROWS][COLS] = { 0 };

	//初始化数组
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');

	//布置地雷
	Setmine(mine, ROW, COL);
	//ShowBoard(mine, ROW, COL);//这一行用于检查雷布置得对不对

	//打印数组
	ShowBoard(show, ROWS, COLS);
	//ShowBoard(mine,ROWS,COLS);

	//排查地雷
	FindMine(mine, show, ROW, COL);
	//ShowBoard(mine, ROW, COL);
}

记得要在game.h中声明所有函数:

//声明函数

//棋盘初始化
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);

//打印棋盘
void ShowBoard(char board[ROWS][COLS], int row, int col);

//布置地雷
void Setmine(char arr[ROWS][COLS], int row, int col);

//排查地雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

也要千万小心,函数传参的时候一定不要传错,传的参数和函数定义的参数一定要一致!

那么至此,排雷小游戏就大功告成了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值