关于扫雷的分析和代码实现

目录

一、扫雷的介绍和玩法

游戏简介

游戏玩法          

二、模块分析

菜单模块

调试模块

 游戏模块

三、游戏原理分析

        雷区呈现问题探讨

雷的布置问题探讨

       话题1

         话题2

用户交互与雷的排查

四、最终的代码呈现(仅供参考)

一、扫雷的介绍和玩法

游戏简介

         扫雷是一款经典的单人电脑游戏,目标是在最短的时间内找出所有非雷的方块,而不触发任何地雷。在9*9的扫雷游戏中,通常会有10个地雷分布在81个方格中。
        

游戏玩法          

         1. 开始游戏:点击开始按钮,游戏界面会出现被方块覆盖的区域,每个方块下面都可能藏着地雷或者数字。
        2. 揭示方块:左键点击一个方块,如果它下面没有雷,则会显示一个数字,这个数字代表周围八个方块中隐藏的地雷数量。如果点击的方块下面是地雷,则游戏失败。如果点击的方块是空的(没有地雷且周围也没有数字),则自动展开一片区域。
        3. 标记地雷:当你认为某个方块下面是地雷时,可以用右键点击来标记它。标记地雷可以帮助你记住哪些位置可能是雷区,避免不小心点击。
        4. 逻辑推理:根据已经揭示的数字,你需要推理出哪些方块下面是地雷,哪些不是。这是扫雷游戏中最具挑战性和趣味性的部分。
        5. 游戏结束:当你揭示所有非雷的方块或者触发地雷时,游戏结束。如果成功找出所有地雷,则获得胜利。
      

         对于9*9的扫雷,由于空间较小,每一步都需要非常谨慎,因为错误的一步很可能导致游戏结束。通常情况下,你需要利用数字之间的逻辑关系来逐步缩小雷区的可能性,直到找到所有的地雷。

二、模块分析

        当然,让我们更深入地探讨扫雷游戏的模块分析。模块化设计是软件开发中的一个关键概念,它将复杂的系统分解为更小、更易于管理的部分。对于扫雷游戏,模块化可以帮助我们清晰地理解每个部分的功能,并独立地开发和测试它们。

菜单模块

         在进入游戏的时候,我们可能需要引导玩游戏的人进入游戏或者退出游戏,在这里我们需要设计一个menu模块

//布置一个菜单
void menu()
{
	printf("**************\n");
	printf("****1.play****\n");
	printf("****2.exit****\n");
	printf("**************\n");
}

         当用户输入“1”的时候会进入游戏,输入“0”则会退出游戏,用于引导用户选择是否玩这个游戏。

调试模块

        然后根据menu模块,我们需要读取用户的选择,我们可以使用一个参数进行读取,并通过这个参数,使用一个分支进行一个方向的选择。

    int imput = 0;
	printf("请选择:");
	scanf("%d", &imput);
	switch (imput)
	{
	case 0:
		printf("退出游戏\n");
		break;
	case 1:
	{
		printf("扫雷开始\n");
		break;
	}
		
    default:
	{
			printf("选择错误,重新选择\n");
			break;
	}
		

         此外,我们还需要留意一个问题,就是如何让用户玩的更加愉快,如果我们每次玩一次就让游戏直接结束,当用户扫雷成功的时候,程序告诉他,游戏要玩需要在运行一下程序,那是在是太扫兴了,所以为了能够满足用户的需求,我们在这里可以用一个while函数,当用户输入“1”的时候,会进入游戏,当用户输入“0”的时候会退出游戏,但是进入游戏并且完成游戏的时候,再设计一个选择是否继续玩的选项。

while (imput);

        这里考虑到需要先读取用户的选择,在进行系统内部的判断,我们就可以使用do...while...函数来实现这个功能 

​
//调试内容
void test()
{
	int imput = 0;
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &imput);
		switch (imput)
		{
		    case 0:
			{
                printf("退出游戏\n");
		    	break;
            }
		    case 1:
		    {
			    printf("扫雷开始\n");
			    break;
		    }
		    default:
		    {
			    printf("选择错误,重新选择\n");
			    break;
		    }
		}
	} 
    while (imput);
}

​

 游戏模块

       进行前面的基础设置后,也就是如果用户选择“1”之后,我们就要进行一个游戏的编写。

        在编写之前,我们需要明确一个前提,就是这个游戏的运行逻辑和原则是怎么样子的?

        假设我们做的是一个9*9的雷区,那么我们如何让这个雷区呈现出来,如果呈现出来?那应该怎么布置雷?然后怎么做到排查雷?排查雷的时候,怎么做到将周围八个格子的雷的个数以数字的形式呈现出来?这些雷的位置是人为固定的还是随机的?游戏结束后怎么做这个优化?

        这些问题,都是我们在设计的时候都是需要考虑的。

