简易扫雷程序的制作与心得

先贴代码

game.h

#define _CRT_SECURE_NO_WARNINGS 1

#define length 9

#define lengths length + 2

#define mine 10

#include

#include

#include

void game();

void print();

void chushi(char a[][lengths], int wide, char filler);

void display(char a[][lengths], int wide);

void insert(char a[][lengths], int wide, int total);

int summing(char a[][lengths], char b[][lengths], int x, int y);

int detection(char a[][lengths], char b[][lengths], int wides, int x, int y);

void wait();

test.c

#include"game.h"

int main()//游戏的总框架
{
again:
	print();//打印菜单
	int input = 0;
	scanf("%d", &input);//输入选项
	int result = 0;
	switch (input)
	{
	case 0:
		game();//进入游戏
		wait();
		goto again;
	case 1:
		break;//退出
	default:
		goto again;
	}
	return 0;
}

game.c

#include"game.h"

int k = 0;//用于计算排雷数量,为了防止函数结束后销毁,故定义为全局变量

void game()//游戏运行的主体框架
{
	char demo[lengths][lengths] = { 0 };//生成棋盘
	char qipan[lengths][lengths] = { 0 };//显示的棋盘
	chushi(demo, lengths, '0');//初始化
	chushi(qipan, lengths, '*');
	insert(demo, length, mine);//放10个雷}
	int result = 1;//用于函数返回跳出循环
	int x = 0;
	int y = 0;//坐标
	while (result)
	{
		//display(demo, lengths);//显示所有的雷(测试用,外挂)
		display(qipan, lengths);//开局棋盘显示
		printf("输入坐标排查雷:\n");
		scanf("%d %d", &x, &y);
		if (x > length && y > length)
		{
			printf("输错啦,再来:\n");
			continue;
		}
		result = detection(demo, qipan, lengths, x, y);//探测并填入数字
	}
}

void print()//菜单显示
{
	system("cls");//清屏
	printf("***************************\n");
	printf("*********0.play************\n");
	printf("*********1.exit************\n");
	printf("***************************\n");
	printf("请输入数字来选择对应的操作:\n");
}

void chushi(char a[][lengths], int wides, char filler)//填充棋盘
{
	for (int i = 0; i < wides; i++)
	{
		for (int j = 0; j < wides; j++)
		{
			a[i][j] = filler;
		}
	}
}

void display(char a[][lengths], int wides)//打印棋盘
{
	for (int i = 1; i < wides - 1; i++)
	{
		for (int j = 1; j < wides - 1; j++)
		{
			printf("%c ", a[i][j]);
		}
		printf("\n");
	}
}

void insert(char a[][lengths], int wide, int total)//放置雷
{
	srand(time(NULL));
	int num = 0;
	int i = 0;
	int j = 0;
	while (num != total)
	{
		i = rand() % wide + 1;
		j = rand() % wide + 1;
		if (a[i][j] != '1')
		{
			a[i][j] = '1';
			num++;
		}
	}
}

int summing(char a[][lengths], int x, int y)//求周围8个格子雷的总数
{
	return (a[x + 1][y - 1] + a[x + 1][y] + a[x + 1][y + 1] 
		+ a[x][y - 1] + a[x][y + 1] + a[x - 1][y - 1] 
		+ a[x - 1][y] + a[x - 1][y + 1] - 48 * 7);
}

int detection(char a[][lengths], char b[][lengths], int wides, int x, int y)//输入并探测雷
{
	if (a[x][y] == '1')
	{
		system("cls");//清屏
		printf("炸死了,");//半句话,与下面的wait函数连接
		return 0;
	}
	else
	{
		if (a[x][y] == '0')
		{
			int n =summing(a, x, y);
			b[x][y] = n;
			k++;
		}
		if (k == length * length - mine)
		{
			system("cls");
			printf("恭喜你,排雷成功,");
			return 0;
		}
		else
		{
			return 1;
		}
	}
}

void wait()//暂停界面
{
	printf("按回车继续。");//半句话,为了接住detection函数的printf
	int input = 0;
	getchar();
	scanf("%c", &input);
}

首先为了使程序井然有序,我创建一个头文件game.h,用来定义常量,声明库,声明函数,这样之后的源文件都只要声明调用这个头文件就行,再然后我创建了两个源文件,一个test.c一个game.c,test.c用来存放main函数,是代码运行的主体,而game.h用来存放test.c中存放的main函数所要调用的函数,扫雷这个程序的实现要细分成很多个模块,我们在写代码时不能一股脑的全写在main中,这样做不仅乱,影响思维,而且代码的可重复利用率低,写出来难免臃肿,我们要做的,就是将扫雷代码的实现先粗略分成几步,每一步都用一个函数对应,再在每一个函数中考虑具体该怎样用代码去实现。
首先进入游戏时首先映入眼帘的肯定是菜单,用来选择开始或结束游戏,所以我们就创建一个函数,这里笔者就叫它print函数了,专门用来打印菜单,这里需要注意,菜单他并不是只会在开始游戏时才需要打印,扫雷成功结束游戏时和扫雷失败结束游戏时我们也需要调用这个函数来打印,以供玩家选择再开一把或退出游戏,那么在结束游戏时界面上就还会有游戏时的画面,笔者出于美观考虑(强迫症),在开头加一个system("cls");用来清屏,这样就美观许多。

