C语言实践:扫雷游戏的初阶和进阶实现(保姆式解读,包含许多实用干货!!!)

本文介绍了使用C语言实现扫雷游戏的详细过程,包括游戏逻辑、棋盘创建与初始化、雷的随机布置、用户交互以及游戏胜利条件。文章特别强调了输入处理、数组边界安全和递归展雷策略,提供了从基础版到进阶版的代码实现。
摘要由CSDN通过智能技术生成

前言:
本文介绍的扫雷游戏的实现,所用到的知识点不难,只需大致掌握以下知识点的基本内容即可:数组,循环语句,分支语句,函数的定义及实现,简单的宏定义。
注:本文章中标题后面带“!!!”代表需要有需要注意的细节或包含实用干货。

由于本人写作经验尚浅,完整的代码实现可查看本人的gitee仓库:扫雷游戏实现


一. 扫雷游戏初了解

要想实现扫雷,首先我们得先了解它的游戏过程和实现的基本逻辑。

1.扫雷体验

c9600e4b80cb34258ddcb35dcb30bab.png
df50d317474ab3770c6c602db14edc9.png
d0bcee5c08a997ce80d8bed81d9bbf0.png
cdbaae8cd1ec07c442e9dfab65b5c8d.png

2.扫雷的基本实现思路

  • 首先正式开始游戏前,需要有一个可以进行选择的菜单,具体的选项内容可以自行设计。
  • 进入游戏后,需设计一个不可视的雷区布置棋盘和可视的用户游戏棋盘,并进行初始化。
  • 打印出来的棋盘需要布置合适数量的雷。(注:这一步在实现的过程中可与下一步进行交换,无明确的先后顺序,因为布置雷的棋盘为不可视的棋盘 。)
  • 打印一个可视的n * m扫雷棋盘。

—> 以下步骤应该是一个循环的过程

  • 等待用户进行排雷选择。
  • 用户选择一个排雷位置后,需按该位置附近实际的雷区分布情况进行雷区信息的展示。若该位置是雷,则展示所有雷的分布情况,游戏结束;若该位置不是雷,则显示该位置3 * 3范围内有几个雷。(可能展开一片区域)

具体实现的流程图如下:
image.png


二. 扫雷游戏具体实现

1.菜单的打印与用户进行选择 !!!

一个菜单的基本选项是开始结束。(本文章设置输入1为开始游戏,0为结束游戏,具体方便之处可以先参考实现代码)。

//void game()
//{
//	char mine[ROWS][COLS];
//	char show[ROWS][COLS];
//	InitBoard(mine, ROWS, COLS, '0');
//	InitBoard(show, ROWS, COLS, '*');
//	DisplayBoard(show, ROW, COL);
//	SetMineBoard(mine, ROW, COL);
///*	FineMine1(mine, show, ROW, COL);	*/			//排雷初始版本
//	FineMine2(mine, show, ROW, COL);					//排雷进阶版本
//}

void menu()								//简易菜单的打印
{
	printf("************************\n");
	printf("******* 1. play ********\n");
	printf("******* 0. exit ********\n");
	printf("************************\n");
}

int main()
{
	int input;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		while (getchar() != '\n');     //可防止用户输入非法字符导致死循环,如:'a',"asd"等
		switch (input)
		{
		case 0:
			printf("退出游戏\n");
			break;
		case 1:
			game();					  //用于集中管理游戏实现的各个函数
			break;
		default:
			printf("输入错误\n");
			break;
		}
	} while (input);
    return 0;
}

若用户正常输入数字1或0,使用输入的整型变量input作为循环条件可保证开始与结束,输入其他数字则进行提示后重新进行输入。

**!!!**但这里可能出现另一种情况,用户若输入一个字符或者字符串,则该程序会进入死循环,原因是:当输入非法字符时(要求的类型和实际输入的类型不同),scanf会直接跳过该次输入,并且输入的内容会被存放在缓冲区内。此时input为初始值,重新进入循环条件判断,因为scanf的缓冲区已有内容,所以会自动跳过input的输入,造成switch语句每次都执行default的内容。
325df153f00f974bec3d1ef2835a567.png
5acc52165582c9ff31d9857d702e7a6.png

