一、源码简介
这是一个可以进行扫雷游戏的小程序,采用C语言进行编写。
上下左右控制光标位置,按j键进行标记,按k进行点击探雷,并且当光标 放在数字上,且周围的雷都已经被正确标记时,按k可以点开周围所有的空白,不过出错会结束游戏。
雷区长宽为25格,初始有10雷,每过一关增加20雷。
编译环境:VC6.0(采取纯C语言写法)
第三方库:无
二、运行截图
三、源码解析
我们先来看游戏的主体逻辑。
虽然下面的代码很长,但逻辑还是较为清晰的。
以下循环
若游戏未开始,初始化。
若游戏开始,则检测键盘输入
按下ASDW则移动光标
第一次按下K,则初始化雷区
按下K则点开空白,或者清雷,并检测是否胜利
按下j则进行标记
游戏结束跳出循环
void Gamerun()//游戏主体 { int first; while(1) { if(gamestate==0)//初始化 { first=0; m_time=time(NULL);//时间初始化 Init_display();//初始化显示区域 CONSOLE_CURSOR_INFO cursor_info = { 1,0 }; SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE),&cursor_info);//设置指定控制台屏幕缓冲区的光标的大小和可见性 gamestate=1; } else if(gamestate==1)//游戏中 { RemainderMine();//计算剩余的雷 Draw_display(); MoveCursor(); PressJ(); if(GetAsyncKeyState('K')&1)//如果按下K { if(first==0) { Init_mine(pos.y,pos.x); OpenDisplay(pos.y,pos.x); first++; if(Victory())//判定是否获胜 { Showmine(); Draw_display(); UINT uint=MessageBox(NULL,TEXT("恭喜过关!是否继续。"),TEXT("提示"),1); if(uint==IDOK) { count+=20; level++; gamestate=0; } else if(uint==IDCANCEL) { gamestate=2; } else; } else; } else { if(TreadMine(pos.y,pos.x))//判断是否踩到雷 { Showmine(); Draw_display(); UINT uint=MessageBox(NULL,TEXT("扫雷失败!是否重新开始?"),TEXT("提示"),MB_OKCANCEL|MB_ICONERROR); if(uint==IDOK) { gamestate=0; } else if(uint==IDCANCEL) { gamestate=2; } else; } else { OpenDisplay(pos.y,pos.x); } if(OpenNumDisplay1(pos.y,pos.x))//按K也可以清雷,返回值为1表示踩雷 { Showmine(); Draw_display(); UINT uint=MessageBox(NULL,TEXT("扫雷失败!是否重新开始?"),TEXT("提示"),MB_OKCANCEL|MB_ICONERROR); if(uint==IDOK) { gamestate=0; } else if(uint==IDCANCEL) { gamestate=2; } else; } else; if(Victory())//判定是否获胜 { Showmine(); Draw_display(); UINT uint=MessageBox(NULL,TEXT("恭喜过关!是否继续。"),TEXT("提示"),1); if(uint==IDOK) { count+=20; level++; gamestate=0; } else if(uint==IDCANCEL) { gamestate=2; } else; } else; } } else; } else if(gamestate==2)//游戏结束 { printf("GAME END!"); break; } else; } }
下面看一些重要部分的实现。
改变光标位置以及定义为输出模式
void Pos(int x,int y)//定义一个设置光标位置到(x,y)的函数 { COORD pos; HANDLE hOutput; pos.X=x; pos.Y=y; hOutput=GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(hOutput,pos); }
初始化雷区实现原理:
void Init_mine(int m, int n)//初始化雷区 { int num=count; int x,y; srand((unsigned)time(NULL));//利用时间获取随机种子 while(num>0) { x=rand()%height+1;//1-25 y=rand()%width+1;//1-25 if(mine[x][y]==0&&(m!=x||n!=y))//避免重复 { mine[x][y]=1; num--; } else; } }
这个函数在第一次按下k的时候调用,输入值为光标的位置。在生成雷的时候,如果与之前的雷或者光标重合,那么会重新生成一个雷,这样做可以避免直接失败。
检测某一点周围的雷数目,以及标记数目
这两者的原理都是一样的。直接对光标周围八个点进行判定,若是则++。
绘制游戏画面
void Draw_display()//绘制游戏画面 { Pos(7, 7);//光标定位 int i,j; for(i=1;i<=height;i++)//逐行逐列输出游戏画面 { for(j=1;j<=width;j++) { if(j==pos.x&&i==pos.y) SetConsoleTextAttribute(ColorHandle,0X5|0XC);//对不同类型的图像设置不同颜色 else if(map[i][j]==10) SetConsoleTextAttribute(ColorHandle,0X94); else SetConsoleTextAttribute(ColorHandle,0X97); switch(map[i][j]) { case 0:printf(" ");break; case 1:printf("1 ");break; case 2:printf("2 ");break; case 3:printf("3 ");break; case 4:printf("4 ");break; case 5:printf("5 ");break; case 6:printf("6 ");break; case 7:printf("7 ");break; case 8:printf("8 ");break; case 9:printf("■");break;//未知区域 case 10:printf("▲");break;//标记 case 11:printf("雷");break;//雷 } if(j==pos.x&&i==pos.y) SetConsoleTextAttribute(ColorHandle,0X7);//关闭颜色 else if(map[i][j]==10) SetConsoleTextAttribute(ColorHandle,0X7); else SetConsoleTextAttribute(ColorHandle,0X7); } Pos(7,7+i);//光标移到下一行 } Pos(7,3); printf("未排除的雷:%d ",RemMine);//打印各种文字信息 Pos(7,5); printf("时间:%ld 秒 ",long(time(NULL)-m_time)); Pos(27,3); printf("关卡:%d ",level); }
采用双层循环结构,对区域的每个点进行一次判定,将对应的标识及颜色输出就可以了。注意不要混淆图像数组和雷的信息数组。
展开无雷区域
在扫雷游戏中,当我们第一次点击时,很有可能点开一大片区域。其实现如下面的代码所示。
void OpenDisplay(int x,int y)//单击后展开显示无雷区域 { int i,j; int offsetX[] = { 0,1,1,1,0,-1,-1,-1 }; int offsetY[] = { -1,-1,0,1,1,1,0,-1 }; if(map[x][y]==9&&x>0&&x<=width&&y>0&&y<=height)//中心点显示有几颗雷 { map[x][y]=Judge_mine(x,y); if(map[x][y]>0)//某点周围有雷就不展开 { return; } else; } else if(map[x][y]==10||(map[x][y]>=1&&map[x][y]<=8))//某点为标记或数字 不展开 { return; } else; for(i=0;i<8;i++)//周围8点 { if(x>0&&x<=width&&y>0&&y<=height) { if(map[x+offsetX[i]][y+offsetY[i]]==10&&mine[x+offsetX[i]][y+offsetY[i]]!=1)//展开的时候,如果周围某点为标记且不是雷 { map[x+offsetX[i]][y+offsetY[i]]=9;//改为未知区域 } if(mine[x+offsetX[i]][y+offsetY[i]]==0&&map[x+offsetX[i]][y+offsetY[i]]==9)//无雷且未知,则展开 { map[x+offsetX[i]][y+offsetY[i]]=Judge_mine(x+offsetX[i],y+offsetY[i]);//得到某点周围的雷的个数 if(map[x+offsetX[i]][y+offsetY[i]]==0)//该点无雷且无数字 { OpenDisplay(x+offsetX[i],y+offsetY[i]);//递归 } } } } return; }
具体来说,就是采取递归的算法。先判定该点是否产生变化,如果产生,则对周围8个点同样做出一次判定。如果这些点周围的雷数为0,则递归调用该函数。
清雷动作
当一个数字周围的雷全部被点开之后,在其上按k会点开周围无雷的区域
int OpenNumDisplay1(int x,int y)//将数字周围区域显示出来。返回值表明是否因为该动作踩雷,1为是 { int i,j; int offsetX[]={ 0,1,1,1,0,-1,-1,-1 }; int offsetY[]={ -1,-1,0,1,1,1,0,-1 }; if(map[x][y]<9&&map[x][y]>0)//1-8 { if(Judge_mine(x,y)==Judge_cur(x,y))//若周围标记数量等于周围雷的数量,展开 { for(i=0;i<8;i++) { if(x>0&&x<=width&&y>0&&y<=height)//越界判断 { if(map[x+offsetX[i]][y+offsetY[i]]==9||map[x+offsetX[i]][y+offsetY[i]]==10)//为标记和未知区域时,进行判定 { if(map[x+offsetX[i]][y+offsetY[i]]==10&&mine[x+offsetX[i]][y+offsetY[i]]==1)//标记正确 { continue; } else if(map[x+offsetX[i]][y+offsetY[i]]==10&&mine[x+offsetX[i]][y+offsetY[i]]!=1) //标记错误 { return 1;//踩雷 } else; } else; } else; } for(i=0;i<8;i++) { if(x>0&&x<=width&&y>0&&y<=height)//越界判断 { if(map[x+offsetX[i]][y+offsetY[i]]==9)//为未知区域则正常展开 { OpenDisplay(x+offsetX[i],y+offsetY[i]);//展开 } } } } } return 0; }
输入为按下k时的光标位置。判定周围标记数是否与雷数相同,是则检查每一个标记处是否为雷,若全部对应,则清雷,若有不对应的,则游戏结束。
其余定义的函数还有检测是否踩雷,计算剩余的雷,判定是否胜利,显示雷,移动光标,做标记。这些函数一般都是用双重循环,或者直接改变变量的方法进行的。这里暂时不给出源码,但完整源码也会有对应的内容。
四、完整源码
本文固定URL:https://www.dotcpp.com/course/1229
C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:
一点编程也不会写的:零基础C语言学练课程
解决困扰你多年的C语言疑难杂症特性的C语言进阶课程
从零到写出一个爬虫的Python编程课程
只会语法写不出代码?手把手带你写100个编程真题的编程百练课程
信息学奥赛或C++选手的 必学C++课程
蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程
手把手讲解近五年真题的蓝桥杯辅导课程