/GEC6818-mines

GEC6818开发板-扫雷项目

Primary LanguageC

项目手册—扫雷

使用说明: 文件内的.bmp为游戏素材图片 文件内的.c与.h文件为源代码与头文件 文件内的a.out为二进制文件可直接在开发板上进行编译,但是编译时要保证a.out与游戏素材图片在同一目录下 若想修改代码可在linux内用arm-linux-gcc *.c进行编译,会自动覆盖a.out文件 使用方法: (1)将文件解压复制进U盘,将U盘插在开发板上,连接开发板后输入cd /mnt/udisk再输入ls查看U盘是否成功挂载(有的U盘无法被识别) 输入ls之后有内容即可识别,cd进入U盘内的目标目录可直接运行a.out文件。 (2)在开发板终端通过rx命令或者tftp将.bmp文件一个一个上传(有两张比较大),再将a.out上传并给权限运行 需要拓展或者理解代码可看一下内容:

字符版扫雷实现:

实现功能:

  • 第一点:要求可以插旗用来标记雷

  • 第二点:要有扩散的操作

  • 第三点:第一次100%不会踩到雷

  • 第四点:第一次必会有一个扩散,也就是说第一次点击的位置必须为空地

关于代码:

大致思路:使用两个棋盘

第一个为内部棋盘,该棋盘在第一次选完点之后会随机落雷并且将雷周围的数字安顿好,该棋盘只有在游戏结束是才会被显示出来,	 即为答案棋盘

第二个为展示棋盘,该棋盘为在外部展示给玩家看的棋盘,通过输入的坐标,然后去获取内部棋盘的元素再展示出来
  • 第一步:初始化两个棋盘(二维数组)

  • 第二步:实现打印棋盘的函数

  • 第三步:获取从键盘上输入的坐标,如果获取有字符则将该字符打印到展示棋盘内,视作插旗操作

  • 第四步:在获取完从键盘输入的坐标之后,在内部棋盘随机布雷

    问题一:如何实现第一次100%不会踩雷并且一定会有, 扩散?

    解决方法:将随机布雷安排在第一次输入坐标之后

    问题二:如何让随机布雷不会把雷布置到第一次选择的坐标周围?

    解决方法:因为随机布雷的布雷先行条件是该方块内不为雷,所以根据这个条件可以先把第一次点击的坐标以及其周围3×3范围内全 部变成雷,然后再进行随机布雷,当布雷结束之后再将坐标以及其周围初始化,从而实现第一次100%不会踩到雷并且一定会有扩散 也就是第一次不会选到数字,核心代码如下:

    //随机落雷(在第一次选点的3×3范围外)
    void DownMines(char board[ROW][COL],int row, int col)
    {
        srand(time(0));//随机种子
        int count = 0;
        //先将第一次输入的点的周围变成雷
        for (int i = row - 1; i <= row + 1; i++)
        {
            for (int j = col - 1; j <= col + 1; j++)
            {
                board[i][j] = '*';//随机生成雷时就不会在这个点生成
            }
        }
        //随机落MINES个雷
        while (count < MINES)
        {
            int x = rand() % ROW;
            int y = rand() % COL;
            if (board[x][y] != '*')
            {
                board[x][y] = '*';//布置雷
                count++;
            }
        }
        //然后再将刚刚的区域消去
        for (int i = row - 1; i <= row + 1; i++)
        {
            for (int j = col - 1; j <= col + 1; j++)
            {
                board[i][j] = '#';//起到保护作用
            }
        }
        
    }
    • 第五步:计算出雷周围的数字并且在内部棋盘内插入,并且在主函数内限制只有第一次选取坐标才会调用随机落雷与插入数字的函数

      大致思路:先写出获取一个格子周围雷数量的函数,再遍历整个棋盘的格子并调用函数,计算其周围雷的数量并将数字写入格子中

      定义一个参数first作为if的判断条件,在第一次选点之后改变frist的值使if不满足从而不调用两个函数

      大致代码为:

      //为了避免第一次就寄掉所以在第一次输入坐标之后再对内部棋盘进行落雷并插入数字
              if (first)//第一次选完点
              {
                  
                  DownMines(board,row,col);//落雷
      
                  InsertData(board,row,col);//在内部棋盘插入数字
      
                  first = 0;//之后输入坐标不再随机落雷与插数字
              }
    • 第六步:实现扩散功能

      问题一:如何实现扩散并且将内部棋盘元素传给外部棋盘

      ​ 解决方法:递归函数,只要该方格内是空地,则进行递归,遍历它周围八个方格,若还有空地则继续递归,核心代码如下:

      //将内部棋盘可展示部分赋值给展示棋盘,并递归找空地
      void revealCell(char board[ROW][COL], char displayBoard[ROW][COL], int row, int col)
      {
          if (row < 0 || row >= ROW || col < 0 || col >= COL || displayBoard[row][col] != '#' || board[row][col] == '*') 
          {
              return;
          }
          //将输入点坐标对应的内部棋盘数据传递给展示棋盘
          displayBoard[row][col] = board[row][col];
          //如果为空地,则递归该指定坐标周围的八个格子,coner
          if (board[row][col] == '\0')
          {
              for (int i = row - 1; i <= row + 1; i++) 
              {
                  for (int j = col - 1; j <= col + 1; j++) 
                  {
                      revealCell(board, displayBoard,i,j);
                  }
              }
          }
      }
    • 第七步:判断游戏结束条件

      • 成功条件:

        条件一:初始化的字符'#'数量与雷的数量相等

        条件二:插的旗子数量等于雷的数量且没有初始化字符

        条件三:插的旗子数量与初始化字符之和等于雷的数量

      • 失败条件:

        条件一:输入的坐标为雷

      实现方法:

      写一个函数去查找棋盘中某个字符的数量,核心代码如下:

      //获取棋盘中字符a的数量
      int Find_sign(char board[ROW][COL],char a)
      {
          int count = 0;
          //遍历整个棋盘
          for (int row = 0; row < ROW; row++)
          {
              for (int col = 0; col < COL; col++)
              {
                  if (board[row][col] == a)
                  {
                      count ++;//统计字符a的数量
                  }
              }
          }
          return count;//返回数量
      }

