今天还是同样的,在c语言课上学习了扫雷这个游戏,所以我又根据上课学到的东西,编写了自己的扫雷程序,也加入了很多新的东西,当然就算加了很多新的东西,也比不过大佬们的程序,这里所编写的就是一个简单的小游戏,希望对大家有帮助。
一、大纲
1.头文件
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
#define easy_count 20
//初始化棋盘
//初始化地雷棋盘
void init_board_mine(char board[ROWS][COLS], int rows,int cols);
//初始化展示棋盘
void init_board_show(char board[ROWS][COLS], int rows,int cols);
//打印棋盘
void display_board(char board[ROWS][COLS],int row,int col);
//设置地雷
void set_mine(char board[ROWS][COLS],int row,int col);
//排查地雷
void find_mine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);
本来想这一部分放在最后的,但是我后来又想了一下,还是放在了前面,因为里面包含了一些,整个程序都在使用的东西,比如定义的ROW和COL,以及ROWS和COLS,还有游戏的难易度easy_count(表示地雷的个数) ,并且还有一点,谁叫它是头文件呢~
至于定义的ROW和COL,以及ROWS和COLS,为什么这样定义呢?是因为要照顾后续的函数,比如计算你选择的坐标周围有多少地雷,如果只定义9*9的话,可能有的时候会超出数组范围,所以设计横竖分别多了两行。
2.测试程序文件
说完了头文件,接下来就是测试程序文件,他是整个游戏的大纲部分,内容还是很简单的,跟我上一个游戏--五子棋里面的一致。
2.1游戏目录
void menu()
{
printf("------------------------------------\n");
printf("--------------1.play----------------\n");
printf("--------------2.exit----------------\n");
printf("------------------------------------\n");
}
这个地方就没有介绍的必要了,就是printf函数打印的东西。
2.2主函数
int main()
{
//设置随机数的生成起点
srand((unsigned int)time(NULL));
int i = 0;
do {
menu();
printf("请输入数字:");
scanf("%d",&i);
switch (i)
{
case 1:
game();
printf("是否想要继续游戏。\n");
break;
case 2:
printf("程序已退出。\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (i != 2);
return 0;
}
这里的东西和我上一次发的五子棋是一样的原理,主要有一个随机数生成起点,是为了后续随机埋地雷所设计的,其余的部分应该仔细看一下就明白了。
2.3游戏程序
void game()
{
char mine[ROWS][COLS] = {0};//存放布置好的雷的信息
char show[ROWS][COLS] = {0};//存放排查的信息
//初始化棋盘
//初始化mine棋盘
init_board_mine(mine,ROWS,COLS);
//初始化show棋盘
init_board_show(show,ROWS,COLS);
//设置地雷
set_mine(mine,ROW,COL);
//打印棋盘
display_board(show,ROW,COL);
//排查地雷
find_mine(mine,show,ROW,COL);
}
首先是创建两个字符数组,一个用于埋放地雷,一个用于给用户展示,因为毕竟不可能直接展示给用户地雷的埋放情况,也需要这部分信息,所以需要2个字符数组。
接下来的部分就是游戏主体部分,包括初始化棋盘、设置地雷、打印棋盘、排查地雷。这四个部分,在接下来我将一一介绍一下。
3.游戏程序文件(game.c)
3.1初始化棋盘
//初始化mine棋盘
void init_board_mine(char board[ROWS][COLS], int rows,int cols)
{
int i = 0;
int j = 0;
for(i = 0;i < rows;i++)
for(j = 0;j < cols;j++)
{
board[i][j] = '0';
}
}
//初始化show棋盘
void init_board_show(char board[ROWS][COLS], int rows,int cols)
{
int i = 0;
int j = 0;
for(i = 0;i < rows;i++)
for(j = 0;j < cols;j++)
{
board[i][j] = '#';
}
}
这一部分也很好理解,就是单纯的数组遍历赋值,我们对mine棋盘,赋值为'0',对show棋盘赋值为'#',如果各位有觉得有什么比较好看的字符,也可以放上去。只不过要记住一点,在后续计算中使用的是这些字符的ascii码,不是对应的数字,因为这是字符变量。
3.2打印棋盘
//打印棋盘
void display_board(char board[ROWS][COLS],int row,int col)
{
int i = 0;
int j = 0;
//对竖行打印造成的空缺,用空格补齐
printf(" ");
//打印横行数字
for(j = 1;j<=col;j++)
{
printf(" %d ",j);
}
printf("\n");//第一个仅仅是为了换行
printf("\n");//第二个是为了和竖行的空格保持对齐
for(i = 1;i<=row;i++)
{
printf(" %d ",i);//打印数行数字
printf(" ");//打印竖行空格
for(j = 1;j<=col;j++)
{
printf(" %c ",board[i][j]);
}
printf("\n");
}
}
这一部分打印棋盘就和之前的五子棋有所不同了,这也是为当时在五子棋上没有做的(太懒了,平时课有点多,就没有进一步优化,呜呜呜~)
首先第一个打印函数,是为了补齐竖行造成的空缺,可以先看后续的步骤,可以更好的理解。接下来打印横行的数字,对应的是棋盘的横向坐标(1-9)。接下来的两个打印函数的用处都标在了注释里。第二个换行是因为强迫症,因为竖行与棋盘的间隔和横行对棋盘的间隔不一样,所以单独又写了一个。
接下来到竖行坐标,在每一次打印竖行棋盘之前,先打印一下对应坐标,然后打印一个空格(这就是为什么上面打印横行时要打印一些空格),本来是想打印一个(|)竖线的,但是观感不好就换成空格了。接下来就是打印棋盘字符,记住我们打印的是show棋盘,不要打印错棋盘了!
3.3设置地雷
//设置地雷
void set_mine(char board[ROWS][COLS],int row,int col)
{
int count = easy_count;//可以在game.h中设置难度
while(count)
{
//生成随机数(1~9)对应坐标
int x = rand() % row + 1;
int y = rand() % col + 1;
if(board[x][y] == '0')
{
board[x][y] = '1';
count--;//当count == 0时,while循环结束
}
}
}
这一部分就是设置地雷的部分,首先我们要让系统知道,我们要设置多少地雷,也就是easy_count的值,我们可以在头文件里面写上normal_count和hard_count对应不同的难度和地雷个数。这里建立了一个整形变量count接收这个值。我用while循环来进行程序的循环执行,当count减到0时表示地雷埋完了,循环条件为false,循环结束。
接下来就是生成随机数,用于埋下地雷。毕竟不能所有的对局,地雷都是在同一个位置吧。
rand()函数就是用于生成随机数的,前面srand()就是用来给它生成一个随机数起点的,具体的使用方法可以参考别的大佬的关于这个函数的介绍。我们随机生成的数字和我们对应的row以及col取余数,得到的结果肯定在(0-8)之间,再加上一,结果即是(1-9)对应了我们的坐标。
接下来使用一个if语句进行判断,假如说mine数组对应的位置上没有雷('0')我们则埋下地雷('1'),记住这里传的变量是mine数组,不是show数组。
3.4排查地雷
这一部分就是整体游戏最难、最复杂的地方,我将拆成四分来数,具体的内容,在大纲中也介绍到了。
3.4.1整体内容
//排查地雷
void find_mine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col)
{
int x = 0;
int y = 0;
while (1)//构建死循环
{
printf("是否要进行地雷标记(y/n):");
char mark = 0;
scanf("\n%c",&mark);//不要惊奇为什么会有\n,因为当你进入菜单,准备游戏,输入1然后回车时,回车的\n会被这个scanf函数收取,所以我先他一步,写了一个\n
if(mark == 'y')
{
mark_mine(show,mine);//地雷标记,再打印
display_board(show,ROW,COL);
}
printf("请输入要排查的坐标(格式1 1):");
scanf("\n%d %d", &x, &y);//此\n同上
printf("\n");
if(x>=1 && x<=row && y >=1 && y <= col)//对输入的合法性进行检查
{
if(mine[x][y] == '1')//如果是雷
{
int i = 0;
int j = 0;
for(i = 1 ;i<=row;i++)
for(j = 1;j<=col;j++)
{
if(mine[i][j] == '1')
{
show[i][j] = '@';//遍历然后展示出所有的地雷,为了与1做区分,所以采用了@
}
}
display_board(show,ROW,COL);
printf("很遗憾,你被炸死了.\n");
printf("\n\n");//为了好看才写的~
break;
}
else if(show[x][y] != '#')//防止重复输入
{
printf("已经检测,请重新输入。\n");
}
else//如果不是雷
{
int count = mine_count(mine,x,y);
show[x][y] = count + '0';
perimeter_search(mine,show,x,y);//进行周边查找
display_board(show,ROW,COL);
}
}
else
{
printf("输入非法,请重新输入。\n");
}
}
}
可以看到这一部分还是很长的,我们一步一步来说明。
首先我们先构成一个死循环,用于用户循环操作。接下来就是第一个功能--地雷标记。但是用户并不可能每一次都需要地雷标记,就比如游戏一开始也没人会去标记。所以在这里,我们先询问用户是否需要标记,不需要则进行正常的游戏,如果选择需要,我们则进行地雷标记功能,这个功能我后续会讲到,在标记后不要忘记打印一次棋盘哦~
注:至于为什么第一个scanf内有一个\n,我在注释里有说明:“不要惊奇为什么会有\n,因为当你进入菜单,准备游戏,输入1然后回车时,回车的\n会被这个scanf函数收取,所以我先他一步,写了一个\n”下面也有类似的代码,原理都是一样的。
接下来进入地雷的排查(惊心动魄的时刻),首先我们对坐标进行合法化判断,如果不合法,则进行重新输入,如果合法则进行下一步判断。
假如你输入的坐标是地雷的话,很遗憾,你被炸死了!当踩到地雷时,我们先对mine棋盘进行遍历,找到地雷所对应的坐标打印在展示棋盘上,然后打印show棋盘,告诉你所有雷的地址。接下来的两个换行是为了好看才写的。
如果你输入的坐标不是地雷,恭喜你,你暂时安全了!接着进行第二个功能--计算地雷,程序会计算你输入的坐标周围有几个地雷(为什么会写show[x][y] = count + '0';,这里先不用好奇,在下面会介绍)然后就是第三个功能--周边查找,这个就是第二个功能的升级版。程序会查找你输入的坐标周围的八个地方是否是地雷,如果不是就输入它对应的周围有几个地雷。然后打印棋盘给用户,让你仔细斟酌一下接下来怎么走。
3.4.2地雷标记
//地雷标记
void mark_mine(char show[ROWS][COLS],char mine[ROWS][COLS])
{
printf("请输入要标记的坐标:");
int x = 0;
int y = 0;
scanf("%d %d",&x,&y);
show[x][y] = '!';
int count = 0;
int i = 0;
int j = 0;
for(i = 1;i <=ROW;i++)//做这一部分的时候思考过一个问题,假如没有进入这个函数,不就不会提示标记完了吗,但是你都没有进入,你怎么标记呢
for(j = 1;j <=COL;j++)
{
if(mine[i][j] == '1' && x == i && y == j )//循环遍历mine数组,如果标记正确则count++
{
count++;
if(count == easy_count)//当count达到了总地雷的值时,便结束游戏
{
printf("恭喜你,找到了所有的地雷!\n");
break;
}
}
}
}
当你在排查地雷之前选择了地雷标记,则会进入上图所示的程序。首先我们获取所需要标记的坐标,直接在show棋盘上进行字符替换,然后进行排查,如果你找到了所有的地雷,则游戏胜利。(做这一部分的时候思考过一个问题,假如没有进入这个函数,不就不会提示标记完了吗。但是你都没有进入,你怎么标记呢~)
3.4.3地雷计算
//计算雷数
int mine_count(char board[ROWS][COLS],int x,int y)
{
//简单的计算,记住对应的'1'和'0'是相对的ascii码值,不是数字1和2
return (board[x-1][y] + board[x -1][y -1] + board[x][y -1] +
board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1] +
board[x][y + 1] + board[x - 1][y + 1] - 8*'0');
}
这里就是简单的计算,记住一点:对应的'1'和'0'是相应的ascii码值,不是数字的1和2。所以在计算上和一般的数值计算有微小的不同。然后我们返回的是int值。所以刚刚才会有这行代码:show[x][y] = count + '0';我们对得到的数字加上'0'即可得到计算的数字所对应的字符值。
3.3.4周边排查
//周边排查(只排查了一圈)
void perimeter_search(char mine[ROWS][COLS],char show[ROWS][COLS],int x,int y)
{
//防止超过了数组的范围,因为是到对相应坐标的四周,8个坐标值进行判断
if(x>=1 && y >=1 && x <= 9 && y <= 9)
{
int i = 0;
int j = 0;
for (i = -1; i < 2; i++)
for (j = -1; j < 2; j++)
{
if (mine[x + i][y + j] != '1')
{
int count = mine_count(mine, x + i, y + j);
show[x + i][y + j] = count + '0';
//这个递归操作,超内存了,所以给注释掉了,后续再想想能不能优化一下
//perimeter_search(mine,show,x + i,y + j);
}
}
}
}
首先为了防止超过数组范围,我们会对相应的x和y的值进行判断。如果在范围内,我们则对相应坐标四周的数值进行判断,如果不是地雷,我们就计算它对应周围有多少地雷,并打印到show棋盘上。
注:本来是想设计成和一般的扫雷一样的功能,设计了一个递归,但是电脑提示内存超了,看一下后续能不能优化一下,或者各位大佬能不能给提一点意见。
4.源码
4.1game.h
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
#define easy_count 20
//初始化棋盘
//初始化地雷棋盘
void init_board_mine(char board[ROWS][COLS], int rows,int cols);
//初始化展示棋盘
void init_board_show(char board[ROWS][COLS], int rows,int cols);
//打印棋盘
void display_board(char board[ROWS][COLS],int row,int col);
//设置地雷
void set_mine(char board[ROWS][COLS],int row,int col);
//排查地雷
void find_mine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);
4.2test.c
#include"game.h"
#include "game.c"
void menu()
{
printf("------------------------------------\n");
printf("--------------1.play----------------\n");
printf("--------------2.exit----------------\n");
printf("------------------------------------\n");
}
void game()
{
char mine[ROWS][COLS] = {0};//存放布置好的雷的信息
char show[ROWS][COLS] = {0};//存放排查的信息
//初始化棋盘
//初始化mine棋盘
init_board_mine(mine,ROWS,COLS);
//初始化show棋盘
init_board_show(show,ROWS,COLS);
//设置地雷
set_mine(mine,ROW,COL);
//打印棋盘
display_board(show,ROW,COL);
//排查地雷
find_mine(mine,show,ROW,COL);
}
int main()
{
//设置随机数的生成起点
srand((unsigned int)time(NULL));
int i = 0;
do {
menu();
printf("请输入数字:");
scanf("%d",&i);
switch (i)
{
case 1:
game();
printf("是否想要继续游戏。\n");
break;
case 2:
printf("程序已退出。\n");
break;
default:
printf("输入错误,请重新输入!\n");
break;
}
} while (i != 2);
return 0;
}
4.3game.c
//
// Created by 石丹阳 on 2023/9/15.
//
#include "game.h"
//初始化mine棋盘
void init_board_mine(char board[ROWS][COLS], int rows,int cols)
{
int i = 0;
int j = 0;
for(i = 0;i < rows;i++)
for(j = 0;j < cols;j++)
{
board[i][j] = '0';
}
}
//初始化show棋盘
void init_board_show(char board[ROWS][COLS], int rows,int cols)
{
int i = 0;
int j = 0;
for(i = 0;i < rows;i++)
for(j = 0;j < cols;j++)
{
board[i][j] = '#';
}
}
//打印棋盘
void display_board(char board[ROWS][COLS],int row,int col)
{
int i = 0;
int j = 0;
//对竖行打印造成的空缺,用空格补齐
printf(" ");
//打印横行数字
for(j = 1;j<=col;j++)
{
printf(" %d ",j);
}
printf("\n");//第一个仅仅是为了换行
printf("\n");//第二个是为了和竖行的空格保持对齐
for(i = 1;i<=row;i++)
{
printf(" %d ",i);//打印数行数字
printf(" ");//打印竖行空格
for(j = 1;j<=col;j++)
{
printf(" %c ",board[i][j]);
}
printf("\n");
}
}
//设置地雷
void set_mine(char board[ROWS][COLS],int row,int col)
{
int count = easy_count;//可以在game.h中设置难度
while(count)
{
//生成随机数(1~9)对应坐标
int x = rand() % row + 1;
int y = rand() % col + 1;
if(board[x][y] == '0')
{
board[x][y] = '1';
count--;//当count == 0时,while循环结束
}
}
}
//计算雷数
int mine_count(char board[ROWS][COLS],int x,int y)
{
//简单的计算,记住对应的'1'和'0'是相对的ascii码值,不是数字1和2
return (board[x-1][y] + board[x -1][y -1] + board[x][y -1] +
board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1] +
board[x][y + 1] + board[x - 1][y + 1] - 8*'0');
}
//周边排查(只排查了一圈)
void perimeter_search(char mine[ROWS][COLS],char show[ROWS][COLS],int x,int y)
{
//防止超过了数组的范围,因为是到对相应坐标的四周,8个坐标值进行判断
if(x>=1 && y >=1 && x <= 9 && y <= 9)
{
int i = 0;
int j = 0;
for (i = -1; i < 2; i++)
for (j = -1; j < 2; j++)
{
if (mine[x + i][y + j] != '1')
{
int count = mine_count(mine, x + i, y + j);
show[x + i][y + j] = count + '0';
//这个递归操作,超内存了,所以给注释掉了,后续再想想能不能优化一下
//perimeter_search(mine,show,x + i,y + j);
}
}
}
}
//地雷标记
void mark_mine(char show[ROWS][COLS],char mine[ROWS][COLS])
{
printf("请输入要标记的坐标:");
int x = 0;
int y = 0;
scanf("%d %d",&x,&y);
show[x][y] = '!';
int count = 0;
int i = 0;
int j = 0;
for(i = 1;i <=ROW;i++)//做这一部分的时候思考过一个问题,假如没有进入这个函数,不就不会提示标记完了吗,但是你都没有进入,你怎么标记呢
for(j = 1;j <=COL;j++)
{
if(mine[i][j] == '1' && x == i && y == j )//循环遍历mine数组,如果标记正确则count++
{
count++;
if(count == easy_count)//当count达到了总地雷的值时,便结束游戏
{
printf("恭喜你,找到了所有的地雷!\n");
break;
}
}
}
}
//排查地雷
void find_mine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col)
{
int x = 0;
int y = 0;
while (1)//构建死循环
{
printf("是否要进行地雷标记(y/n):");
char mark = 0;
scanf("\n%c",&mark);//不要惊奇为什么会有\n,因为当你进入菜单,准备游戏,输入1然后回车时,回车的\n会被这个scanf函数收取,所以我先他一步,写了一个\n
if(mark == 'y')
{
mark_mine(show,mine);//地雷标记,再打印
display_board(show,ROW,COL);
}
printf("请输入要排查的坐标(格式1 1):");
scanf("\n%d %d", &x, &y);//此\n同上
printf("\n");
if(x>=1 && x<=row && y >=1 && y <= col)//对输入的合法性进行检查
{
if(mine[x][y] == '1')//如果是雷
{
int i = 0;
int j = 0;
for(i = 1 ;i<=row;i++)
for(j = 1;j<=col;j++)
{
if(mine[i][j] == '1')
{
show[i][j] = '@';//遍历然后展示出所有的地雷,为了与1做区分,所以采用了@
}
}
display_board(show,ROW,COL);
printf("很遗憾,你被炸死了.\n");
printf("\n\n");//为了好看才写的~
break;
}
else if(show[x][y] != '#')//防止重复输入
{
printf("已经检测,请重新输入。\n");
}
else//如果不是雷
{
int count = mine_count(mine,x,y);
show[x][y] = count + '0';
perimeter_search(mine,show,x,y);//进行周边查找
display_board(show,ROW,COL);
}
}
else
{
printf("输入非法,请重新输入。\n");
}
}
}
5.附
感谢各位的阅读,有问题的地方或者建议可以留言告诉我~感谢各位的支持。