【全网最强C语言学习】扫雷游戏——递归自动展开(手把手思路引领)

?前言?

 C语言学的怎么样,做个小项目检验自己的能力吧。通过这个游戏你能巩固什么?

·二维数组的创建与使用

·自定义函数的设计与使用

·递归(深度优先搜索思想)

?‍?作者概况:  就读南京邮电大学努力学习的大一小伙

?‍?联系方式:2879377052(QQ小号)             

?‍?资源推荐:C语言从入门到进阶

?‍?今日书籍分享:  《高质量程序设计指南 》                  提取码:CSDN

?‍?gitee码云链接: 全部代码开源道Gitee仓库

【姊妹篇】 【全网最强C语言学习】五子棋游戏


目录

一、思路引导

二、test.c中的函数设计

1.功能构思

2.函数封装

三、game.c中的函数设计

1.功能构思

2.函数封装

三、function.c中的函数设计

①initboard函数

②displayboard函数

③setmine函数

④count_mine函数

⑤spread_mine函数

⑥课外练习题

四、后记


 

一、思路引导

先来看看最终的效果吧!

对于初学者而言,独立设计一个将近两百行代码的小项目的确是一个不小的挑战。但是不用害怕,只要你学习过数组,函数,递归的基础知识,你就可以设计出属于你的扫雷游戏! 

有了基础知识后,那我们应该怎样切入呢?【后续教程将以这三点为基本逻辑展开】

1.构思好你所需要实现的功能

2.分装到各个去函数实现

(有同学初学可能不理解为什么要分装,举个例子:我们要实现打印棋盘的功能,是每次用到的时候重新写一份好呢?还是先设计封装到函数中去每次只要调用就好呢?结论是显而易见的)

3.视情况将将函数分类到不同文件中去

(举个例子) 

【game.h】:存放所有的头文件和函数声明,.c文件只要引用game.h文件即可(#include"game.h")

【test.c】:存放main函数和基本的结构框架,程序由这里开始

【game.c】:存放扫雷游戏的主体

【function.c】:存放game.c中需要使用的函数,game.c只需调用函数即可实现相应功能

(当然对于函数的分类和存放你也可以有自己的标准)

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include<stdlib.h>
#include<time.h>

#define COL 9  //设置雷区的宽
#define ROW 9  //设置雷区的长
#define NUM 16 //设定地雷的数量
//定义常量使得我们修改方便

void game(); //游戏主题函数
void initboard(char arr[ROW + 1][COL + 1],char key); //初始化棋盘函数
void displayboard(char arr[ROW + 1][COL + 1]);       //展示棋盘函数
void setmine(char mine[ROW + 1][COL + 1], int count);//设置地雷函数
int count_mine(char mine[ROW + 1][COL + 1], int x, int y);//计算周围地雷数函数
void spread_mine(char mine[ROW + 1][COL + 1], char show[ROW + 1][COL + 1], int x, int y);
//自动展开函数

(头文件内容) 


二、test.c中的函数设计

1.功能构思

①打印登录界面菜单 → 封装menu函数

②提供游戏进行或退出选择 →  switch语句

③实现重复游戏功能 → do{ } while 语句(注:中间省略了游戏过程,先设计最基本框架)

③实现对输入选项进行范围判断,输入范围错误则需要重新输入

2.函数封装

void menu()//菜单函数
{
	printf("******************n");
	printf("**    1.play    **n");
	printf("**    0.exit    **n");
	printf("******************n");
}

int tag = 0;//指定是否关机

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择执行项:>");
		scanf("%d",&input);
		//if (tag == 1 && input == 1)后续恶搞用到,这里先不管
		//{
		//	system("shutdown -a");
		//	tag = 0;
		//}
		switch (input)
		{
			case 0:break;
			case 1:  game(); break;
			default: printf("输入错误,请重新输入:n");
		}

	} while (input);
	printf("游戏结束n");
}

【评析】一步一步来,先设计好最基本的框架。其他的功能先全部丢到game函数中去


三、game.c中的函数设计

1.功能构思

①“棋盘式结构” → 创建二维数组

②对数组初始化   →  封装 initboard 函数

(原因:因为要重复play,所以每次都要先对数组初始化,防止上次游戏的干扰)

③显示棋盘 →  封装 displayboard 函数

④埋雷 →  封装 setmine 函数

⑤防止第一次踩雷

⑥判断是否踩雷

2.函数封装

#include"game.h"//每次引用头文件就不需要声明函数,调用库函数了

char mine[ROW + 1][COL + 1] = { 0 };//埋雷棋盘
char show[ROW + 1][COL + 1] = { 0 };//展示棋盘

extern int tag;//声明全局变量,先不管他