void print()//菜单显示
{
	system("cls");//清屏
	printf("***************************\n");
	printf("*********0.play************\n");
	printf("*********1.exit************\n");
	printf("***************************\n");
	printf("请输入数字来选择对应的操作:\n");
}

打印完菜单之后就是玩家输入数字来选择对应的选项了,创建一个变量input,scanf输入,switch语句来选择对应的操作,case 0进入游戏,case 1退出,其他就显示错误,goto语句再来一遍,重新输入。
到这里main函数就结束了,我们会发现main在这里只被我用来给玩家选择进不进入游戏,而其他的部分都被我做成了函数,在case 0中game()函数是游戏真正运行时的主体

int main()//游戏的总框架
{
again:
	print();//打印菜单
	int input = 0;
	scanf("%d", &input);//输入选项
	int result = 0;
	switch (input)
	{
	case 0:
		game();//进入游戏
		wait();
		goto again;
	case 1:
		break;//退出
	default:
		goto again;
	}
	return 0;
}

转到game()函数运行的部分,这里我们就要真正考虑游戏运行时的逻辑了,通过搜索可知,扫雷分为好几个难度,每个难度的棋盘大小,雷的数量都不同,这里笔者选了中等难度,也就是9×9的棋盘,共计10个雷作为本次程序的目标。那么考虑到棋盘,我们不难想到要用二维数组的方式来实现,也就是9×9的棋盘,通过随机数生成,填入10个雷,这是我最初的想法。到这样的考究无疑是有些稚嫩的,我们知道,扫雷这个游戏就是点击格子,格子里要么有雷要么没有,有雷就gameover,没有的话它也并不会显示空格子,而是会显示一个数字来告知你在这个格子的周围8个格子中总共有几个雷,那么这时就出现了一个问题,在我们选择边缘的格子时,如果我们是9×9的棋盘,周围的格子数会不满8个,诚然我们可以选择诸如if语句在运算时对边缘特殊化处理,但那无疑增加了代码的复杂程度,这里笔者选择在创建数组时上下左右各增加一行,也就是9+2=11行和列,而在布置雷的时候不往最外围布置,也就是设置随机数在1到9之间(数组长宽11,也就是0到10,0和10都去掉),笔者认为这样处理更合理。在创建完数组后,之后的问题无疑就是填入内容,对于扫雷来讲,填入的无非就是雷,那么大致思路就有了,先初始化棋盘全为0再在除去最外围的数组元素中填入雷就行了,这么一想问题就来了,我们点开格子显示的数字又要往哪填呢,笔者这里最先想到的是先在棋盘上填入1和0之后要显示数字时再计算重新填入就好了,但那样问题就更大了,填入的数字会影响后续的计算,比如我点开一个没有雷的格子,显示出1表示周围有雷,那再点开周围格子时就分不清之前那个格子里是雷还是什么别的了,这个思路大有问题,而在笔者思考一番后终于茅塞顿开,为什么要只往一个棋盘上想呢,原本我们在进行游戏时就需要一个全部隐藏的棋盘,随着玩家的点击逐渐展开,那么我们势必不能直接把填入雷的棋盘打印给玩家,这还玩个毛线,所以在最开始我们就应该创建两个棋盘,一个全空的棋盘,一个填入雷的棋盘,将空棋盘先打印给玩家,随着玩家的输入,再去调用埋雷的棋盘,计算完将数字填入空棋盘。这么一想,思路就有了,先创建两个11×11的数组,分别命名为demo和qipan,demo填雷qipan显示,然后初始化,demo全填0,qipan为了显示效果,就全填的*,然后统一为char数组方便管理。所以我们写出一个函数用来初始化,传入初始化的数组,数组的尺寸,和要填充的内容,函数内部用两个for循环嵌套即可。

void chushi(char a[][lengths], int wides, char filler)//填充棋盘
{
	for (int i = 0; i < wides; i++)
	{
		for (int j = 0; j < wides; j++)
		{
			a[i][j] = filler;
		}
	}
}

之后就是埋雷了,srand(time(NULL))生成随机数先判断之前又没有埋过雷,没有就埋入,创建局部变量num计算数量,但10就跳出while循环即可。

void insert(char a[][lengths], int wide, int total)//放置雷
{
	srand(time(NULL));
	int num = 0;
	int i = 0;
	int j = 0;
	while (num != total)
	{
		i = rand() % wide + 1;
		j = rand() % wide + 1;
		if (a[i][j] != '1')
		{
			a[i][j] = '1';
			num++;
		}
	}
}

之后就是打印棋盘了,创建函数display,两层for循环遍历数组打印即可。

void display(char a[][lengths], int wides)//打印棋盘
{
	for (int i = 1; i < wides - 1; i++)
	{
		for (int j = 1; j < wides - 1; j++)
		{
			printf("%c ", a[i][j]);
		}
		printf("\n");
	}
}

