C语言实现扫雷(递归展开,标记功能,兼顾胜利判断)

整体设计思路

扫雷咋玩

还记得小时候玩扫雷可懵了,以为是个仅仅靠运气的游戏,没想到还是个益智类游戏。

现在应该也有好多小伙伴是不懂扫雷怎么玩的

扫雷游戏可以锻炼观察力、思考能力。

下面普及一下扫雷的玩法。

方法/步骤
扫雷模式一般分为初级,中级,高级与自定义。下面以高级模式为例。高级模式有16×30(即480)个方格,而地雷数是99.所以不懂玩法,光凭运气的不可能将这99颗雷全部找出来的。
在这里插入图片描述

玩扫雷,第一步,随便用鼠标左键点开任意方格。这一步完全是看运气。很可能会出现两种情况。1.点了好几个方格都没开出空白区域。2.没点开空白区域就以及点到雷。(额,认命吧,重新开始)
在这里插入图片描述

第二步,点开空白区域以后。可以看到有很多数字1,2,3,甚至还会出现4,5,6,这些数字代表的意思是,与数字相邻的几个方格(如果数字在四个角为三个方格,在四条边为五个,其他地方都为八个)中必有而且只有x个雷,所以说,数字1 的周围就会有一个雷,数字2的周围就会有两个雷,以此类推。
在这里插入图片描述

第三步,标记。如果确定某一格为雷后只要点鼠标右键(点一下)标记就可以了,这时会看到那个上面会有一个小旗。说明已经标记成功,如果某一个格你不确定是不是雷,也是可以有鼠标右键(点两次)标记的,这时方格上会出现一个问号。如果发现标记错误,就继续用右键点击该方格即可取消。
在这里插入图片描述

如果地雷已经全部被标记,而且不存在未点开的方格,那么,恭喜你,你已经成功完成了本次扫雷。

简言之:就是点开的一个数字就是周围八个格子内雷的数量。鼠标右键标记完全确定是雷的位置,全部标记出即可。

test.c 用来存放游戏的整体逻辑。

实现游戏菜单

首先在test.c中建立一个主函数,主函数中是一个选择语句,游戏的最初逻辑。给定一个变量input,当input为1时,开始扫雷游戏。当input为0时,退出游戏。

void menu()//菜单
{
	printf("*************************\n");
	printf("*********1.play**********\n");
	printf("*********0.exit**********\n");
	printf("*************************\n");
}
int main()
{
	srand((unsigned int)time(NULL));//后面随机放地雷时会用到这个随机值种子
	int input = 0;//给定一个变量input

	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("扫雷游戏\n");
			game();//进入扫雷游戏的函数
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (input);

	return 0;
}

游戏主逻辑game函数

  • 游戏主逻辑存放在上面代码中提到的game函数里,扫雷的棋盘其实就是一个二维数组。假设数组中1为雷,0为非雷,但是排雷时又需要在数组中显示数据来表示某个坐标附近有几颗雷。这样的话一个数组会严重混淆我们,并且无法正常游戏。所以我们建立两个数组,一个数组是mine用来存放地雷信息。另一个是show,用来展示给玩家。

扫雷标准是9行9列,但是由于考虑到靠边的坐标计算周围的地雷数时,还需要不包括越界的地方,所以将两个数组都扩大一圈,就不会有这方面的困扰了,即变为了11行11列的二维数组,下文中会提到使用宏定义的方式分别定义ROW 9,COL 9,ROWS 11,COLS 11,以及设置的地雷数EASY_COUNT
在这里插入图片描述

  • 然后我们需要给两个数组(棋盘)进行初始化,说白了就是地雷数组中先全部放'0'(就是暂时还没雷呢,后面再埋雷),show数组中放星号'*',用来表示玩家还没排查过的迷雾区域。
  • 然后我们需要给地雷数组中埋地雷了
  • 最后就是玩家们排雷的操作了,当然在排雷前先把show数组打印一下。

以上如初始化数组,放置地雷,排雷,都做成单独的函数模块。game函数中只存放逻辑。以下是代码

创建数组中的ROW,COL,ROWS,COLS.是提前define定义的常量
因为这些常量后面会经常用到,且能增加代码可拓展性。如随时改变棋盘大小。后面说

void game()
{
	char mine[ROWS][COLS] = { 0 };//创建用来存放地雷的棋盘
	char show[ROWS][COLS] = { 0 };//创建用来展示给玩家的棋盘
	
	//初始化数组
	Initboard(mine, ROWS, COLS, '0');
	Initboard(show, ROWS, COLS, '*');

	//放置雷
	setmine(mine, ROW, COL);
	

	//排雷,排雷前先把show数组打印一下。方便玩家选坐标。
	Showboard(show, ROW, COL);
	findmine(mine, show, ROW, COL);

}

game.h 函数声明和符号定义

包含一些头文件,和自定义的常量,以及后面会用到的函数定义。

注:项目中其他文件要引用一下这个头文件#include "game.h"

#pragma once
#include <stdio.h>
#include <windows.h>
#include <time.h>

