对于初学编程者,刚开始的那一段时间来说,C语言的确是有点枯燥的,因为学习这么长时间来看好像并没有做出来什么能被其他专业同学认可的东西,和学PS或者Pr的同学不一样,他们学习一节课之后立马可以自己实践出可见的效果。
学习写代码,拿回家给阿姨看:“啊?这什么啊?有什么用啊?”
但是数媒就不一样了:“哇,修的这个照片真好看”。
这就是区别呐。
所以今天咱们尝试用C语言实现一个简单小游戏,也能让学习C语言一段时间的同学们感到自己的成果。
目录
C语言需要学习到什么程度可以独立实现这个小游戏?
在我看来呢,这个游戏对于初学者来说虽然结构复杂,但是的确涉及的知识点很少,我们只需要简单的理解:分支、循环、二维数组、函数调用。即可实现小游戏,甚至完全不会涉及到指针和结构体,最重要的就是分支结构,所以小伙伴们可以放心的食用本文章。
扫雷这个游戏大家应该都非常熟悉的,初中微机课经常偷偷玩呐,它呢初级难度游戏界面非常简单,看起来只有一个9*9的棋盘,所以我们完全可以用printf函数打印出来我们的棋盘。整体来说想要简洁的实现这个游戏是非常简单的,接下来给大家描述一下大致的思路。
大致思路
游戏主体流程:新建棋盘,初始化棋盘,随机布置雷区,打印出玩家界面棋盘,判断玩家键入坐标并作出回应,接下有三种情况:
(1). 游戏失败,坐标有雷,结束游戏并显示雷区。
(2). 游戏继续,该坐标无雷显示周围雷数。
(3). 游戏胜利,打印出雷区。
代码实现流程
- 首先我们肯定得有个棋盘,这边我们可以用二维数组实现我们代码的虚拟棋盘。扫雷游戏呢,虽说在我们玩家看来是只有一个棋盘,雷只不过被覆盖住了,但其实我们后台需要两个棋盘进行操作,一个是给用户看的,一个是给我们的代码用来判断的雷区。
- 有了棋盘之后我们就需要初始化棋盘,把两个棋盘统一初始化方便处理。
- 给雷区棋盘布置雷区,布置雷区可以用随机值函数布置。
- 然后我们需要实现一个打印棋盘的函数,每走一步之后把给用户的看的棋盘更新并打印。
- 当布置完雷区之后游戏就可以直接开始了,游戏开始之后即进入循环,持续获取玩家键入坐标并判断,直至游戏结束。
需要构建的函数
void menu();//菜单
void game();//游戏主体
void InitBoard(char board[ROWS][COLS],int rows,int cols, char boom);//初始化棋盘
void InputBoard(char board[ROWS][COLS], int row, int col);//打印棋盘
void MakeBoom(char board[ROWS][COLS], int row, int col);//造雷区
int GoBoom(char BoomBoard[ROWS][COLS], char IfBoard[ROWS][COLS], int row, int col);//走一步
int WinBoom(char ifboard[ROWS][COLS], char boomboard[ROWS][COLS], int row, int col);//判断是否赢了
char int_char(int n);//字符转换整形
void openUp(char BoomBoard[ROWS][COLS], char IfBoard[ROWS][COLS], int x, int y);//外加函数-展开
可以看到游戏需要用到的函数是非常少的。下来我们就用游戏进行的步骤逐一实现代码。
需要用到的头文件和预定义常量
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define BOOM 10
这里我们需要的头文件只要最常用的:标准输入输出头文件、standard library标准库函数头文件、时间头文件,预定义几个常量值分别是:棋盘的显示行长度和列长度、实际行长度和列长度、棋盘中雷的个数。这样方便我们调试以及更改游戏难度时直接更改预定义常量即可。
实现函数
main函数
主函数部分很简单,运行程序后打印菜单,然后分支语句(三个分支):
1.开始游戏,进入game函数
2.退出游戏,退出循环,结束程序。
int input = 0;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
break;
default:
printf("Error Again");
break;
}
} while (input);
game函数
进入game函数之后创建两个二维数组,一个是雷区棋盘,一个是显示的棋盘。
为了方便统一处理,两个棋盘都创建为 char类型的二维数组。实际行长和列长分别为ROWS、COLS。为什么实际行长和列长要比打印的行长和列长多2后面会解释。
创建完两个数组之后就对他们进行初始化操作。
初始化完成之后再对雷区进行布雷。
打印出棋盘,此时棋盘应该为全 ' * ' 。
进入循环,执行GoBoom函数,获取玩家输入坐标,并判断该坐标是否踩雷:
若踩雷:打印雷区并退出游戏。
若未踩雷:打印棋盘,并判断是否获胜(即排完雷):
获胜:打印雷区,退出游戏。
未获胜:继续循环。
代码:
void game()
{
char IfBoard[ROWS][COLS];
char BoomBoard[ROWS][COLS];//建两个数组
InitBoard(IfBoard, ROWS, COLS, '*');//初始化显示棋盘
InitBoard(BoomBoard, ROWS, COLS, '0');//初始化雷区
MakeBoom(BoomBoard, ROW, COL);//布置雷区
InputBoard(IfBoard, ROW, COL);//打印棋盘
while (1)
{
if (GoBoom(BoomBoard, IfBoard, ROW, COL) == 0)
{
printf("很遗憾,踩雷了\n");
printf("雷区:\n");
InputBoard(BoomBoard, ROW, COL);//打印雷区
break;
}
InputBoard(IfBoard, ROW, COL);
if (WinBoom(IfBoard, BoomBoard, ROW, COL) == 1)//每次走完一步判断赢了没
{
printf("恭喜你获胜了\n");
printf("雷区:\n");
InputBoard(BoomBoard, ROW, COL);//打印雷区
break;
}
}
}
InitBoard函数
初始化棋盘函数,传参棋盘、行列的长度、传入棋盘的展现样式字符。
遍历整个二维数组,给数组元素赋值。
void InitBoard(char board[ROWS][COLS], int rows, int cols, char boom)
{
for (int i = 0; i < rows ; i++)
{
for (int j = 0; j < cols; j++)
{
board[i][j] = boom;
}
}
}
InputBoard函数
打印棋盘函数,为了棋盘的美观和便捷性,我选择给行和列给上序号,方便玩家查看坐标,也使棋盘的美观度提升。
为了方便,打印行列和打印棋盘函数都可以一起完成,先循环打出第一排,代表每一列号,
然后遍历二维数组,每遍历一行打印一个 ' n ' 换行, 这样就可以很好的打印出棋盘了。
代码:
void InputBoard(char board[ROWS][COLS],int row,int col)
{
printf("\n");
for (int i = 0; i <= row; i++)
{
printf("%d ",i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ",i);
for (int j = 1; j <= col; j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
}
MakeBoom函数
布置雷区函数,为了保证雷区的随机性,我们需要用到time函数和srand随机值函数,他们分别包含在 <stdlib.h> <time.h>文件中。
具体srand函数的用法我就不赘述了,可以参考C 库函数 – srand() | 菜鸟教程 (runoob.com)
因为srand函数需要给值初始化,所以我们再用到time函数。
这边就需要给主函数里面加一句
srand((unsigned int)time(NULL));
调用srand函数,传给它的参数为:调用time函数并把time函数的返回值强制类型转换为(unsigned int)无符号整型。
这样我们的srand函数就完成了初始化,可以在布雷函数里面调用它了。
函数开始,进入循环,循环次数为炸弹数量次数次。
然后用srand得到两个值,并用该数值分别取行列长度的余数,这样就可以保证随机值的大小在二维数组的合理下标范围内。
将这两个值分别作为行号和列号也就是二维数组的两个下标值,将雷区棋盘的该坐标的位置改为为字符 ' 1 ',也就完成了在该坐标位置的布雷,完成一次布雷之后继续循环直到把雷布够。
为了保证我们随机到的坐标位置不会重复,导致雷量减少,每一次得到随机值之后应该进行条件判断,若该位置已经有则直接进行下一次循环。
代码:
void MakeBoom(char board[ROWS][COLS], int row, int col)
{
int k = 0;
while (k < BOOM)
{
int rw = rand() % row;
int cl = rand() % row;
if (board[rw + 1][cl + 1] == '0')
{
board[rw + 1][cl + 1] = '1';
k++;
}
}
}
GoBoom函数
布置完雷区并且可以打印玩家界面棋盘之后,游戏可以正式开始了,这时就需要让玩家输入坐标位置来进行判断。
该函数实现的是,获取一个玩家键入的坐标,然后判断该位置是否为合理的坐标位置,位置合法则再判断该位置有没有雷,若有雷游戏结束。无雷,则将此位置的数据改为周围雷数并继续循环。
仔细回想我们现实的扫雷游戏,在我们点了一个坐标之后,一般只有三种情况:
1.有雷,游戏结束。
2.无雷,周围一格内有雷,该坐标显示周围8个格子的总雷数。
3.无雷,但是周围一格内也无雷。
前面两种情况都比较好处理,但是遇到第三种情况时,如果该位置的操作和第二种情况统一处理,写上数值0,会导致玩家和我们都明知道周围8个格子无雷,却还要一个一个的输入这几个坐标并展开。
为了让游戏显得更高级一些,联系实际的扫雷,我们还应该用到一个展开的函数,如果判断玩家键入坐标位置没有雷,那么就调用该展开函数。
代码:
int GoBoom(char BoomBoard[ROWS][COLS],char IfBoard[ROWS][COLS],int row, int col)
{
int rw, cl;
do
{
printf("请输入要排查的坐标:>");
quanju = 0;
scanf("%d%d", &rw, &cl);
if (rw < 1 || cl < 1 || rw>row || cl>col)
printf("坐标非法,重新输入:>\n");
} while (rw<1 || cl<1 || rw>row || cl>col);
if (BoomBoard[rw][cl] == '1')//每次输入判断有没有踩雷,踩了返回0
{
return 0;
}
else
{
openUp(BoomBoard, IfBoard, rw, cl);//这个情况就是 它不是雷
return 1;
}
}
openUp函数
实际上到这里我们的游戏已经基本上完成了,但是为了更好的还原游戏,我们需要实现一个用来展开空白区域的函数,就比如你在玩扫雷的时候点到了一大片没有雷的区域,这片连续的空白区域回自动扩展。接下来我们实现这个功能,可以说这个函数是整个游戏项目最复杂的函数了,会涉及到简单的一次递归。
我们这个函数首先要实现的是判断周围的雷数量,所以一开始需要创建一个变量k来记录探索到的雷数量,然后遍历判断3*3区域内九个格子的雷,若是有雷则 k++,无雷继续遍历。那么问题来了?怎么得到玩家键入坐标位置一格内的8个格子的坐标呢?
根据简单的画图能看到,比如给定坐标(x,y)=(2,2),则它的周围8个坐标则为:
(x-1,y-1),(x-1,y+0),(x-1,y+1)....
那么我们就可以很简单的用嵌套for循环的循环变量为-1 至 1 正好可以得到这8个坐标,因为能走到这一步中间的坐标必然是没有雷的,所以不需要排除中间坐标。
循环结束后我们就得到了玩家键入坐标位置周围的雷数,那么现在需要做的就是判断变量k的值是否为0
若不为0:
则在此位置显示k的值。这边就需要我们注意一下了,因为我们的棋盘是两个char类型的数组,而k的值是整型,这边就需要我们把k的值转换为字符型并存入数组。根据ascⅡ码
表我们知道字符型的 '1' 的整型的1二进制码是不一样的,转换的方法很多,这这边我们可以用一个间的函数实现整型1-8到字符 '1' - '8' 的转换,函数代码贴到后面了,就是用switch选择语句。
有更简单的方法就是给ascⅡ码+49,它就是对应的数字了,但是这个方法会涉及到一些复杂的理论我也讲不清楚0.0,就用笨办法吧...
若为0:
如果这片区域的雷数为0,则展开此区域,用代码实现就是分别用这八个坐标调用我们的openup函数,也就是函数递归,但是因为递归实在不好控制,所以我们得严格判断,首先可以加一个全局变量。
int quanju = 0;
每一次调用递归使全局变量的值++,若是递归次数达到一定值结束递归,这样我们的函数就暂时可控了,还有就是在调用递归时候一定要排除玩家键入坐标本身,要不然会进入死循环,我这边的处理方法是在递归前将玩家键入坐标的值暂时改为其他符号,判断是否递归时就可以判断坐标是什么符号,一定满足该区域无雷并且该区域还未打开,比如咱们初始化棋盘的时候,未进行操作的棋盘全是 ' * ',那么判断递归是就需要判断它是不是*号即可。递归结束把键入坐标改为空格字符即可。
代码:
void openUp(char BoomBoard[ROWS][COLS], char IfBoard[ROWS][COLS], int x, int y)
{
int k = 0;
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
if (BoomBoard[x + i][y + j] == '1')
{
k++;
}
}
}
if (k == 0)
{
if (quanju >= 9)
return;
IfBoard[x][y] = '-';
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
if (IfBoard[x + i][y + j] == '*' && (BoomBoard)[x + i][y + j] != '1')
{
quanju++;
openUp(BoomBoard, IfBoard, x + i, y + j);
}
}
}
IfBoard[x][y] = ' ';
}
else
{
IfBoard[x][y] = int_char(k);
}
return;
}
int_char函数
char int_char(int n)
{
switch (n)
{
case 1:
return '1';
case 2:
return '2';
case 3:
return '3';
case 4:
return '4';
case 5:
return '5';
case 6:
return '6';
case 7:
return '7';
case 8:
return '8';
case 9:
return '9';
default:
return 'n';
}
}
接下里贴上完整的打包代码:
完整代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define BOOM 10
void menu();//菜单
void game();//游戏主体
void InitBoard(char board[ROWS][COLS],int rows,int cols, char boom);//初始化棋盘
void InputBoard(char board[ROWS][COLS], int row, int col);//打印棋盘
void MakeBoom(char board[ROWS][COLS], int row, int col);//造雷区
int GoBoom(char BoomBoard[ROWS][COLS], char IfBoard[ROWS][COLS], int row, int col);//走一步
int WinBoom(char ifboard[ROWS][COLS], char boomboard[ROWS][COLS], int row, int col);//判断是否赢了
char int_char(int n);//字符转换整形
void openUp(char BoomBoard[ROWS][COLS], char IfBoard[ROWS][COLS], int x, int y);//展开
int quanju = 0;
//菜单函数
void menu()
{
printf("\n------------------\n");
printf("------1.Play------\n");
printf("------0.Exit------\n");
printf("------------------\n");
}
//游戏函数主体
void game()
{
char IfBoard[ROWS][COLS];
char BoomBoard[ROWS][COLS];//建两个数组
InitBoard(IfBoard, ROWS, COLS, '*');//初始化显示棋盘
InitBoard(BoomBoard, ROWS, COLS, '0');//初始化雷区
MakeBoom(BoomBoard, ROW, COL);//布置雷区
InputBoard(IfBoard, ROW, COL);//打印棋盘
while (1)
{
if (GoBoom(BoomBoard, IfBoard, ROW, COL) == 0)
{
printf("很遗憾,踩雷了\n");
printf("雷区:\n");
InputBoard(BoomBoard, ROW, COL);//打印雷区
break;
}
InputBoard(IfBoard, ROW, COL);
if (WinBoom(IfBoard, BoomBoard, ROW, COL) == 1)//每次走完一步判断赢了没
{
printf("恭喜你获胜了\n");
printf("雷区:\n");
InputBoard(BoomBoard, ROW, COL);//打印雷区
break;
}
}
}
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char boom)
{
for (int i = 0; i < rows ; i++)
{
for (int j = 0; j < cols; j++)
{
board[i][j] = boom;
}
}
}
//打印棋盘
void InputBoard(char board[ROWS][COLS],int row,int col)
{
printf("\n");
for (int i = 0; i <= row; i++)
{
printf("%d ",i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ",i);
for (int j = 1; j <= col; j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
}
//布雷
void MakeBoom(char board[ROWS][COLS], int row, int col)
{
int k = 0;
while (k < BOOM)
{
int rw = rand() % row;
int cl = rand() % row;
if (board[rw + 1][cl + 1] == '0')
{
board[rw + 1][cl + 1] = '1';
k++;
}
}
}
//整形转字符
char int_char(int n)
{
switch (n)
{
case 1:
return '1';
case 2:
return '2';
case 3:
return '3';
case 4:
return '4';
case 5:
return '5';
case 6:
return '6';
case 7:
return '7';
case 8:
return '8';
case 9:
return '9';
default:
return 'n';
}
}
//是否赢了 返回1赢了,0没有
int WinBoom(char ifboard[ROWS][COLS],char boomboard [ROWS][COLS],int row, int col)
{
for (int i = 1; i <= row; i++)
{
for (int j = 1; j <= col; j++)
{
if (ifboard[i][j] == '*' && boomboard[i][j] == '0')
return 0;
}
}
return 1;
}
//输入坐标,走一步
int GoBoom(char BoomBoard[ROWS][COLS],char IfBoard[ROWS][COLS],int row, int col)
{
int rw, cl;
do
{
printf("请输入要排查的坐标:>");
quanju = 0;
scanf("%d%d", &rw, &cl);
if (rw < 1 || cl < 1 || rw>row || cl>col)
printf("坐标非法,重新输入:>\n");
} while (rw<1 || cl<1 || rw>row || cl>col);
if (BoomBoard[rw][cl] == '1')//每次输入判断有没有踩雷,踩了返回0
{
return 0;
}
else
{
openUp(BoomBoard, IfBoard, rw, cl);//这个情况就是 它不是雷
return 1;
}
}
//写个函数,用来判断周围并展开
void openUp(char BoomBoard[ROWS][COLS], char IfBoard[ROWS][COLS], int x, int y)
{
int k = 0;
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
if (BoomBoard[x + i][y + j] == '1')
{
k++;
}
}
}
if (k == 0)
{
if (quanju >= 9)
return;
IfBoard[x][y] = '-';
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
if (IfBoard[x + i][y + j] == '*' && (BoomBoard)[x + i][y + j] != '1')
{
quanju++;
openUp(BoomBoard, IfBoard, x + i, y + j);
}
}
}
IfBoard[x][y] = ' ';
}
else
{
IfBoard[x][y] = int_char(k);
}
return;
}
结束语
好了,到这里我们的小游戏已经基本实现,在主函数文件加上函数声明即可正常运行,但是相较于真正的扫雷游戏还是有很大的差距,但毕竟我们涉及到的语法都是C语言基础的语法,能做到这一步已经很不错了,当然游戏还是有一些bug没有解决,比如玩家键入坐标为最边缘的坐标时,递归函数就会出现越界的问题,为了代码的简洁这些东西也就忽略掉了。
感兴趣的小伙伴可以自己尝试解决bug,或者再优化代码,也欢迎各位大佬指出代码中的不足,小摆会及时改正,感谢!!!
如果文章对您有帮助可以点个赞支持一下哦。再次感谢~