扫雷代码实现及解析

前言

整个扫雷的代码在文末。
扫雷涉及到的函数会进行逐个解析,哪里不会看哪里(doge)

分析

扫雷游戏分为以下几个步骤:
进入游戏,布置棋盘和雷,排查雷,排到雷和未排到雷时的判定

前置准备

游戏将由三个文件共同实现
test.c:专门测试游戏逻辑
game.c:游戏实现
game.h:对游戏中涉及到的函数等进行声明

当然,也可以不建立头文件,直接使用extern进行声明,但是这样的话会比较混乱,你在a文件里面定义一个函数,然后在b文件里面调用就要声明,然后在b里面定义另外一个函数,在a中使用又得声明一下,很凌乱无序。所以我们不妨就设一个头文件,它就相当于一个“目录”,你想实现什么功能,就先在一个源文件中定义对应的函数,然后在头文件中声明,想在其他源文件中使用就直接包含头文件,里面所有的函数就都可以使用。
扫雷代码实现及解析_第1张图片

设计游戏开始页面

先打印一个菜单,让玩家进行选择,然后创建一个变量input,这里先使用do while语句,再结合switch语句处理不同的输入情况。

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

然后就可以进行游戏主体的设计了(通过game函数来实现)

游戏主体

注:排雷过程是以输入对应位置的坐标来进行的。
首先需要一个棋盘,默认棋盘规格为99,在扫雷过程中需要做到:若踩到雷则显示失败,若没有则显示周围八格雷的总数
也就是说我们需要设计一个二维数组mine,但需要注意,数组规格不可以为9
9,因为如果这样会出现数组越界的情况,即点到边缘部分格子的时候,无法判断雷的数量。所以我们将棋盘设为1111,并默认最外圈雷数为0。
棋盘设置好后,接下来需要区分一个格子有雷和无雷两种情况,我们规定,1为有雷,0为无雷。但这会出现一个问题,如果排查的位置周围雷数是1,那它也会显示1,这时候就会与表示有雷的“1”产生矛盾。
所以我们不妨这样,再设置一个一模一样的棋盘(二维数组),里面初始填满
,当我们在第一个棋盘排查一个位置的时候,若无雷,就在第二张棋盘上对应的位置标上数字,将两个数组分别命名为mine和show,如图:
扫雷代码实现及解析_第2张图片
由于show数组里面的*为字符,而mine数组为数字,为了方便打印,我们输入mine数组时使用字符‘1’和‘0’,同理,show中的雷数也用数字字符表示

那么我们现在有了两个棋盘:mine和show,show是给玩家看的,mine供我们调试代码,测试游戏是否可以正常运行(比如后面要看所有雷都排查出来时是否会显示“恭喜你,排雷成功”,你直接看雷咋布置,把安全的位置排出来就行了,不用费时间一个一个位置去推理)。

初始化棋盘的函数:Initboard

初始化棋盘,要在mine里面布置雷,在show里面用*填满数组。

函数实现

这个函数需要四个参数:数组,行,列,填充数组的符号
考虑到我们需要多处将行和列作为参数进行传递,所以我们这样处理:
game.h

#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2

定义后,ROW就是9,其他以此类推,这样做的好处在于:你如果后续想改变棋盘行和列大小的话,只需直接在头文件里面直接改个数字就行了,不用在game.c 和test.c 里面把所有数字都改一遍(因为二维数组传参时你在形参和实参都得标明数组大小,要改的话就都要改)。

函数内部就写一个for的嵌套循环,对棋盘每个格子进行赋值即可,注意这里棋盘大小是11*11
game.c

void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
	for (int i = 0; i < ROWS; i++)
	{
		for (int j = 0; j < COLS; j++)
		{
			board[i][j] = set;
		}
	}
}

test.c

	InitBoard(mine, ROWS, COLS,'0');
	InitBoard(show, ROWS, COLS, '*');

展示棋盘的函数:DisplayBoard

mine棋盘是雷的位置信息,这个不要展示。只需展示show棋盘,我们要在进入游戏、每次排雷后展示下棋盘,让玩家看到雷数的情况。
上面已经初始化好了,接下来只要打印出来就行了。注意展示的棋盘是9*9
为了方便玩家输入排雷的坐标,我们把行数和列数也打印出来。
game.c

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	for (int k = 0; k <= row; k++) //打印列数
	{
		printf("%d ", k);
	}
	printf("\n");
	for (int i = 1; i <= row; i++)
	{
		printf("%d", i);   //每行打印棋盘之前打印行数
		for (int j = 1; j <=col; j++)
		{
			printf(" %c", board[i][j]);
		}
		printf("\n");    //记得换行
	}
}