void game()
{
	int num = NUM;
	srand((unsigned)time(NULL));//初始化rand函数,基本格式记住就行了
	printf("游戏开始,祝你游戏愉快!n");
	initboard(mine, '0');//棋盘初始化内容为‘0’
	initboard(show, '*');//棋盘初始化内容为‘*’
	displayboard(show);//展示棋盘
	setmine(mine, NUM);//设置地雷

	int flag = 1;//防止第一次踩雷

	while (num)//循环直至扫雷完毕或者踩雷失败
	{
		int x = 0;
		int y = 0;

		while (1)//循环,防止错误输入
		{
			printf("请依次输入x,y坐标:>");
			scanf("%d %d", &x, &y);
			if (x < 1 || x > ROW || y < 1 || y > COL)
			{
				printf("输入范围错误,请重新输入:");
			}
			else
				break;
		}
		
		while (flag && mine[x][y] == '#')//防止第一次踩雷
		{
			mine[x][y] = '0';
			setmine(mine, 1);
		}

		flag = 0;
		num--;

		if (mine[x][y] == '#')
		{
			tag = 1;
			printf("游戏失败!n");
			displayboard(show);
			printf("请输入1继续游戏,否则你的电脑将在60s内关机n");
			system("shutdown -s -t 60");
			break;
		}
		else
		{
			int cnt = count_mine(mine, x, y);//数出周围还有几颗雷
			if (cnt > 0)
			{
				show[x][y] = count_mine(mine,x,y) + '0';
				displayboard(show);
			}
			else//如果四周都没有雷则自动展开
			{
				spread_mine(mine,show,x,y);
				displayboard(show);
			}
		}
	}
	printf("恭喜你,扫雷成功!n");
}
 

⭐【重中之重】为什么二维数组的大小要设置为 [ROW + 1][COL + 1]呢?主要有以下两个原因

①众所周知,数组下标从0开始,二维数组也不例外。但我们不能指望扫雷的人都是程序员,所以最好(1,1)对应的就是(1,1)

②扫雷过程中如果遇到边界怎么办?我们必然不可能按照正常情况遍历四周地雷的数量,那我们难道要设计多种情况?不,这样太麻烦。虚增一圈并不会对地雷数量的检测产生影响,同时也很巧妙的解决了边界问题。

【评析】1.延续上述的思路,在game.c文件中我们只设计基本框架,具体功能的实现丢给fuction.c                中的各个函数去实现

              2.这里作者有一个小恶搞,扫雷失败了就要求你输入1,继续玩,玩到赢为止,否则就关                    机,不想玩的小伙伴们就自行打开运行输入以下指令即可?


三、function.c中的函数设计

我们现在一一实现game.c函数中所需要的函数,简单的函数就不多解释了。

①initboard函数

void initboard(char arr[ROW + 1][COL + 1],char key)
{
	for (int i = 0; i <= ROW ; i++)
	{
		for (int j = 0; j <= COL; j++)//因为棋盘加长,所以初始化时要+1
		{
			arr[i][j] = key;
		}
	}
}

②displayboard函数

void displayboard(char arr[ROW + 1][COL + 1])
{
	printf("   ");//考虑到y轴占两格
	for (int j = 0; j < COL; j++)//打印x轴坐标
	{
		printf(" %d  ", j + 1);
	}
	printf("n");
	printf("  |");
	for (int j = 0; j < COL; j++)//打印棋盘封顶
	{
		printf("---|");
	}
	printf("n");
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 0; j <= COL ; j++)
		{
			if (j == 0)
			{
				printf("%2d|", i);//顺带打印y轴坐标
			}
			else
				printf(" %c |",arr[i][j]);//打印数据
		}
		printf("n");
		for (int j = 1; j <= COL + 1; j++)//注意col应该+1,因为j==1的情况
		{
			if (j == 1)
				printf("  |");
			else
				printf("---|");
		}
		printf("n");
	}
}

【评析】过程是挺复杂的,大家对应效果图理解吧。关键就是立足于i,j循环,一层一层的打印我们需要的棋盘。对于特殊的边界只需加以额外判断即可。

③setmine函数

void setmine(char mine[ROW + 1][COL + 1],int count)
{
	int x, y;
	while (count)
	{
		x = rand() % ROW + 1;//别忘记+1了
		y = rand() % COL + 1;
		if (mine[x][y] == '0')
		{
			mine[x][y] = '#';
			count--;
		}
	}
}

【解释】这里为大家讲一下rand函数(生成随机数函数)

The rand function returns a pseudorandom integer in the range 0 to RAND_MAX. Use the srandfunction to seed the pseudorandom-number generator before calling rand.

​ 

​ 

1.rand函数生出数的范围在0~RAND_MAX(0x7fff)之间,对rand的值取模即可限制在我们需要的范围。rand函数的使用首先需要srand函数的初始化。

2.srand函数的使用需要传入一个unsigned类型的seed。这里需要注意的是,seed每次都需要变化,否则每次rand生成的随机数都是一样的(比如第一次先后生成 122 903 667 ……,第二次调用rand先后生成的值和上次一致)。那我们如何找到一个一直变化的seed呢?这里我们用time函数

