C语言实现扫雷游戏

前言

今天来讲讲如何利用c语言实现扫雷这个游戏程序,这是学c的人一定会遇到的项目实战。

扫雷相信大家都玩过,毕竟是windows上最经典的游戏之一。扫雷的源码功能实现并不困难,困难的是如何将其逻辑转化为计算机语言的形式。

游戏不同于那些比较简单的程序,首先需要调用很多的函数以及变量,这样就需要单独设置一个源文件存储游戏函数;此外,也需要一个主函数进行菜单以及基础的逻辑选择——哪个游戏没菜单选项嘛,这样又需要一个源文件;要想进行游戏项目编程,肯定需要很多的宏定义以及库函数头文件声明,这样的话又需要创建一个头文件。

综上,我们需要创建一个头文件两个源文件:

1.game.h(头文件)————用以存储宏定义、函数声明以及头文件声明

2.game.c(源文件)————用以存储游戏所需要调用的函数

3.test.c(源文件)—————用以存储主函数以及菜单选项

接下来分别讲解三个文件的内容,首先是test.c源文件~

test.c

首先,创建一个menu函数打印菜单:

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

选择1.play即游玩扫雷程序

选择0.over即退出程序

我们希望它一直循环,想继续玩不需要再ctrl+F5再执行一次程序,不想玩了直接退出,这样就用到do——while循环里嵌套switch的方法来做菜单选择逻辑:

	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("游戏结束....\n");
			break;
		default:
			printf("选择错误,请重新选择!\n");
			break;
		}
	} while (input);

判断是否循环的主体为变量input,switch的判断主体也是变量input。循环一次scanf一次input的值,当input==0的时候刚好switch退出,同时do——while循环也终止。

当选择——也就是input==1时,进入game()函数,开始游戏程序。

注意在do——while前有行代码为:

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

因为后期埋雷的时候希望雷每次都是随机分布的,所以需要用到time.h库函数来创造随机,这部分就不过多详解了。time.h将在后文game.h头文件讲解中包含到。

game函数主要功能有:

void game()
{
	int win = 0;
	char mine[ROWS][COLS];//基础棋盘
	char show[ROWS][COLS];//打印棋盘

	//初始化基础棋盘和打印棋盘
	gameinitial(mine, ROWS, COLS, '0');
	gameinitial(show, ROWS, COLS, '*');

	//打印棋盘的函数
	printchess(show, ROW, COL,win);

	//埋雷函数
	arrangemine(mine, ROW, COL);

	//排雷函数
	finemine(mine, show, ROW, COL);
}

为了方便游玩扫雷,我们可以创建一个变量win用来记录所排查的坐标个数。

对于“10雷”的扫雷棋盘,共有71格是无雷的,10格是有雷的,当我们排完了空格的71个坐标时,游戏也即胜利了,也就是说win除了能显示还有多少个空格需要我们排,也表明了我们还需要排多少格才能胜利。

为了方便区别,所以需要创建两个棋盘,一个mine[ ][ ]用来埋雷,一个show[ ][ ]用来打印棋盘,也就是我们玩扫雷时显示数字的那个棋盘。

棋盘大小该设为多少呢?经典扫雷有10、40、99个雷分别对应基础,中级、专家三个难度,为了方便讲解本文以基础难度也就是10雷的难度进行设置。

也就是说,定义棋盘mine与show的长度宽度应该一样,但为了后续拓展我们需要宏定义两个常量名不一样的常量,也就是上面代码里的ROWS和COLS。

具体讲解留到头文件game.h再说。

总之,扫雷一共要用到四个主要函数,也就是:

gameinitial————初始化棋盘函数

printchess————打印棋盘函数

arrangemine———埋雷函数

finemine—————排雷函数

在这些函数里面还需嵌套一些额外函数才能实现这些函数的功能,在此之前,我们先讲讲头文件game.h的内容。

以下是test.c全部代码:

#include"game.h"
void menu()
{
	printf("******************************\n");
	printf("*****      1. play      ******\n");
	printf("*****      0. over      ******\n");
	printf("******************************\n");
}

void game()
{
	int win = 0;
	char mine[ROWS][COLS];//基础棋盘
	char show[ROWS][COLS];//打印棋盘

	//初始化基础棋盘和打印棋盘
	gameinitial(mine, ROWS, COLS, '0');
	gameinitial(show, ROWS, COLS, '*');

	//打印棋盘的函数
	printchess(show, ROW, COL,win);

	//埋雷函数
	arrangemine(mine, ROW, COL);

	//排雷函数
	finemine(mine, show, ROW, COL);
}


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

game.h

