C语言——数组和函数实践:扫雷游戏

扫雷游戏分析和设计

扫雷游戏的功能说明

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

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

  • 扫雷的棋盘是9*9的格子在这里插入图片描述

  • 默认随机布置10个雷

  • 可以排查雷在这里插入图片描述
    如果位置不是雷,就显示周围有几个雷
    如果位置是雷,就炸死游戏结束在这里插入图片描述

把除10个雷之外的所有非雷都找出来,排雷成功,游戏结束


游戏的分析和设计

因为我们学过多文件的使用,所以在这个游戏中我们创建3个文件:test.c(用来完成游戏的测试逻辑),game.c和game.h一起完成的是游戏逻辑的实现(.h里面放的函数的声明;.c里面放的是函数的实现)
大概代码逻辑:

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void menu()
{
	printf("*****************************\n");
	printf("******    1.play   **********\n");
	printf("******    0.exit   **********\n");
	printf("*****************************\n");

}

void test()//这个函数的内部完成的是整个游戏的逻辑
{
	int input = 0;
	do
	{
		menu();//写个函数把菜单打印出来
		printf("请选择:>");
		scanf("%d", &input);//输入之后要判断情况
		switch (input)//根据input进行处理
		{
		case 1:
			printf("扫雷");
			break;
		case 0:
			printf("游戏结束,退出游戏");
			break;
		default:
			printf("选择错误,重新选择");
			break;
		}
	} while (input);
}

int main()
{
	test();
	return 0;
}

数据结构的分析

在扫雷的过程中,布置的雷和排查出的雷的信息都需要存储,所以我们需要一定的数据结构来存储这些信息。
因为我们需要在9×9的棋盘上布置雷的信息和排查雷,我们首先想到的就是创建一个9×9的数组来存放信息。
在这里插入图片描述
如果这个位置布置雷,我们就存放1,没有布置雷就存放0.
在这里插入图片描述

假设我们排查(2,5)(第2行,第5列)这个坐标时,我们访问周围的一圈8个黄色位置,然后统计周围雷的个数,发现有1个雷。
假设我们排查(8,6)这个坐标时,我们访问周围的一圈8个黄色位置,统计周围雷的个数时,最下面的三个坐标就会越界,为了防止越界,我们在设计的时候,给数组扩大一圈,雷还是布置在中间的9×9的坐标上,周围⼀圈不去布置雷就行(周围一圈都为0),这样就解决了越界的问题。所以我们将存放数据的数组创建成11×11是比较合适。
在这里插入图片描述
再继续分析,假设我们排查了某一个位置后,这个坐标处不是雷,但这个坐标的周围有1个雷,那我们需要将排查出的雷的数量在这个坐标上打印出来,用来提示周围有多少个雷。可是在布置雷的数组中1已经代表雷了,如果在(2,5)这个坐标上打印一个1的话,这个1到底是雷的个数还是雷呢?这就产生了混淆。

所以,这个雷的个数信息该存放在哪里呢?如果存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能产生混淆和打印上的困难。

解决方法:
我们专门给一个数组mine存放布置好的雷的信息,排查出的信息为了避免和mine数组里的信息冲突了,我们再创建好另一个数组:show存放排查出的雷的信息,打印时也是打印这个数组的信息。

eg:我们在mine数组中排查(3,6)这个坐标,发现周围有1个雷,所以我们在show数组里面的(3,6)这个坐标显示一个1。

这样就互不干扰了,把雷布置到mine数组,在mine数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给后期排查做参考。

在扫雷时,为了保持神秘,先把界面初始化为*在这里插入图片描述
所以:show数组开始时初始化为字符 ‘*’,排查后的信息在相应的坐标上打印出来做提示,其余部分依旧为以下初始的字符,打印的时候也直接打印这个数组,让别人知道雷的信息。
在这里插入图片描述
我们发现mine数组与show数组元素个数一样(11×11),元素类型也一样,所以对数组的操作用同一套函数就可以了

文件结构设计

创建三个文件:
在这里插入图片描述
在头文件定义:
在这里插入图片描述

为什么还要定义ROW和COL为9呢?因为我们在做棋盘的时候要用的是9×9,为了防止越界,创建的时候要用的是11×11。所以既要11也要9.

