一、源码简介
这是一个可以进行扫雷游戏的小程序,采用C语言进行编写。
上下左右控制光标位置,按j键进行标记,按k进行点击探雷,并且当光标 放在数字上,且周围的雷都已经被正确标记时,按k可以点开周围所有的空白,不过出错会结束游戏。
雷区长宽为25格,初始有10雷,每过一关增加20雷。
编译环境:VC6.0(采取纯C语言写法)
第三方库:无
二、运行截图
三、源码解析
我们先来看游戏的主体逻辑。
虽然下面的代码很长,但逻辑还是较为清晰的。
以下循环
若游戏未开始,初始化。
若游戏开始,则检测键盘输入
按下ASDW则移动光标
第一次按下K,则初始化雷区
按下K则点开空白,或者清雷,并检测是否胜利
按下j则进行标记
游戏结束跳出循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | 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 ; } } |
下面看一些重要部分的实现。
改变光标位置以及定义为输出模式
1 2 3 4 5 6 7 8 9 | 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); } |
初始化雷区实现原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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的时候调用,输入值为光标的位置。在生成雷的时候,如果与之前的雷或者光标重合,那么会重新生成一个雷,这样做可以避免直接失败。
检测某一点周围的雷数目,以及标记数目
这两者的原理都是一样的。直接对光标周围八个点进行判定,若是则++。
绘制游戏画面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 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); } |
采用双层循环结构,对区域的每个点进行一次判定,将对应的标识及颜色输出就可以了。注意不要混淆图像数组和雷的信息数组。
展开无雷区域
在扫雷游戏中,当我们第一次点击时,很有可能点开一大片区域。其实现如下面的代码所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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会点开周围无雷的区域
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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时的光标位置。判定周围标记数是否与雷数相同,是则检查每一个标记处是否为雷,若全部对应,则清雷,若有不对应的,则游戏结束。
其余定义的函数还有检测是否踩雷,计算剩余的雷,判定是否胜利,显示雷,移动光标,做标记。这些函数一般都是用双重循环,或者直接改变变量的方法进行的。这里暂时不给出源码,但完整源码也会有对应的内容。
四、完整源码
C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:
一点编程也不会写的:零基础C语言学练课程
解决困扰你多年的C语言疑难杂症特性的C语言进阶课程
从零到写出一个爬虫的Python编程课程
只会语法写不出代码?手把手带你写100个编程真题的编程百练课程
信息学奥赛或C++选手的 必学C++课程
蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程
手把手讲解近五年真题的蓝桥杯辅导课程