需注意,这里以vs2022为例。

头文件在解决方案管理器里当前项目的头文件里创建,当然,你也可以利用ctrl+shift+A快捷键来创建。

a31ba1e31d874b13b5df4c5e7289b88e.png

选择头文件(.h),再在下面名称那里改下名称,就可以使用创建的头文件了。

头文件的使用非常简单,只需要在需要的源文件声明一下这个头文件即可,也就是:

#include"game.h"

前面讲到,我们需要利用time.h这个库函数来让雷随机分布,同时也需要最基础的库函数stdio.h,为了简洁我们都可以在game.h里声明再在其他源文件里调用:

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

此外,我们也需要宏定义一些常量,比如棋盘的长宽度,雷的数量以及三个难度的棋盘格格数,这样在源文件里就可以直接调用无需再创建变量。

#define GAMEMINE 10 //基础难度雷数

#define ROW 9 //棋盘长度
#define COL 9 //棋盘宽度


#define ROWS ROW+2 //内部计算棋盘长度
#define COLS COL+2 //内部计算棋盘宽度

#define win1 81 //棋盘总格数(胜利格数)

宏定义常量以#define开头,空格+常量名+数值 为内容,也就是:

#define 常量名 数值

常量名的话尽量大写,这里就不改win1了哈哈哈。

在test.c中game()函数里的创建棋盘以及初始化棋盘函数调用中,并没有用到ROW与COL来设置,而是用ROWS以及COLS这是为了方便后续判断该格周边8格内雷数计算,也就是扫雷游戏里的数字格计算,因此需要设计多一圈用以方便计算,所以需要+2。

难理解吗?画个图试试:

aa761486a0794c908196a7146438f6d1.png

+2是为了上下左右都多一行,这样的话角落计算就会方便很多。具体请见后文讲解。

接下来声明下函数就行。

//初始化基础棋盘
void gameinitial(char mine[ROWS][COLS], int rows, int cols, char re);
//打印棋盘的函数
void printchess(char show[ROWS][COLS], int rows, int cols,int win);
//埋雷函数
void arrangemine(char mine[ROW][COL], int row, int col);
//排雷函数
void finemine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

game.h头文件全部代码如下:

#pragma once

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

#define GAMEMINE 10

#define ROW 9
#define COL 9


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

#define win1 81

//初始化基础棋盘
void gameinitial(char mine[ROWS][COLS], int rows, int cols, char re);
//打印棋盘的函数
void printchess(char show[ROWS][COLS], int rows, int cols,int win);
//埋雷函数
void arrangemine(char mine[ROWS][COLS], int row, int col);
//排雷函数
void finemine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

好的接下来就是整个程序里最重要的几行代码。

game.c

这里全部存放扫雷设置相关的函数,主要有四个主要函数以及三个次要函数:

*1.——gameinitial——初始化函数

*2. ——printchess——打印棋盘函数

*3.——arrangemine—埋雷函数

*4.——findmine———排雷函数

4.1——retmine———计算函数

4.2——install————扫荡函数

4.3——tab—————标记函数

三个次要函数都包含在排雷函数中,计算函数的功能是返回指定坐标周边格雷的个数,也就是实现扫雷游戏中数字格的功能;扫荡函数的功能是连片清除非数字格,也就是扫雷游戏中周边没雷则连片扫的功能;而标记函数,就是实现了扫雷游戏中标记推断出的雷格的功能。

接下来的讲解中,要注意实参和形参的区别,以及mine棋盘和show棋盘的区别,同时也要注意区分什么是未知格,雷格,数字格以及标记格等等。

*1. gameinitial  初始化函数

//初始化棋盘的函数
void gameinitial(char mine[ROWS][COLS], int rows, int cols, char re)
{
	int i = 0;
	for (i = 0; i < rows; i++)
	{
		int j = 0;
		for (j = 0; j < cols; j++)
		{
			mine[i][j] = re;
		}
	}
}

主要传了四个参数到gameinitial函数中,一个是棋盘,两个分别是长度和宽度,最后一个参数是字符类型。为了方便游玩,show棋盘中各个格子我们都设置为 “ * ”的形式;mine棋盘中,我们规定雷格表示为“ 1 ”,空格表示为“ 0 ”。所以,两个棋盘分别初始化,分别要传字符类型“ * ”与“ 0 ”过来该函数中。

至于长度宽度,show棋盘自然没啥影响,只需打印中间部分就行;而mine棋盘,为了计算方便,我们一样得初始化多一圈的那部分内容,既然对show没影响而mine又需要,干脆直接传ROWS和COLS过来。