#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
//初始化二维数组(棋盘)
void Initboard(char arr[ROWS][COLS], int rows, int cols, char set);
//打印棋盘的定义
void Showboard(char arr[ROWS][COLS], int row, int col);
//放置地雷的定义
void setmine(char arr[ROWS][COLS], int row, int col);
//排雷函数的定义
void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

game.c 扫雷游戏各项功能函数具体实现

棋盘初始化

没啥说的,遍历整个数组,往里塞。
mine里面先塞满0
show里面塞满星号

//初始化棋盘,    给定棋盘大小         行数       列数      要塞进去的字符
void Initboard(char arr[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++)
		{
			arr[i][j] = set;
		}
	}
}

布置地雷

随机给mine数组中我们需要的空间9*9中埋雷,假定1就是地雷,所以我们随机存放1.
EAST_COUNT 个(头文件中定义的常量)

//放置地雷函数的实现
void setmine(char arr[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int count = EASY_COUNT;
	while (count)
	{
		x = rand() % row + 1;  //随机获得1-9的数字
		y = rand() % col + 1;

		if (arr[x][y] == '0')
		{
			arr[x][y] = '1';
			count--;
		}
	}
}

记得在提前用一下·srand((unsigned int)time(NULL));随机放地雷时需要这个随机值种子
放在主函数就可以了

玩家扫雷

雷放置好了之后,就可以开始扫雷了。
我的逻辑是这样的:
先创建个win变量,初始情况下win变量就是当前安全格子的数量,棋盘9*9=81个,雷的数量10个,那么win就是71.
开始扫雷之后首先输入1或者2来选择是清理地雷还是标记地雷,

  • 清理地雷就是展开show数组中存放的*号,每展开一个,win就减1。
  • 标记地雷就是将show数组中未展开的*区域中某个坐标,变为M就是我们假定M就是标记这里有雷了,每标记一次win也减1,但是标记数量不能大于雷的总数。
  • 当win降为0后,代表着我们已排雷区域加上标记区域已经充满了所有无雷区域,玩家就胜利了。当然也可以不使用标记功能,全部排完也是可以胜利的(^-^)V。
  • 但是如果你输入了雷的坐标,就是踩到雷了额,我们的胜利判断标准win就会瞬间变成748,然后玩家就无了。
void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int input = 0;
	int win = row * col - EASY_COUNT;//判断胜利的参数,win代表剩余未排查的个数。
	int* pwin = &win;
	do
	{
		printf("请输入(1或2),1为排雷,2为标记 => ");
		scanf(" %d", &input);
		switch (input)
		{
		case 1:
			printf("清理地雷\n");
			//带有递归展开的清理地雷功能的函数,把胜利参数地址加进去,每递归一次,胜利判断的参数就--,直到为0
			clear_mine(mine, show, ROW, COL, pwin);//进入递归展开功能
			//判断胜利或者失败
			if (win == 748)//win变成了748,那就去死吧
			{
				goto die;//跳转到死亡界面(最下面)
			}
			else if (win == 0)//win变成了0,我们赢了
			{
				goto cleard;//跳转到胜利界面(最下面)
			}
			break;
		case 2:
			printf("标记地雷\n");
			//把胜利参数地址加进去,每标记一次,胜利判断的参数就--
			mark_mine(mine, show, ROW, COL, pwin);//进入标记功能
			if (win == 748)
			{
				goto die;
			}
			else if (win == 0)
			{
				goto cleard;
			}
			break;
		default:
			printf("输入错误,请从新输入\n");
		}
	} while (1);
die:
	printf("被炸死了炸死了死了了。。。。。。。。。。。。\n");
	Showboard(mine, ROW, COL);
	return;
cleard:
	printf("排雷完毕(^-^)V!!!\n");
	return;
}
递归展开功能(附加胜利判断)
  • 首先我们要知道如果不递归,每次输入坐标只展开一个方块,这个方块要显示什么。这个方块应该显示的是附近8个块的地雷数量,如果附近没有地雷,那么这个方块我们让它显示空格。所以我们需要一个函数,输入某个坐标,然后这个函数去寻找这个坐标附近8个坐标的地雷数量,并且能够有返回值把地雷数量返回。

下图代码中,因为mine数组中存放的都是字符0或者1,而字符在没内存中是以ASCII码的形式存储的字符0的ASCII码值是48,字符1是49.所以我们把这个坐标附近8个坐标内存储的ASCII值相加再减去8个字符0的值就是附近存在的地雷数量。我们以int形式返回。

