项目思路
经过一段时间认真的学习和沉淀,今天我们今天来个写一个大项目,用c语言所学的知识写一个《扫雷游戏》,那《扫雷游戏》怎么写呢?让我们先来看一下网页上的《扫雷游戏》是怎么做的
图例1
首先《扫雷游戏》
1.得有一个扫雷的棋盘,怎么把棋盘显示出来
2.我们用什么样的方式来布置雷
3.排查雷后在周围有雷的情况下会返回数字,怎么返回
先解决第一条,扫雷的棋盘,我们可以用二维数组来搞定,往里面存字符,然后再打印出来,我们就先叫他show数组吧,最终的效果我们如图例2所示。
图例2
好,现在我们有了一个扫雷的棋盘,但是现在这个扫雷的棋盘只是一个空壳,里面是没有储存雷分布的信息的,那怎么办呢?
那我们就再创建一个和扫雷棋盘一样大小的二维数组,这个数组是不打印出来的, 我们就先叫他mine数组,这个数组用来储存雷在棋盘上的分布情况,里面只放两种字符,‘1’和‘0’;字符‘1’,就表示该位置有雷,‘0’则表示没有雷,最后把mine数组和打印出来的棋盘进行链接,如图例3。
图例3
但是现在又出现一个新的问题,如果扫雷的位置是在棋盘的边缘怎么办,如果还要继续硬扫那将会越界访问,这是我们不希望的,所以我们把数组的宽高都多加上2,在棋盘周围加上了一圈可访问的位置,这样就不会越界访问了。
代码实现
主体程序的实现(text.c)
讲完了思路后我们开始来写项目了,为了代码更加容易阅读,我们把这个项目划分出三个文件,分别是:game.c(函数的实现)、game.h(函数的声明,或者是叫汇总)、text.c(运行的程序),再说一下这个项目所能实现的功能:
首先我们写主函数内部的代码,一开始先创建input变量,用来储存选择的值;再创建一个do...while循环,一进到循环里打一个菜单,我们用mune();函数来实现,然后是根据输入的整型进行跳转,输入1进入游戏,输入0就结束程序,未定义的就重新输入
void mune()
{
printf("****************************************\n");
printf("******* 1.play *********\n");
printf("******* 0.exit *********\n");
printf("****************************************\n");
}
int main()
{
//扫雷游戏
int input = 0;
srand((unsigned int)time(NULL));
do
{
mune();//打印菜单
printf("请输入你的选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏!\n");
break;
default:
printf("选择错误请重新选择!\n");
break;
}
} while (input);
return 0;
}
好,页面基本介绍完了,我们现在开始进入游戏函数中。
game函数系列的实现(game.c)
void game()
{
//创建两个数组
char mine[ROWS][COLS] = { 0 };//一个是数据源数组,初始化全放“0”
char show[ROWS][COLS] = { 0 };//一个是棋盘数组,初始化全放“*”
//初始化棋盘,把数组内的内容都定义为“0”
//初始化函数
Intboard(mine,ROWS,COLS,'0');//注意;是单引号(‘’),不是双引号("")
Intboard(show,ROWS,COLS,'*');
//布置雷
checkboard(mine,ROW,COL);
//打印扫雷棋盘
Decboard(show, ROWS, COLS);
//排查雷
FindMine(mine, show, ROW, COL);
}
一进入游戏函数中我们就先创建两个char类型的数组,两个数组的大小都是一样的,至于为什么,上面已经说了,这里就不多赘述了,你问我ROWS和COLS,这两个东西是啥?先别急,等会讲到。还有,创建完数组后记得给它们赋值,养成良好编码习惯。
创建完数组后,下面就是一系列的函数运用了,在开头我们讲到,为个代码更容易看的懂,不会那么乱,我们把项目分为了三个文件,其中game函数中一系列的函数本体就都放在了game.c这个文件中,下面我们先来一一介绍一下这些函数
Intboard函数【初始化】
//初始化棋盘
void Intboard(char arr[ROWS][COLS], int rows, int cols,char set)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
该函数的作用就是来初始化函数的,它所要传的参数从左到右依次为(要初始化的数组,行数,列数,要初始化的字符)
Decboard函数【打印】
//打印扫雷棋盘
void Decboard(char arr[ROWS][COLS], int rows, int cols)
{
printf("----------扫雷游戏----------\n");
//在棋盘打印之前先打印一行列编号,像个瓶盖一样,盖在棋盘上面
for (int i = 0; i < rows-1; i++)
{
printf("%d ", i);
}
printf("\n");
for (int i = 1; i < rows-1; i++)
{
//在每一行开始打印之前,先打印行编号
printf("%d ", i);
for (int j = 1; j < cols-1; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
}
他的作用是来打印棋盘和一些界面装饰物的,它所要传的参数从左到右依次为(要打印的数组,打印行数,打印列数)打印效果如下图。
checkboard函数【布置雷】
//布置雷
void checkboard(char arr[ROWS][COLS], int row, int col)
{
int count = esey_t;
while (count)
{
int x = rand() % row + 1;//%row+1,把随机数的范围控制在1~9
int y = rand() % col + 1;//% col + 1,也是一样
if (arr[x][y] == '0')
{
arr[x][y] = '1';
count--;
}
}
}
该函数的作用是布置雷, 它所要传的参数从左到右依次为(要放雷的数组,数组的行数,数组的列数)
函数过程讲解
一进入进入函数,创建变量count,作用是储存布置雷的个数,然后进入while循环,rand函数,生成随机数,%row+1和%col+1的作用是把布置雷的坐标控制在合理的范围内,两个随机数结合,就产生出了一个随机坐标,if语句,判断该数组的随机坐标是否被布置过雷,布置过则为‘1’,没布置过为‘0’,为‘0’就进入if语句,赋值为‘1’,count--,直到count值为0,跳出while循环
FindMine函数【排查雷】
//排查雷
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 - esey_t)
{
printf("请输入你要排查的坐标:>");
scanf("%d %d", &y, &x);
//坐标合法判断
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//判断该坐标是否有雷
if (mine[x][y] == '1')
{
printf("抱歉,你被炸死了\n");
Decboard(mine,ROWS,COLS);
break;
}
/*else if (mine[x][y] != '0')
{
printf("你干嘛!已经排查过了,请从新输入。");
}*/
else
{
int count = GetMineCount(mine, x, y);
show[x][y] = count+ '0';
Decboard(show,ROWS,COLS);
//开挂
//Decboard(mine, ROW, COL);
win++;
}
}
else
{
printf("你在搞什么飞机!坐标非法请重新输入\n");
}
}
if (win == row * col - esey_t)
{
printf("恭喜你,排雷成功\n");
Decboard(mine, ROW, COL);
}
}
该函数的作用就是接收玩家输入的值,并根据规则给出相应的反应;它所要传的参数从左到右依次为(mine数组【存储雷信息的数组】,show数组【扫雷的棋盘数组】,数组的行数,数组的列数)
函数过程讲解
进入函数,创建三个变量,x、y、win,x和y作用是储存玩家输入的坐标值;win记录次数,作为判断赢得游戏的条件;然后是while循环,判断条件我们等一下再讲,我们进入循环,首先是让玩家输入要排查雷的坐标,然后判断输入坐标的合法性,也就是要输入属于棋盘内的坐标参数,x和y都要大于等于1,且不能超过各自对应的行或列,如果输入不合法,打印语句,重新输入
接下来就是判断有没有雷了,判断的条件就是判断mine数组【储存雷信息的数组】第y-1行,第x-1个内存入的字符是否为‘1’,如果结果为真,那是就踩到雷了,那就打印语句,打印mine棋盘,游戏结束。
GetMineCount函数(返回周围有几个雷)
若结果为假,那就证明没有踩到雷,那就排查周围雷的个数,并返回值到该坐标,说回程序,为假进入else,创建储存返回值的变量count,并赋值GetMineCount函数,那这个GetMineCount函数又是何方神圣呢?下面就是 GetMineCount函数函数的实现过程了
int GetMineCount(char mine[ROWS][ROWS], int x, int y)
{
return (mine[x-1][y-1] + mine[x-1][y] + mine[x-1][y+1] + mine[x][y-1] + mine[x][y+1] + mine[x+1][y-1] + mine[x+1][y] + mine[x+1][y+1] - 8 * '0');
}
函数讲解
这个函数只有一行语句,就是返回了一大串的由mine数组元素相加的式子,那返回的这一大段式子有什么作用呢?原因如上图,因为有雷的坐标里面存放的字符是‘1’,没雷的坐标内存放的是字符‘0’,所以只要我们把x,y周围一圈坐标内的【字符值】ASCII值加起来再减去8个字符的‘0’ASCII值,得出来的数字就是排查坐标周围一圈雷的数量了,当然这个字符也是整型类型的。
如果还是不懂,推荐去b站看up主“比特鹏哥”讲解,链接如下扫雷详解_。
函数讲完了我们继续往下走,把count的值放到show数组中相应坐标当中,当然因为show数组的类型是char,所以还要在后面加上一个字符‘0’,打印出来的时候才会出现相对应的字符数组,后面就是打印出show数组,然后是win++,
再就是判断有没有获得胜利,若条件为真,则进入if,打印语句,打印mine【存放雷信息的数组】,
最后我们再来说game.h这个文件里的语句,
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//这个game.h是放定义的常量和自定义函数名的
//打印扫雷棋盘的大小
#define ROW 9
#define COL 9
//定义数组的大小
#define ROWS ROW+2
#define COLS COL+2
//雷的个数
#define esey_t 10
//初始化棋盘
void Intboard(char arr[ROWS][COLS], int rows, int cols, char set);
//布置雷
void checkboard(char arr[ROWS][COLS], int row, int col);
//打印棋盘
void Decboard(char arr[ROWS][COLS], int rows, int cols);
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
首先是(ROW、COL)和(ROWS、COLS)四个常量,它们分别表示的是打印区域的行列大小和数组行列的真实大小,至于为什么?在上面的项目思路里有讲过,这里就不再讲了。然后是下面函数声明,作用就是更好的管理函数,节省开发时间。然后就是esey_t这个常量了,它的作用就是设置雷的个数,除了设置雷的个数外,在FindMine函数中的while循环判断表达式里有用到esey_t常量,
就如下面这堆代码一样,那这个判断表达式具体是什么意思呢?
int win = 0;
while (win < row*col - esey_t)
{
........
{
.....
win++;
}
}
if (win == row * col - esey_t)
{
//判断为真就获胜了
}
首先win变量初始值为0,row*col表式整个棋盘可排查的坐标个数,esey_t有雷的坐标的个数,那“row*col - esey_t” 就表示,整个棋盘可排查的坐标个数 - 有雷坐标的个数 = 没有雷坐标的个数,排雷成功一次,没有雷的坐标就少一个,win++,win的变量值和排雷成功的次数相等,当排雷成功的次数等于没有雷坐标的个数时,那就证明所有没雷的坐标都排查过了,排雷成功,跳出while循环,进行后面的if语句判断,结果为真,进入语句,执行获胜语句。
结尾
好了,这次的c语言项目到此就全部讲解完了,我会把这个项目的源码放在我的gitee仓库里,gitee里搜索“卡非熊”,就可以了。其实这也是我第一次写这么长的文章,如果有什么错误或者是不足的地方也欢迎在评论区指正出来,我是ahsjj,我们青山不改,绿水长流,下次见,拜拜!