初始化非常容易,两层for循环遍历棋盘每一格,分别把字符赋给每一格即可。

注意形参是mine,是“复制拷贝”的,这里可以改成其他名。

*2. printchess  打印棋盘函数

//打印棋盘的函数
void printchess(char show[ROWS][COLS], int row, int col,int win)
{
	int i = 0;
	int j = 0;
	printf(" ———扫 雷 游 戏——— \n");
	printf("距离胜利还有 %d 个坐标\n", win1-10-win);
	printf("输入0 0 进入或退出标记程序\n");
	for (j = 0; j <= col; j++)
	{
		printf("%d ", j);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%d ", i);
		j = 0;
		for (j = 1; j <= col; j++)
		{
			printf("%c ", show[i][j]);
		}
		printf("\n");
	}
}

传参传了四个,一个棋盘三个整型。

win的作用前面讲过,不再赘述。

printf("输入0 0 进入或退出标记程序\n")  这一行是标明如何进入标记程序,具体实现还在后面。因为是打印棋盘嘛,排雷函数中也要调用这个函数打印棋盘,所以需要多打印一行,要不然就得写个说明书附在程序文件里了。

为了定位要排查的坐标,需要格外打印一行数字,用以标明行数列数,也就是长度和宽度。所以在打印棋盘之前需要for循环先打印行数出来,然后换行再打印其余部分。

打印列数需要在打印棋盘的俩层for循环中打印,放在第一层for循环第一行,如上代码。打印棋盘原理与初始化棋盘两层循环原理无异,不再赘述。

*3. arrangemine  埋雷函数

//埋雷函数
void arrangemine(char mine[ROWS][COLS], int row, int col)
{
	int mid = GAMEMINE;
	while (mid)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			mid--;
		}
	}
}

传参传了三个,mine棋盘和长宽。

首先定义一个中间变量mid,将基础难度的雷数“10”赋值给mid,先前在game.h中已宏定义过,所以直接调用赋值即可。

因为需要一个一个雷来埋,而且需要埋十个,故利用while循环来埋。

循环判断条件为mid,每埋雷成功一次mid--

为什么是“每埋雷成功”?

因为mine棋盘有11行11列!我们需要将雷准确埋到9 x 9的正方形里,所以多的那一圈不能埋不能埋不能埋!

此外,雷需要“随机地”埋,而且需要满足上述的条件。

首先,在test.c中我们已预先用到了srand((unsigned int)time(NULL)),在这里我们直接使用rand()就行。怎样使得埋的雷落在9 x 9的方格里呢?

用%。当得到的随机数<=9时,此时埋的雷恰好满足上限条件——埋不到mine[  ][ 10 ]和mine[ 10 ][ ]这行列里,因为11%9==2;要想再满足下限条件——埋不到mine[ ][ 0 ]和mine[ 0 ][  ]这行列里,就需要当随机数=9时结果不为0,这样就需要+1。

例如:

x = 9%9 +1(注意%优先级>加的优先级)==1,这样就埋不到mine[ ][ 0 ]和mine[ 0 ][  ]。

x=11%9 +1==3,满足条件。

x=76%9+1==4+1==5,也满足条件。

x=0%9+1==0+1==1,同样满足条件。

具体定位到一个二维坐标(x,y),就需要创建两个式子计算,就是上面代码的:

		int x = rand() % row + 1;
		int y = rand() % col + 1;

无论长度宽度传参传多少,都不会落在大一圈的范围里。

如果格子内已经埋了雷,而随机坐标刚好重复埋雷在该雷格的话,可以借助if判断该坐标是否是雷格。不是雷格则埋下雷,同时mid--减少剩余要埋的雷数。

至此棋盘的设置已经完成,接下来是玩家操作的函数的编写。

*4. findmine  排雷函数

接下来从三个次函数分别讲起,再讲回排雷函数的主体。

4.1. retmine  计算函数

