多文件写代码的方式可以让我们的写的代码的逻辑结构更加清晰,还有就是,代码多的话,全部写在一个 .c文件中会显得很冗杂;一个项目多个文件实现的形式同时也符合实际工作中一个项目的实现过程,有利于我们养成良好的编程习惯。
扫雷游戏可以用三个文件实现:
头文件,用来包含项目用到的所有头文件,一些宏定义也写在这个文件中,还有项目中用到的函数也在这个文件中声明。
这个 .c 文件是用来实现大部分基本函数的。(fun.c 中没有main()函数)
这个 .c 文件是扫雷游戏的主体,包含main() 函数。
有了以上的逻辑,就可以大概写出扫雷游戏的主体代码了,如下:
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入你的选择->");
scanf("%d", &input);
switch (input)
{
case 0:
printf("退出游戏!");
break;
case 1:
printf("******扫雷游戏开始******\n");
game();
break;
default:
printf("输入错误!请按要求输入!");
break;
}
} while (input);
return 0;
}
menu()的实现比较简单,printf()打印想要的效果就行。
//菜单
void menu()
{
printf("*********************\n");
printf("******* 1.PLAY ******\n");
printf("******* 0.EXIT ******\n");
printf("*********************\n");
}
因为程序一开始运行就要打印菜单,所以采用do_while() 循环。
首先,根据前文的对扫雷游戏的逻辑分析可知,game()函数要实现几个功能:
这里很容易想到是用二维数组当作棋盘。确实,设置棋盘这一步骤,我用了两个字符类型的,大小为11*11的二维数组:mine[11][11]和show[11][11]。
mine数组是用来存储雷的布局的,正常情况下不给用户看,只有触发了游戏结束的条件的时候(踩雷了或者游戏胜利了)才打印mine数组给用户看;show数组是给用户看的,是游戏的主要界面,用户输入要排查的坐标,show数组更新一次,显示该坐标周围雷的个数。
这里对设置棋盘为什么用**两个字符类型的,大小为11*11的二维数组**解释一下:
为了方便后续操作。两个数组,一个存储雷的信息(非必要不给用户看);一个存储排查的雷的信息(就是给用户看的);逻辑简单清晰易懂。试想一下,如果只用一个数组,这个数组要存储雷的布局信息,还要存储排查雷的信息,还要判断哪个是要给用户看的才能打印显示,这样逻辑就会很冗杂,后面代码的实现也很复杂。
所以,为了方便后续操作,用两个数组好些。
首先,这个show数组(程序运行从头到尾都给用户看),为了美观性,采用全部初始化为字符 ‘*’ 的形式。如果用数字类型的数组的话,在后面排雷过程中容易混淆。比如采用数字类型数组,把show数组全部初始化为数字8,当你排查的坐标周围也有8个雷的时候,也显示8,这就混淆,也不美观。
所以,以上这些因素决定了show数组定义成字符类型的二维数组比较好。
至于mine数组也定义为字符类型,是为了后面写打印数组的函数的时候,只用封装成一个函数。其实也不是必要的,只是为了后面写代码的时候比较方便。如果mine数组定义为数字类型的,后面封装打印数组的函数的时候,就需要两个函数,一个打印 char 类型,一个打印 int 类型。
为了防止后面排查雷的过程中方便,访问数组的时候不会越界。
考虑到后面排查雷的过程中要计算给定的坐标周围八个位置的雷的个数:
如果是9*9大小的数组的话,排查位置在周围一圈的坐标的时候就只用计算周围5个位置雷的个数;位于四个角坐标的位置,只用排查周围3个位置雷的个数,这就要求要分很多种情况进行分析,否则如果还是按周围8个位置的方式统计周围雷的个数,就会因为数组访问越界而报错。
但是,采用11*11的二维数组就不用这样,只要多出来的位置不布置雷就好(全部为字符‘0’),在统计雷的个数的时候就采用同一个函数就好,不用分类,方便快捷高效。
这里我们在头文件game.h中对行和列进行宏定义,方便以后对游戏的升级或者优化。同时对后面要用到的头文件进行包含:
#include
#include
#include
//宏定义行和列
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
然后在func.c文件中对game()函数进行编写
void game()
{
//创建两个二维数组作棋盘用,
/*mine数组用来存储雷的位置信息,
show数组用来记录排查出的雷的信息*/
char mine[ROWS][COLS];
char show[ROWS][COLS];
//以下两个语句是对mine数组和show数组进行初始化处理
InitBoard(mine, ROWS, COLS, '0');//把mine数组11*11个元素全部初始化为字符‘0’
InitBoard(show, ROWS, COLS, '*');//把show数组11*11个元素全部初始化为字符‘*’
}
接着我们实现 InitBoard()函数:
InitBoard()函数的返回值是 void,有三个参数,可以用for循环遍历二维数组中的每个元素,把所有元素都初始化成想要的字符。
void InitBoard(char board[ROWS][COLS], int rows, int cols, char flag);
//初始化棋盘的函数,有四个参数,棋盘数组的名称,行数,列数,初始化的字符
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char flag)
{
int j;
for (j = 0; j < rows; j++)
{
int k;
for (k = 0; k < cols; k++)
{
board[j][k] = flag;
}
}
}
对两个数组初始化结束后就该往mine数组里面随机布置雷了。
这里用要用到C语言生成随机数相关知识:设置随机数种子,包含相应的头文件等。这里我们利用一个循环实现布置雷的操作,while()循环,用count作为进入循环的条件,count初始化为10,每次循环都生成一对1~9之间的随机数作为雷的坐标,然后对该坐标进行判断,如果该坐标没有布置过雷,即mine数组中该坐标为字符‘0’的话,把‘0’改为‘1’,这样就成功在mine数组中布置了一个雷,同时count–,直到10个雷全部都布置完了,count=0,跳出while()循环。
void SetMine(char mine[ROWS][COLS], int row, int col);
//布置雷的函数,三个参数,数组名称,行数,列数
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT; //在头文件中宏定义了雷的个数为10
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
和前文的初始化棋盘类似,用for循环遍历想要打印的二维数组,用printf()函数输出就好。这里为了界面的美观性,还顺带把行号和列号也打印了。
void Print(char board[ROWS][COLS], int row, int col);
//打印棋盘的函数。三个参数,数组名称,行数,列数
//打印数组
void Print(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("------扫雷游戏------\n");
for (i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= ROW; i++)//只打印中间的九乘九的二维数组
{
int j = 0;
printf("%d ", i);
for (j = 1; j <= COL; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
排查雷这里也用了一个循环实现,因为在没踩雷前,要一直循环输入坐标并判断位置是否为雷。循环的条件是win
同时,在排雷的函数中,也考虑到了用户输入非法的问题,排雷的坐标要求必须在1到9之间,这里用if 语句进行判断,如果用户输入非法,则提示,并继续循环。
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//排查雷的函数,四个参数,布置雷的数组名称,用来展示的数组名称,行数,列数
//排查雷的函数
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)
{
printf("请输入你要排查的坐标->");
scanf("%d %d", &x, &y);
if (x > 0 && x <= ROW && y > 0 && y <= COL)
{
if (mine[x][y] == '1')
{
printf("游戏结束!你踩到雷了!\n");
Print(mine, ROW, COL);
break;
}
else
{
int num = GetMineCount(mine,x,y); //统计(x y)坐标周围的8个位置共有几个雷
show[x][y] = num + '0';
Print(show, ROW, COL);
win++;
}
}
else
{
printf("输入坐标错误!请重新输入\n");
}
}
if (win == ROW * COL - EASY_COUNT)
{
printf("恭喜你!游戏通关!\n");
Print(mine, ROW, COL);
}
}
排雷函数中还包含了计算坐标周围雷的个数的函数,GetMineCount(),该函数返回一个int类型的数据,表明坐标周围雷的个数。
这里说明一下,因为之前在初始化棋盘的时候,为了方便,把布置雷的mine数组设置成了一个字符类型的数组,这里计算周围8个坐标雷的个数就不能简单的相加,而是要转化成数字再相加。
数字1 + 字符‘0’ = 字符‘1’
字符‘1’ - 字符‘0’ = 数字1
所以,坐标(x y) 处周围雷的个数可以把周围8个位置的字符元素加起来再减去8个字符‘0’。
//数x,y位置周围雷的个数
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
int count = (mine[x - 1][y] + mine[x + 1][y] + mine[x + 1][y - 1] + mine[x + 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x - 1][y - 1] + mine[x - 1][y + 1] - 8 * '0');
return count;
}
#include
#include
#include
//宏定义行和列
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
//函数的声明
void menu();//菜单打印
void game();//游戏运行体
void InitBoard(char board[ROWS][COLS], int rows, int cols, char flag);
//初始化棋盘的函数,有四个参数,棋盘数组的名称,行数,列数,初始化的字符
void Print(char board[ROWS][COLS], int row, int col);
//打印棋盘的函数。三个参数,数组名称,行数,列数
void SetMine(char mine[ROWS][COLS], int row, int col);
//布置雷的函数,三个参数,数组名称,行数,列数
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//排查雷的函数,四个参数,布置雷的数组名称,用来展示的数组名称,行数,列数
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//函数的实现
//
//菜单
void menu()
{
printf("*********************\n");
printf("******* 1.PLAY ******\n");
printf("******* 0.EXIT ******\n");
printf("*********************\n");
}
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char flag)
{
int j;
for (j = 0; j < rows; j++)
{
int k;
for (k = 0; k < cols; k++)
{
board[j][k] = flag;
}
}
}
//打印数组
void Print(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("------扫雷游戏------\n");
for (i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= ROW; i++)//只打印中间的九乘九的二维数组
{
int j = 0;
printf("%d ", i);
for (j = 1; j <= COL; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
//布置雷
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
//数x,y位置周围雷的个数
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
int count = (mine[x - 1][y] + mine[x + 1][y] + mine[x + 1][y - 1] + mine[x + 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x - 1][y - 1] + mine[x - 1][y + 1] - 8 * '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)
{
printf("请输入你要排查的坐标->");
scanf("%d %d", &x, &y);
if (x > 0 && x <= ROW && y > 0 && y <= COL)
{
if (mine[x][y] == '1')
{
printf("游戏结束!你踩到雷了!\n");
Print(mine, ROW, COL);
break;
}
else
{
int num = GetMineCount(mine,x,y);
show[x][y] = num + '0';
Print(show, ROW, COL);
win++;
}
}
else
{
printf("输入坐标错误!请重新输入\n");
}
}
if (win == ROW * COL - EASY_COUNT)
{
printf("恭喜你!游戏通关!\n");
Print(mine, ROW, COL);
}
}
//游戏主体
void game()
{
//创建两个二维数组作棋盘用,
/*mine数组用来存储雷的位置信息,
show数组用来记录排查出的雷的信息*/
char mine[ROWS][COLS];
char show[ROWS][COLS];
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
SetMine(mine,ROW,COL);//设置雷的位置
Print(show, ROW, COL);//打印*界面给用户看
Print(mine,ROW,COL);//打印雷的位置
FindMine(mine, show, ROW, COL);//排查雷
}
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include "game.h"
#include
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入你的选择->");
scanf("%d", &input);
switch (input)
{
case 0:
printf("退出游戏!");
break;
case 1:
printf("******扫雷游戏开始******\n");
game();
break;
default:
printf("输入错误!请按要求输入!");
break;
}
} while (input);
return 0;
}
扫雷游布置雷的数组采用的是字符类型的数组,很妙的一点:“数字字符”和“数字”之间的转换。
写代码重要的是实现的思路和逻辑。
• 是否可以选择游戏难度
• 如果排查位置不是雷,周围也没有雷,可以展开周围的一片
• 是否可以标记雷
• 是否可以加上排雷的时间显示