在这里插入图片描述

一、扫雷游戏整体设计思路

1.扫雷游戏功能说明

    使⽤控制台实现经典的扫雷游戏
(1)游戏可以通过菜单实现继续玩或者退出游戏
(2)扫雷的棋盘是9*9的格⼦
(3) 默认随机布置10个雷
(4)可以排查雷
    ◦ 如果位置不是雷,就显⽰周围有⼏个雷
    ◦ 如果位置是雷,就炸死游戏结束
    ◦ 把除10个雷之外的所有⾮雷都找出来,排雷成功,游戏结束

2.游戏的分析和设计

    扫雷的过程中,布置的雷和排查出的雷的信息都需要存储,所以我们需要⼀定的数据结构来存储这些信息
    因为我们需要在99的棋盘上布置雷的信息和排查雷,我们⾸先想到的就是创建⼀个99的数组来存放信息
在这里插入图片描述

    如图,创建一个9 * 9的棋盘,我们这样定义,在这样一个数组中,字符0表示不是雷,字符1表示雷,当我们排查2,4这个坐标时,如果不是雷,就算出它周围8个坐标内有几个雷,然后显示在2,4这个位置,这就是排查雷
    但是我们来看另一个坐标,如0,0这个坐标,我们发现要排查的有一部分区域超出边界了,会产生越界访问的情况,但是也不能专门写一个代码解决,因为除了这个0,0还有0,1、0,2·····等等的坐标会出现越界访问,如果每一个都写一个代码来解决就太麻烦了,怎么做呢?
    我们可以在创建棋盘的时候扩大一圈,变成11 * 11的棋盘,但是只显示内部9 * 9的棋盘,如:
在这里插入图片描述
    这样就有效避免了越界访问
    再继续分析,我们在棋盘上布置了雷,棋盘上雷的信息(1)和⾮雷的信息(0),假设我们排查了某⼀个位置后,这个坐标处不是雷,这个坐标的周围有1个雷,那我们需要将排查出的雷的数量信息记录存储,并打印出来,作为排雷的重要参考信息的。那这个雷的个数信息存放在哪⾥呢?如果存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能或产⽣混淆和打印上的困难,比如周围有1个雷,就要显示1,但是1原本是表示雷的,就导致了混淆
    解决办法就是:我们专⻔给⼀个棋盘(对应⼀个数组hide)存放布置好的雷的信息,再给另外⼀个棋盘(对应另外⼀个数组show)存放排查出的雷的信息。这样就互不⼲扰了,把雷布置到hide数组,在hide数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给后期排查参考
    同时为了保持神秘,show数组开始时初始化为字符 ‘*’,为了保持两个数组的类型⼀致,可以使⽤同⼀套函数处理,hide数组最开始也初始化为字符’0’,布置雷改成’1’,如图:
    hide数组:
在这里插入图片描述
    show数组:
在这里插入图片描述

3.文件结构设计:

    我们之前学习了多⽂件的形式对函数的声明和定义,这⾥我们实践⼀下,我们设计三个⽂件:

test.c //⽂件中写游戏的测试逻辑 
game.c //⽂件中写游戏中函数的实现等
game.h //⽂件中写游戏需要的数据类型和函数声明等

二 、主函数大致模型

    主函数中的大致模型就和之前讲过的猜数字游戏相似,这里就不再过多赘述,直接上代码,后面的游戏制作才是重点

//文件test.c
void menu()
{
	printf("********************\n");
	printf("**** 1.开始游戏 ****\n");
	printf("**** 0.退出游戏 ****\n");
	printf("********************\n\n");
	printf("提示:请输入1或0\n");

}

void game()
{

}

int main()
{
	srand((unsigned int)time(NULL));
	int input = 0;
	do
	{
		menu();
		printf("\n请输入:");
		scanf("%d", &input);

		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("游戏退出成功\n");
			break;
		default:
			printf("\n输入错误,请重新输入!\n\n");
			break;
		}
	} while (input);

	return 0;
}

三、创建棋盘

    经过前面的分析,我们知道了需要11 * 11的数组,并且要创建两个,一个作为表面显示的棋盘,用于排雷,一个不让用户看见,用来布置雷,并且类型为char,代码如下:

void game()
{
      //隐藏起来用于布置雷:
      char hide[11][11] = {0};
      //展示出来用于排查雷:
      char show[11][11] = {0};
}