test.c

	DisplayBoard(show, ROW, COL);

布置雷的函数:Setmine

我们要确保雷是随机布下的,所以这里需要用到rand函数,要用rand生成真正的随机数,我们要先调用srand来对它初始化,使用time函数的返回值作为srand的参数,time的参数要用NULL(空指针),这样time会返回时间戳,时间戳是一个随机值。(详见第四讲的课件)
在棋盘中,要使布雷坐标随机,就要使横、纵坐标都是随机数。

		int x = rand() % row + 1;     //控制布雷的范围在1-9,注意这里是row而不是ROW,row和下面的col是Setmine的形参,你等下看整个函数就明白了。
		int y = rand() % col + 1;

然后我们要设定雷的数量,考虑到要方便修改雷的数量,且别的地方可能也要用到雷数这个变量,所以就跟前面定义ROW和COL一样,定义一个Easy_Count(因为真正的扫雷不同难度的雷数不一样,所以我们先设定一个“简单难度的雷数”),设置10个雷
game.h

#define Easy_Count 10

现在有10个雷,布雷我们采用循环,设雷数为count,每次循环后count就减1,减到0就停止。还记得前面说用字符 ‘1’ 来表示雷吧?注意布雷要布在原来没放过雷的地方,这里用一个条件语句进行判断。整个函数如下:
game.c

void Setmine(char board[ROWS][COLS], int row, int col)
{
	int count = Easy_Count;
	while (count)
	{
		int x = rand() % row + 1;  
		int y = rand() % col + 1;
		if (board[x][y] == '0')    //该位置没有雷
		{
			board[x][y] = '1';
			count--;
		}
	}
}

test.c

	Setmine(mine, ROW, COL);

排查雷的函数:FindMine

首先明确我们在这个函数需要实现:输入坐标,排查该坐标后的反馈,胜利条件的判定

输入坐标

先初始化变量x,y表示行和列,使用scanf输入,因为要多次输入,所以要用到循环,那啥时候循环会结束呢?一种是被雷炸死,另外一种是把所有无雷的坐标都排出来了,所以这里来一个计数器,记为win,初始化为0,每排一个无雷的坐标win就加1,无雷的坐标总共有ROWCOL - Easy_Count,所以只要设定winCOL - Easy_Count,这样总共排(ROW*COL - Easy_Count)次,然后退出循环。到这里你会发现,被炸死和排完雷这两种情况都可以退出循环,类似河水从分岔口流向合流处,所以需要设置条件语句将它们区分开,这两者的区别在于退出循环的时候win的大小不同,于是在while循环出来后这样写:

	if (win == ROW * COL - Easy_Count)
		{
		printf("恭喜你,扫雷成功\n");
	    }

游戏不管赢了还是输了,都可以展示一下棋盘,让玩家看一下雷的分布情况,这时候可以再用一下之前展示棋盘的函数,放在这个if语句的后面。

排查坐标

若为雷,则显示“很遗憾,你被炸死”,然后退出循环。若不为雷,给这个坐标打印周围雷数。

得到雷数的函数:GetMineCount

这个函数返回类型为int,在mine棋盘里面统计雷数,然后将返回值打印在show对应位置。mine里面的数字是字符,要把它们转换为整型数字的话只需减掉 ’ 0 ',因为字符数字在加减运算的时候是用对应的ASCII码运算。比如‘3’-‘0’,因为它们的ASCII码相差3,相减之后以整型形式打印出来就是3,那现在有8个字符,就要减掉8个字符0咯。上代码:

int GetMineCount(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');
}
补充

因为这个函数只在FindMine里面使用,所以它只需要在game.c里面定义(记得是在FindMine前面定义),再在FindMine里面调用就行了,没必要在头文件里面声明。

Q:GetMineCount为啥要减掉字符0转换为整型数字再求和?直接返回字符数字相加不是更好?
A:因为mine里面是字符1和字符0,你不减掉8个字符0,直接让它们相加不能得到正确结果,你要区分字符数字运算和整型数字运算的差别(比如字符1加字符1不等于字符2)。

在show中打印雷数

现在我们已经得到了雷数(整型),但是show是个字符数组,所以再给雷数加上字符0就行了,然后展示棋盘。

综上,FindMine函数可以这样写:

void FindMine(char show[ROWS][COLS],char mine[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	while (win < row * col - Easy_Count)     
	{
		printf("请输入要排查的坐标\n");
		scanf("%d %d", &x,&y);
		if (1 <= x && x <= ROW && 1 <= y && y <= COL)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死\n");
				DisplayBoard(mine, ROW, COL);
				break;
			}
			else
			{
				int count = GetMineCount(mine, x, y);
				show [x][y] = count + '0';
				DisplayBoard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("坐标非法,请重新输入");
		}
	}
	if (win == ROW * COL - Easy_Count)
	{
		printf("恭喜你,扫雷成功,雷的分布如下:\n");
		//DisplayBoard(mine[ROWS][COLS], ROW, COL);
		DisplayBoard(mine, ROW, COL);
	}
}

