C语言扫雷(标雷+展开)

引言

在了解了C语言的知识之后,就可以尝试实现一些有意思的小程序。

在这篇文章中将详细介绍在编写扫雷时的思路。包括布雷、标雷、扫雷以及展开的一些实现,希望对大家有所帮助:

扫雷思路简述

分析模板

在实现扫雷之前,我们可以在网络上找到扫雷游戏,来分析一下它的功能:
在这里插入图片描述
首先,对于扫雷的难度等级有基础、中级、高级以及自定义。这里展示的是基础的难度,棋盘大小为9*9,共有10个雷待扫。

有标雷与扫雷两种模式,当点击鼠标右键时是标雷;点击鼠标左键时是扫雷。
在这里插入图片描述
当标雷时,会出现一个小红旗;
扫雷时,会点开该位置:若该位置有雷时,就会被炸到,游戏结束;若没有雷时,会显示该位置周围8个位置雷的数量。若周围的8个位置没有雷,就会向外展开,直到遇到周围有雷的位置停止,并显示雷数量。

当点到有雷的位置就会被炸到,游戏结束;当扫到只剩余10个方块且时,就说明已经排雷成功,游戏结束:
在这里插入图片描述
在这里插入图片描述

实现思路

我们需要将这些功能大概实现出来。由于还没有学到图形化界面与鼠标点击,所以暂时只考虑通过标准输入与标准输出来玩这个游戏:

布雷前,首先我们需要一个9*9的二维数组用来存储雷的信息,但是我们不能直接将这个雷的信息展示给玩家,所以还需要一个9*9的二维数组用来展示给玩家。
但又出现一个问题,就是当我们需要计算一个位置周围雷的大小时,需要遍历这个位置周围的8个位置。此时如果这个位置在边缘的位置时,遍历时就会出现数组越界的问题。所以我们需要的应该是两个11*11的二维数组用来存储雷的信息与展示给玩家的信息;

布雷时,我们可以使用随机数函数与时间戳来实现;

然后就是扫雷了,我们可以用switch语句来实现标雷与扫雷的区分。

并且封装一些函数来实现游戏中的一些操作。

在这次的实现中,为了更加清晰,将分文件实现。即声明在game.h头文件中;函数实现在game.c源文件中;主函数部分在test.c源文件中实现:

逐步实现

主体部分

main函数

首先,我们需要打印一个菜单,用于展示两种操作play与exit,以便用户选择:

void menu()
{
	printf(" +----- 扫  雷 -----+\n");
	printf(" |----  1.play  ----|\n");
	printf(" |----  0.exit  ----|\n");
	printf(" +------------------+\n");
}

再结合scanf与switch语句来实现游戏或退出:
当选择play时,进入game函数;选择exit时退出。

switch (input)
{
case 1:
	game();
	break;
case 0:
	printf("退出游戏\n");
	break;
default:
	printf("输入错误,请重新输入。。。\n");
	break;
}

同时,我们需要实现当选择错误后或者再玩一遍后还能继续选择。所以我们使用do_while循环:

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

以上就是原函数的大概框架。

game函数

接下来需要在这个源文件中实现game函数的框架:
当进入game函数后,表示玩家选择开始游戏。

首先,我们需要创建两个11*11的二维数组:show数组表示展示给玩家的信息;mine数组表示布好后雷的位置。

为了在更改难度时方便一次更改所有数组的大小,我们可以将数组的大小定义为常量在game.h头文件中。当需要使用这个常量时,只需要引用这个自己的头文件即可:#include <game.h>

#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
char show[ROWS][COLS] = { 0 };
char mine[ROWS][COLS] = { 0 };

接下来就需要初始化两个棋盘,我们将这步封装为一个函数init。这个函数可以实现将二维字符数组的每个元素初始化为指定的字符;

然后需要布雷。我们将布雷的操作也封装为一个函数set;

在布雷之后,我们需要将show数组打印出来,同时也需要将mine数组打印以方便调试代码。所以我们需要一个print函数,可以打印出一个指定的二维字符数组;

然后就是扫雷的部分了。这部分也可以封装为一个函数search。在这个函数中,将实现玩家寻找雷的一系列操作:

void game()
{
	char show[ROWS][COLS] = { 0 };
	char mine[ROWS][COLS] = { 0 };
	//初始化棋盘
	init(show, ROWS, COLS, '*');
	init(mine, ROWS, COLS, '0');
	//设置雷
	set(mine, ROW, COL);
	//打印棋盘
	print(show, ROW, COL);

	//print(mine, ROW, COL);//调试用
	//扫雷
	search(show, mine, ROW, COL);
}