四、初始化棋盘

    经过最上面的思路分析,我们最好将hide数组全部初始化为字符0,表示全部都不是雷,然后后续再对其加入雷(也就是字符1),将show数组全部初始化为字符*,增加神秘感
    由于game函数中可能会有很多的代码,所以为了简明大方,我们将初始化棋盘以及后面的步骤都封装为函数,然后再game.h中进行声明,在game.c中实现
    我们开始分析:

  1. 初始化棋盘的函数名:我推荐叫Initboard,init是initialise的缩写,含义是初始化,当然这个不用死记,函数名完全可以按照自己的想法命名,我这里只是做一个推荐
  2. 函数参数:我们要初始化棋盘,也就是初始化数组,自然要把数组传过去,由于要初始化,就要遍历数组,我们就要把行数和列数传过去,最后就是要把函数初始化为什么字符,我们可以把这个字符也传给函数,所以函数参数有:数组,行,列,初始化用到的字符
  3. 优化:当我们把代码写出来后,我们会发现我们写了很多11,后面也会有很多11和9,它们代表了棋盘的大小,试想一下,如果后面我觉得9 * 9的棋盘玩着不过瘾,想要把棋盘变大,就只能去把所有的11以及9改掉,太麻烦了,所以为了方便,我们这里可以把代表棋盘大小的数字用define定义一下,比如现在是显示9*9的棋盘,为了防止越界访问,实际上棋盘大小为11 * 11,后者比前者多2,因此我们可以用define来定义一个常数了,如:
//由于这是声明,我们写在game.h中

//显示的棋盘大小
#define Row 9
#define Col 9

//真正的棋盘大小
#define Rows Row+2
#define Cols Col+2

      以后我们需要用到棋盘大小的地方就用定义的这些符号就行,当然,这些符号也是可以自行定义的,这里只是我的写法,然后如果以后想改棋盘大小就只需要把9改掉,就特别方便(记得把上面创建数组)

  1. 函数的声明:函数的声明要写在game.h中,由于很简单,这里只讲一次,后面讲到的函数就不会再讲到,自行推导就行,声明只需要写出函数的返回值,函数名,括号,形参,最后还有一个分号,如:
//声明初始化棋盘函数
void initboard(char board[Rows][Cols], int rows, int cols, char x);
  1. 函数的实现:函数的实现我们就放在game.c中,思路就是通过函数传过来的行列进行遍历数组,把每一个元素初始化为传过来的字符,代码如下:
//实现初始化棋盘函数:
void initboard(char board[Rows][Cols], int rows, int cols, char x)
{
	int i = 0;
	int j = 0;

	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			board[i][j] = x;
		}
	}
}
  1. 函数的使用:这个很简单,按照我们的形参,把实参写出来就行,如:
//初始化棋盘:
initboard(show, Rows, Cols, '0');
initboard(hide, Rows, Cols, '*');

五、打印棋盘

经过上面的初始化,我们现在还看不到棋盘,我们就先写一个打印棋盘的函数,将它们打印出来,看看我们初始化成功没有,步骤也和刚刚一样
我们需要这个函数帮我们把棋盘中间9 * 9的大小打印出来,并且有一个标识,设计步骤如下:

  1. 打印棋盘函数名:推荐printboard,直观简易,一眼看出来是打印棋盘,也可自行定义名称
  2. 函数参数:首先一定是要把数组传过去,由于数组的大小是真正棋盘的大小,所以后面跟的行和列是Rows和Cols,然后由于要遍历数组,所以要把行和列传过去,但是注意一点,我们打印数组的真正大小是11 * 11,但是真正显示的是中间9 * 9的部分,传参要传Row和Col而不是Rows和Cols,所以参数就是:数组,行,列
  3. 函数的声明:同初始化棋盘函数,此处不在赘述,代码如下:
//声明打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col);
  1. 函数的实现:
    (1)在平常一样打印数组时,我们需要注意,每打印完一行就要进行一次换行
    (2)最好标出行号和列号,这样玩家在玩的时候才方便输入坐标进行排雷
    (3)我们可以在棋盘的上下做一点标识,让玩家更加醒目的看到棋盘
    (4)虽然棋盘也就是数组的真正大小是11 * 11,但是真正显示的是中间9 * 9的部分,传参要传Row和Col而不是Rows和Cols
    (5)代码:
//实现打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col)
{
	int i = 1;
	int j = 1;
	//打印棋盘标志,让棋盘更显眼
    printf("------ 扫雷 -------\n");
	//打印列号:
	for (i = 0; i <= col; i++)
	{
		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");
	}
	//打印棋盘标志,让棋盘更显眼
    printf("------ 扫雷 -------\n");
	//棋盘打印完之后进行换行
	printf("\n");
}
  1. 函数的使用:函数的一切都已经准备好了,这时我们只需要按照参数传参即可,我们游戏的要求只需要显示show数组,为了观察一下hide数组是否成功初始化可以使用一下
    ,随后进行注释操作,如:
//打印棋盘:
printboard(show, Row, Col);
//printboard(hide, Row, Col);

六、布置雷

经过第一部分的分析,我们可以知道,字符0代表不是雷,字符1表示雷,并且布置雷的对象是数组hide,之前我们把hide数组全部初始化为了字符0,这时我们只需要随机地在整个棋盘上布置雷,本质上就是随机地将一些字符0改成字符1,而随机数的产生要依靠rand函数和srand函数,以及时间戳,这些在猜数字游戏中有将到,如果忘了可以自行复习,后面直接贴上代码

  1. 布置雷函数的命名:我将其命名为setboard,也可自行命名
  2. 函数参数:首先要把hide数组传过去,随后我们会使用行和列,要将Row和Col传过去,然后就是要布置的雷的个数,由于雷的个数也是经常出现的常数,所以我们也可以用define定义一下,方便以后的更改,如:
//雷的个数:
#define Count 10
  1. 函数的声明:此处不做赘述,代码如下:
//声明布置雷函数:
void setboard(char board[Rows][Cols],int row,int col, int count);
  1. 函数的实现:
    (1)首先我们需要产生1~Row的随机数,也就是1到雷的个数的随机数,只需要rand()%Row+1或者rand()%Col+1即可,rand如何产生随机数此处不在赘述,之前讲的猜数字游戏有,这里提醒一点就是记得将头文件放在game.h中
    (2)随后就是布置雷,也就是将字符0改成字符1的过程,这个很简单,只需要用到一个if语句
    (3)代码:
//实现布置雷函数:
void setboard(char board[Rows][Cols],int row,int col, int count)
{
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (board[x][y] != '1')
		{
			board[x][y] = '1';
			count--;
		}
	}
}
  1. 函数的使用:只需要将hide函数和雷的个数(即Count)传给函数即可,如:
//布置雷:
setboard(hide, Count);
  1. 注意,在用的时候,要把布置雷这个步骤写在打印棋盘前,如图:
    在这里插入图片描述

七、排查雷

排查雷的重点就是,我们要看玩家输入的坐标位置是不是雷,也就是是不是字符1,如果是字符1,说明踩到雷了,游戏失败,如果是字符0,也就是不是雷,那么该处就显示为周围8个坐标的雷的个数,所以它既要涉及到show数组,又要涉及到hide数组,hide数组用来查看玩家输入的坐标是否是雷,show数组用于该坐标不是雷的时候,在该坐标处显示周围有几个雷

  1. 函数的命名:我将它命名为findboard,可以自行命名
  2. 函数的参数:由于既要涉及到show数组,又要涉及到hide数组,所以前两个参数就是两个数组,由于我们要判断用户输入的值是否合法,也就是是否在1~Row或者Col,所以我们需要将函数的Row和Col传过去,又因为游戏胜负和雷的个数有关,所以我们要将雷的个数传给函数
  3. 函数的声明:声明如下:
//声明排查雷函数
void findboard(char show[Rows][Cols], char hide[Rows][Cols], int row,int col,int count);

4.函数的实现:
(1)首先我们需要让用户输入一个坐标,并且要确保坐标正确,否则显示提示信息
(2)如果坐标正确,那么就判断坐标处是否是雷,如果是雷就直接炸死,游戏失败,当然,为了让玩家知道自己怎么失败的,我们可以将含有雷信息的数组hide打印出来
(3)如果不是雷,那么我们就想办法获取周围雷的个数,我们可以再创建一个函数getcount来解决,这个函数就帮我们统计周围雷的个数
(4)由于代表是否是雷的元素是字符0或者字符1,无法像整型一样直接相加,因为字符0代表的ascll码值是48,与整型的0不同,怎么解决呢?我们可以这样,让玩家周围的所有字符相加然后全部减去字符0,如果一个位置是字符1,减去字符0就成了真正的1,字符0减去字符0就成了真正的0,我们可以让坐标周围的八个坐标的字符相加,再减去8个字符0,就得到了雷的个数
(5)坐标周围雷的个数也最多就8个,也就是最多返回一个8的整数,我们怎么把它变成字符,存放在show数组的相应位置呢?此时我们又可以加上一个’0’,这时雷的个数就变成了字符
(6)如果玩家排雷成功要如何结束游戏呢?我们可以使用一个新的变量win,每次用户排雷就对它进行自增1的操作,直到win= =row * col-count,也就是循环条件是win<row*col-count,如果win= =row * col-count也就说明了玩家排雷成功,这时就可以输出扫雷成功的信息
(7)getcount函数实现:将其放在findboard函数前定义就行,不用在game.h进行声明,代码如下:

