C语言:扫雷,启动!

在掌握了数组和函数内容的知识之后,在本篇博客中我将会带着大家一步步地来写经典小游戏——扫雷。

前排提示:该文章中提及的扫雷游戏基于VS2022实现~


目录

一、扫雷游戏分析和设计

二、Minesweeper game.h的实现

三、Minesweeper.c的实现

1.InitBoard()函数

2.DisplayBoard()函数

3.SetMine()函数

4.FindMine()函数

四、Minesweeper game.c的实现


一、扫雷游戏分析和设计

相信大家都玩过扫雷游戏吧。大家在游玩的时候有没有思考过扫雷游戏是怎样运行起来的呢?今天我们就来一步步地写出扫雷游戏的C程序。

首先来说下我们实现扫雷游戏的C程序的原理:

使用控制台实现经典的扫雷游戏

游戏可以通过菜单实现继续玩或者退出游戏

扫雷的棋盘是9*9的格子 

默认随机布置10个雷,可以排查雷

如果位置不是雷,就显示周围有几个雷。如果位置是雷,游戏结束;把除10个雷之外的所有非雷都找出来,排雷成功,游戏结束

当然,以当前的知识来说我们只能实现非常非常简单的扫雷,做不到像电脑上的扫雷游戏一样精致。

我们来看一下游戏界面,也就是我们写出C程序后应该得到的结果:

那我们就先来还原一下棋盘:

扫雷的过程中,布置的雷和排查出的雷的信息都需要存储。所以我们需要一定的数据结构来存储这些信息。因为我们需要在9*9的棋盘上布置雷的信息和排查雷,我们首先想到的就是创建一个9*9的数组来存放信息。假如某个位置是雷,那我们就用1来表示,如果没有就用0。

我们先来假定有如下棋盘:

根据扫雷游戏的规则,当我们排查一个位置时如果以其为中心附近3*3内没有雷,这个位置排查后显示的应该是0,如果有一个雷那就会显示1,以此类推。假设我们排查(2,5)这个坐标时,我们访问周围的一圈8个黄色位置,统计周围雷的个数就是1。

假设我们排查(8,6)这个坐标时,我们访问周围一圈的8个位置。但是统计周围雷的个数时,最下面的三个坐标就会越界。为了防止越界,我们在设计的时候,需要给数组扩大一圈,雷还是布置在中间的9*9的坐标上,周围一圈不去布置雷就行,这样就解决了越界的问题。所以我们将存放数据的数组创建成11*11比较合适。

再继续分析。我们在棋盘上布置了雷,棋盘上雷的信息(1)和非雷的信息(0)。假设我们排查了某个位置后,这个坐标处不是雷,但是这个坐标的周围有1个雷,那我们需要将排查出的雷的数量信息记录存储并打印出来,作为排雷的重要参考信息。那这个雷的个数信息应该存放在哪里呢?如果存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能或产生混淆和打印上的困难。

这里我们肯定有办法解决。比如雷和非雷的信息不要使用数字而是使用某些字符,这样就避免冲 突了。但是这样做的话棋盘上雷和非雷的信息与排查出的雷的个数信息会比较混杂,不够方便。

这里我们采用另外一种方案。我们专门给一个棋盘(对应一个数组mine)存放布置好的雷的信息,再 给另外一个棋盘(对应另外一个数组show)存放排查出的雷的信息,这样就互不干扰了,把雷布置到 mine数组,在mine数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给后期排查参考。

同时为了保持神秘,show数组开始时初始化为字符*,为了保持两个数组的类型一致,可以使用同一套函数处理,mine数组最开始也初始化为字符0,布置雷改成1。如下:

对应的数组应该分别是:

char mine[11][11] = {0};//用来存放布置好的雷的信息

char show[11][11] = {0};//用来存放排查出的雷的个数信息

为了不让我们的扫雷C程序显得冗杂死板,我们可以使用上一篇文章所讲到的文件来包含我们所需的函数:C语言:函数-CSDN博客

这里我们需要实现三个文件:

Minesweeper game.h//文件中写游戏需要的数据类型和函数声明等

Minesweeper.c//文件中写游戏中函数的实现等

Minesweeper game.c//文件中写游戏的测试逻辑