首先初始化棋盘,将show棋盘全部初始化为字符’*‘;将mine棋盘全部初始化为字符’0’(注意不是数字0);

接下来在mine棋盘中布雷;

打印棋盘,给玩家展示初始化的show棋盘;

最后开始扫雷。

函数部分

初始化棋盘 init

我们可以通过两层for循环来实现将二维字符数组的元素改为指定字符:

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

这个函数有4个参数:
第一个表示指定二维字符数组;
第二个与第三个表示数组的实际大小;
第四个表示指定的字符。

返回值为void。

设置雷 set

我们可以通过srand与rand函数结合时间戳来生成一些任意的坐标,实现在棋盘中随机布雷:

void set(char board[ROWS][COLS], int row, int col)
{
	int n = 0;
	int x = 0;
	int y = 0;
	while (n < WIN)
	{
		x = (rand() % row) + 1;
		y = (rand() % col) + 1;
		if (board[y][x] == '0')
		{
			board[y][x] = '1';
			n++;
		}
	}
}

rand函数可以实现随机生成一个0到0x7fff的数字。
调用rand函数前需要调用srand(seek)以给rand函数设置一个值,盗用srand函数时,后面的seek相同时,产生的随机值也是相同的,所以我们可以通过时间戳作为srand的参数以获取随机值: srand((unsigned int)time(NULL));这句代码需要写在main函数中。

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

由于生成的是非常大的数,所以我们需要将生成的数%row与col得到的是最终的结果(0到8的数)。
将雷的位置设置为字符’1’(mine棋盘中)。

打印棋盘 print

通过两个for循环实现打印指定棋盘:

