数组和函数实践:扫雷游戏
文章目录
- 数组和函数实践:扫雷游戏
- 一.扫雷游戏分析和设计
- (一)扫雷游戏的功能说明
- (二)游戏的分析和设计
- 1.数据结构的分析
- 2.文件结构设计
- 二.扫雷游戏的代码实现
- (一)游戏大致流程
- (二)`game`函数内容
- 1.定义`mine`数组和`show`数组
- 2.初始化棋盘
- 3.打印棋盘
- 4.布置雷
- 5.排雷
- (三)完整代码
一.扫雷游戏分析和设计
(一)扫雷游戏的功能说明
使用控制台实现经典的扫雷游戏:
- 游戏可以通过菜单实现继续玩或者退出游戏
- 扫雷的棋盘默认是9*9的格子
- 默认随机布置10个雷
- 可以排查雷
- 如果位置不是雷,就显示周围有几个雷
- 如果位置是雷,就炸死,游戏结束
- 把除10个雷之外的所有非雷都找出来,排雷成功,游戏结束
游戏界面:
(二)游戏的分析和设计
1.数据结构的分析
扫雷的过程中,布置的雷和排查出的雷的信息都需要存储,所以需要一定的数据结构来存储这些信息。
因为需要在9*9的棋盘上布置雷的信息和排查雷,首先想到的就是创建一个9*9的数组来存放信息。
某位置布置雷,就存放1,没有布置雷就存放0。
假设排查(2,4)这个坐标时,访问周围的一圈8个绿色位置,统计周围雷的个数是1,但是排查(8,5)这个坐标时,访问周围的⼀圈8个绿色位置,统计周围雷的个数时,最下面的三个坐标就会越界,为了防止越界,在设计的时候,给数组扩大一圈,雷还是布置在中间的9*9的坐标上,周围一圈不去布置雷,这样就解决了越界的问题。所以将存放数据的数组创建成11*11是比较合适。
解决方案:专门给一个棋盘(对应一个数组mine)存放布置好的雷的信息,再给另外一个棋盘(对应另外一个数组show)存放排查出的雷的信息。
这样就互不干扰了,把雷布置到mine数组,在mine数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给后期排查参考。
同时为了保持神秘,show数组开始时初始化为字符'*',为了保持两个数组的类型一致,可以使用同一套函数处理,mine数组最开始也初始化为字符'0',布置雷改成字符'1'。如下图:
charmine[11][11]={0};//⽤来存放布置好的雷的信息charshow[11][11]={0};//⽤来存放排查出的雷的个数信息2.文件结构设计
上一篇函数里介绍了多文件的形式对函数的声明和定义,这里实践一下,设计三个文件:
test.c :写游戏的测试逻辑
game.c :写游戏中函数的实现
game.h :写游戏需要的数据类型和函数声明
二.扫雷游戏的代码实现
(一)游戏大致流程
test.c:
用do_while循环,上来先打印菜单。选1,进入游戏(由后面要写的game函数实现),选0,while的判断表达式的值为0,循环直接结束,退出游戏。
(二)game函数内容
1.定义mine数组和show数组
game.h中:
test.c中:
game.c中:
在game.h头文件中用define定义常量ROW,COL(实际棋盘行和列),ROWS,COLS(加上外围一圈后棋盘行和列),方便test.c,game.c中多次调用,也方便后续修改棋盘大小(直接在define处修改大小,不用一个一个去修改)。直接在test.c,game.h中包含game.h头文件即可使用。
为了更加方便,把stdio.h也包含在game.h中,这样包含game.h的同时也可以包含stdio.h。
技巧:多个.c文件公用的东西(库函数,自定义函数,常量)都放到一个头文件中定义和声明会方便清晰很多。
2.初始化棋盘
- 在test.c中的
game函数里写调用的函数雏形:
//初始化棋盘InitBoard(mine,ROWS,COLS,'0');InitBoard(show,ROWS,COLS,'*');注:初始化的棋盘是加了外围一圈后的棋盘,所以传入行列为:ROWS,COLS。
- 在game.c中写函数定义:
//初始化棋盘voidInitBoard(charBoard[ROWS][COLS],intr,intc,charset){for(inti=0;i<r;i++){for(intj=0;j<c;j++){Board[i][j]=set;}}}Board:接收要初始化的数组。r,c:接收要遍历的行数和列数。set:接收要初始化的目标字符。
通过遍历数组的行和列,拿到每个元素并赋给它目标字符完成初始化。
- 在game.h中声明函数:
voidInitBoard(charBoard[ROWS][COLS],intr,intc,charset);3.打印棋盘
- 在test.c中的
game函数里写调用函数雏形:
//打印棋盘DisplayBoard(show,ROW,COL);//DisplayBoard(mine, ROW, COL);注:
(1) 打印的棋盘是真实棋盘大小,所以传入行列为:ROW,COL。
(2) 在正式玩游戏时,只显示show棋盘。显示mine棋盘是为了后面调试检查代码。
- 在game.c中写函数定义:
//打印棋盘voidDisplayBoard(charBoard[ROWS][COLS],intr,intc){printf("------扫雷游戏------\n");inti=0,j=0;for(j=0;j<=c;j++)//打印列号{printf("%d ",j);}printf("\n");//打印完列号换行for(i=1;i<=r;i++){printf("%d ",i);//打印行号for(j=1;j<=c;j++){printf("%c ",Board[i][j]);}printf("\n");//打印完一行,换行}printf("\n");//整体打完再换行}Board:接收要打印的数组。r,c:接收要遍历的行数和列数。
通过遍历数组的行和列,拿到每个元素并打印。同时要注意打印行号和列号,方便玩游戏时快速找到某位置的行号和列号。
- 在game.h中声明函数:
voidDisplayBoard(charBoard[ROWS][COLS],intr,intc);- 打印棋盘效果展示:
4.布置雷
- 在test.c中的
game函数里写调用函数雏形:
//布置雷SetBoard(mine,ROW,COL);DisplayBoard(mine,ROW,COL);//布雷成功后,打印出来检查注:是在真实棋盘上布置雷,所以传入行列为:ROW,COL。
- 在
main函数里生成随机的种子:
srand((unsignedint)time(NULL));注:
(1)初始化随机数生成器在整个工程里只用调用一次,所以必须写到main函数里。
(2)记得在game.h里包含头文件stdlib.h和time.h。
- 在game.c中写函数定义:
//布置雷voidSetBoard(charBoard[ROWS][COLS],intr,intc){intcount=EASY_COUNT;while(count){intx=rand()%r+1;//x的范围:1~rinty=rand()%c+1;//y的范围:1~cif(Board[x][y]=='0')//防止重复布雷{Board[x][y]='1';count--;//每成功布置一个雷,循环-1}}}Board:接收要布雷的数组。r,c:接收真实棋盘的行数和列数。
在game.h中定义布雷常量EASY_COUNT,方便随时修改。
用真实棋盘的行数和列数r,c限制随机坐标x,y的范围,把随即坐标对应的数组元素赋为'1',就成功布置了一个雷。
- 在game.h中声明函数和布雷个数:
#defineEASY_COUNT10voidSetBoard(charBoard[ROWS][COLS],intr,intc);- 布雷效果展示:
5.排雷
- 在test.c中的
game函数里写调用函数雏形:
//排雷FindBoard(mine,show,ROW,COL);- 在game.c中写函数定义:
辅助函数,获得排查位置周围雷的个数:
size_tGetMineCount(charmine[ROWS][COLS],intx,inty)//返回雷的个数一定是正数,所以用无符号整数{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';//转换为对应的数字}计算某位置雷的个数原理:
- 数字字符 -
'0'=该数字字符对应的数字 - 数字 +
'0'=该数字对应的数字字符
把排查位置的周围的8个数字字符转换为数字再相加,得该位置周围雷的个数。
待排查坐标(x,y)周围8个字符的坐标如下图:
//排雷voidFindBoard(charmine[ROWS][COLS],charshow[ROWS][COLS],intr,intc){intx=0,y=0;intwin=0;//成功排雷的个数while(win<r*c-EASY_COUNT){printf("请输入要排查的坐标:\n");scanf("%d%d",&x,&y);if((x>=1&&x<=r)&&(y>=1&&y<=c)){if(show[x][y]=='*'){if(mine[x][y]!='1'){size_tcount=GetMineCount(mine,x,y);show[x][y]=count+'0';//转换为对应的字符DisplayBoard(show,ROW,COL);win++;}else{printf("很遗憾,你被炸死了\n");DisplayBoard(mine,ROW,COL);//为了让玩家死得瞑目,打印minebreak;//跳出循环}}else{printf("该坐标已排查过,请重新输入\n");}}else{printf("输入的坐标非法,请重新输入\n");}}//来到这里有两种情况,1.炸死了,游戏结束。2.排雷成功if(win==r*c-EASY_COUNT){printf("恭喜你,排雷成功\n");DisplayBoard(mine,ROW,COL);}}mine:接收mine数组,用于排查某位置雷的个数,游戏最终显示。show:接受show数组,用于游戏中的显示。r,c:接收真实棋盘的行列。
把除雷以外剩余的位置作循环次数,每成功排完一个雷,循环次数减一。
- 在game.h中声明函数:
voidFindBoard(charmine[ROWS][COLS],charshow[ROWS][COLS],intr,intc);- 效果展示:
检查排雷不成功的情况:
检查排雷成功的情况:为方便,把EASY_COUNT设为80,即只有一个位置不是雷,并打印出mine棋盘来作弊,以便快速检查。
(三)完整代码
game.h:
#include<stdio.h>#include<stdlib.h>#include<time.h>#defineROW9#defineCOL9#defineROWSROW+2#defineCOLSCOL+2#defineEASY_COUNT80voidInitBoard(charBoard[ROWS][COLS],intr,intc,charset);voidDisplayBoard(charBoard[ROWS][COLS],intr,intc);voidSetBoard(charBoard[ROWS][COLS],intr,intc);voidFindBoard(charmine[ROWS][COLS],charshow[ROWS][COLS],intr,intc);game.c:
#include"game.h"//初始化棋盘voidInitBoard(charBoard[ROWS][COLS],intr,intc,charset){for(inti=0;i<r;i++){for(intj=0;j<c;j++){Board[i][j]=set;}}}//打印棋盘voidDisplayBoard(charBoard[ROWS][COLS],intr,intc){printf("------扫雷游戏------\n");inti=0,j=0;for(j=0;j<=c;j++)//打印列号{printf("%d ",j);}printf("\n");//打印完列号换行for(i=1;i<=r;i++){printf("%d ",i);//打印行号for(j=1;j<=c;j++){printf("%c ",Board[i][j]);}printf("\n");//打印完一行,换行}printf("\n");//整体打完再换行}//布置雷voidSetBoard(charBoard[ROWS][COLS],intr,intc){intcount=EASY_COUNT;while(count){intx=rand()%r+1;//x的范围:1~rinty=rand()%c+1;//y的范围:1~cif(Board[x][y]=='0')//防止重复布雷{Board[x][y]='1';count--;//每成功布置一个雷,循环-1}}}size_tGetMineCount(charmine[ROWS][COLS],intx,inty)//返回雷的个数一定是正数,所以用无符号整数{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';//转换为对应的数字}//排雷voidFindBoard(charmine[ROWS][COLS],charshow[ROWS][COLS],intr,intc){intx=0,y=0;intwin=0;//成功排雷的个数while(win<r*c-EASY_COUNT){printf("请输入要排查的坐标:\n");scanf("%d%d",&x,&y);if((x>=1&&x<=r)&&(y>=1&&y<=c)){if(show[x][y]=='*'){if(mine[x][y]!='1'){size_tcount=GetMineCount(mine,x,y);show[x][y]=count+'0';//转换为对应的字符DisplayBoard(show,ROW,COL);win++;}else{printf("很遗憾,你被炸死了\n");DisplayBoard(mine,ROW,COL);//为了让玩家死得瞑目,打印minebreak;//跳出循环}}else{printf("该坐标已排查过,请重新输入\n");}}else{printf("输入的坐标非法,请重新输入\n");}}//来到这里有两种情况,1.炸死了,游戏结束。2.排雷成功if(win==r*c-EASY_COUNT){printf("恭喜你,排雷成功\n");DisplayBoard(mine,ROW,COL);}}test.c:
#include"game.h"voidmenu(){printf("-----------------------------------\n");printf("----------- 1.play ---------\n");printf("----------- 2.exit ---------\n");printf("-----------------------------------\n");}voidgame(){//定义两个棋盘数组charmine[ROWS][COLS];//存放雷的信息charshow[ROWS][COLS];//存放排查雷的信息//初始化棋盘InitBoard(mine,ROWS,COLS,'0');InitBoard(show,ROWS,COLS,'*');//打印棋盘DisplayBoard(show,ROW,COL);//DisplayBoard(mine, ROW, COL);//布置雷SetBoard(mine,ROW,COL);DisplayBoard(mine,ROW,COL);//排雷FindBoard(mine,show,ROW,COL);}intmain(){srand((unsignedint)time(NULL));intinput=0;do{menu();printf("请选择:\n");scanf("%d",&input);switch(input){case1:game();break;case0:printf("退出游戏\n");break;default:printf("输入错误,请重新输入\n");break;}}while(input);return0;}