//如果坐标不是雷,获取坐标周围雷的个数
int getcount(char hide[Rows][Cols], int x, int y)
{
	return hide[x][y-1] + hide[x][y+1] + 
		   hide[x - 1][y] + hide[x-1][y-1] +hide[x-1][y+1]+
		   hide[x + 1][y]+ hide[x + 1][y-1] + hide[x + 1][y+1]-8*'0';
}

这是我觉得比较简单的思路,当然还有另一种思路,如:

//如果不是雷,获取坐标周围雷的个数
char getcount(char hide[Rows][Cols],int x,int y)
{
	char num = 0;
	int i = 0;
	int j = 0;
	for (i = -1; i <= 1; i++)
	{
		for (j = -1; j <= 1; j++)
		{
			num += hide[x + i][y + j];
		}
	}
	num = num - 8 * '0';
	return num;
}

这样也可以直接返回字符,思路也不难,就是比较难写
(8)findboard函数实现:此处的代码使用的是第一个getcount的方法,代码如下:

//实现排查雷函数:
void findboard(char show[Rows][Cols], char hide[Rows][Cols],int row,int col, int count)
{
	int x = 0;
	int y = 0;
	int win = 0;
	printf("请输入空格或英文逗号隔开坐标!\n\n");
	while (win<Row*Col-Count)
	{
		printf("请输入要排查的坐标:");
		//这里需要用到之前学过的%*c,就是
		//scanf的赋值忽略符*
		//使用过后就可以输入空格或者英文逗号隔开坐标,但是不能用中文
		//这里中文和英文逗号不能都使用
		//所以我们最好在开始时给予一些提示
		scanf("%d%*c%d", &x, &y);
		printf("\n");
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (hide[x][y] == '1')
			{
				printf("很遗憾,你踩到雷了,游戏失败!\n\n");
				printf("显示的0代表不是雷,1表示雷\n\n");
				printboard(hide, Row, Col);
				break;
			}
			else
			{
				win++;
				int ret = getcount(hide, x, y);
				show[x][y] = ret + '0';
				printboard(show, Row, Col);
			}
		}
		else
		{
			printf("输入有误,请重新输入!\n");
		}
	}
	if (win == Row * Col - Count)
	{
		printf("恭喜你,扫雷成功!\n\n");
		printf("显示的0代表不是雷,1表示雷\n\n");
		printboard(hide, Row, Col);
	}
}

八、源码

整个扫雷游戏的思路已经讲解完毕,接下来附上每个文件的源码,中间为了美观,可能会加了不少\n进行换行,但是不影响阅读代码,源码如下:
game.h:

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

//显示的棋盘大小
#define Row 9
#define Col 9

//实际的棋盘大小
#define Rows Row+2
#define Cols Col+2

//雷的个数:
#define Count 10

//声明初始化棋盘函数:
void initboard(char board[Rows][Cols], int rows, int cols, char x);

//声明打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col);

//声明布置雷函数:
void setboard(char board[Rows][Cols], int count);

//声明排查雷函数
void findboard(char show[Rows][Cols], char hide[Rows][Cols], int row,int col,int count);

game.c:

//game.c
#include "game.h"

//实现初始化棋盘函数:
void initboard(char board[Rows][Cols], int rows, int cols, char x)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			board[i][j] = x;
		}
	}
}

//实现打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col)
{
	int i = 1;
	int j = 1;
	//打印棋盘标志,让棋盘更显眼
	printf("------ 扫雷 -------\n");
	//打印列号:
	for (i = 0; i <= col; i++)
	{
		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");
	}
	//打印棋盘标志,让棋盘更显眼
	printf("------ 扫雷 -------\n");
	//棋盘打印完之后进行换行
	printf("\n");
}

