C语言中的扫雷游戏

游戏规则

只有点开所有的非雷的格子,游戏才可取得胜利。即使是找到所有的藏有雷的格子,但没有点击除雷之外的所有的格子,游戏仍在进行中。在此期间,可以在我们认为是雷的格子上插上红旗,来提醒我们此处有雷。知道了游戏规则,那么让我们借助C语言基础模式为例来编写一个扫雷游戏的代码吧。

设计思路

玩过扫雷游戏的同学肯定都知道,在游戏的开始界面,最先映入眼帘的是一个9*9的棋盘格,左上角是雷的个数提醒,中间的是游戏重新开始的笑脸标志,右边是时间提醒。每点开一个格子,如果是雷则游戏结束;如果不是雷,则会显示周围雷的个数。
所以在编写代码的过程中,我们按照这个思路来开展我们的工作:

  1. 设置菜单栏,以便玩家知晓什么操作会开始游戏,什么操作会退出游戏。
  2. 设置游戏的棋盘格(可在棋盘格的上面和左面各加一行/列,以此来提示我们后面操作的格子的位置,即设置一个10x10的棋格盘。但是在统计位于四周的格子的周围雷的数量时,为了能够使用处于其他地方的格子的统计方法,也是为了简便过程,因为设置为11x11的棋盘格)
  3. 以“1”表示此处有雷,“0”表示此处无雷。但是有雷的标识“1”可能会与周围有几个雷的“1”产生冲突。所以创建2个数组,一个用来存储雷的信息,一个用来向玩家展示游戏界面。
  4. 初始化数组,一个初始化为“0”来作为对雷的操作的数组,一个初始化为“*”来作为向玩家展示的数组。
  5. 打印棋盘格,玩家在游戏时可以看到游戏界面。
  6. 设置雷,在棋盘格中随机的放置10个雷。
  7. 标记雷,在棋盘格中可以标记10个雷且只可以标记10个雷。若要取消标记,再次标记此位置即可取消。
  8. 要注意坐标的合法性

每次操作前,先选择标记or点开。当选择标记时,会对标记的个数进行提示;当选择点开时,会显示雷的信息。

代码展示

1.主函数

int main()
{
	int input = 0;
	srand((unsigned)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;
}

2.游戏部分

<1>初始化数组

首先要初始化,不然在打印时可能会打印出一堆乱码。利用for循环使得数组中的内容变为我们想要展示的内容。
因为涉及到两个数组的初始化,所以在参数的设计部分会将要初始化的字符加入其中,用一个函数实现两个数组的初始化,其中一个数组全部初始化为“0”,另一个全部初始化为“*”。

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

<2>打印棋盘

同样利用for循环将数组中的内容打印出来。为了后续可以方便找到格子是位于哪一行哪一列,可以借用我们创建的 i 和 j 来实现列号和行号的展示,其中还打印了“|”和“-”将行列号与棋盘内容分割开。
棋盘部分打印中的中间的9x9部分,最上面打印列号,最左面打印行号,这样使得棋盘具有可读性,便于之后对格子进行一些操作。

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

<3>放置雷

利用rand()函数来产生随机数,达到随机放置雷的目的。因为雷的位置是随机产生的,所以在放置雷的过程中,可能会出现在同一个位置放置多个雷的情况,所以只有当该格子是原始内容即我们初始化的“0”时,才会在此放置雷。

void SetMine(char mine[ROWS][COLS], int row, int col)
{
	int count = EASY_COUNT;
	while (count)
	{
		int a = rand() % row + 1;
		int b = rand() % col + 1;
		if (mine[a][b] == '0')
		{
			mine[a][b] = '1';
			count--;
		}
	}
}

<4>标记or点开

在Choose函数中可以输入数字,以实现标记和点开的功能。
利用switch语句来选择是要标记某个位置还是要点开某个位置。创建一个全局变量flag,当游戏失败时,flag为0,此时循环结束。其他情况都会一直循环是要点开还是要标记,直到游戏胜利。

do
	{
		int ret = Choose();
		switch (ret)
		{
		case 1:
			FindMine(mine, show, ROW, COL);//在mine中排查雷,放到show中
			break;
		case 2:
			MarkMine(mine, show, ROW, COL);
			break;
		default:
			printf("选择错误,请重新选择\n");
		}
	} while (flag);
(1)标记

创建mark变量来计算我们标记的数量,以防超过我们设计的数量。
因为只有十个雷,所以只能标记十次。当达到标记次数时,便不能标记。每次标记结束后,会弹出提示信息。
再次标记已被标记了的位置时,该位置的标记会取消。

void MarkMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	static mark = 0;
	printf("请输入要标记的位置:->");
	scanf("%d %d", &x, &y);
	if (x > 0 && x <= row && y > 0 && y <= col)
	{
		if (show[x][y] == '$')
		{
			printf("此标记已被取消\n");
			show[x][y] = '*';
			DisPlayBoard(show, row, col);
			mark--;
		}
		else if (show[x][y] == '*')
		{
			if (mark == EASY_COUNT)
			{
				printf("你已标记%d次,无法在此标记\n", EASY_COUNT);
			}
			else
			{
				show[x][y] = '$';
				mark++;
				DisPlayBoard(show, row, col);
			}
		}
		else
		{
			printf("此位置已被排查,不能标记\n");
		}
		printf("当前已标记%d次,还可再标记%d次\n", mark, EASY_COUNT - mark);
	}
	else
	{
		printf("选择的位置超出范围,请重新选择\n");
	}
}
(2)点开
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	//输入排查的坐标,检查坐标处是不是雷
	//是雷,游戏结束;不是雷,统计周围有几个雷,并存储到show
	int x = 0;
	int y = 0;
	static int win = 0;
	printf("请输入要排查的位置:->");
	scanf("%d %d", &x, &y);
	if (x > 0 && x <= row && y > 0 && y <= col)
	{
		if (show[x][y] != '*')
		{
			printf("此位置不可进行排查!!!请重新选择\n");
		}
		if (mine[x][y] == '1')
		{
			printf("很遗憾,此处是雷,游戏结束\n");
			DisPlayBoard(mine, row, col);
			flag = 0;
		}
		else
		{
			unfold(mine, show, row, col, x, y);
			DisPlayBoard(show, row, col);
			win++;
		}
	}
	else
	{
		printf("坐标超出范围,请重新选择\n");
	}
	if (win == row * col - EASY_COUNT)
	{
		printf("你已排除出所有非雷的位置,");
		Win(mine);
	}
}