三、游戏原理分析

        雷区呈现问题探讨

        所谓雷区,无非就类似于一个9*9的棋盘,我们需要做的,就是分析这个棋盘的属性,它可以是什么属性呢?

        在这之前,我们先思考一个问题,设计这个棋盘之后,我们是不是需要对其中的一些位置布置雷,要不止这个雷的话,是不是需要先明确这个位置才行,因此我们设计的棋盘应该要能够精准地定位其中的某个元素,所以我们采用二维数组刚好能够满足我们的需求。

        接下来就是这个棋盘的呈现问题,我们需要先制作一个棋盘,我们就要先思考这个棋盘在还没有开始扫雷之前,它的元素应该用什么来表示,有人可能会思考说要不用数字0来表示吧,这样只需要在里面设计若干个数字1来表示雷。

        但是如果这样子去思考的话,就会面临一个问题,我们在扫雷的时候,如果没有踩到雷,是需要在哪个位置显示周围八格的雷的数量的,这个数字会与我们的雷有冲突,因此该想法的实现明显不可靠。

        那我们还能怎么思考呢?有聪明的小伙伴可能就会说了,我们可以采用两个数组,一个用来做棋盘,另一个用来处理周围的雷,然后将这两个棋盘进行整合不就行了。

        于是我们就就参考这个方法,我们先设计两个二维数组

​

int ROWS;//定义行数
int COLS;//定义列数

char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的信息

​

        考虑到两个棋盘的功能性是不一致的,我们对其中的元素就需要进行一个初始化,首先对于mine数组,我们不妨对他的元素统一为'0',而对于另一个数组show,就定义为’*',这样两个数组就进行了一个区分 ,然后就开始进行棋盘的打印和初始化这里进行一个初始化范围和初始化内容的方式进行

​
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;
		}
	}
}
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');

​

         这样,棋盘就已经创建好并初始化成功了,接下来就是打印棋盘,由于这个打印棋盘的次数可能会比较频繁,所以我们直接设计一个函数用来来简化我们的代码

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

然后开始打印棋盘

	int ROW;
    int COL;

    //打印棋盘
	//DisplayBoard(mine, ROW, COL);
	DisplayBoard(show, ROW, COL);

        这里打印棋盘的时候需要留意的就是,show才是我们向用户呈现的棋盘,这里不要搞混淆 。

雷的布置问题探讨

        在布置雷的时候,我们首先要明确有多少个雷?如果我们假设有10个雷的话,又应该怎么布置这个雷,每一次雷的位置是随机的还是人为设置的?

        这里我们参考9*9的棋盘里面有10颗雷,那我们应该怎么布置这个雷呢?

        在前面,我们使用了二维数组进行雷区的定位,我们可以使用这个功能,这时候我们需要做的就是怎么将这个雷表示进去,我们已经用过的图标就是空白的*,来作为我们未扫过的雷区,那么我们不妨就按照1的方式来表示雷,之后如果要显示这个八格内有多少雷,就转化成了这八格内1的和应该是多少。

        但是有聪明的小伙伴就会说了,我们初始化的时候输入的明明是字符'1'啊,这个相加会不会出错啊?其实这个是会出错的,为什么呢?如果我们进行字符的相加,会是什么结果,比如说'1'+'1',这个应该等于什么呢?我们在这里插入一个话题。

       话题1

        在C语言中,字符相加减的操作实质上是对字符对应的ASCII码值进行算术运算。字符在C语言中通常以整数形式存储,即它们对应的ASCII码值。因此,当你对两个字符进行加法或减法操作时,实际是在对它们的ASCII码值进行加法或减法。

例如,假设有两个字符 ‘A’ 和 ‘B’:

  • ‘A’ 的ASCII码值是 65
  • ‘B’ 的ASCII码值是 66

        如果你将它们相加:‘A’ + ‘B’,结果是 65 + 66 = 131。这个结果是一个整数,如果你想要将其转换回字符,需要使用类型转换。

        同理,如果你进行减法操作:‘B’ - ‘A’,结果是 '66' - '65'= 1。

        言归正传,我们通过这个话题会发现如果我的每一个雷,也就是'1',在进行计算的时候,是很难去处理的,因为'1'+'1'='b',如果按照这个进行运算的话无非就是将问题困难化,因此我们可以考虑让每一项都进行一次相减处理,也就是'1'-'0',这样子得到的值就是一个1,然后我们通过一个循环函数就可以得到这个值。

        到这里,发现想象很美好,但事实上,我们应该还会遇到一个问题,什么问题呢?

        就是周围八格对于四周的角落貌似是没有的,但是如果我么专门取针对这个去处理的话我们会多做很多这个步骤,这里为了减少不必要的麻烦,我们设计行数和列数为11*11的棋盘,并定义这个行数和列数

//向用户呈现的
#define ROW 9
#define COL 9

//内部调整的
#define ROWS ROW+2
#define COLS COL+2