//实现布置雷函数:
void setboard(char board[Rows][Cols], int count)
{
	while (count)
	{
		int x = rand() % Row + 1;
		int y = rand() % Col + 1;
		if (board[x][y] != '1')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

//如果坐标不是雷,获取坐标周围雷的个数
int getcount(char hide[Rows][Cols], int x, int y)
{
	return (hide[x][y-1] + hide[x][y+1] + 
		   hide[x - 1][y] + hide[x-1][y-1] +hide[x-1][y+1]+
		   hide[x + 1][y]+ hide[x + 1][y-1] + hide[x + 1][y+1] - 8*'0');
}

//实现排查雷函数:
void findboard(char show[Rows][Cols], char hide[Rows][Cols],int row,int col, int count)
{
	int x = 0;
	int y = 0;
	int win = 0;
	printf("请输入空格或英文逗号隔开坐标!\n\n");
	while (win<Row*Col-Count)
	{
		printf("请输入要排查的坐标:");
		//这里需要用到之前学过的%*c,就是
		//scanf的赋值忽略符*
		//使用过后就可以输入空格或者英文逗号隔开坐标,但是不能用中文
		//这里中文和英文逗号不能都使用
		//所以我们最好在开始时给予一些提示
		scanf("%d%*c%d", &x, &y);
		printf("\n");
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (hide[x][y] == '1')
			{
				printf("很遗憾,你踩到雷了,游戏失败!\n\n");
				printf("显示的0代表不是雷,1表示雷\n\n");
				printboard(hide, Row, Col);
				break;
			}
			else
			{
				win++;
				int ret = getcount(hide, x, y);
				show[x][y] = ret + '0';
				printboard(show, Row, Col);
			}
		}
		else
		{
			printf("输入有误,请重新输入!\n");
		}
	}
	if (win == Row * Col - Count)
	{
		printf("恭喜你,扫雷成功!\n\n");
		printf("显示的0代表不是雷,1表示雷\n\n");
		printboard(hide, Row, Col);
	}
}

test.c:

#include "game.h"

void menu()
{
	printf("********************\n");
	printf("**** 1.开始游戏 ****\n");
	printf("**** 0.退出游戏 ****\n");
	printf("********************\n\n");
	printf("提示:请输入1或0\n");
}


void game()
{
	//不显示出来,用于布置雷
	char hide[Rows][Cols];

	//显示出来,用于排查雷
	char show[Rows][Cols];

	//初始化棋盘:
	initboard(hide, Rows, Cols, '0');
	initboard(show, Rows, Cols, '*');

	//布置雷:
	setboard(hide, Count);

	//打印棋盘:
	printboard(show, Row, Col);
	//printboard(hide, Row, Col);

	//排查雷:
	findboard(show, hide,Row,Col, Count);



}


int main()
{
	srand((unsigned int)time(NULL));
	int input = 0;
	do
	{
		menu();
		printf("\n请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("游戏退出成功\n");
			break;
		default:
			printf("选择错误,请重新输入!\n");
			break;
		}
	} while (input);
	return 0;
}

九、如何把游戏分享给小伙伴

    相信你已经尝试玩了这个扫雷游戏,是不是特别想分享给朋友室友玩一玩呢?跟着我来学习吧
    首先打开VS,将上方的debug模式改为release版本,随后将代码运行一次,如图:
在这里插入图片描述

    然后找到存放该文件的文件夹,找到x64文件夹,然后找到release文件夹,打开后找到扩展名为exe的可执行文件,直接将其发送给朋友就可以了,是不是非常简单呢!

十、扫雷进阶的一些思路

• 是否可以选择游戏难度
    ◦ 简单 9 * 9 棋盘,10个雷
    ◦ 中等 16 * 16棋盘,40个雷
    ◦ 困难 30 * 16棋盘,99个雷
• 如果排查位置不是雷,周围也没有雷,可以展开周围的⼀⽚
• 是否可以标记雷
• 是否可以加上排雷的时间显⽰

如果小伙伴们有兴趣,后续还可以更新这些进阶扫雷需求,当然,如果排查位置不是雷,周围也没有雷,可以展开周围的⼀⽚,这个需要用到函数递归的知识点,后面讲到递归直接讲了,其它进阶需求出不出可以看小伙伴们的意愿,想看哪个直接评论区留言哦~

这篇关于扫雷的博客到此结束了,如果对你有帮助的话记得一键三连哦~

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