关注

扫雷游戏的实现(c语言精简版,不看后悔呦)

hello吖,大家!
今天我们要实现的是一个精简版的扫雷游戏,还记得十多年前,在学校机房里没有网络跟同学一起偷偷玩扫雷游戏,被老师逮到的情景,至今记忆深刻,难以忘怀啊!
借着那深刻且被老师爱戴过的回忆,让我们一起带着感情进入这场扫雷游戏的实现!

序言

我们要从整体的视角、思路和代码实现的方式来进行扫雷游戏逻辑的实现,这里我把它们分为了四个部分来进行一一讲解:

第一部分:我们的预期
第二部分:文件结构(其实就是头文件和源文件的使用)
第三部分:游戏逻辑分析
第四部分:游戏逻辑实现+分析

一、我们的预期

扫雷游戏,我们大家都不陌生,就是在一个正方形棋盘上踩格子,踩到地雷就被炸死了,没有踩到就会显示这个格子周围有几个雷以此来作为提示信息,帮助我们通关游戏,如果不够玩,还可以再来一把,这就是我们对扫雷游戏的游戏运行预期,但从游戏开发者的视角来看,就不仅仅是玩游戏了,更多的是对相关知识理解的深入 和 用心码完整个工程的那种由内而外散发出来的成就感。正是因为视角的不同,我们才可以用心去对待我们正在做的事——但没什么事情,是比坚持下来更酷啦,相信我,如果你对自己不自信,那就请认真的看完这篇博客,我保证你可以深有体会,感同身受!!!
为什么,你可以这么肯定?因为我跟大家一样,对未知充满了疑惑,恐惧和不安,但我也知道,当我勇敢的去面对这些未知时,那些疑惑、恐惧和不安都会消失而答案则会渐渐清晰,正是因为有行动,所以才产生了答案!!!
你能看到这篇博客,正是因为你行动了;你的行动,促成了我们之间的缘分,而这缘分正是我俩共同进步的桥梁!!!


二、文件结构

c语言程序通常分为两个文件,一个文件是专门保存程序需要内容的声明——头文件(.h), 另一个文件是用于实现程序执行逻辑的 / 函数的定义(关于游戏相关函数的实现)——源文件(.c)

1、游戏所需文件

我们一般把整个工程(整个程序)分为三个文件来实现:
①、test.c源文件:用于测试整个工程的执行逻辑
②、game.c源文件:用于实现游戏相关函数的定义(定义嘛,就是我定义这个函数该怎么做它就怎么做)
③、game.h头文件:用于保存游戏所需内容的声明——头文件的包含啊、符号的定义啊、函数的声明啊等等知道了这些我们正式进入主题,开始我们扫雷游戏的实现


三、游戏逻辑分析

1、实际上扫雷游戏的本质是这样的:我们扫雷的时候,扫一个 横纵 坐标的位置时,如果不是雷,它就会显示它周围八个坐标一共有几个雷(对于9×9的棋盘来说就是显示它周围一圈有几个雷),如果是雷,那就被炸死了,游戏结束!!!
2、我们来看游戏的整个框架:它是一个9×9的正方形棋盘,里面有10颗地雷。根据上面解析我们得知,请看下图:
在这里插入图片描述
3、🐍再看下面我们对假设的信息进行分析:
我们肯定是 从无到有 开始实现的,我们不仅需要排查雷,还需要布置10颗雷
那把雷布置到哪里去呢?我们假设布置到一个9×9的二维数组里比较合适:

在这里插入图片描述
上面已经把雷布置进去了,布置完雷后,我们该排查雷了,那怎么排查雷呢?
我们刚刚说了:排查雷的时候,排查一个 横纵 坐标后,如果这个坐标处不是雷——那就显示这个坐标周围一圈八个坐标有几个雷,如果是雷就被炸死。

在这里插入图片描述
这就是扫雷游戏的大体框架,相信同志们多看几遍很容易就能理解了吧!但以上只是对它的片面分析,它的内核我们还没有分析,如果放到这里分析的话,我怕同志们会懵,所以我们决定先把游戏相关代码逻辑实现一下,然后边实现代码边分析关于扫雷内核的逻辑!!!