—>解决这个问题的办法是:循环使用getchar()直到将缓存区中输入的非法字符读取完。
while (getchar() != '\n');
924e3ca50e49100d5b7ff30339647e8.png


2.棋盘的创建与初始化

2.1棋盘的创建

首先,由扫雷的逻辑可知需要创建2个棋盘:一个用于雷区的存储,一个用于扫雷过程中雷区分布情况的展示。
棋盘的创建可以使用二维数组,同时为了改变扫雷游戏的难度,能更加方便地调整棋盘大小,这里推荐使用宏定义来决定二维数组的大小。
对用户来说,棋盘的展示实际只需要9 * 9的大小,因此可以定义两个宏ROW和COL分别对应行和列。

image.png
考虑到后面用户排雷时选择坐标可能选择到棋盘四周的位置,在遍历其3 * 3范围内雷的分布情况时,(假设此时游戏棋盘大小为9 * 9),若真实创建的二维数组大小也为9 * 9,则遍历雷的分布情况时可能造成数组的越界访问,因此真实创建的数组的 行ROWS 和 列COLS 都应该各加上2。

#define ROW 9						//用户展示棋盘大小
#define COL 9

#define ROWS ROW+2				   //实际创建棋盘大小
#define COLS COL+2

	char mine[ROWS][COLS];   		//布雷棋盘
	char show[ROWS][COLS];			//用户展示棋盘

** ** 实际创建的棋盘如下图所示:
image.png

2.2棋盘的初始化

对于雷区布置的棋盘,本文推荐对某位置存在雷,存放字符** ‘1’** ;对于其它非雷区存放字符** ‘0’ **。后面5.2.1中信息展示的实现会具体说明原因。(实际的实现也可以依个人喜好存放不同的字符)

对用户展示的棋盘,本文章用字符** ‘*’ **进行初始化,具体实现可以用个人喜欢的字符替代。
实现的代码如下:

	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');     //都放在game()函数中


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

用户游戏棋盘初始的展示效果如下:
e2c0ea3d48bede7fbce8cb4cc57ea15.png


3.不可视棋盘雷的布置 !!!

本文中对雷的分布位置存放字符 ‘1’,并且每局游戏生成的雷区都不一样。那么问题来了:如何在不同位置随机布置雷呢?
—>这里需要用到**int rand(void)**函数,它的作用是:产生一个随机数。

** !!!而使用rand函数之前,需要先使用另一个函数void srand(unsigned int seed)**,*它是随机数发生器的初始化函数。由函数的参数可知:实参需要输入一个无符号整型,但如果实参是一个常量或一个不会自动变化的变量a,如srand(1)、srand(a),则rand函数返回值也是一个固定的整型值。
那么如何解决这个问题呢?
—>这里需要在srand函数中使用另一个函数
time_t time(time_t
timer)**作为实参,它的作用是:返回当前时间的时间戳。(具体的原理这里不展开讲述,如果感兴趣可自行去了解)
注:srand( (unsigned int) time(NULL) )在整个程序中只需使用一次,因此可直接放在主函数main()之中。

解决了随机数的生成问题,就可以进行随机坐标的生成了。(以9 * 9的游戏棋盘为例,棋盘随机分布了10颗雷)
前面说过:实际创建的棋盘大小为11 * 11,而使用的区间为9 * 9,所以随机生成的坐标可用:
int x = rand() % 9 + 1;

布置雷的具体代码实现如下:

#include <stdlib.h>              //使用rand函数与srand函数所需的头文件
#include <time.h>				 //使用time函数所需的头文件

	srand((umsigned int)time(NULL));        //放在主函数main()中