int retmine(char mine[ROWS][COLS], int x, int y)
{
	int mid = (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');
	return mid;
}

传参传的是mine棋盘,因为要从雷棋盘计算再返回到show棋盘上,同时也要传x,y坐标。

计算函数的作用是返回计算输入坐标周边八格内的雷的数量。

此前说过棋盘需要大一圈,就是为了统计周边雷方便,因为大一圈的那个范围内没有埋雷且已经置  ‘ 0 ’处理,所以无需再判断是否为棋盘边界的情况。

扫雷的数字格可以想象成一个九宫格,输入的排查坐标位于该九宫格的中心点,如果除中心外其余八格有雷,则该中心数字格就会显示周边有多少个雷。

85498ea17b94454e8769edd1ac239f7d.png

比如这一轮游戏,绿色圈起来的数字格除了右下角的红色圈圈格有雷,周边都是数字或者空格,反过来推理也能证明红圈格是雷格。

对于绿圈格右边的这个数字“ 1 ”格,因为已经确定该格周边两个未知格里的唯一一个雷的位置,则红圈格右边的就不会是雷格。

71de30835b3e40149f18660b8faf563d.png

我们点击一下,红圈格右边果然不是雷格。像这样,就算是排了一些未知格。

实现这个功能非常简单,我们只需要将mine棋盘排查坐标的周边格的值一并加起来,最后减去八个“ 0 ”,就能得到该格周边的雷数,也就得到了该格若是数字格它应该显示的数字。

因为埋雷时我们预先规定“ 0 ”是空格,也就是没有雷的格,而“ 1 ”则表示埋了雷的格,字符类型的“ 0 ”和“ 1 ”的差值(也就是ASCII码值)刚好是1,这样都会就能统计周边的雷数了。

92dc41934a0545e8a7dfe716f23b9902.png

像这样子将mine棋盘上的周边格一一加起来就可以了,具体见上面粘贴的代码。

最后定义一个中间变量mid存放数值并返回即可。

4.2. install  扫荡函数

void install(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y,int *win)
{
	if (x<1 || x>ROW || y<1 || y>COL)
		return;
	if (show[x][y] != '*')
		return;
	int mid = retmine(mine, x, y);
	if (mid > 0)
	{
		(*win)++;
		show[x][y] = mid + '0';
		return;
	}
	else if (mid == 0)
	{
		(*win)++;
		show[x][y] = ' ';
		install(mine, show, x - 1, y,win);
		install(mine, show, x - 1, y - 1,win);
		install(mine, show, x - 1, y + 1,win);
		install(mine, show, x, y - 1,win);
		install(mine, show, x, y + 1,win);
		install(mine, show, x + 1, y,win);
		install(mine, show, x + 1, y - 1,win);
		install(mine, show, x + 1, y + 1,win);
	}
}

注意返回类型和最后一个形参是*win,传指针是为了修改win的数值,因为最后判断胜利的条件就是win

先理解后面两个if。

首先创建一个中间变量mid,用来存放计算函数返回的值,这个值代表了周边格可能存在的雷数。

如果mid > 0,也就是说明周边格存在雷格,此时 (*win)++表明排查了一个坐标,同时在show棋盘中将排查坐标由未知格改为数字格,也就是将mid+‘ 0 ’,就可以实现显示数字格的功能。

注意此时是排查格是数字格的情况,在扫雷游戏中,当扫出的是数字格时,就会停止向周边格的“清扫”,也就是连片排雷。所以,当当前格是数字格时,我们不需要连片清扫,此时需要return退出该函数。

当当前格不是数字格时,也即mid == 0时,我们需要再次判断当前情况并进行递归,由当前格向周边八格进行清扫。也因为当前格不是雷格,所以也需要 (*win)++,并且将show棋盘改为“ ”,也就是空格。

之后,我们写八行当前格周边八格的install函数,递归判断是否是数字格,以及是否需要再次对这些周边八格的周边八格再次递归判断。

以上,我们需要再判断数字格之前进行两次if判断。

一次是判断当前格是否位于棋盘边界,若是,则超过棋盘边界的格无需判断,直接return即可,见install函数的第一个if。

而第二个if是判断当前清扫格是否已经清扫过,也即show棋盘上当前格是否为未知格 ‘ * ’ ,若不是则代表此前排查过了,直接return。

如果当前格非数字格非雷格,同时也没被排查过,则再进行递归计算, 直到将附近所有非雷格以及数字格清扫干净,同时,win也会记录清扫了多少格。

4.3. tab 标记函数

void tab(char show[ROWS][COLS], int row, int col)
{
	int x = 1;
	int y = 1;
	int j = 0;
	int k = 0;
	printf("输入0 0退出程序,输入0 1取消标记\n");
	while (1)
	{
		printf("请输入要标记的坐标:");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			show[x][y] = '!';
		}
		else if (x == 0&&y == 0)
		{
			printf("标记程序结束...\n");
			break;
		}
		else if (x == 0 && y == 1)
		{
			printf("请再次输入要取消标记的坐标:");
			scanf("%d %d", &j, &k);
			show[j][k] = '*';
		}
		else
			printf("坐标错误!请重新输入!\n");
	}
	
}

在经典扫雷游戏中,鼠标右击即将点击的格子标记为标记格,也就是当你推测出当前格为雷格时,可以借用标记功能标记,以方便后一步的推测。