/*接下来我们就正式地进入编写扫雷C程序的讲解*\


为了方便理解,我们依次按照上述三个文件的顺序来进行讲解~

二、Minesweeper game.h的实现

这个文件中包含着我们在编写程序时所需的宏定义和函数声明等等。该如何创建Minesweeper game.h头文件呢?

首先我们打开VS2022,创建一个新建项目或进入一个已创建项目,在最左侧的解决方案资源管理器中找到头文件

随后我们单击右键一下头文件,会弹出一个窗口,然后选择新建项。这里记住要把文件后缀的.cpp改为.h 文件名可以命名为Minesweeper game,也可以命名为其他名字,只要便于自己和他人理解就行

随后我们进入Minesweeper game.h这个头文件项目的文件:

对于自定义头文件项目,编译器会自动为我们生成第一行代码——#pragma once。它是一种预处理指令,用于防止头文件的内容被重复包含。这里我们暂时记住这点就行。

然后我们开始编写我们的自定义头文件。首先编入的应该是我们在扫雷程序中所用到的库函数。stdio.h头文件固然不可少。我们再分析,扫雷游戏中的雷肯定是随机在棋盘上生成的,所以我们需要用到stdlib.h头文件中的rand()函数和srand()函数。同时为了避免出现伪随机的现象,我们还要用到time.h头文件来实现真正的随机。

随后我们进入棋盘的布局。首先我们得先定义雷的个数,这里需要用到宏定义。使用宏定义定义一个变量的基本格式如下:

#define 变量名 数

我们就用以上格式来定义雷的个数,防止后续频繁声明雷的数量。扫雷游戏中最简单的模式为10个雷,那么我们就将变量的名字命名为EASY_COUNT,然后将10赋值给它。

既然我们都用宏定义来定义雷的个数了,那么为了便利和程序的理解,我们将棋盘的大小也用宏定义来表示。上面我们提到,简单模式下的棋盘大小为11*11,但实际上我们排雷的范围只有9*9,那么这里我们就需要定义两对变量:第一对是我们排雷的范围,第二对是我们棋盘的大小。那我们定义行为ROW,列为COL。这里的ROW和COL是我们排雷的范围。然后我们再定义棋盘的大小,我们定义行为ROWS,列为COLS。ROW和COL分别与ROWS和COLS相差2,那我们为其赋值的思路就非常清晰了:

定义完我们所需要的常量后,接下来就到了函数的环节。这里我们总共需要定义四个函数!

InitBoard()//初始化mine棋盘和show棋盘

DisplayBoard()//打印show棋盘

SetMine()//布置雷

FindMine()//排查雷

具体这四个函数的形参是什么,我们在接下来的Minesweeper.c文件中一一道来。

三、Minesweeper.c的实现

再实现这个文件之前,同样也需要创建一个新项目。与Minesweeper game.h不同,这里我们要在源文件中创建这个项目。

还是单击右键后,会弹出一个窗口,然后点击新建项,将文件原始后缀改为.c,命名为Minesweeper:

这个文件里就专门实现我们在Minesweeper game.h中声明到的函数。

1.InitBoard()函数

InitBoard()函数的作用是初始化我们的棋盘。我们在初始化一维数组的时候一般都是直接初始化为0,例如:

int arr[10] = {0};

但是二维数组不一样,二维数组既有行也有列,所以我们要一行一行的进行初始化。比如这里有一个3行3列的二维数组,我们要对它进行初始化,该怎么做呢?

#include <stdio.h>

int main()
{
	int a = 0;
	int arr[3][3];
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
			arr[i][j] = a++;
	}
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
			printf("%d ", arr[i][j]);
		printf("\n");
	}
	return 0;
}

其中变量i为行,j为列。i为0时,j依次递增,也就是依次初始化第一行的数据。以此类推:

那么当我们初始化mine和show棋盘时,只要分别将其二维数组初始化为字符0和字符*就行了。那么其代码分别如下:

#include <stdio.h>

//mine棋盘
int main()
{
	int arr[3][3];
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
			arr[i][j] = '0';
	}
	return 0;
}

//show棋盘
int main()
{
	int arr[3][3];
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
			arr[i][j] = '*';
	}
	return 0;
}

如果用InitBoard()函数表示的话,我们需要的形参分别是一个二维数组二维数组的行数二维数组的列数。由于InitBoard()函数的作用是初始化两个棋盘,所以它不需要返回任何值:

//mine棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols)
{
    int i = 0;
    for (i = 0; i < rows; i++)
    {
        int j = 0;
        for (j = 0; j < cols; j++)
        {
            board[i][j] = '0';
        }
    }
}

//show棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols)
{
    int i = 0;
    for (i = 0; i < rows; i++)
    {
        int j = 0;
        for (j = 0; j < cols; j++)
        {
            board[i][j] = '*';
        }
    }
}

但是呢,我们编写C程序的一个重要原则就是追求简洁明了。一个函数用两遍就会显得太冗余。观察这两个棋盘的初始化程序,唯一不同的就是初始化的符号不一样,所以我们不妨再引入一个形参,用来传输我们需要初始化的符号:

void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)

这样初始化棋盘的函数就大功告成了!我们在Minesweeper.c文件下进行InitBoad()函数的编写,只需把上述程序中的符号改为set即可!

记得包含我们自定义的头文件Minesweeper game.h!也不要忘了在Minesweeper game.h中完善函数的形参!

这里要注意,由于我们包含的是我们自定义的头文件,而自定义的头文件中已经引用了所需要的库函数,所以我们在Minesweeper.c和Minesweeper game.c中不需要再引用库函数。

2.DisplayBoard()函数

初始化了棋盘后,就需要来打印我们show棋盘。由于是打印棋盘函数,所以这里不需要再对棋盘进行初始化了。

我们在进行扫雷的时候,选择的是棋盘上的坐标,就像前面提到的在棋盘外围还排着一行和一列的数字。所以我们最好也要先打印一下这些数字以后续表示坐标。

我们先在第一行打印数字。这里要以列数来作为判断条件进行打印数字。然后换行来正式进行show棋盘的打印。

我们就不再以右上角的方块为坐标原点了,我们将其定义为点(1,1)。那么在其左上角的空位上应该是0,所以打印第一行的数字时应该从0开始!由于整个棋盘是由9*9再加上外围的空白组成,所以数字打印到10就会停止。

在打印棋盘时,应该从第二行开始,因为第一行已经被我们的坐标数字占用了。然后打印纵坐标的数字也应该从1开始到10停止。假若从0开始就会出现这样的情况:

所以我们的打印棋盘函数就是以下代码:

方块圈起来的这行是起到装饰作用,各位可根据自己的喜好来尽情装饰棋盘界面~

3.SetMine()函数

由于是布雷函数,布雷是随机的。所以从这里开始就用到我们的rand()函数了。我们先将雷的数量设置成我们前面已经定义好的宏常量EASY_COUNT(也就是10)。布雷的过程是一个个布下的,所以每布下一个雷,总数就要减去一个。这里用for循环可以,while循环也可以。这里我选择了while循环,for循环的布雷实现由各位在后续进行拓展练习~

SetMine()函数的形参和DisplayBoard()函数的形参一模一样,这里不再做过多解释。

前面我们就已经讲过怎么将while循环编写的和for循环的功能一模一样:C语言:分支与循环-CSDN博客 只需要在while语句块中加入调整部分即可,圆括号内加入我们要调整的变量。

因为是随机的布雷,且是在排雷棋盘内随机布雷。根据我们在前面设置的坐标,我们可以根据坐标的变化来布雷,那我们就将所设的坐标和rand()函数结合在一起,这样就能实现真正的随机布雷。在前面已经讲过rand()函数怎么取范围:C语言:猜数字,启动!-CSDN博客

我们要在排雷棋盘内进行布雷,那么x和y的范围均不超过9,也不小于1。我们先让x或者y先取模上一个9,那么rand()函数返回的范围是0~8。这里为什么是0~8呢?我们以0~10以内的数字为例:

数字取模9后的结果
00
18
27
36
45
54
63
72
81
90
101

0取模任何数都是0,所以0%9的结果就是0。这里就不再过多解释取模操作符了。

所以就可以写出以下的代码:

int x = rand() % row + 1;

int y = rand() % col + 1;

既然模上9的范围是0~8,那么根据rand()函数的用法,我们在后面加上1,范围就变成了1~9。这样,就会随机生成9*9范围内的坐标值(x,y)

如果我们随机到了没有布上雷的位置,即mine棋盘上的某一表格为字符0,那么我们就令这个表格内的值为字符1;同样地,若mine棋盘上的某一表格为字符1,就说明这里已经被布上雷了,我们就不需要再布雷了,这时就要另找他地进行布雷。所以这里运用if语句判断即可~当我们布上雷后,手中的雷就会减少一颗,所以要将count--这一语句放入if语句中:

这样,我们的布雷函数也就完成了。

4.FindMine()函数

接下来讲解扫雷游戏中最重要的函数FindMine()函数。

在前面已经讲过在我们这个扫雷游戏中有两个棋盘——mine棋盘和show棋盘,一个是看不见的,一个是看得见的。mine棋盘是存放存放雷的信息的,而show棋盘是存放排查雷的信息的。那么FindMine()函数实际上是同时对两个棋盘进行操作的。所以形参中我们要将show棋盘和mine棋盘囊括在中。行和列是固然不可少的形参,用来操作棋盘内部的数据。那么FindMine()函数的形式如下:

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)

我们想象一下,再排雷的过程中要一直排,直到排到雷或排出所有的雷,同时也要保证坐标的有效性。所以这里我们用到了while循环。话不多说,用代码理解:

首先就是判断坐标是否越界。然后如果排到的是雷,就会提示被炸死,游戏结束。在扫雷游戏中,一旦游戏结束,就会将雷的位置展示给玩家。所以这里不妨在玩家被炸死后将mine棋盘展示给玩家。如果坐标输入的是非法的,就要提示玩家重新输入。

那如果我们排到的不是雷呢?

在扫雷游戏中,我们排到了一个非雷的位置,一般都会显示其3*3范围内雷的数量。这里我们不妨再引入一个函数:GetMineCount()函数。这个函数的作用就是返回一个非雷的位置所在的3*3范围内的雷的数量,所以它的返回结果是int类型。在此之前,我们先来思考几个问题:

字符1 - 字符0的结果是多少?

字符2 - 字符0的结果是多少?

字符3 - 字符0的结果是多少?

根据ASCII码值,字符0的ASCII码值为48,字符1为49,字符2为50。而字符之间的运算就是根据其ASCII码值来计算的。我们用字符1减去字符0,以十进制的方式来打印,打印出来的结果就数字1,因为两者的ASCII码值之差为1。同样道理,字符2减去字符0和字符3减去字符0的结果分别是数字2和数字3。不难发现,十进制数字的字符与字符0之间的差值是一个十进制数。我们可以根据这个结论来表示GetMineCount的返回值,然后令一个非雷的位置的值等于函数的返回值。所以:

那这个函数的形参该如何表示呢?既然是统计周围雷的个数,那我们就要检测3*3范围的所有位置,棋盘位置用坐标来表示,所以我们要将坐标传输进去,由于show棋盘只负责存放排雷的信息,在这里我们就要用到mine棋盘。同时该位置不是雷,所以我们要把排完位置后的show棋盘打印在界面上。所以:

那GetMineCount()函数该如何实现呢?根据我们传进去的参数,它的形式应该是这样的:

int GetMineCount(char mine[ROWS][COLS], int x, int y)

既然负责扫描3*3范围内雷的数量,那我们就要将3*3范围内的所有坐标表示出来。

假设有下列3*3表格:

(0,0)y
x
(5,5)

我们要写出(5,5)的3*3范围内的坐标,对我们来说是易如反掌的。这道题简直就易如反掌,易如反掌呀!~

(0,0)

y

x(4,4)(4,5)(4,6)
(5,4)(5,5)(5,6)
(6,4)(6,5)(6,6)

那么(5,5)所在的3*3范围内的所有坐标表达式就可以表示出来了。这里我们不妨将3*3范围的中心点设为(x,y):

(0,0)Y
X(x-1,y-1)(x-1,y)(x-1,y+1)
(x,y-1)(x,y)(x,y+1)
(x+1,y-1)(x+1,y)(x+1,y+1)

由于这里是mine棋盘,上面存放的不是字符0就是字符1,我们先让除中心点以外的周围八个点的值(即字符0和字符1)相加(得到的是一个比较大的数),随后我们让这个数减去8个字符0(实际上就是8乘以48),由于返回类型是int类型,所以就会将返回值赋给中心点。那么GetMineCount()函数就非常简单了:

还有一种比较高端的写法就是运用static。在C语言:函数-CSDN博客中讲过static的作用。然后我们将3*3的范围想象成一个二维数组,让其范围内的所有值减去字符0再相加,也会返回中心点3*3范围内的雷数。实际上跟我们上面的思路是一模一样的,更多的是体现了计算机语言的灵活性。这里不再过多解释,上代码:

然后我们再继续完善FindMine()函数。扫雷游戏的胜利条件是排出所有的雷。9*9棋盘中有10个位置是雷,那么就代表有71个位置不是雷。只要我们将这71个位置排除干净,我们就获得胜利了。这里定义一个变量,要求它在我们每排除一个非雷位置后自身递增,直到排完所有非雷的位置。一旦排完所有非雷位置,那么玩家获胜,同时并把mine棋盘展示给玩家。那么FindMine()函数具体如下:


以上就是扫雷游戏函数的实现了~还是非常简单的!

四、Minesweeper game.c的实现

这里我们来讲解一下Minesweeper game.c的实现。首先Minesweeper game.c文件中也要包含我们自定义好的Minesweeper game.h头文件。随后打印我们的游戏界面(像猜数字游戏的界面一样即可)。那我们这里同样定义一个menu()函数用来打印游戏界面:

随后定义一个game()函数用来承载我们已经定义好的函数(Minesweeper.c中的函数),同时输入我们定义好的形参

接下来的main函数就和C语言:猜数字,启动!-CSDN博客中的main函数一样了,这里不再过多解释了。


以上就是扫雷游戏程序的实现!思路方面可能有点不太清晰,毕竟是根据扫雷C程序函数的顺序来进行实现的,如有错误,请各位大佬批评指正!

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值