对于不少编程语言学习者,都困扰于枯燥乏味的上课与刷题中,在此我十分建议大家在学习编程语言不久后尝试自主写一个简易版扫雷游戏。它所需的知识量并不多,重点是函数,基本语法,循环语句,选择语句,随机值的使用等等。若真的能写好这样一个小游戏,对初学者的能力锻炼以及兴趣提升都有不小的帮助,话不多说我们开始讲解吧!
在玩家打开游戏程序时,我们需要给出一个菜单,供玩家选择开始游戏,难度选择,退出游戏等,在这一小段,我们先用代码来实现游戏的进入。在此我们用到一个do-while循环和switch语句来达成。
int input = 0;
do
{
menu();//打印菜单的函数
printf("请选择\n");
scanf("%d", &input);
switch (input)//选择语句
{
case 1:
game();//游戏的主体
break;
case 0:
printf("正在退出游戏\n");
break;
default:
printf("请在0和1中选择\n");
}
} while (input);
在这有几个需要注意的点
①这里的do-while循环的特点就是不论条件是否满足,都会先执行一次循环,而在我们执行第一次循环后,input会被玩家修改,这样的好处是,当玩家完成一次游戏后,可以进入下一次循环,在menu中再次选择是否进行下一次游戏。
②这里的break并非跳出外层的do-while循环,在switch语句的case后需要接上break来跳出switch。switch有一个性质,就是当执行完一个case后,若没有break辅助跳出switch语句,那就会执行下一个case或者defult的内容,那这个switch语句的选择就没有意义了。
在开始写入game主体之前,我们先规划一下整个程序的主体,让我们写的代码逻辑清晰易懂。
我们可以把自定义的各种函数专门创建一个game.c的文件夹承载,但是game.c中的函数test.c怎么可以使用呢?在这头文件就起到一个桥梁的作用,我们把game.c中的函数在game.h中声明,然后把整个game.c文件在test.c中声明,那么test.c文件就可以使用game.c中定义的函数了。
此外我们可以发现,在上图中test.c文件中没有声明stdio.h的头文件,而是声明了一个"game.h"的头文件,在这里,我们把
我们以99的简单模式为例,由于扫雷过程中,有一个展示周围雷的机制,我们可以用1来表示雷,0来表示“空地”,在确认一个坐标后,直接把该坐标周围的数字加和,就可以得到雷的数量。我们可以用一个二维数组来存放我们的”雷“与“空地”,不少人会想创建一个99的数组,可是当我们的坐标在边界的时候,99的数组就会出现无法计算雷的总数的问题,以(3,1)坐标为例,当在1111的数组中(3,1)的坐标排查后求和得到2,而9*9数组中由于缺少三个坐标上的数字,计算就会出错,那代码就会有bug。
我们把扫雷游戏的制作细分为以下几个步骤:
1.制作扫雷的数组
2.在数组中存放雷.
3.向玩家展示数组
4.玩家排查雷
在扫雷游戏中,我们需要用到两个数组,一个用来向玩家呈现用“*”来表示未知区域。另外一个数组用“0”“1”来表达雷与空地,那我们在此将两个数组分别定义为mine数组和show数组。当定义完数组后,就需要对其初始化,让mine数组的所有元素为“0”,show数组的所有元素为"**“。然后再用一个函数向mine数组中随机放雷,即把部分”0“改为"1”。提到更改一个二位数组的元素,不少人会想到用两个for循环嵌套,在此我们要更改两个数组,那写两个循环未免过于冗杂,所以我们可以用一个函数来同时初始化两个数组。
void renew_board(char board[ROWS][COLS],int rows,int cols,char set)
{
int i, j;
for (i = 0; i <= rows; i++)
{
for (j = 0; j <= cols; j++)
{
board[i][j] = 'set';
}
}
}
void game()
{
char show[ROWS][COLS];
char mine[ROWS][COLS];
renew_board(show, ROWS, COLS, '*');
renew_board(mine, ROWS, COLS, '0');
}
在上述代码中,row与col的出现十分突兀我们没有先前定义它,在此我们是用到了宏定义,在game.h中以#define的形式,把9赋给row和col。它有一个作用就是,我们在这个游戏中必然多次使用到数组的行和列,也就是说9和11两个数字会频繁出现,当我们把所有的9和11都改为row,col,rows,cols,以后在更改数据的时候,只需要在宏定义处修改数值,整个游戏的所有数值也就会随之改变,极大提高了修改的效率。在此要注意,宏定义不是一个语句,后面不能接";",否则数值就不是9和11,而是”9;“和"11;"了。
那我们回到这个初始化的函数,想必大部分人都能看懂代码的逻辑,在此我提及一个点。在函数中,由于两个数组需要的元素不同,所以我们需要额外设置一个形参”set“,用于输入我们需要的元素。这样就可以做到“求同存异”,把相同过程的提取出来用函数一起执行,把不同部分用形参修改避免混乱。
既然要使游戏重复可玩,那每次的雷位置就应该不同,那就需要做到随机。在这我们需要用到rand函数以及time函数。
rand函数用于产生一个随机值,但该随机值是一种伪随机,它取决于seed(种子),在不额外修改的情况下,seed默认为1,这就会导致rand函数在每一次生成随机数的时候都使用了同一个种子,进而导致每一次的随机数相同。在这里我引入一个mine craft的概念帮助大家理解:在mc中也有种子一说,在创建世界的时候输入不同的种子就会产生不同的地形,当种子不变地形也就相同。在c语言中seed就是种子,随机数就是地形,那如何让rand函数的seed改变呢?在此我们需要用到time函数,time函数的返回值是1970.1.1到现在的时间秒数,在使用time函数前,需要用到
(unsigned int)time(NULL)
在time函数的内部,需要用到NULL,这样才能获得系统的时间。那我们获得了系统的时间,就需要把他放到rand函数的seed中,只需要把上述代码放在一个srand函数中,srand函数可以修改rand函数的seed,这样在程序后续使用rand函数的时候就会用srand中的值作为seed。而time函数的返回值又是随时间改变的,这样就可以做到每次生成的随机值都不同了。
#include //rand函数的头文件
#include //time函数的头文件
srand((unsigned int)time(NULL))//生成随机的seed
那我们在用rand函数生成随机值的时候,它的范围是极大的,为了将它限制在一个范围,我们可以用一个算式。即取模,我们知道0 <= a%b < b,就是说当a为任意值,让a取模b,得到的值一定小于b但不等于b。
当我们需要一个0到x(包含0不包含x)的范围,那只需要rand()%x。
当我们需要一个1到x(包含1也包含x)的范围,那只需要rand()%x + 1。这个式子的范围是[1,x+1)取整数的情况下,范围就变成了1到x的整数。那我们的随机数生成的代码就是:
#include //放在game.h的头文件中
#include //放在game.h的头文件中
srand((unsigned int)time(NULL))//放在main函数的第一句
void set_mine(char board[ROWS][COLS], int row,int col)
{
int count = easy_count;
while(count)
{
int x = rand() % col + 1;
int y = rand() % row + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
在此,我们解决了随机生成数字及其范围限定的问题,就再来简单讲下这个set_mine函数的结构。我们先宏定义一个easy_count,来存放不同难度的雷的数量,方便后续修改。为了限制雷的数量,每放置一个雷就count–,可以保证刚好放10个雷。那如果真的有这么巧,两次存放的雷在一个位置,那我不就会少存放一个雷了吗?于是用一个if语句,只有当前坐标为‘0’即不是雷时才能存入一个雷。
此外,我们的雷存放在1111数组中的99的位置,所以我们的数组下标为1-9,对应的数组位置就是2-10,刚好是最中间9*9的位置。
对于一个二维数组的打印,相信大部分人已经十分熟练,只需要用两个变量i和j嵌套for循环然后用数组的行数和列数限制就可以打印一个数组。在此我直接展示print_board()函数,再解释部分细节。
void print_board(char board[ROWS][COLS],int row,int col)
{
int i, j;
printf("-------------------\n");
for (i = 0; i <= col; i++)
{
printf("%d ", i);
}//打印横坐标
printf("\n");
for (i = 1; i <= row; i++)//打印数组
{
printf("%d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("-------------------\n");
}
①我在每个字符的打印占位符后接了一个小空格,这样可以让数字更加松散,让玩家看起来更舒服。
②由于知识的限制,我用输入坐标的方式来排雷,那就需要向玩家展示出坐标,所以在打印数组前我用了一个循环来打印横坐标,在j所属的循环开始前打印i,表示纵坐标。这里为什么不直接打印“0 1 2 3 4 5 6 7 8 9”来表示?答案是这个i是被col限制的,col与难度挂钩,当我们修改难度,只需要在宏定义修改col就可以修改这里的横坐标,方便后续代码的修改。
③在这个1111的数组中,我们只需要中间99的数组,所以这里i和j的初始值为1,终止值为col和row。
在雷的查找的过程中,玩家需要重复排雷,我们也要重复向玩家展示数组,要在扫完所有雷的时候判断游戏的胜利,在触碰到雷后判断失败,在扫到空地的时候向玩家展示周围的雷数。在此我分开讲述,尽量细致。
局部变量的定义:
在这个find_mine函数中我们需要创建x,y用于存储玩家输入的坐标,再用一个变量来判断何时结束循环,我命名它为over。
扫雷的循环条件:
对于这个简易版扫雷,我们的结束标志就是找出所有的‘0’。整个棋盘共有rowcol个区域,有easy_count个雷,那‘0’就有rowcol - easy_count个,我们只需要在输入坐标没有雷的时候over++就可以记录已经扫清‘0’的数目。在此while循环的条件就是
while (over<row * col - easy_count)
对输入坐标的判断:
玩家输入的坐标有三种情况,即坐标为‘1’坐标为‘0’以及坐标不在数组范围内。这里用if和else if语句来判断,分别输出结果。判断标准就是mine[x][y]内容是‘0’还是‘1’。
当扫到雷地的时候,要输出“你被炸死了”,然后向玩家展示所有雷的分布情况,就是用先前写的打印函数然后传入mine数组即可。最后还需要使用一个break来强制跳出循环。
当扫到空地的时候就要计算周围的雷的数量,通过下面的图可以发现,我们只需要把这个3*3的范围内的数字加和(除去最中间的),就可以得到总雷数。但由于我们输入的是字符,这里的返回值是ASCII码值,所以我们要减去8个‘0’的ASCII码值,然后再在接受返回值的时候再加上一个‘0’的ASCII码值,这样就可以往数组内输入我们需要的数字的ASCII码值,在下一次打印数组的时候,也就会打印对应的字符。在此我们制作了一个sum函数,计算雷数。
int sum(char board[ROWS][COLS], int x, int y)
{
int num =board[x + 1][y] +
board[x - 1][y] +
board[x][y + 1] +
board[x][y - 1] +
board[x + 1][y + 1] +
board[x - 1][y - 1] +
board[x - 1][y + 1] +
board[x + 1][y - 1] - 8 * '0';
return num;
}
那么循环主体就应该如下:
void find_mine(char show[ROWS][COLS],char mine[ROWS][COLS],int row,int col)
{
int x, y, over;
int count = easy_count;
while (over<row * col - easy_count)
{
print_board(show, ROW, COL);
printf("请输入你想排查的坐标\n");
scanf("%d %d", &x, &y);
if (mine[x][y] == '0')
{
show[x][y] = sum(show, x, y) + '0';
over++;
}
else if (mine[x][y] == '1')
{
printf("很遗恨你被炸死了");
print_board(show, ROW, COL);
break;
}
else
{
printf("坐标非法,重新输入");
}
}
当我们写好循环主体后,还缺少一个玩家游戏成功的回馈。我们是如何判断游戏成功的?那就是while循环是条件不满足而停止循环的时候,即over = row * col - easy_count,当玩家是因为扫雷错误,导致break跳出循环,该条件不满足,就无法触发游戏成功的回馈。,那我们最后的函数就是:
void find_mine(char show[ROWS][COLS],char mine[ROWS][COLS],int row,int col)
{
int x, y, over;
int count = easy_count;
while (over<row * col - easy_count)
{
print_board(show, ROW, COL);
printf("请输入你想排查的坐标\n");
scanf("%d %d", &x, &y);
if (mine[x][y] == '0')
{
show[x][y] = sum(show, x, y) + '0';
over++;
}
else if (mine[x][y] == '1')
{
printf("很遗恨你被炸死了");
print_board(show, ROW, COL);
break;
}
else
{
printf("坐标非法,重新输入");
}
}
if (over == row * col - easy_count)
{
printf("恭喜你,扫雷成功");
}
}
当前这个扫雷游戏模式较为简单,部分c语言入门者可以尝试挑战一下。在此我提出几个问题,供大家思考:
①如何在扫到空地的时候把一整片空地展示出来?
②如何向玩家显示扫雷用时?
③如何让玩家标记雷地?
④如何让玩家可以自主修改难度?
⑤如何让程序在每次向玩家展示新的雷地的时候清空先前生成的文字?
我会在不久后发布扫雷2.0,向大家解答这些问题。