void print(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	printf(" ------ 扫 雷 ------\n"); 
	printf(" -------------------\n");
	for (j = 0; j <= col; j++)
	{
		printf(" %d", j);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf(" %d ", i);
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

这段代码打印的效果就是:
在这里插入图片描述

排雷 search

接下来就是排雷的部分。
在这部分中,我们需要实现两种模式,即扫雷与标雷;
在标雷模式下,指定的坐标将被标记为’?';在扫雷模式下,指定的坐标为雷则游戏结束,不为雷则该位置为其周围8个位置的雷数,需要时展开。
并且能够判断游戏是否结束(胜利或失败)。

我们可以通过switch语句来实现扫雷或标雷的选择:

void search(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int judge = 0;
	int input = 0;
	while (1)
	{
		printf(" ----  1. find  ----\n");
		printf(" ----  2. mark  ----\n");
		printf("  请选择标雷或排雷:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			while (1)
			{
				printf("请输入坐标:");
				scanf("%d%d", &x, &y);
				if ((x<1 || x>col) || (y<1 || y>row))
				{
					printf("输入非法,请重新输入。。。\n");
				}
				else if (show[y][x]!='*')
				{
					if (show[y][x] == '?')
					{
						show[y][x] = '*';
						print(show, ROW, COL);
						break;
					}
					else
					{
						printf("该坐标已排过,请重新输入:\n");
					}
				}
				else
				{
					if (mine[y][x] == '1')
					{
						printf("恭喜你,炸到了好樊鸭。。。\n");
						show[y][x] = 'X';
						print(show, ROW, COL);
						judge = 1;
						break;
					}
					else
					{
						DisplayNum(show, mine, ROW, COL, x, y);
						print(show, ROW, COL);
						int ret=IsWin(show, ROW, COL);
						if (ret == 1)
						{
							printf("很遗憾,没有炸到好樊鸭。。。\n");
							print(show, ROW, COL);
							judge = 1;
						}
						break;
					}
				}
			}
			break;
		case 2:
			while (1)
			{
				printf("请输入坐标:");
				scanf("%d%d", &x, &y);
				if ((x<1 || x>col) || (y<1 || y>row))
				{
					printf("输入非法,请重新输入。。。\n");
				}
				else if (show[y][x] != '*')
				{
					if (show[y][x] == '?')
					{
						show[y][x] = '*';
						print(show, ROW, COL);
						break;
					}
					else
					{
						printf("该坐标已排过,请重新输入:\n");
					}
				}
				else
				{
					show[y][x] = '?';
					print(show, ROW, COL);
					break;
				}
			}
			break;
		default:
			printf("输入非法,请重新输入。。。\n");
			break;
		}
		if (judge == 1)
		{
			break;
		}
	}
}

输入一个坐标,首先判断这个坐标的合法性。不能越界,也不能已经排过。

当扫雷模式时,若输入坐标在的mine棋盘的对应位置为’1’时,游戏失败;对应位置不为’1’时,进入我们封装的一个DisplayNum函数来实现显示数字或展开。之后再进入我们封装的IsWin函数来判断是否胜利。

当标雷模式是,将输入的坐标转换为’?'(show棋盘中)。

需要注意的是,在排到或标记到被标记为’?‘的位置时,’?'标记消除,不产生任何影响。

展示数字或展开函数DisplayNum

这个DisplayNum函数有6个参数,除了需要将两个二位字符数组以及棋盘大小传给DisplayNum函数外,还需要将前面输入的坐标传给DisplayNum。

我们可以通过两遍for循环来计算该坐标周围8个位置的雷的数量,由于已经确定输入坐标在mine棋盘中的字符为’0’所以直接遍历9个位置是可以的。
当这个数字不等于0是时将这个数字放到show棋盘中的相应位置。(需要注意的是,求出的sum是数字而非字符,所以在放到show棋盘中时,需要加’0’以转换为字符)
当这个数字等于0时,即该坐标周围的8个坐标都非雷,此时就需要进行展开:

展开时,我们可以采用函数递归的方式:
将输入位置坐标周围的8个坐标作为输入坐标再传递给DisplayNum,再进行判断,计算。。。
当某次坐标计算结果不为0时,该方向停止递归,并将计算的结果放在show数组的相应位置。

void DisplayNum(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int x, int y)
{
	int x2 = 0;
	int y2 = 0;
	int sum = 0;
	//计算周围八个格子的雷数量
	for (y2 = y - 1; y2 < y + 2; y2++)
	{
		for (x2 = x - 1; x2 < x + 2; x2++)
		{
			if (mine[y2][x2] == '1')
			{
				sum++;
			}
		}
	}
	if (sum != 0)
	{
		show[y][x] = sum + '0';
		//print(show, ROW, COL);//调试用
	}
	//展开
	else
	{
		show[y][x] = ' ';
		for (y2 = y - 1; y2 < y + 2; y2++)
		{
			if (y2 < 1 || y2 > row)//如果y2超出限制,y2++后再判断
			{
				continue;
			}
			for (x2 = x - 1; x2 < x + 2; x2++)
			{
				if (x2 < 1 || x2 > col)//如果x2超出限制,x2++后再判断
				{
					continue;
				}
				else
				{
					if ((show[y2][x2] != '*') || ((x2 == x) && (y2 == y)))//遍历到已经遍历过的或与中心坐标相同就跳过
					{
						continue;
					}
					else
					{
						DisplayNum(show, mine, ROW, COL, x2, y2);
						//print(show, ROW, COL);//调试用
					}
				}
			}
		}
	}
}
判断胜利函数IsWin

每次排雷之后,都需要进行是否胜利的判断。

判断的原理为,当被标记的位置的数量加为扫的位置的数量等于最大雷数时('?‘与’*'的和),就表示游戏胜利。

直接遍历这个show棋盘即可。

int IsWin(char show[ROWS][COLS], int row, int col)
{
	int count = 0;
	int x = 0;
	int y = 0;
	for (y = 1; y <= row; y++)
	{
		for (x = 1; x <=col; x++)
		{
			if ((show[y][x] == '*') || (show[y][x] == '?'))
			{
				count++;
			}
		}
	}
	if (count == WIN)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

当然,我们的所有函数的声明都在game.h头文件中。

参考源码

//game.h


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


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

//初始化
void init(char board[ROWS][COLS], int rows, int cols, char c);
//设置雷
void set(char board[ROWS][COLS], int row, int col);
//打印棋盘
void print(char board[ROWS][COLS], int row, int col);
//排雷
void search(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col);
//计算或展开
void DisplayNum(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int x, int y);
//判断是否胜利
int IsWin(char show[ROWS][COLS], int row, int col);

//game.c


#include"game.h"

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

void set(char board[ROWS][COLS], int row, int col)
{
	int n = 0;
	int x = 0;
	int y = 0;
	while (n < WIN)
	{
		x = (rand() % row) + 1;
		y = (rand() % col) + 1;
		if (board[y][x] == '0')
		{
			board[y][x] = '1';
			n++;
		}
	}
}

void print(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	printf(" ------ 扫 雷 ------\n"); 
	printf(" -------------------\n");
	for (j = 0; j <= col; j++)
	{
		printf(" %d", j);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf(" %d ", i);
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

void search(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int judge = 0;
	int input = 0;
	while (1)
	{
		printf(" ----  1. find  ----\n");
		printf(" ----  2. mark  ----\n");
		printf("  请选择标雷或排雷:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			while (1)
			{
				printf("请输入坐标:");
				scanf("%d%d", &x, &y);
				if ((x<1 || x>col) || (y<1 || y>row))
				{
					printf("输入非法,请重新输入。。。\n");
				}
				else if (show[y][x]!='*')
				{
					if (show[y][x] == '?')
					{
						show[y][x] = '*';
						print(show, ROW, COL);
						break;
					}
					else
					{
						printf("该坐标已排过,请重新输入:\n");
					}
				}
				else
				{
					if (mine[y][x] == '1')
					{
						printf("恭喜你,炸到了好樊鸭。。。\n");
						show[y][x] = 'X';
						print(show, ROW, COL);
						judge = 1;
						break;
					}
					else
					{
						DisplayNum(show, mine, ROW, COL, x, y);
						print(show, ROW, COL);
						int ret=IsWin(show, ROW, COL);
						if (ret == 1)
						{
							printf("很遗憾,没有炸到好樊鸭。。。\n");
							print(show, ROW, COL);
							judge = 1;
						}
						break;
					}
				}
			}
			break;
		case 2:
			while (1)
			{
				printf("请输入坐标:");
				scanf("%d%d", &x, &y);
				if ((x<1 || x>col) || (y<1 || y>row))
				{
					printf("输入非法,请重新输入。。。\n");
				}
				else if (show[y][x] != '*')
				{
					if (show[y][x] == '?')
					{
						show[y][x] = '*';
						print(show, ROW, COL);
						break;
					}
					else
					{
						printf("该坐标已排过,请重新输入:\n");
					}
				}
				else
				{
					show[y][x] = '?';
					print(show, ROW, COL);
					break;
				}
			}
			break;
		default:
			printf("输入非法,请重新输入。。。\n");
			break;
		}
		if (judge == 1)
		{
			break;
		}
	}
}

void DisplayNum(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int x, int y)
{
	int x2 = 0;
	int y2 = 0;
	int sum = 0;
	//计算周围八个格子的雷数量
	for (y2 = y - 1; y2 < y + 2; y2++)
	{
		for (x2 = x - 1; x2 < x + 2; x2++)
		{
			if (mine[y2][x2] == '1')
			{
				sum++;
			}
		}
	}
	if (sum != 0)
	{
		show[y][x] = sum + '0';
		//print(show, ROW, COL);//调试用
	}
	//展开
	else
	{
		show[y][x] = ' ';
		for (y2 = y - 1; y2 < y + 2; y2++)
		{
			if (y2 < 1 || y2 > row)//如果y2超出限制y2++,再判断
			{
				continue;
			}
			for (x2 = x - 1; x2 < x + 2; x2++)
			{
				if (x2 < 1 || x2 > col)//如果x2超出限制x2++,再判断
				{
					continue;
				}
				else
				{
					if ((show[y2][x2] != '*') || ((x2 == x) && (y2 == y)))//遍历到已经遍历过的或与中心坐标相同就跳过
					{
						continue;
					}
					else
					{
						DisplayNum(show, mine, ROW, COL, x2, y2);
						//print(show, ROW, COL);//调试用
					}
				}
			}
		}
	}
}

int IsWin(char show[ROWS][COLS], int row, int col)
{
	int count = 0;
	int x = 0;
	int y = 0;
	for (y = 1; y <= row; y++)
	{
		for (x = 1; x <=col; x++)
		{
			if ((show[y][x] == '*') || (show[y][x] == '?'))
			{
				count++;
			}
		}
	}
	if (count == WIN)
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

//test.c


#include"game.h"
void menu()
{
	printf(" +----- 扫  雷 -----+\n");
	printf(" |----  1.play  ----|\n");
	printf(" |----  0.exit  ----|\n");
	printf(" +------------------+\n");
}
void game()
{
	char show[ROWS][COLS] = { 0 };
	char mine[ROWS][COLS] = { 0 };
	//初始化棋盘
	init(show, ROWS, COLS, '*');
	init(mine, ROWS, COLS, '0');
	//设置雷
	set(mine, ROW, COL);
	//打印棋盘
	print(show, ROW, COL);

	//print(mine, ROW, COL);//调试用
	//扫雷
	search(show, mine, ROW, COL);
}
int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误,请重新输入。。。\n");
			break;
		}
	} while (input);
	return 0;
}

运行结果

在这里插入图片描述
在这里插入图片描述

总结

到此,扫雷的所有内容都已经结束了
这个工程量比较大,可能有一些细节的部分是没有说清楚的,如果有任何问题欢迎在评论区提出

代码在我的测试中目前还没有发现问题,若大家在测试过程中发现了任何的bug请在评论区指出,谢谢大家(参考源码在文末有展示,稍后会再将源码发布,方便大家测试)

如果本文对你有帮助,希望一键三连哦

希望与大家共同进步哦

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿qiu不熬夜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值