Dotcpp  >  编程教程  >  C/C++游戏类项目  >  C语言实现扫雷游戏教程及源码

C语言实现扫雷游戏教程及源码

点击打开在线编译器,边学边练

一、源码简介

这是一个可以进行扫雷游戏的小程序,采用C语言进行编写。

上下左右控制光标位置,按j键进行标记,按k进行点击探雷,并且当光标 放在数字上,且周围的雷都已经被正确标记时,按k可以点开周围所有的空白,不过出错会结束游戏。

雷区长宽为25格,初始有10雷,每过一关增加20雷。

 编译环境:VC6.0(采取纯C语言写法)

第三方库:无

 

二、运行截图

扫雷1


扫雷2


三、源码解析

我们先来看游戏的主体逻辑。

虽然下面的代码很长,但逻辑还是较为清晰的。

以下循环

若游戏未开始,初始化。

若游戏开始,则检测键盘输入

  按下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时的光标位置。判定周围标记数是否与雷数相同,是则检查每一个标记处是否为雷,若全部对应,则清雷,若有不对应的,则游戏结束。

其余定义的函数还有检测是否踩雷,计算剩余的雷,判定是否胜利,显示雷,移动光标,做标记。这些函数一般都是用双重循环,或者直接改变变量的方法进行的。这里暂时不给出源码,但完整源码也会有对应的内容。


四、完整源码

C语言扫雷完整源码


本文固定URL:https://www.dotcpp.com/course/1229

C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:

一点编程也不会写的:零基础C语言学练课程

解决困扰你多年的C语言疑难杂症特性的C语言进阶课程

从零到写出一个爬虫的Python编程课程

只会语法写不出代码?手把手带你写100个编程真题的编程百练课程

信息学奥赛或C++选手的 必学C++课程

蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程

手把手讲解近五年真题的蓝桥杯辅导课程

趣味项目教程
第一章 C/C++游戏类项目
第二章 C/C++工具及其他类项目
第三章 Python趣味项目
Dotcpp在线编译      (登录可减少运行等待时间)