,有了这个定义,我们就可以进行周围八格的处理,也就是对mine进行周围八格处理,在周围八格中,读取范围是9*9,但运行范围是11*11,这样他就避免了四周角落八格取不到的问题。

        再然后我们还会遇到一个问题,就是怎么放雷。

        总的思路是这样的,先选定选定10个自由不重复的坐标,在这些坐标中进行字符转置,就是将原来的'0'改为'1',通过这样的方式,就能够将雷放进去这个坐标。

        在这里就有几个需要处理的地方,第一个是10个不重复的坐标,这里我们使用一个if语句,用来避免重复放置的情况,然后if语句判断成功,就执行内容的转置

        此外,这里我们用到了生成随机坐标的函数rand,这个要怎么用呢?

         话题2

        在C语言中,随机数通常通过 rand() 函数生成,而这个函数的随机性依赖于一个称为“随机数种子”的初始值。如果你不手动设置随机数种子,rand() 函数会使用一个默认的种子,通常是1。这意味着如果你不改变种子,每次程序运行时产生的随机数序列都是相同的。

        要改变随机数种子,你可以使用 srand() 函数。这个函数接受一个 unsigned int 类型的参数作为种子。以下是一些常用的方法来设置随机数种子:

        使用当前时间:最常见的方法是使用当前时间作为种子。这可以通过包含 <time.h> 头文件并使用 time(NULL) 实现。这样每次程序运行时,种子都是不同的,从而产生不同的随机数序列。

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

int main() {
    srand((unsigned int)time(NULL)); // 使用当前时间作为种子
    int randomNum = rand();
    printf("随机数: %d\n", randomNum);
    return 0;
}

        重要的是要记住,虽然改变种子可以让你得到不同的随机数序列,但这并不意味着你得到了真正的随机数。rand() 函数生成的是伪随机数,即它们遵循一个可预测的数学公式。

放雷的代码大致是这样的,在这之前调用了头文件<time.h>和调用了

                                                srand((unsigned int)time(NULL));

        否则生成的随机数就不再是一个真正的随机数,而是一个伪随机数

void SetMine(char board[ROWS][COLS], int row, int col)
{
	//布置10个雷
	//⽣成随机的坐标,布置雷
	int count = 10;
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (board[x][y] == '0')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

用户交互与雷的排查

最后一个模块也是相对好处理的一个模块,应为对于雷的排查,我们就需要思考读取用户的坐标,踩雷怎么样,没踩雷怎么样等简单的问题,下面是一个简单的示例函数

​
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	while (win < row * col - EASY_COUNT)
	{
		printf("请输入要排查的坐标:");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && 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("坐标错误,请重新输⼊\n");
		}
	}
	if (win == row * col - 10)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}

​

四、最终的代码呈现(仅供参考)

         先编写一个主文件,考虑到每个模块功能的不一致,所以为了整体的美观和总体的效率,我采用多文件进行编写


#include"game.h"

//布置一个菜单
void menu()
{
	printf("**************\n");
	printf("****1.play****\n");
	printf("****2.exit****\n");
	printf("**************\n");
}
//设置运行流程
void game()
{
	srand((unsigned int)time(NULL));
	char mine[ROWS][COLS];//存放布置好的雷
	char show[ROWS][COLS];//存放排查出的雷的信息
	//初始化棋盘
	//1. mine数组最开始是全'0'
	//2. show数组最开始是全'*'
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');
	//打印棋盘
	//DisplayBoard(mine, ROW, COL);
	DisplayBoard(show, ROW, COL);
	//1. 布置雷
	SetMine(mine, ROW, COL);
	//DisplayBoard(mine, ROW, COL);
	//2. 排查雷
	FindMine(mine, show, ROW, COL);
}
//调试内容
void test()
{
	int imput = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &imput);
		switch (imput)
		{
		case 0:
			printf("退出游戏\n");
			break;
		case 1:
		{
			printf("扫雷开始\n");
			game();
			break;
		}
		default:
		{
			printf("选择错误,重新选择\n");
			break;
		}
		}
	} while (imput);
}
//主函数
int main()
{
	test();
	return 0;
}

在上面的代码中我包含了一个头文件 :game.h

用于声明函数和包含其他头文件的同时定义变量

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

#define EASY_COUNT 10

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

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

//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);

//布置雷
void SetMine(char board[ROWS][COLS], int row, int col);

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

主函数中其实基本上都是函数的调用,而函数的功能编写以下的代码中进行实现 

#include "game.h"
//棋盘初始化
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
	int i = 0;
	for (i = 0; i < rows; i++)
	{
		int j = 0;
		for (j = 0; j < cols; j++)
		{
			board[i][j] = set;
		}
	}
}
//展示棋盘用的
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("--------扫雷游戏-------\n");
	for (i = 0; i <= col; i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%d ", i);
		int j = 0;
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
	//布置10个雷
	//⽣成随机的坐标,布置雷
	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--;
		}
	}
}
//排雷之九宫格雷
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
	int i, j,count = 0;
	for (i = -1; i <= 1; i++)
	{
		for (j = -1; j <= 1; j++)
		{
			count += mine[x + i][y + j] - '0';
		}
	}
	return count;
}
//排雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	while (win < row * col - EASY_COUNT)
	{
		printf("请输入要排查的坐标:");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && 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("坐标错误,请重新输⼊\n");
		}
	}
	if (win == row * col - EASY_COUNT)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值