前言
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
#include
#include
#include
#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);
//自动展开函数
(头文件内容)
①打印登录界面菜单 → 封装menu函数
②提供游戏进行或退出选择 → switch语句
③实现重复游戏功能 → do{ } while 语句(注:中间省略了游戏过程,先设计最基本框架)
③实现对输入选项进行范围判断,输入范围错误则需要重新输入
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函数中去
①“棋盘式结构” → 创建二维数组
②对数组初始化 → 封装 initboard 函数
(原因:因为要重复play,所以每次都要先对数组初始化,防止上次游戏的干扰)
③显示棋盘 → 封装 displayboard 函数
④埋雷 → 封装 setmine 函数
⑤防止第一次踩雷
⑥判断是否踩雷
#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,继续玩,玩到赢为止,否则就关 机,不想玩的小伙伴们就自行打开运行输入以下指令即可
我们现在一一实现game.c函数中所需要的函数,简单的函数就不多解释了。
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;
}
}
}
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循环,一层一层的打印我们需要的棋盘。对于特殊的边界只需加以额外判断即可。
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函数的讲解不理解也没关系,会使用即可。
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函数是这里的难点。递归实现,用到了深度优先搜索的思想。但别担心,深度优先搜索并不是什么神乎其神的东西,理解起来也不费劲。
看到过一篇文章讲解的很详细,有兴趣可以深入学习,这里我只做简单讲述。
拔高篇之深度优先搜索(DFS)https://baike.baidu.com/item/%E6%97%B6%E9%97%B4%E6%88%B3/6439235?fr=aladdinhttps://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]的值进行判断会怎样呢?
【答】①②之间互相“踢皮球”,从而程序陷入死循环。
对于深度优先搜索感兴趣的同学还可以看看这道题目(选做)
岛屿的最大面积https://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;
}
相信大家经过这么一个扫雷的程序设计,无论是知识点的掌握还是个人能力都有了极大的提升。最重要的一点是学会如何去设计一个小项目——从功能切入,具体实现封装到函数中去。