四、游戏逻辑实现+分析

🐍下面我们就开始分文件讲解关于扫雷游戏逻辑的代码实现+分析喽!!!

4.1 test.c文件——游戏整体框架的实现

//我们要在.c文件里包含一下.h文件,这样就可以使用.h里的所有内容了
//我们自己的头文件要用 “ ” ,库里的头文件是用< >
#include "game.h"

void menu(void) //void代表这个函数不需要有返回值、不需要有参数,光打印一个菜单就可以
{
        printf("***********************\n");
		printf("*****    1.play   *****\n");//1.玩游戏
		printf("*****    0.exit   *****\n");//0.退出游戏
		printf("***********************\n");
}

int main()
{
	int input = 0;//初始化变量,局部变量不初始化,在内存的栈区是随机值
 	//我们选择do while循环来实现扫雷游戏代码逻辑的实现,上来直接就可以玩一把,因为这个循环是先执行在再
	//判断嘛!!!
	do
	{
	
		menu();//打印一个提示菜单
		printf("请选择:");//提示玩家的信息(是玩游戏,还是退出游戏)
		scanf("%d", &input);//输入值
		
		switch(input)//switch开关接收input输入的值,切记括号里只能是整型表达式
		{
		case 1://选择 1 进入游戏内部执行游戏相关函数的逻辑实现
			printf("扫雷游戏\n");//测试代码逻辑是否正常, 一定要边写边测, 不然代码多了错哪你都不知道
			break;
		case 0://选择 0 退出游戏
			printf("退出游戏\n");
			break;
		default://选择其它的数字,将重新选择
			printf("选择错误,请重新选择\n");
			break;
		}
		
	} while(input);//根据接收的input值进行选择:
	               //1为真,循环上去,继续玩
                   //0为假,直接跳出循环,退出游戏    
                   //其它数字都是非0,也是真,循环上去执行对应语句
	return 0;
}
  • 执行程序观看当前逻辑:
    在这里插入图片描述

4.2 如何放雷、排查雷以及对二维数组的分析(分析)

4.2.1 如何放雷、排查雷

  • 我们假设我们有一个二维数组,里面放的全是0,当我们要放进去雷的时候,就把0改成1,放一颗雷就改一次0,直到把10颗雷全部放进去的时候,我们的数组里,就有了10个1了(就是10个雷)
  • 我们说当1代表雷的时候,那就有另一个地方出问题了,如果说我们把雷当成是1的话,那当我们排查雷的时候,它会显示我们排查雷的那个坐标处周围一圈有多少个雷的数字,这里就产生歧义了,假如我们排查的那个坐标周围有1颗雷,那显示的 那个1 到底是我们排查出来的信息1呢?还是雷代表的1呢?我们不知道。
    在这里插入图片描述
  • 关于这一点我们想了一个办法:就是我们可以创建两个二维数组(也就是两个棋盘),一个二维数组用来布置雷,一个二维数组用来排查雷,然后我们可以这样做:从布置雷的数组里获得排查出来雷的信息,然后把这个信息放到排查雷的数组里(这里有点绕,多读几遍就可以理解啦!),这样不就解决了关于“1”的歧义了吗!,当然给玩家看的棋盘也是排查雷的那个棋盘,打印;布置雷的那个棋盘可不能给玩家看,不打印。不然就没意义了!请看下图:在这里插入图片描述

4.2.2 对二维数组的分析

  • 假设我们为了让棋盘神秘一点,把排查雷的那个二维数组里面全部放成‘ * ’然后当排查一次雷的时候就显示排查出来周围雷的信息(也就是数字嘛),但是我们已经把数组里全部放成char类型的数据了,如果显示的是int类型的数字的话,那不就又产生歧义了嘛,所以我们决定把排查出来的信息(也就是数字),用字符数字来表示,再如果,排查雷的数组是char类型的数组的话,那布置雷的数组也用char类型的数组创建的话,那不就可以更方便的组合它们俩之间的关系了嘛,是雷,我用字符‘1’来表示,非雷我用‘0’来表示。在这里插入图片描述
  • 我们接着分析,当我们一切正常的时候,当我们排查边上一圈坐标的时候问题就出现了。请看下图(先看文字,再看对应的图):在这里插入图片描述
  • 所以我们最后决定创建两个11×11的二维数组来实现我们的扫雷游戏(访问11×11的空间,实际上我们只打印和用9×9的空间),一个布置雷的二维数组、一个排查雷的二维数组。