bug及改进

你会发现,一直排查同一个无雷的坐标win也会一直++,这就是游戏的bug,所以我们要进行改进,改为已排查的坐标不能再排查。
最开始show里面会被初始化填满*,所以只要你排查的坐标不为*,就显示“该坐标已被排查,请重新输入”,改进后如下:

if (1 <= x && x <= ROW && 1 <= y && y <= COL)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死\n");
				DisplayBoard(mine, ROW, COL);
				break;
			}
			else if (show[x][y] != '*')
			{
				printf("该坐标已被排查,请重新输入\n");
			}
			else
			{
				int count = GetMineCount(mine, x, y);
				show [x][y] = count + '0';
				DisplayBoard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("坐标非法,请重新输入");
		}

其他需要改进的地方

是否可以选择游戏难度
◦ 简单 99 棋盘,10个雷
◦ 中等 16
16棋盘,40个雷
◦ 困难 30*16棋盘,99个雷
如果排查位置不是雷,周围也没有雷,可以展开周围的一片
是否可以标记雷
是否可以加上排雷的时间显示

唉写到这里已经很累了,上面这个后续有时间再来填坑(doge)

代码

game.h

#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
#include

#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2

#define Easy_Count 10

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

//展示棋盘
void DisplayBoard(char board[ROWS][COLS], int rows, int cols);

//布雷  设定雷数,随机布雷
void Setmine(char board[ROWS][COLS], int row, int col);

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

//得到雷数
//int GetMineCount(char board[ROWS][COLS], int row, int col);

game.c

#include "game.h"

//将不同棋盘初始化填上不同的东西  mine:0   show:*
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
	for (int i = 0; i < ROWS; i++)
	{

		for (int j = 0; j < COLS; j++)
		{
			board[i][j] = set;
		}
	}
}

//展示棋盘  打印出来,并且还要打印坐标
//9*9棋盘这个二维数组中,第0,10行和第0,10列要空出来,后续才能实现计算周围雷个数的目标
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	for (int k = 0; k <= row; k++) //打印列数
	{
		printf("%d ", k);
	}
	printf("\n");
	for (int i = 1; i <= row; i++)
	{
		printf("%d", i);
		for (int j = 1; j <=col; j++)
		{
			printf(" %c", board[i][j]);
		}
		printf("\n");
	}
}

void Setmine(char board[ROWS][COLS], int row, int col)
{
	int count = Easy_Count;
	while (count)
	{
		int x = rand() % row + 1;  //控制布雷的范围在1-9
		int y = rand() % col + 1;
		if (board[x][y] == '0')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

//得到雷数   由于只在找雷函数中调用,所以无需放在头文件里
int GetMineCount(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 FindMine(char show[ROWS][COLS],char mine[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	while (win < row * col - Easy_Count)   //不断地扫雷
	{
		printf("请输入要排查的坐标\n");
		scanf("%d %d", &x,&y);
		if (1 <= x && x <= ROW && 1 <= y && y <= COL)
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死\n");
				DisplayBoard(mine, ROW, COL);
				break;
			}
			else if (show[x][y] != '*')
			{
				printf("该坐标已被排查,请重新输入\n");
			}
			else
			{
				int count = GetMineCount(mine, x, y);
				show [x][y] = count + '0';
				DisplayBoard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("坐标非法,请重新输入");
		}
	}
	if (win == ROW * COL - Easy_Count)
	{
		printf("恭喜你,扫雷成功,雷的分布如下:\n");
		//DisplayBoard(mine[ROWS][COLS], ROW, COL);
		DisplayBoard(mine, ROW, COL);
	}
}

test.c

#include "game.h"

void menu()
{
	printf("***********扫雷游戏***********\n");
	printf("请选择>>开始游戏:1  退出游戏:0\n");	
}

void game()
{
	char mine[ROWS][COLS];
	char show[ROWS][COLS];
	InitBoard(mine, ROWS, COLS,'0');
	InitBoard(show, ROWS, COLS, '*');
	DisplayBoard(show, ROW, COL);
	printf("\n");
	Setmine(mine, ROW, COL);
	//DisplayBoard(mine, ROW, COL);
	FindMine(show,mine, ROW, COL);


}

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

	return 0;
}

你可能感兴趣的:(c语言,开发语言)