创建这两个数组,我们发现有“未定义标识符”,这是因为ROWS和COLS这两个符号是定义在头文件中的,所以我们要包含头文件
在这里插入图片描述
写上头文件:
在这里插入图片描述
我们要将棋盘初始化,就写个初始化函数
在这里插入图片描述
再在头文件中写上初始化函数的声明
在这里插入图片描述
接在在game.c文件里实现这个函数
在这里插入图片描述
将棋盘初始化为0写完了,但是show数组得初始化为*,如果再写一个初始化函数就会显得冗余了,有没有其他的办法了呢?
我们可以多写一个参数:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

调用两次初始化函数,这样就可以将两个数组都初始化想要的字符了在这里插入图片描述

我们可以把棋盘打印出来看看效果是什么样的,因为平时玩游戏都只看9×9,所以此时只用打印9×9的棋盘就可以了,前面把数组设置成11×11是为了防止越界。
在这里插入图片描述
在这里插入图片描述
DisplayBoard在game.c文件里进行函数实现时注意:在11×11的棋盘里打印中间的9×9的棋盘时,行号与列号都是从1开始的,到9结束
在这里插入图片描述
所以函数实现时:
在这里插入图片描述
我们在测试打印mine棋盘时发现报错,这是因为我们只在test.c中包含了头文件<stdio.h>,在game.c中用了printf却没包含

在这里插入图片描述
因为game.c与test.c都包含了头文件game.h,所以我们可以将test.c中的<stdio.h>删掉放在game.h中,这样就相当于test.c和game.c都包含了<stdio.h>
mine棋盘测试成功:
在这里插入图片描述
接下里我们测试show棋盘:
在这里插入图片描述
打印成功
在这里插入图片描述
因为show数组是我们玩游戏时需要打印出来的,但是这样打印出来的show棋盘不太好看,如果在show棋盘上把行和列都显示出来的话,说定位的时候就会直观,所以我们将代码改一改
行号:
在这里插入图片描述
运行看效果:
在这里插入图片描述
因为列号在棋盘的最上方,所以在打印棋盘之前先写个for循环打印列号,打印完列号后要写个换行
在这里插入图片描述
我们发现列号错位了,第2列那里应该是第1列的,该怎么让1在对的位置呢?就在前面加上一个数字0.
在这里插入图片描述
这样的话坐标会非常的直观。我们将两个棋盘都打印出来看看:
在这里插入图片描述
打印出来的两个棋盘太拥挤了,可以加上一个分割的东西在这里插入图片描述
因为mine数组中都是雷布置好的信息,要是打印出来就露馅了,所以我们要打印的是show数组。
当我们初始化棋盘后,应该是布置雷,打印暂时还用不上。

所以为什么上面写了初始化棋盘后写的就是打印棋盘呢?

因为只有打印了棋盘,我们才能知道初始化的对不对。所以接下来将打印棋盘暂时注释掉,写个SetMine函数开始布置雷

因为mine数组中存放布置好的雷的信息,show数组中存放排查出的雷的信息,所以雷要布置到mine数组里面

在这里插入图片描述
在头文件中对SetMine函数进行声明
在这里插入图片描述
接下来在game.c文件中进行函数实现
首先在头文件中定义雷的个数
在这里插入图片描述
因为我们要利用的是中间9×9的棋盘,所以坐标范围要变为1~9
在这里插入图片描述
在这里插入图片描述
而生成随机数要用rand,并且在此之间还要调用一次srand,所以

在这里插入图片描述
包含它们相应的头文件:
在这里插入图片描述
在前面我们已经将坐标的范围决定好了,但在我们生成坐标时还要想一个问题:在布置雷时这个坐标会不会已经布置过雷了?
所以我们可以写个if语句判断一个,要是为字符0的时候就说明不是雷,此时可以进入这个if语句,在这个坐标上布置雷;如果这个坐标不是字符0(为1:雷),就进入不了if语句,接着重新生成坐标,一直循环,直到count为0循环结束。
在这里插入图片描述
这个代码循环次数应该大于等于10次的,因为没有走if的时候count是不减的,会一直循环。
现在我们打印以下看看有没有布置成功:
在这里插入图片描述在这里插入图片描述
将雷布置好后,我们将整个流程还原:应该是先初始化棋盘,再布置雷,接着打印棋盘,最后排查雷
在这里插入图片描述
排查雷是在mine数组中排查,然后排查的信息放在show数组中,所以排查的过程中,这两个数组都会涉及到,即传参的时候这两个数组都要传。并且排查的过程中也只是关心中间的9×9,所以行为ROW,列为COL
在这里插入图片描述
同样的,在头文件中对这个函数进行声明:
在这里插入图片描述

在这里插入图片描述

注意:两个数组传参的时候,顺序要保持一致