再然后就是玩家输入坐标的环节了,if语句判断一下输入的坐标是否正确,以后创建detection函数,传入两个数组,数组尺寸,和输入的坐标,函数中用if语句判断输入的坐标是都是雷,是就炸死了,return 0结束函数,不是的话就要计算周围的雷的数量填入了,

int detection(char a[][lengths], char b[][lengths], int wides, int x, int y)//输入并探测雷
{
	if (a[x][y] == '1')
	{
		system("cls");//清屏
		printf("炸死了,");//半句话,与下面的wait函数连接
		return 0;
	}
	else
	{
		if (a[x][y] == '0')
		{
			int n =summing(a, x, y);
			b[x][y] = n;
			k++;
		}
		if (k == length * length - mine)
		{
			system("cls");
			printf("恭喜你,排雷成功,");
			return 0;
		}
		else
		{
			return 1;
		}
	}
}

这里我们设置一个summing函数用来计算,计算雷的数量有几种方法,我们可以创建for循环逐个判断计算,但我这里用了另外一种方法,众所周知,字符0的ASCII值是48,1则是49,字符在计算时是用ASCII值进行计算的,所以我这里把周围八个元素直接全加起来,减去0的ASCII值乘以8这样不就是周围雷的数量了吗,正当笔者兴奋的去调试时发现怎么也显示不出来,看了半天才发现,我减去了8个48,剩的数确实是雷的数量,但那是字符,字符的数ASCII码值对应的字符都是不显示的字符,这里应该减7个48才对,改完发现果真如此(笑)。

int summing(char a[][lengths], int x, int y)//求周围8个格子雷的总数
{
	return (a[x + 1][y - 1] + a[x + 1][y] + a[x + 1][y + 1] 
		+ a[x][y - 1] + a[x][y + 1] + a[x - 1][y - 1] 
		+ a[x - 1][y] + a[x - 1][y + 1] - 48 * 7);
}

这么写完后,这个程序的大体就完成了。在测试时,笔者发现一个令我感到不适的地方,那就是不论是扫雷成功还是扫雷失败,都是瞬间回到主菜单,这未免有些突兀,我还没看清呢就回去了,我不管赢了输了好歹告诉我一下吧,所以我创建了一个wait函数,不管扫雷成功还是失败都会在detection函数return之前进入这个函数,函数的功能很简单,就是打印告诉你输了赢了,然后告诉你按回车继续,设置一个scanf读取一个字符也就是回车,之后就会回到detection函数继续,这样体验上好很多,到等到我测试时,却发现怎么也不行,在调试时发现确实进入了wait,但到了scanf时,却没有提示我要输入,语句执行了,却没有让我输入,这是怎么回事?通过搜索发现,原来由于我之前也进行了输入操作,输入要就按回车,这个回车没有被读取,到了wait函数中,scanf直接把我之前输入的回车读取了,所以我们在前面加入一个getchar()函数,可以从输入设备接受一个字符,把回车吃掉,以后就成了。

void wait()//暂停界面
{
	printf("按回车继续。");//半句话,为了接住detection函数的printf
	int input = 0;
	getchar();
	scanf("%c", &input);
}

到此整个函数基本完成了,之后稍微在细节上做一些调整就结束了。笔者写到这里想到game.h中的内容我是一点也没提及(尴尬),

#define _CRT_SECURE_NO_WARNINGS 1

#define length 9

#define lengths length + 2

#define mine 10

#include

#include

#include

void game();

void print();

void chushi(char a[][lengths], int wide, char filler);

void display(char a[][lengths], int wide);

void insert(char a[][lengths], int wide, int total);

int summing(char a[][lengths], char b[][lengths], int x, int y);

int detection(char a[][lengths], char b[][lengths], int wides, int x, int y);

void wait();

这里我就简单补充一下,game.h被我拿来做了三件事,声明函数,声明库,定义符号常量。首先是声明函数,这里的函数就是我在game.c中创建的函数,之所以在创建函数之前还要在头文件中声明一下的原因是防止程序出错,因为我们都知道,在不提前声明函数的情况下,如果一个函数中若要调用的函数在这个函数之后才被创建,那么程序就会报错,而如果提前声明就不会有问题,game.c中存在函数相互调用的情况,所以我们在头文件中全部定义一遍就能防止这种情况的出现。这之后是库,由于我们运用了很多库函数,所以需要声明调用各种库,但我们创建了很多个源文件,一个一个声明未免麻烦,我们直接在头文件中声明完,之后就都只要声明调用这个头文件即可,然后就是字符常量的声明,#define_CRT_SECURE_NO_WARNINGS 1不用说了,防止scanf函数报错,之后的字符常量就是数组的尺寸和雷的数量这其实是扫雷难度本质区别的数字,如果我们想改难度,就改这三个字符常量就行。为了是代码更具普遍性,更具范用性,我们再设计程序时应该尽量避免代码变成死代码,多去定义这样的符号常量,多去设置符号变量,而不是能用数字就用数字,让代码可重塑性可利用提高,这样的代码才是好代码。初入代码的领域,有些地方的理解不到位还望多多指出,让我们一起交流,共同进步。

你可能感兴趣的:(1024程序员节)