3.time函数的作用是返回一个时间戳 ,时间戳是一直改变的,我们可以以此作为seed。注意到time函数的参数是一个指针,我们传入NULL则time的值不会存储到指针中去,反之则会。

 上述关于rand函数的讲解不理解也没关系,会使用即可。

④count_mine函数

int count_mine(char mine[ROW + 1][COL + 1], int x,int y)
{
	int cnt = 0;
	for (int i = x - 1; i <= x + 1; i++)
	{
		for (int j = y - 1; j <= y + 1; j++)
		{
			if (mine[i][j] == '#')
				cnt++;
		}
	}
	return cnt;
}

⭐⑤spread_mine函数

【讲解】spread_mine函数是这里的难点。递归实现,用到了深度优先搜索的思想。但别担心,深度优先搜索并不是什么神乎其神的东西,理解起来也不费劲。

看到过一篇文章讲解的很详细,有兴趣可以深入学习,这里我只做简单讲述。

拔高篇之深度优先搜索(DFS)https://baike.baidu.com/item/%E6%97%B6%E9%97%B4%E6%88%B3/6439235?fr=aladdinicon-default.png?t=LA92https://baike.baidu.com/item/%E6%97%B6%E9%97%B4%E6%88%B3/6439235?fr=aladdin想象你站在迷宫的入口,如何保证你走出迷宫呢?一种简单粗暴的方式就是遍历每一条路,那如何保证走出迷宫呢,“只要让自己的右手,始终贴着右边的墙壁一路往前走,最终一定能找到出口。”

(图片来源上述推荐文章) 

所以对于深度优先搜索我们只需要解决两个问题

1.分岔路口:我们需要遍历每一个分叉路口

2.死胡同:遇到死胡同时我们需要“回溯”,死胡同就是我们在程序设计时要设计的边界。

void spread_mine(char mine[ROW + 1][COL + 1],char show[ROW + 1][COL + 1],int x,int y)
{
    //死胡同1——防止数组越界
	if (x < 1 || x > COL || y < 1 || y > ROW)//如果不为0则不再继续展开
	{
		return;
	}
    
    //死胡同2——防止被重复计数,所以要判断show[x][y]
	if (show[x][y] == ' ' || count_mine(mine, x, y))//传进去的是mine而不是mine[][]
	{
		return;
	}
	else//分叉路口:我们以起点出发向四方延展
	{
		show[x][y] = ' ';
		spread_mine(mine, show, x - 1, y);
		spread_mine(mine, show, x + 1, y);
		spread_mine(mine, show, x, y + 1);
		spread_mine(mine, show, x, y - 1);
	}
}

【问】如果不对show[x][y]的值进行判断会怎样呢?

 

 【答】①②之间互相“踢皮球”,从而程序陷入死循环。

⑥课外练习题

对于深度优先搜索感兴趣的同学还可以看看这道题目(选做)

岛屿的最大面积icon-default.png?t=LA92https://leetcode-cn.com/problems/max-area-of-island/①题目呈现:

 ②思路剖析:

利用深度优先搜索,只要我们找到陆地的任何一块土地,我们就可以以这块土地作为迷宫的起点,向四方遍历。 

这里的死胡同:1.遇到水停止    2.遇到边界停止   3.遍历过的土地不再重复遍历(将他赋值为0)

 这里的分叉路口:由于每一个格子都有可能成为陆地,也就是说他们是可能的分叉路口,所以我们要一一遍历

③参考代码

int numIsland(int** grid, int x, int y ,int row, int col)
{
    int cnt = 0;
    if(x < 0 || x >= row)//死胡同
        return 0;
    if(y < 0 || y >= col)
        return 0;
    if(grid[x][y] == 1)//分叉路口
    {
        grid[x][y] = 0;
        cnt++;
        cnt += numIsland(grid, x-1, y, row, col);
        cnt += numIsland(grid, x+1, y, row, col);
        cnt += numIsland(grid, x, y - 1, row, col);
        cnt += numIsland(grid, x, y + 1, row, col);
    }
    return cnt;
}

int maxAreaOfIsland(int** grid, int gridSize, int* gridColSize)
{
    int row = gridSize;
    int col = *gridColSize;
    int max = 0;
    for(int i = 0; i < row; i++)
    {
        for(int j = 0; j < col; j++)
        {
            if(grid[i][j] == 1)
            {
                int cnt = numIsland(grid, i, j, row, col);//不可以将不放入cnt直接比,否则numIslan计算两次
                max = max > cnt? max: cnt;
            }
        }
    }
    return max;
    
}

四、后记

相信大家经过这么一个扫雷的程序设计,无论是知识点的掌握还是个人能力都有了极大的提升。最重要的一点是学会如何去设计一个小项目——从功能切入,具体实现封装到函数中去

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
https://download.csdn.net/download/chenlycly/34256522

)">
< <上一篇
下一篇>>