在game.c文件中进行函数的实现

  • 首先要输入坐标

  • 在别人玩游戏中,万一输入的坐标不是我们所规定的棋盘的坐标范围,例如:(18,20).这样的话就越界了,所以在输入坐标后我们应判断坐标的有效性。在这里插入图片描述重新输入是再重新输入坐标,所以这里应该写个循环
    在这里插入图片描述

  • 当输入的坐标有效时就进入这个if语句了,有了坐标后我们就要判断这个坐标是不是雷,是雷就会被炸死,不是雷则要显示周围雷的信息在这里插入图片描述

  • 接着完成GetMineCount函数,如果我们不想GetMineCount这个函数放在头文件中,不想被其他人看到,可以直接写在game.c中。
    在这里插入图片描述

  • 那么该怎么直到这个坐标周围一圈雷的个数呢?在这里插入图片描述
    我们可以将这8个坐标找出来,然后对它们的值进行判断。如果是字符0,count不变,如果是字符1,count++。这样8个坐标统计完后就找出来了

还有另外一种方法:直接把周围8个坐标的值加起来,但是这个棋盘里的不是数字0和1,而是字符0和1.所以我们可以利用ASCII码值。

‘1’—49 ;
‘0’—48 ;
‘2’—50;
‘1’-‘0’=1;‘2’-‘0’=2
我们发现这样字符相减时会变成相应的ASCII码值相减,可以变成数字。
所以:字符0减去字符0可以等于数字0,字符1减去字符0=数字1.

即周围8个坐标所对应的字符都减去字符0,再将得到的数字再相加就可以了

在这里插入图片描述
如果想让 GetMineCount这个函数只在game.c这个文件中使用,可以在前面加上static
在这里插入图片描述

  • 此时已经将该坐标周围雷的数量统计出来了,我们应该将count放进show数组里面去
    在这里插入图片描述
    但是不能直接将count放进show数组

因为我们打印棋盘的时候是按%c的格式打印的,而数字是用%d的格式打印的,如果这里将数字直接放上棋盘打印的话,会将该数字通过ASCII码值变成相应的字符打印出来,所以这个地方必须要变成字符数字。

那该数字这么变成字符数字呢?
通过ASCII码值我们可以发现:
在这里插入图片描述
‘0’+1=48+1=49=‘1’
字符0对应十进制数字是48,数字48加数字1等于数字49,十进制数字49也就是字符1.
这样就实现了数字与字符之间的相互转换。
再如:
4+‘0’=4+48=52=‘4’
所以假如统计有3个雷,返回到count的值为数字3,直接数字3+‘0’=3+48=51=‘3’
即我们要在count后面+一个字符0.
在这里插入图片描述

  • 当我们排雷的时候应该显示那个棋盘
    在这里插入图片描述
  • 此时想看看统计的雷的数量对不对,我们可以将布置好雷的棋盘打印出来
    在这里插入图片描述
    将布置雷的棋盘打印出来后输入坐标,看统计的雷的个数正不正确
    输入坐标:2 6
    在这里插入图片描述
    排查雷的数量正确,因为我们只需要向别人显示show棋盘,所以测试后将mine棋盘重新注释掉
    在这里插入图片描述
    此种方法代码的另一种写法:
    在这里插入图片描述
    虽然这种方法将9个坐标都遍历了,但是中间那个坐标一定不是雷,所以将它统计进去也不会影响最后的统计雷的数据。

count+=(mine[i][j]-‘0’)
count=count+(mine[i][j]-‘0’)
加static是因为想让这个函数只在game.c这个文件中使用,不想让别的文件看到,不让它暴露出去

此时我们发现,当我们在排查的过程中踩到雷的时候会被炸死结束游戏,而直到最后要是把不是雷的位置都已经排查完,只剩下雷的话,该怎么结束这个游戏呢?

雷是在9×9的棋盘内随机布置的,每行有9个坐标,一共有9行,所以总共有81个坐标。而这81个坐标里有10个坐标是雷,所以还剩71个坐标不是雷,在我们中途没踩到雷死掉的情况下,把这71个坐标全部找出来就是游戏结束的时候。

所以,接下来对这段代码进行修改:
在这里插入图片描述
在这里插入图片描述
当所输入的坐标不是雷时win就自增1,直到win刚好循环71次时,也就是排查了71个坐标,将所有不是雷的坐标都排查完了,此时游戏该结束了。

win<3循环了几次?
win等于0时,循环一次后win等于1
win等于1再循环,循环两次后win等于2
win等于2再循环,循环三次后win等于3