要想实现这个功能非常简单因为只需要标记,所以我们只需要调用show棋盘将要标记的格子改为标记符号就可以了,因此只需要传参传show,row以及col即可。

在这里我规定进入标记模式只需要输入 "0 0" 即进入。因为 "0 0" 不是埋雷格也不是需要操作的格子,同时也是简单的数字。当然你也可以更改进入的方式,需要注意的是进入标记模式的判断并不在tab函数里,而是在由tab函数组成的findmine函数里,判断进入将在那里进行判断。同时,退出tab函数也需要判断,此时 "0 0" 也可以充当退出标记模式的判定。

因为只需要判断show棋盘,数值范围应当在1~9的范围内,所以需要定义两个变量x和y用以接收标记格的坐标,同时这两个坐标也用来判断是否需要退出函数。

有喜就有悲,有对就有错,标记也有被取消的时候,所以还需要定义两个坐标用以接收需要取消标记的坐标,同时进入标记取消模式的判定也由x和y来判定。

由于需要进行多次判断,我们需要打印提醒信息,见上文代码。

当需要标记时,这里我用符号 ' ! ' 来表示标记格,当然你用其他的也可以。

当不需要标记时,只需要将符号 ' ! ' 改回 ' * ' 即可。

要注意循环是一直进行的,因为很多时候都是推理出多数雷格才进行标记,所以循环一直进行方便标记,不然就得一个一个敲进入标记模式的0 0再进行标记了。

排雷函数主体

void finemine(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 - GAMEMINE)
	{
		printf("请输入要排查的坐标:");
		scanf("%d %d", &x, &y);
		if ((x >= 1 && x <= row) && (y >= 1 && y <= col))
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了...\n");
				printchess(mine, ROW, COL,win);
				break;
			}
			else
			{
				install(mine,show, x, y,&win);
				printchess(show, ROW, COL,win);
			}
		}
		else if (x == 0&&y == 0)
		{
			tab(show, ROWS, COLS);
			printchess(show, ROW, COL,win);
		}
		else
		{
			printf("坐标值错误,请重新输入!\n");
		}
	}
	if (win == row * col - GAMEMINE)
	{
		printf("恭喜你排雷成功!游戏通关!\n");
		printchess(mine, ROW, COL,win);
	}
}

排雷函数findmine的传参依据上面的三个函数就可以猜的七七八八了,没错只需要传mine,show,row以及col即可。

在这里我们需要定义三个变量,x和y用以接收排雷坐标,win用以判断胜利。

使用while循环,循环判定条件是:

while (win < row * col - GAMEMINE)

因为这里讲的是最基础的简单模式,所以GAMEMINE的值是10,也就是10个雷。胜利条件就是当把棋盘中81个未知格中的10个雷格排查掉,即获得胜利,故当 win == 71 时,扫雷成功,游戏结束,所以循环判定条件如上。

			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了...\n");
				printchess(mine, ROW, COL,win);
				break;
			}

当排查的未知格是雷时,游戏结束,此时可以打印雷棋盘以供玩家观看,同时break退出该函数。

			else
			{
				install(mine,show, x, y,&win);
				printchess(show, ROW, COL,win);
			}

若不是,则进行一系列的扫荡清扫,也就是判定是否为数字格,是否为空格,是否需要清扫等等。在此调用扫荡函数install,然后再打印一次show棋盘给玩家进行下一步操作。

		else if (x == 0&&y == 0)
		{
			tab(show, ROWS, COLS);
			printchess(show, ROW, COL,win);
		}
		else
		{
			printf("坐标值错误,请重新输入!\n");
		}

当输入 " 0 0" 时,循环判断进入标记函数tab进行标记操作,退出标记函数后再打印一次棋盘供玩家进行下一步操作。

当输入坐标出错时,则提醒玩家。

	if (win == row * col - GAMEMINE)
	{
		printf("恭喜你排雷成功!游戏通关!\n");
		printchess(mine, ROW, COL,win);
	}

当达到胜利条件时,while循环自动退出,进入胜利条件判断。

在这里提醒玩家排雷成功,游戏通关,并打印mine棋盘即可。

最后回到 test.c ,以确认玩家下一步操作。

 

结语

至此,扫雷游戏的基本代码已经讲完了。

至于其他难度比如中等难度以及困难难度,还有时间功能等等就请读者自己推导,只需要再多加几个宏,多加几段函数几段代码,就可以实现和原版扫雷几乎一样的功能了。

功能归功能,棋盘展示的话也可以自行学习easyx图形库用绘图窗口制作。

咱们下一期贪吃蛇见~

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值