void SetMineBoard(char board[ROWS][COLS], int row, int col)
{
	int x, y;                      //x,y分别是随机生成的雷的横纵坐标
	int count = Easy_Mine_Count;   // Easy_Mine_Count为自定义布置的雷的数量
	while (count > 0)
	{
		x = rand() % 9 + 1;
		y = rand() % 9 + 1;
		if (board[x][y] != '1')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

实现效果如下:(以下棋盘用户不可见)
68cb2e903e110e5d8f714b848f2cd11.png


4.用户游戏棋盘的打印

用户游戏棋盘的打印比较简单,但有几个需要注意的点:

  1. 用户游戏棋盘只需打印9 * 9的大小,所以函数传参的行和列为ROW,COL。
  2. 为了使棋盘相对美观,打印 ‘*’ 时需要顺便留一个空格。
  3. 对用户来说,需要方便且快速的辨识出每个位置的坐标,因此打印相应的行号和列号。
  4. 可以前面的步骤之前打印一个游戏标题。(这个可有可无,具体实现因人而异)

具体的实现代码如下:

	DisplayBoard(show, ROW, COL);      //放在game()函数中

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i, j;
	printf("******扫雷游戏******\n");
	for (j = 0; j <= col; j++)                 //打印棋盘前先打印所有列号
		printf("%c ", j + '0');
	printf("\n");
	for (i = 1; i <= row; i++)				   //打印用户棋盘
	{
		printf("%c ", i + '0');				  //打印每行内容前,先打印一个行号
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

展示效果如下:
b6dab1d6f966c4c707e0559df0fde83.png


5.用户排雷与雷区情况展示

5.1排雷坐标的输入可能性

对于获取的排雷坐标,可能出现以下4种情况:

  1. 输入坐标非法,即输入坐标越界,超过规定游戏棋盘的大小,如(10,2),需进行报错提示并重新输入。(此处游戏棋盘大小为9 * 9)
  2. 输入坐标合法,且该坐标的位置未被排查过,接下来应展开该坐标周围 3 * 3 范围内的雷区情况。
  3. 输入坐标合法,但该坐标的位置已被排查过或附近雷区情况已知晓,需报错提示并重新输入。
  4. 输入坐标合法,但该坐标位置存在雷,游戏结束。
  5. 输入坐标合法,且本次排查的坐标为最后一个未被排查过的非雷位置,游戏胜利。

5.2雷区信息展示(初阶版)

5.2.1 信息展示

当用户输入合法坐标且所在位置未被排查过时,将该坐标的信息替换成四周雷的数量,并打印游戏棋盘的信息。
若用户输入的坐标为(x,y),当mine数组中字符 ‘1’ 表示该位置存在雷时,若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’ 即可得到该位置四周的雷的数量。
注:‘0’ + a 可得到字符 ‘a’。(a为整型,且0<= a <= 9)
若用其他字符作为雷的标志,则需另外创建一个计数器count,逐个对比每个位置是否为设定的雷的标志,是则count++。


5.2.2 游戏胜利的条件

在9 * 9的棋盘中,若布置了10颗雷,则需要排查71个位置,即展示71次雷区信息就能获得胜利,因此可以设置一个整型标志win,每成功排查一次,win的值加1,直到win的值为71时结束排查,游戏胜利。

整个模块具体的实现代码如下:


int GetMineCount(char board[ROWS][COLS], int x, int y)
{
	return board[x - 1][y - 1] + board[x - 1][y] + board[x - 1][y + 1] + board[x][y - 1] + board[x][y + 1]
		+ board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1] - 8 * '0';
}

void FineMine1(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)				//排雷     初始版本
{
	int x, y;
	int win = 0;
	while (win < ROW * COL - Easy_Mine_Count)
	{
		printf("请输入你要排查雷的坐标:>");
		scanf("%d%d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (show[x][y] != '*')
			{
				printf("输入坐标无效,请重新输入");
				break;
			}
		    if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了,本局游戏结束\n");
				DisplayBoard(mine, ROW, COL);             
				printf("注:数字1代表该位置有雷\n");
				break;
			}
			else
			{
				int count = GetMineCount(mine, x, y);        //得到雷的数量
				show[x][y] = count + '0';
				DisplayBoard(show, ROW, COL);        
				win++;
			}
		}
		else
		{
			printf("输入坐标非法,请重新输入\n");
		}
	}
	if (win == ROW * COL - Easy_Mine_Count)
		printf("所有的雷已排完,恭喜你获得本局游戏的胜利!\n");
}

运行效果如下:
QQ截图20230616150449.png


5.2雷区信息展示(进阶版) !!!

5.2.1 信息展示

经过上面的步骤,扫雷的整个运行逻辑似乎已经完成了,但不知大家是否有这样一个疑问:“这个扫雷跟我平时玩过的有点不一样,它每次得到的有效信息这么少,这样每局游戏玩的时间和游戏难度不是大大增加了吗?”
—>没错,上面的游戏实现可以算是一种“残血”版的扫雷。
2865d86e651dd8de51eeb996d7dc80d.png <—排查坐标为(5,5)

**!!!**那么怎么实现排查一个位置,一下子得到出一大片的有效信息呢?
通过观察我们可以得知:
当选择的排雷位置四周不存在雷时,这个位置不显示任何信息,并对四周的每个位置再进行一次雷区情况的排查,若四周其中任意一个位置四周依然不存在雷,则再对该位置的四周进行雷区情况的排查,直到所排查的位置四周存在至少1个雷时停止该操作,只显示当前排查位置四周雷的信息。
—>通过这个规律易知:此操作具有递归性。

这里的递归操作有2个需要注意的点:

  1. 每个递归都需要一个明确的结束条件。在雷区排查的操作中,不能对已经排查过的位置进行重复的递归操作。

例如:假设粉红色区域为第一次排查的位置,它的四周即蓝色斜线区域都不存在雷,并且粉红色区域左上角位置的周围也没有雷,也需要进行递归操作,但是排查的位置包含了第一次排查过的位置,即粉色区域的位置,如果不对递归加以限制,递归就会进入死循环。
image.png
—>因此,我们可以先将排查过的且四周无雷的位置存放字符空格 ’ ’ ,在递归过程中,若访问的位置show数组中字符为 ’ ’ 时,不再进行雷区情况的遍历。
因此限制条件如下:

if(show[x][y] == ' ' )
    return;
  1. 在11 * 11的mine棋盘中,因为四周位置也存放着字符 ‘0’ ,若用户选择了棋盘边缘的位置排雷,且恰好该位置周围不存在雷,则非游戏棋盘位置的字符 ‘0’ 也可能被当作非雷位置进入递归。

image.png
因此限制条件如下:

if(x < 1 || x > 9 || y < 1 || y > 9)
    return;

5.2.2游戏胜利的条件

与初阶版的扫雷一样,游戏同样需要一个标志以判断游戏的胜利,所以这里采取将整型变量win作为函数参数,每排查一个位置,win的值加1。(win需要传址,即&win)

整个模块具体的实现代码如下:

void GetMineCount2(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* win)
{
	if (show[x][y] == ' '|| x < 1 || x > 9 || y < 1 || y > 9)
		return;
	int count = 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';
	if (count == 0  )
	{
		show[x][y] = ' ';
		(*win)++;
		GetMineCount2(mine, show, x - 1, y - 1, win);
		GetMineCount2(mine, show, x - 1, y, win);
		GetMineCount2(mine, show, x - 1, y + 1, win);
		GetMineCount2(mine, show, x, y - 1, win);
		GetMineCount2(mine, show, x, y + 1, win);
		GetMineCount2(mine, show, x + 1, y - 1, win);
		GetMineCount2(mine, show, x + 1, y, win);
		GetMineCount2(mine, show, x + 1, y + 1, win);
	} 
	else if(count && show[x][y] == '*')
	{
		show[x][y] = '0' + count;
		(*win)++;
	}
}

void FineMine2(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x, y;
	int win = 0;
	while (win < ROW * COL - Easy_Mine_Count)
	{
		printf("请输入你要排查雷的坐标:>");
		scanf("%d%d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (show[x][y] != '*')
			{
				printf("输入坐标无效,请重新输入\n");
			}
			else if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine, row, col);
				printf("注:数字1代表该位置有雷\n");
				break;
			}
			else
			{
				GetMineCount2(mine, show, x, y, &win);
				DisplayBoard(show, row, col);
				printf("\n\n");
			}
		}
		else
		{
			printf("输入坐标非法,请重新输入\n");
		}
	}
	if (win == ROW * COL - Easy_Mine_Count)
		printf("所有的雷已排完,恭喜你获得本局游戏的胜利!\n");
}

运行效果如下:
QQ截图20230618005229.png


结语:这是我的第二篇个人博客,若有错误或者不足的地方,欢迎大家指出错误或给出建议,谢谢大家的观看。

由于本人写作经验尚浅,完整的代码实现可查看本人的gitee仓库:扫雷游戏实现

  • 7
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值