我们要排查71个坐标,所以要循环71次,循环71次后win等于71,为了不再循环所以while后面的表达式要写win<71(等于71的话会循环72次)
71个坐标全找出来后跳出循环,结束游戏:
在这里插入图片描述
此时我们也可以将雷的信息再打印出来
在这里插入图片描述
以上就是排雷的整个过程。
接下来我们可以测试一下,但是真的要把71个坐标全部排查完有点难,并且万一排查的过程中被雷炸死了的话,又得从头开始。所以我们可以把雷的个数改成80个,这样的话要排查的位置就只有一个了。
在这里插入图片描述
而且我们还可以把布置好雷的棋盘打印出来,让我们知道不是雷的位置在哪
在这里插入图片描述
结果:
在这里插入图片描述
测试完接着把雷的数量恢复10个,把Mine棋盘注释掉
还有个问题我们需要注意:就是已排查过的坐标就不要再重复输入了,在以上代码中我们只考虑了坐标的有效性,这点也需要注意,所以:
在这里插入图片描述
接着把代码补全:
在这里插入图片描述

扫雷游戏的代码实现

game.h

#pragma once

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

#define ROW 9//ROW:行。这句代码的意思是定义一个符号ROW为9,也就是9行
#define COL 9//COL:列。定义一个符号COL为9,也就是9列

#define ROWS ROW+2//定义一个符号ROWS,内容为ROW+2,也就是11行
#define COLS COL+2//定义一个符号COLS,内容为COL+2,也就是11列

#define EASY_COUNT 10//表示定义10个(雷)

//棋盘初始化的函数声明
void InitBoard(char arr[ROWS][COLS],int rows,int cols,char set);

//打印棋盘函数声明
void DisplayBoard(char arr[ROWS][COLS], int row, int col);

//布置雷的函数声明
void SetMine(char arr[ROWS][COLS],int row,int col);

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

game.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void InitBoard(char arr[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++)
		{
			arr[i][j] = set;
		}
	}
}


void DisplayBoard(char arr[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++)
	{
		int j = 0;
		printf("%d ",i);//在每一行打印之前,先打印i(行号)
		for (j = 1; j <= col; j++)
		{
			printf("%c ",arr[i][j]);
		}
		printf("\n");
	}
}


void SetMine(char arr[ROWS][COLS], int row, int col)
{
	int count = EASY_COUNT;//10个雷
	while (count)
	{
		//生成随机坐标
		int x = rand()% row + 1;
		int y = rand()% col + 1;
		//任何数%9余的是0~8,再加上一个1就是1~9了
		if (arr[x][y] == '0')
		{
			arr[x][y] = '1';
			count--;
		}
	}
}

static int GetMineCount(char mine[ROWS][COLS],int x,int y)
{
	int i = 0;
	int count = 0;
	for (i = x - 1; i <= x + 1; i++)
	{
		int j = 0;
		for (j = y - 1; j <= y + 1; j++)
		{
			count += (mine[i][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)//(win<71)
	{
		printf("请输入要排查的坐标:");
		scanf("%d %d", &x, &y);
		//判断坐标的有效性
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (show[x][y] == '*')
			{
				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");
			}
		}
		else
		{
			printf("坐标不正确,请重新输入\n");
		}
	}
	if (win == row * col - EASY_COUNT)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}

test.c

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

void game()
{
	//完成扫雷游戏
	char mine[ROWS][COLS] = {0};
	char show[ROWS][COLS] = {0};
	//写个初始化函数(棋盘)
	InitBoard(mine,ROWS,COLS,'0');//这个数组全部初始化为0
	InitBoard(show,ROWS,COLS,'*');//这个数组为了保持神秘全部初始化为*

	//布置雷
	//就是在9*9的棋盘上随机布置10个雷
	SetMine(mine,ROW,COL);
	//DisplayBoard(mine,ROW,COL);


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


	//排查雷
	FindMine(mine,show,ROW,COL);
}

void test()//这个函数的内部完成的是整个游戏的逻辑
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();//写个函数把菜单打印出来
		printf("请选择:>");
		scanf("%d", &input);//输入之后要判断情况
		switch (input)//根据input进行处理
		{
		case 1:
			game();
			break;
		case 0:
			printf("游戏结束,退出游戏");
			break;
		default:
			printf("选择错误,重新选择");
			break;
		}
	} while (input);
}

int main()
{
	test();
	return 0;
}

扫雷游戏的扩展

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

在线扫雷游戏:http://www.minesweeper.cn/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值