static int get_mine_count(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 + 1][y + 1] +
		mine[x + 1][y] +
		mine[x + 1][y - 1] +
		mine[x][y - 1] - 8 * '0';
}
  • 不过递归才能让玩家有更好的游戏体验
    我们需要用到递归来实现在扫雷中点一下炸开一片无雷空间的功能。
    但是递归一定要存在限制条件,当满足这个限制条件的时候,递归便不再继续。
    并且每次递归调用之后越来越接近这个限制条件。(这个好理解,展开一次,棋盘就少一块,棋盘是有数的)
    但是限制条件是什么呢(O_o)??
    boom展开一片,无非就是这个坐标附近的8个坐标没有地雷的话,再以其他8个坐标为中心,再遍历这8个坐标各自附近的8个坐标,这样延伸下去。所以限制条件可以这样判断
  1. 首先判断坐标是否有效,就是不能超过我们的棋盘大小,x和y要在1-9之间。
  2. 这个坐标不能是地雷
  3. 这个坐标不能是之前展开过的,不然就死递归了。

所以代码如下

//递归爆炸式展开
static void boom(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* p)
{
	//坐标有效性判断
		
	if ((x >= 1 && x <= ROW) && (y >= 1 && y <= COL))
	{
		//是否地雷判断
		if (mine[x][y] != '1')
		{
			//是否已清理判断,只有show数组中是星号才是没清理过的
			if (show[x][y] == '*')
			{
				int count = get_mine_count(mine, x, y);
				if (count != 0)
				{
					show[x][y] = count + '0';
					(*p)--;//每展开一下离胜利又近一分

				}
				else
				{
					show[x][y] = ' ';
					(*p)--;//每展开一下离胜利又近一分
					boom(mine, show, x - 1, y - 1, p);
					boom(mine, show, x - 1, y, p);
					boom(mine, show, x - 1, y + 1, p);
					boom(mine, show, x, y + 1, p);
					boom(mine, show, x + 1, y + 1, p);
					boom(mine, show, x + 1, y, p);
					boom(mine, show, x + 1, y - 1, p);
					boom(mine, show, x, y - 1, p);
				}
			}
		}
	}
}
标记功能( 附加胜利判断)

标记很简单的,直接输入一个坐标你,把show数组中这个坐标变成M,然后胜利标准减一下。就OK了,对了还要加一个限制,标记数量不能超过雷的数量,不然把所有地方都标记为雷也能胜利,这样摆烂式排雷法简直是太妙了,但是违背了我们的设计初衷。

//标记功能
void mark_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int* p)
{
	int x = 0;
	int y = 0;
	int count = 0;
again:
	printf("请输入坐标,先输入行坐标再输入列坐标,两个坐标之间隔一个空格。(已标记过的用M表示)\n");
	printf("请输入=> ");
	scanf(" %d %d", &x, &y);
	if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y] == '*')
	{
		if (count < EASY_COUNT)
		{
			show[x][y] = 'M';
			(*p)--;
			Showboard(show, ROW, COL);
		}
		else
		{
			printf("标记不可超过雷的数量\n");
		}
	}
	else
	{
		printf("坐标越界或坐标已被排查,请重新输入\n");
		goto again;
	}
	
}

尾声

搞到这里c语言扫雷基本上是做完了,但这并不是最终版,还有很多地方可以优化。比如地雷的埋放的顺序放到玩家第一次排雷之后,排除玩家的坐标后其他坐标随机摆放。这样可以保证玩家第一次排雷的时候不会被炸死。等等一些其他的细节。
代码我上传到Gitee中了 点击进入Gitee链接https://gitee.com/wei-leyuan666/for_green_dot.git
第一次写这么长的文章。
(‘-ωก̀ )好困

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。在编写C程序时,需要注意变量的声明和定义、指针的使用、内存的分配与释放等问题。C语言中常用的数据结构包括: 1. 数组:一种存储同类型数据的结构,可以进行索引访问和修改。 2. 链表:一种存储不同类型数据的结构,每个节点包含数据和指向下一个节点的指针。 3. 栈:一种后进先出(LIFO)的数据结构,可以通过压入(push)和弹出(pop)操作进行数据的存储和取出。 4. 队列:一种先进先出(FIFO)的数据结构,可以通过入队(enqueue)和出队(dequeue)操作进行数据的存储和取出。 5. 树:一种存储具有父子关系的数据结构,可以通过中序遍历、前序遍历和后序遍历等方式进行数据的访问和修改。 6. 图:一种存储具有节点和边关系的数据结构,可以通过广度优先搜索、深度优先搜索等方式进行数据的访问和修改。 这些数据结构在C语言中都有相应的实现方式,可以应用于各种不同的场景。C语言中的各种数据结构都有其优缺点,下面列举一些常见的数据结构的优缺点: 数组: 优点:访问和修改元素的速度非常快,适用于需要频繁读取和修改数据的场合。 缺点:数组的长度是固定的,不适合存储大小不固定的动态数据,另外数组在内存中是连续分配的,当数组较大时可能会导致内存碎片化。 链表: 优点:可以方便地插入和删除元素,适用于需要频繁插入和删除数据的场合。 缺点:访问和修改元素的速度相对较慢,因为需要遍历链表找到指定的节点。 栈: 优点:后进先出(LIFO)的特性使得栈在处理递归和括号匹配等问题时非常方便。 缺点:栈的空间有限,当数据量较大时可能会导致栈溢出。 队列: 优点:先进先出(FIFO)的特性使得
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值