一、前言
扫雷小游戏是C语言数组、函数应用的典型案例。本文将以通俗易懂的语言,讲清楚如何用C语言从0到1实现基础版的扫雷游戏。
二、准备工作
2.1 创建工程文件
以VS2022为例,我们创建一个头文件game.h,两个源文件game.c和test.c。
三个文件各司其职:
game.h是游戏的“使用说明书”。我们在game.h中声明函数但不写函数具体怎么实现,只写“目录”,让test.c知道game.c里有什么功能可用。
game.c是游戏的“发动机”,包含了函数的具体实现。在这个文件里只需专注实现各个函数的功能,不需要管玩家怎么操作菜单。
test.c是游戏的“外壳”,这个文件包含main函数,是程序的入口。它一方面组织流程,根据用户的输入去调用各个功能;另一方面与用户的交互,完成一些打印菜单(欢迎界面)、游戏循环(玩完一局是否再来一局)之类的事情。
注意,要在game.c和test.c中都包含game.h这个头文件。
#include"game.h"
顺带说一嘴,这样把一个工程分成多个文件,不仅逻辑清晰,而且方便多人协作,想象一下,把一个工程的各个功能交给不同程序员实现,每个人负责一个功能模块(一个头文件和一个源文件),然后再统一放在一个源文件中整合,效率是不是提高了?
另外,这样也有利于适当隐藏代码:以这个扫雷游戏为例,可以只把game.c编译产生的一些文件(乱码,可以用来调用函数功能,但用户无法解码成看得懂的代码)以及game.h(只告诉用户函数的使用方式)交给用户,就把game.c里的具体代码保护起来了~
2.2 在test.c中构建整体逻辑
我们想要实现的是:一上来先打印个菜单,让用户选择开始玩游戏还是退出。选择开始玩游戏后进入游戏,玩完一轮后再选择继续玩还是退出。从刚才的描述中可以看出,我们需要一个循环结构来构建游戏逻辑。由于我们需要用户一上来就看到菜单,不需要判断条件再打印菜单,所以选择do while循环,其中需要具体功能的部分先用空的函数临时代替,我们后面再来实现这些函数的功能:
//test.c
int main()
{
int input = 0;
do
{
menu(); //临时代替一下打印菜单的代码,一会儿再来填充
scanf("%d", &input);//让用户输入1/0来选择是否进入游戏
switch (input)
{
case 1:
game();//临时代替一下游戏中的所有功能,一会儿再来填充
break;
case 0:
printf("已选择退出,游戏结束\n");
break;
default://防止用户输入其他数字,设置default分支
printf("选择错误,请重新选择\n");
break;
}
} while (input);//用户输入非零数字,继续循环;用户输入0,退出游戏
return 0;
}
填充一下menu函数,打印一个简单菜单:
//test.c
void menu()
{
printf("********************\n");
printf("*******1.play*******\n");
printf("*******2.exit*******\n");
printf("********************\n");
printf("请输入1或0:>");
}
对了,VS2022中,为了防止使用scanf函数时报错,可以在文件开头加上这一句:
//test.c
#define _CRT_SECURE_NO_WARNINGS
到这里,游戏的整体逻辑就构建完成了,多简单。
接着就要开始实现具体的功能模块了。
三、具体功能模块
在test.c中写上game函数,准备往里填充具体功能模块:
//test.c
void game()
{
}
3.1 创建数组作为游戏棋盘
根据游戏经验我们知道需要两个数组,一个是储存地雷位置信息的“雷区”,一个是玩游戏时展示给用户看某坐标周围有几个雷的“展示板”。我们在game.h函数中定义一下行数和列数这两个常量,先以9*9棋盘为例:
//game.h
#define ROW 9
#define COL 9
用#define来定义常量,而不是把行数列数作为数字直接写进代码里,有以下三个好处:
① 一次修改,全局生效
如果想把棋盘从 9×9 改成 16×16 ,只需要在game.h里修改这两行:
//game.h
#define ROW 16
#define COL 16
而不用在代码里找遍所有写着 9 的地方去改。
② 让代码更“自解释”
char mine[ROW][COL] 一看就知道是“行×列”的棋盘。
如果写成 char mine[9][9] ,别人可能要猜很久这个 9 代表什么。
③ 避免“魔法数字”
直接在代码里写 9 这种没有上下文的数字,被称为“魔法数字”,会让代码难以维护。
用宏定义给数字赋予有意义的名字( ROW 、 COL ),能让代码更专业。
需要注意的是,当我们需要判断边界位置处的坐标周围的地雷数量时,会超出数组的边界。如果要判断边界,需要额外写一些代码。为了避免这种问题,让代码更简洁,我们为这两个数组设置个“虚拟边框”:
//game.h
#define ROWS ROW+2
#define COLS COL+2
3.2 初始化数组
我们定义一个函数用于初始化数组。
//game.c
void InitBoard(char board[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++)
{
board[i][j] = set;
}
}
}
利用这个函数,把“雷区”数组中的元素全都设为'0',“展示板”数组中的元素全都设为'*':
//test.c
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
3.3 布置地雷
我们定义一个函数用于布置地雷。
我们将地雷数量也进行宏定义。
//game.h
#define EASY_COUNT 10
要游戏玩得起来,雷的位置需要随机生成。此时就需要用到rand函数(需要包含头文件<stdlib.h>)。然而rand函数生成的数是从一个“种子”出发生成的伪随机数,默认的种子为1。如果不做更改,每次调用都会生成同一序列的伪随机数。要得到变化的随机数,关键在于使“种子”发生变化。因此我们使用srand函数和time函数(使用time函数需包含头文件time.h),将种子设置为程序运行时的时间,这样每次运行,种子都会变化,我们就可以每次获得不同的随机地雷坐标了。
//test.c
srand((unsigned int)time(NULL));
接着我们就可以完成布置地雷的函数了。
//game.c
void Setmine(char arr[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (arr[x][y] == '0')
{
arr[x][y] = '1';
count--;
}
}
}
3.4 打印展示板
我们定义一个函数用于打印展示板,打印时加上行号列号。
//game.c
void ShowBoard(char board[ROWS][COLS], int row, int col)
{
printf("------扫雷游戏------\n");
for (int k = 0; k <= ROW; k++)
{
printf("%2d", k);
}//这个循环用于打印行号
printf("\n");
for (int i = 1; i <= ROW; i++)
{
printf("%2d", i);//打印行号
int j = 1;
for (j; j <= COL; j++)
{
printf("%2c", board[i][j]);
}//这个循环用于打印每一行行号之后的数组内容
printf("\n");
}
}
3.5 排查地雷
在这一部分,我们希望实现这样的功能:用户输入要排查的坐标,程序判断坐标是否是地雷。如果是雷,显示踩到地雷的提示语,显示出雷区数组,结束游戏。如果不是地雷,在展示板上显示此坐标周围有几个地雷。当用户排查完所有不是雷的坐标时,显示游戏胜利的提示语并结束游戏。
需要注意,判断是否是雷前要判断用户输入坐标的合法性(是否在棋盘范围之内)。
此外注意我们创建的数组中放的数据是char类型的,转换成整数类型需要进行一些运算。
‘0’对应的ASCII码为48,‘1’对应的ASCII码为49,由此可以推断出字符类型的数据减去字符‘0’即可转换成整数类型,反之也一样。
由此我们写出这样的循环,统计周围地雷数的过程有点复杂,我们先用一个函数GetMineCount代替,一会儿再来填充:
//game.c
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);
//printf("%c\n", mine[x][y]);
//判断坐标合法性
if (x >= 1 && x <= row && y >= 1 && y <= row)
{
if (show[x][y] == '*')
{
if (mine[x][y] == '1')
{
printf("BOMB!!!!!踩到地雷啦!\n");
ShowBoard(mine, row, col);
break;
}
else
{
//该坐标不是雷,需要排查周围有几个雷
win++;
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
ShowBoard(show, ROWS, COLS);
}
}
else
printf("该坐标已经排查过,重新输入");
}
else
printf("您输入的坐标无效,请重新输入\n");
}
if (win == ROW * COL - EASY_COUNT)
printf("恭喜你,排雷成功!!!\n");
}
现在我们来填充统计地雷数的函数。由于进入统计周围地雷数这一步的坐标一定是‘0’,我们只需遍历这一坐标周围的三行三列共 9 个坐标,就可以统计出地雷数量了。
game.c
int GetMineCount(char arr[ROWS][COLS], int x, int y)
{
int count = 0;
int i = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
count += (arr[i][j] - '0');
}
}
return count;
}
至此,所有的功能模块都完成了。
四、整合
把所有模块按顺序排列,现在我们的game函数是这样的:
//test.c
void game()
{
//创建数组
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
//初始化数组
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//布置地雷
Setmine(mine, ROW, COL);
//ShowBoard(mine, ROW, COL);//这一行用于检查雷布置得对不对
//打印数组
ShowBoard(show, ROWS, COLS);
//ShowBoard(mine,ROWS,COLS);
//排查地雷
FindMine(mine, show, ROW, COL);
//ShowBoard(mine, ROW, COL);
}
记得要在game.h中声明所有函数:
//声明函数
//棋盘初始化
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void ShowBoard(char board[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);
也要千万小心,函数传参的时候一定不要传错,传的参数和函数定义的参数一定要一致!
那么至此,排雷小游戏就大功告成了。
785

被折叠的 条评论
为什么被折叠?