4.3 game.c文件——扫雷游戏的定义部分(就是用函数实现的过程)

🐍然后我们来单独实现game()函数的执行逻辑以及代码实现!!!咱把别的代码都注释了,只留了game函数,看它咋实现的就可以了,相关符号定义、头文件的包含、函数的声明都放在下面game.h文件里了

所需步骤:
1、创建两个11x11的二维数组——布置雷、排查雷
2、初始化数组
3、打印排查雷的数组
4、布置雷
5、排查雷

//我们要在.c文件里包含一下.h文件,这样就可以使用.h里的所有内容了
//我们自己的头文件要用 “ ” ,库里的头文件是用< >
#include "game.h"
//初始化棋盘
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;//这样就把二维数组里的所有元素初始化成我们想要的内容了
		}
	}
}

//打印棋盘
void DisplayBoard(char board[ROWS][COLS],int row,int col)
{
	int i = 0;
	int j = 0;
	//我们希望棋盘好看易观察一些,所以先把行号和列号打印出来
	//打印列号
	for(j = 0;j<=col;j++)
	{
		printf("%d ",i);
	}
	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 SetMine(char mine[ROWS][COLS],int row,int col)
{
	//首先定义雷的个数 ——	COUNT
	//再生成随机坐标 —— x y
	//这个库函数就是生成随机数的意思 % row就可以生成0-8之间的随机数,+1可以生成1-9之间的随机数
	//这里注意要使用rand 必须 配合srand一起使用,而且srand函数参数部分还需要调用time函数来进行操作
	//因为随机数的生成要想让它一直变,就得配合上我们的时间戳,因为时间是一直在变化的,而time函数
	//的返回值刚好是时间戳,所以可以一气呵成,完成我们想要的效果。
	//这里注意srand只需要在main函数里调用一次就可以了,如果调用多次的话,时间重置的太快,会导致
	//随机坐标重复。
	int count = COUNT;//我们定义的雷的个数
	while(count)
	{
	    int x = rand() % row + 1;
		int y = rand() % col + 1;
		//如果mine数组里元素是'0',那就给它放一个'1'(雷)进去
		//利用循环就把10颗雷全部放进去了
		if(mine[x][y]=='0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
	
}

//get_num函数
//因为这个函数只是给FindMine用的所以可以给它加上一个
static int get_num(char mine[ROWS][COLS],int x,int y)
{
	//mine不是字符数组?怎么返回值能用int,接下来看操作,你就知道了,这里面有我自己的一些门道
	//下面返回的是我们排查的x、y坐标处周围8个坐标的位置
	//因为我们mine数组里放的不是'0'非雷,就是'1'雷
	//当我们把8个不同‘0’、‘1’组成的字符加起来 减去 8*‘0’不就等于我们要排查出来的那个整型数字了吗
	//举个例子:
	//假如x、y坐标处周围有3个雷那就是3个‘1’嘛,那还剩5个‘0’,那3‘1’+5‘0’-8‘0’不就等于3吗。
	//是不是,3个‘1’是3个‘49’ 比 3个‘0’多 3个1,那这三个1算出来不就是数字3吗,多看两眼,应该可以理解
	return (
	mine[x-1][y]+
	mine[x+1][y]+
	mine[x][y-1]+
	mine[x][y+1]+
	mine[x-1][y-1]+
	mine[x+1][y+1]+
	mine[x-1][y+1]+
	mine[x+1][y-1] - 8 * '0');
}

//排查雷
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-COUNT))//当win<71的时候我们就赢了
	{
		
	printf("请输入要排查的坐标:")//提示信息
	scanf("%d %d",&x,&y);//输入坐标
	//然后判断坐标合法性
		if(x>=1 && x<=row && y>=1 && y<=col)
		{
			//如果排查的坐标已经排查过了,那我们还得提示一下该坐标已经被排查过了
			if(show[x][y]!= '*')
			{
				printf("该坐标已被排查,请重新排查\n");
				continue;
			}
			if(mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine,ROW,COL);//被炸死之后,给玩家看一下棋盘,雷都放哪了
				break;//跳出循环即可
			}
			else//这种情况没被炸死,就统计周围一圈雷的个数嘛
			{
				//还记得我们要从mine数组里x 、y坐标处统计周围8个坐标的信息放到show数组里吧
				//这里我们涉及到一个小知识,我来给大家讲解一下
				//关于ASCII的知识科普:
				//'0'的ASCII值是48,'1'的ASCII值是49,那数字1 + '0'是不是就等于'1':因为1+48==49嘛
				//所以我们可以利用这个规律来完成把数字转化为字符数字的动作
			
				//这个get_num函数是专门给排查雷的数组用的,所以不用在头文件里声明,那我们写一下它吧
				int n = get_num(mine,x,y)//得到mine数组xy坐标处周围8个坐标的信息,
				show[x][y] = n+'0';//我们在得到8个坐标的信息之后只要加上'0'就可以得出排查出来的信息了
				DisplayBoard(show,ROW,COL);//统计完信息,再展示给玩家就可以了
				win++;
			}
		}
		else
		{
			printf("坐标非法,请重新输入\n");
		}
 	}
 	//到最后我们的win++如果==71那我们就赢啦
 if(win == (row * col - COUNT))
 {
 	printf("恭喜你,排雷成功!!!\n");
 }
}