输入要点开的位置,如果是雷,那么游戏结束;如果不是雷,那么显示周围雷的个数。

<5>判定输赢

在上面的点开部分中的FindMine()函数中,创建一个win变量,每点开一次就自加1。因为游戏取得胜利的方法是,点开所有非雷的格子,所以当win的值等于非雷格子的数量时,游戏胜利,这也是为什么我们用static修饰的原因。

优化思路

可以按照真正的扫雷游戏来添加一些功能,使我们的扫雷游戏更有可玩性。

  1. 确保第一次点开不会点到雷。
  2. 如果点开的位置的周围雷的个数是0,那么向四面八方展开直到某个格子周围雷的个数不为0。
  3. 添加计时功能。
  4. 可以选择重开一局。

完成优化

1.还记得我们之前创建的win变量吗?当我们第一次点击格子时,会第一次调用FindMine()函数,此时win的值是0,所以当win的值为0且点击的格子有雷即第一次点击就点中雷时,要重写放置雷,也就是再次调用SetMine()函数,不过,要注意的是,再次调用ta之前,记得将棋盘格初始化,不然上一次放置的雷还是存在的。在初始化的时候,可以借助我们最开始写的初始化函数。
在这里,我有一个小疑问:既然调用的初始化函数会将会将mine数组全部初始化为0,那么棋盘格的行号和列号不就没了吗?事实上,他们还存在,因为我们展示出来的棋盘格实际上是show数组,这是我们当初创建2个数组的另一个作用。
我们自己在调试第一次点中雷会重新放置雷的时候,会调用DisPlayBoard()函数来观察是否实现这一功能,ta会把mine数组的内容都打印出来,那么此时行号和列号会是“0”吗?实际上,不会,仔细观察我们的DisPlayBoard()函数,行号和列号是按照 i 和 j 来打印的。

while (win == 0 && mine[x][y] == '1')
		{
			InitBoard(mine, ROWS, COLS, '0');
			SetMine(mine, ROW, COL);
		}

2.当我们要点开的这个格子不是雷时,会计算它周围的雷的个数。如果周围没有雷,会依次对它周围的8个格子进行上面的操作,直到统计的某个格子周围有雷为止。按照上面说的,我们可以很容易的想到要使用递归来完成这一操作。
【注意】这样递归展开的方式会出现,win的值不等于非雷格子的数量,而,玩家已经点开所有的非雷格子,这一情况。所以对win的处理要有些变化,不然即使点开所有的非雷格子,游戏也不会取得胜利。

void unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
	int count = get_mine_count(mine, x, y);
	if (count == 0)
		show[x][y] = ' ';
	else
		show[x][y] = count + '0';
	if(count == 0)
	{
		int i = 0;
		for (i = -1; i <= 1; i++)
		{
			int j = 0;
			for (j = -1; j <= 1; j++)
			{
				if (x + i > 0 && x + i <= row && y + j > 0 && y + j <= col && show[x+i][y+j] == '*')
				{
					unfold(mine, show, row, col, x + i, y + j);
				}
			}
		}
	}
}

将count的值传给win,此时当棋盘中的所有非雷位置被点开后,可以认为是游戏胜利。

int IsWin(char show[ROWS][COLS], int row, int col)
{
	int count = 0;
	int i = 0;
	int j = 0;
	for (i = 1; i <= row; i++)
	{
		for (j = 1; j <= col; j++)
		{
			if (show[i][j] != '*')
				count++;
		}
	}
	return count;
}

3.借助time函数,在第一次输入“1”(开始游戏)时有表达式:start=time(NULL),在游戏胜利或失败即game()后有表达式:end=time(NULL),再使用difftime()函数即可得到他们的差值,即我们游戏所用时间。
只有在游戏结束时才会显示游戏用时,这一点与我们实际玩的扫雷有些差距。
4.每次操作前添加一个选项:返回菜单栏。输入数字0,就会跳转到菜单栏,这时可以选择重开游戏或者退出游戏。
在这里插入图片描述
switch语句中添加case 0,这样在每次操作前会有3个选择:点开、标记、退出。
在这里插入图片描述

写在最后

整个代码可能还有一些部分是可以优化的,但是目前我只想到这些;同时也许有一些地方有错误但是我没有发现,在运行过程中也很凑巧的没有发现ta,等等的其他问题。毕竟有时候“当局者迷”,希望大家可以谅解。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

好运包围

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

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

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

打赏作者

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

抵扣说明:

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

余额充值