GEC6818开发板扫雷实现:

实现功能:

  • 第一点:扩散
  • 第二点:第一次100%不会踩雷且会扩散
  • 第三点:图形化显示
  • 第四点:可以重新开始游戏
  • 第五点:可以主动结束本局游戏
  • 第六点:可以退出游戏

关于代码:

大致思路

在字符版扫雷的基础上将字符版扫雷的打印输出改为显示图片输出将字符版的键盘输入改为触摸屏输入加入对开发板屏幕的操作以及触摸屏的操作通过函数获取到手指点击的坐标.

将坐标转化为字符版扫雷中二维数组的下标再传入对应的图片将展棋盘中的元素显示在屏幕上
  • 第一步:初始化屏幕

    • 打开屏幕文件
    • 建立内存映射区域
  • 第二步:初始化游戏界面

    • 显示游戏界面图片
    • 获取手指点击的坐标
    • 根据坐标限制进入游戏的区域
  • 第三步:初始化棋盘,设置背景板,实现显示图片的函数

  • 第四步:展示棋盘,但是在第一次点击前无法主动结束游戏所以第一次不会显示结束游戏的图标

    困难一:如何在屏幕上显示对应的二维矩阵的元素

    解决方法:通过棋盘大小规格以及调试手指点击坐标,在相应的位置显示图片,核心代码如下:

    //显示棋盘
    void Darw_board(int first,char board[ROW][COL])
    {
        //最开始不显示结束游戏的图标
        if (first)
        {
            DisplayBMPPicture(680,5,"over.bmp");
        }
        for(int h = 0;h < ROW;h++)
        { 
            for(int w = 0; w < COL; w++)
            {
                //根据棋盘元素在对应位置打印相应的图片
                switch (board[h][w])
                {
                case '\0':
                    DisplayBMPPicture(w*40,h*40,"0.bmp");
                    break;
                case '1':
                    DisplayBMPPicture(w*40,h*40,"1.bmp");
                    break;
                case '2':
                    DisplayBMPPicture(w*40,h*40,"2.bmp");
                    break;
                case '3':
                    DisplayBMPPicture(w*40,h*40,"3.bmp");
                    break;
                case '4':
                    DisplayBMPPicture(w*40,h*40,"4.bmp");
                    break;
                case '5':
                    DisplayBMPPicture(w*40,h*40,"5.bmp");
                    break;
                case '6':
                    DisplayBMPPicture(w*40,h*40,"6.bmp");
                    break;
                case '7':
                    DisplayBMPPicture(w*40,h*40,"7.bmp");
                    break;
                case '8':
                    DisplayBMPPicture(w*40,h*40,"8.bmp");
                    break;
                case '*':
                    DisplayBMPPicture(w*40,h*40,"mines.bmp");
                    break;
                default:
                    DisplayBMPPicture(w*40,h*40,"initial.bmp");
                    break;
                }
            }
        }
        //画棋盘线
        Drawboard_line(0x000000);
    }
  • 第五步:获取手指点击坐标

    困难一:因为要实现函数的模块化,方便逻辑的整理应该单独写一个函数去获取手指点击屏幕的坐标,但坐标是两个值,C语言函数无法返回两个int 类型的参数。

    解决方法:通过指针对实参的值进行修改

    核心代码如下:

    //获取手指点击屏幕的坐标,超出范围返回1,未超出范围返回-1
    void Get_XY(int *x,int *y)
    {
        int x1 = -1,y1 = -1;//滑动开始坐标
    
        int fd = open("/dev/input/event0",O_RDWR);
        if(fd == -1)
        {
            printf("open error\n");
            return;
        }
        struct input_event ev;
    
        while(1)
        {
            int ret = read(fd,&ev,sizeof(ev));
            if(ret != sizeof(ev))
            {
                continue;
            }
            //将获取到的坐标值赋值给x1,y1
            if(ev.type == EV_ABS && ev.code == ABS_X)
            {
                x1 = ev.value*800/1024;
            }
            if(ev.type == EV_ABS && ev.code == ABS_Y)
            {
                y1 = ev.value*480/600;
            }
            //手指离开时才会满足if条件
            if(ev.type == EV_KEY && ev.code == BTN_TOUCH && ev.value == 0)
            {
                *x = x1 / 40;//用指针来更新传入函数的坐标
                *y = y1 / 40;
                close(fd);
                return;
            }
        }
    }
  • 第六步:进行开始游戏之后的操作

    • 进入循环,获取手指点击屏幕的坐标

    • 限制只有第一次点击屏幕之后才会随机落雷以及在棋盘内插入数字

    • 如果踩到雷,显示游戏失败的图片,延迟一秒后打印出内部棋盘,并退出循环

    • 如果没有踩到雷则进行扩散函数操作

    • 将展示棋盘打印出来

    • 如果雷被全部排完,则成功通关,显示通关的图片并打印出内部棋盘,退出循环

    • 限制主动结束游戏的区域,若手指点击到该区域则进行与游戏失败相同的操作

    • 循环结束后代表本局游戏结束,显示退出游戏以及重新开始游戏的界面

  • 第七步:设置重新开始游戏的功能

    • 写出重新开始游戏的函数Restart,首先获取手指点击的坐标,限制重新开始游戏的区域,若手指点进该区域则函数返回1

      否则返回0

    • 在主函数内嵌套一层循环,循环判断条件为重新开始游戏函数返回值为1

      • 困难一:由于第一次进入游戏时是只要点击初始游戏界面的进入游戏区域即可开始游戏,但是该区域不满足 Restart函数。

        解决方法:设定第一次无条件进入循环

        大致代码如下:

         //第一次无条件开始游戏,first跟Restart不能替换位置,否则需要点两下
            while (first || Restart())
            {
                Creat_board(board);//初始化内部棋盘
                Creat_board(displayBoard);//初始化展示棋盘
        
                //设置背景板
                DrawBackgournd(0xffffff);
                
                int X = -1;//初始化坐标
                int Y = -1;
        
                //显示棋盘,最开始的时候无法结束游戏
                Darw_board(0,board);
        
                //感应触摸屏幕将XY值修改成点击的坐标
                Get_XY(&Y,&X);
                
                //获取到坐标之后开始进行游戏
                Start_game(board,displayBoard,Y,X);
        
                //将frist置零,之后的循环条件都是判断Restart
                first = 0;
            }
      • 困难二:如果循环条件为:

        while (Restart() || first)

        因为Restart函数也需要获取一个坐标然后返回,虽然first第一次为1也会进入循环,但是此时就需要点两下才能进入游戏

        解决方法:

        利用C语言编译器的惰性运算,将两个判断条件调换位置:

        while (first || Restart())

        此时因为第一次的first为1,所以编译器不会再去判断Restart的真假,也就是说不会进入Restart函数,从而解决需要点两下才能进入游戏的缺陷

  • 第八步:如果循环退出则证明游戏程序结束,显示游戏结束的图片

  • 第九步:解除屏幕初始化

    • 解除内存映射区域
    • 关闭屏幕文件,主函数结束