4.3.1 test.c文件里关于game()函数内相关函数的调用

//我们要在.c文件里包含一下.h文件,这样就可以使用.h里的所有内容了
//我们自己的头文件要用 “ ” ,库里的头文件是用< >
#include "game.h"
void game(void)
{
	//根据以上代码分析,我们得知我们需要两个二维数组:布置雷、排查雷
	//为了防止排查坐标时数组越界,我们把最初9x9的棋盘,扩大到了11x11的棋盘
	//用我们只用9x9的空间、但访问我们访问的是11x11的空间
	char mine[ROWS][COLS] = {0};//布置雷的数组
	char show[ROWS][COLS] = {0};//排查雷的数组
	
	//初始化棋盘
	//我们上面分析的时候希望show数组最开始都是‘*’,而mine数组最开始都是‘0’
	InitBoard(mine,ROWS,COLS,'0');
	InitBoard(show,ROWS,COLS,'*');
	
	//接着我们想看看棋盘了,那就打印一下看看现在棋盘里的状态的吧
	//这里我们要打印的是9x9的棋盘,所以传过去的参数是ROW、COL,但我们还是在11x11的棋盘里操作它们
	//DisplayBoard(mine,ROW,COL);布置雷的数组是不打印的,这里我们便于调试
	DisplayBoard(show,ROW,COL);
	
   //布置雷
   SetMine(mine,ROW,COL);
   //DisplayBoard(mine,ROW,COL);布置完雷我们打印一下看看,运行的时候可不打印,这里我们就调试看一下

  //排查雷
  //我们上面分析的时候说:排查雷是从mine数组里排查,然后排查出来的信息放到show数组里
  //所以参数部分需要有两个数组——mine数组、show数组
  //我们依然是在11x11的棋盘上访问,然后操作9x9的棋盘,所以依然传参ROW、COL
  FindMine(mine,show,ROW,COL);
}
int main()
{
    //我们在这里调用srand函数,当然完整版下面还要很多语句,我们这里只是演示rand函数需要调用srand函数
    //srand函数的参数部分是无符号整型,所以我们把time函数的返回值强制类型转换成无符号整型
    //而time函数的参数部分是NULL(空指针,也就是0)
    //当然别忘了引用头文件
    //srand/rand:#include<stdlib.h>
    //time:#include<time.h>
    srand((unsigned int)time(NULL));
	game( );//假设我们的main函数只调用了一个game函数。等讲解完直接看完整版!!!
}

4.4 game.h文件——包含函数的声明、符号的定义、头文件的包含等


//因为我们要打印9x9的棋盘所以也要定义一个9行9列的符号
#define COUNT 10

#define ROW 9//行
#define COL 9//列

//11行11列的符号定义
#define ROWS ROW+2//行
#define COLS COL+2//列

//头文件的包含
#include<stdio.h>
#include<stdlib.h>
#include<time.h>

//函数的声明
//初始化棋盘
void InitBoard(char board[ROWS][COLS],int rows,int cols,set);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS],int row,int col);
//布置雷
void SetMine(char mine[ROWS][COLS],int row,int col);
//排查雷
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);

五、总结

🐍🐍🐍上面我们把扫雷游戏深度解析了一下,乍一看可能有些乱,因为我们不熟悉它,跟它没有建立深度联系,所以可能有点蒙,不过没关系!下面我给大家发整个工程比较清晰一点,如果哪里不懂,你们可以对照上面的分析和深度解析,进行理解,多花些时间来弄一弄,肯定没问题的,因为上天不会辜负有心人的!

六、扫雷游戏完整版

6.1 test.c

#include "game.h"

void menu(void)
{
	printf("***************************\n");
	printf("****       1.play      ****\n");
	printf("****       0.exit      ****\n");
	printf("***************************\n");
}

void game(void)
{
	char mine[ROWS][COLS] = { 0 };//布置好的雷的信息
	char show[ROWS][COLS] = { 0 };//排查出的雷的信息

	//初始化棋盘
	//功能一样,参数不一样那就可以这么写
	Initboard(mine, ROWS, COLS, '0');
	Initboard(show, ROWS, COLS, '*');

	//打印棋盘
	//Displayboard(mine, ROW, COL);//这个不展示的
	Displayboard(show, ROW, COL);

	//布置雷
	set_mine(mine, ROW, COL);
	//Displayboard(mine, ROW, COL);

	//排查雷
	find_mine(mine, show, 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;
}

//待拓展
//1、能够展开一片的操作
//2、标记和取消雷
//3、显示剩余雷的个数

6.2 game.c

#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;
	int j = 0;

	//控制列号
	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");
	}
	printf("-----------扫雷------------- \n");
}

//布置雷
void set_mine(char mine[ROWS][COLS], int row, int col)
{
	int count = COUNT;

	while (count)
	{
		//生成随机下标
		int x = rand() % row + 1;
		int y = rand() % col + 1;

		//布置雷
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

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

//排查雷
void find_mine(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 - COUNT))//win小于71时,就没赢
	{
		printf("请输入要排查的坐标:");
		scanf("%d %d", &x, &y);
		//检查坐标合法性
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (show[x][y] != '*')//被排查过的坐标都会放到show数组里,如果show数组的坐标不是’*‘,那就是被排查过了
			{
				printf("该坐标已被排查,请重新排查\n");
				continue;
			}
			//被炸死
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了\n");
				Displayboard(mine, ROW, COL);
				break;
			}
			//显示排查信息
			else
			{
				int n = get_num(mine, x, y);
				show[x][y] = n + '0';//数字+字符0就可以==字符数字 because字符0的ASILL值是48 1、2、3依次是49、50、51
				Displayboard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("坐标非法,请重新输入\n");
		}
	}
	if (win == (row * col - COUNT))
	{
		printf("恭喜你,排雷成功!!!\n");
	}
}

6.3 game.h

#define ROW 9
#define COL 9

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

#define COUNT 10

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

//初始化盘
void Initboard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void Displayboard(char board[ROWS][COLS], int row, int col);
//布置雷
void set_mine(char mine[ROWS][COLS], int row, int col);
//排查雷
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

转载自CSDN-专业IT技术社区

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/Itsrealonepiece/article/details/146165773

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--