使用java实现自动扫雷

news2024/11/24 14:30:29

写在前面

本项目已在github开源,链接https://github.com/QZero233/JavaAutoMinesweeper
本文的写作风格可能会有些奇怪,这是笔者的一次全新的尝试,后续会换回写blog的文风的

摘要

本文提出了一个全自动完成扫雷游戏的解决方案,这套方案使用了固定策略的图像分割以及求像素点RGB差值的方式来实现扫雷游戏的导入,使用一个简单的算法来实现游戏求解,使用Robot类来模拟点击,来落实最终的操作。我们使用了java语言进行实现和验证,并取得了较好的成果

引言

扫雷是一个很经典的游戏,这个游戏的规则很简单:玩家需要根据已经被扫过的区域,来判断地雷和安全区的位置,并最终排除出全部的地雷,具体游戏规则大家可以上网了解,这里不再赘述
理论上而言,扫雷游戏求解十分简单,只需要对所有的数字格子进行遍历,查看是否能确定某个地方一定是地雷,或者某个地方一定是安全的,进行标记和打开后,再重复此操作。这样的做法最后的结局有两种可能性:

  1. 排查出所有地雷,游戏结束
  2. 现有条件无法使我们再做出准确的判断,这也就是遇到了所谓的“死亡二选一”的情况。关于这种情况,本文会在后续工作部分讨论

在实践方面,这个问题的难点在于如何把扫雷游戏转化成一个矩阵,然后使用算法来求解。一般而言,程序能获取到的只有游戏的屏幕截图,挑战就在于如何识别这个截图。本文参考了一篇关于自动扫雷的文章【1】,借鉴了其中图像对比的方法,并提出了固定策略图像分割的方法,来实现游戏的导入

方法与代码实现

图像识别

总体思路是:

  1. 将整个扫雷游戏截图按照方格分割成一个一个的小图片
  2. 将每个小图片和每种方块的图片进行对比,得出这个位置的方块的类型
  3. 记录每个位置的方块类型,形成游戏矩阵

图片分割

这一步的目的就是把下面这张图变成
在这里插入图片描述
下面这样的一张张小图片
在这里插入图片描述
这一步的难点就在于,如何准确知道每个方块在图片上的坐标。这里我们是采取了一种比较偷懒的办法,那就是首先使用ps等软件测量最左边的一列的左边缘的x值,作为一个xOffset,同理,测量yOffset,然后再测量每个格子的大小blockSize,最终可以用公式 ( x O f f s e t + x ∗ b l o c k S i z e , y O f f s e t + y ∗ b l o c k S i z e ) (xOffset + x*blockSize,yOffset+y*blockSize) (xOffset+xblockSize,yOffset+yblockSize)计算出每个方块左上角的坐标
在这里插入图片描述
但由于每次缩放窗口后,这些参数都会发生变化,所以我们直接采取了把游戏窗口最大化的办法,这样可以尽可能减少变数。剩下的工作就是对这些参数进行微调,直到能获取一个让人满意的分割
在代码实现方面,我们是使用了Java的BufferedImage这个类,通过ImageIO.read来打开图片,使用ImageIO.write来保存图片,使用BufferedImage.getRGB和BufferedImage.setRGB来获取图片的每个像素点的RGB值。由于我们的目标是搭建一个原型系统,所以这种方式并不是最高效的实现方式,这也是后续优化工作的一个方向,分割部分的具体代码如下

    public static void segmentImage(BufferedImage game,ImageProfile profile) throws Exception{
        int blockSize=profile.getBlockSize();
        for(int x=0;x<profile.getRowNum();x++){
            for(int y=0;y<profile.getRowNum();y++){
                BufferedImage newImage=new BufferedImage(blockSize,blockSize,BufferedImage.TYPE_INT_ARGB);

                for(int i=0;i<blockSize;i++){
                    for(int j=0;j<blockSize;j++){
                        int color=game.getRGB(profile.getxOffset()+x*blockSize+i,profile.getyOffset()+y*blockSize+j);
                        newImage.setRGB(i,j,color);
                    }
                }
                ImageIO.write(newImage,"png",new File("segment/"+x+"-"+y+".png"));
            }
        }
    }

(注:在实际图像识别的时候,我们并不会把图像分割之后,再保存成文件再识别,事实上我们会直接获取像素点并进行比较,这个函数是用来调整xOffset等参数时使用的)

识别图片

图片识别部分参考了【1】中的做法,在完成图片分割之后,将分割好的图片,和提前打好了标记的图片进行比较,来判断这个分割好的图片所属的类别。具体比较方法为:将两张图片的对应位置的RGB值做差,再取绝对值并求和,得到一个差值,这个差值越小,就代表分割好的图片和打好标记的图片越像。如果某个类别的图片,比如数字1的图片,和游戏图片(即一个个分割好的图片)的差值最小,那么就可以确定这个游戏图片就是数字1
在实际操作中,还是会遇到几个问题:

  1. 带标签的图片和游戏图片大小不一致怎么办?
  2. RGB值的表示实际上是用int数字的低24位来表示的,这就会使得在求差值的过程中,红色天然的就具有很大的权重,这对于类别判断的影响是很大的
  3. win7的扫雷界面自带渐变,这使得准确的识别变得更加困难

对于这些问题,我们的解决办法是:

  1. 对图片进行缩放,这是目前尝试下来效果比较好的办法。之前还尝试过类似于卷积的操作,就是故意把标记图片做小,然后把它作为卷积窗口,不停的滑动,求差值,然后求和,最后把所有的求和累计起来,但是这种办法的效果并不理想。推测可能原因是:这种方法下,会出现大量的mismatch,会导致大量的大差值覆盖掉了小差值,使得最终结果差异不大
  2. 这里的思路是灰度化之后再求差值,这样可以平衡一下各个颜色的权重,实验下来结果确实比之前要好
    private static int getGrayAvg(int color){
        int red=(color & 0xff0000) >> 16;
        int green=(color & 0xff00) >> 8;
        int blue=color & 0x0000ff;
        return Math.round((red * 0.299f + green * 0.587f + blue * 0.114f));
    }
  1. 这个确实没办法了,只能是不停的增加样本量,来减少误判率。或者就换个思路,使用神经网络来解决,但是可能是因为数据量不够,实际测试下来神经网络的效果并不理想

对比图片部分的代码如下

    private static int getGrayAvg(int color){
        int red=(color & 0xff0000) >> 16;
        int green=(color & 0xff00) >> 8;
        int blue=color & 0x0000ff;
        return Math.round((red * 0.299f + green * 0.587f + blue * 0.114f));
    }

    public static double getLikelihood(BufferedImage game,BufferedImage target,int x,int y,int blockSize,int xOffset,int yOffset){
        double currentSum=0;

        //Only compare central parts 1/4 to 3/4
        for(int k=target.getWidth()/4;k<target.getWidth()*3/4;k++){
            for(int l=target.getHeight()/4;l<target.getHeight()*3/4;l++){
                //Origin x: x*blockSize + k
                //Origin y: y*blockSize + l
                //Target x: k
                //Target y: l

                int srcColor=game.getRGB(x*blockSize + k + xOffset,y*blockSize + l + yOffset);
                int targetColor=target.getRGB(k,l);

                //Get gray average
                srcColor=getGrayAvg(srcColor);
                targetColor=getGrayAvg(targetColor);


                int abs=Math.abs(srcColor-targetColor);
                currentSum+=abs;
            }
        }

        double averageSum=currentSum/(target.getWidth()*target.getHeight());

        return averageSum;
    }

后面就不断调用这个方法,对每个游戏图片,遍历所有的样本图片,来找返回值最小的那个,这样就可以最终形成游戏矩阵

游戏求解

游戏求解就略显简单了,我们事先约定:在游戏矩阵里,-1代表格子未打开,-2表示这个格子被插旗了(也就是一定有雷),0表示格子被打开了,但是没数字,其他数字就对应着游戏里格子上显示的数字
例如,下面这一个小区块
在这里插入图片描述

化为游戏矩阵就是
[
[-1,-1,-1],
[2,2,2],
[0,1,-2]
]

有了前面的工作后,求解就很简单了,大致思路是模仿了人类玩扫雷时的思维方式,就是根据已知信息去寻找一定可以确定是雷,或者一定可以确定不是雷的地方,然后进行标记或是打开。如果遍历了整个游戏,仍然无法确定,那就遇到了所谓的“死亡二选一”的情况,这个问题会留到后面的 后续工作 部分再来讨论
思路很简单,那直接贴代码了

    public static List<Action> getSolution(int[][] game, int rowNum){

        List<Action> result=new ArrayList<>();

        int[][] directions={
                {1,1},
                {1,-1},
                {-1,1},
                {-1,-1},
                {1,0},
                {-1,0},
                {0,1},
                {0,-1}
        };
        for(int i=0;i<rowNum;i++){
            for(int j=0;j<rowNum;j++){

                int current=game[i][j];
                if(current<=0)
                    continue;

                int covered=0;
                int flagged=0;
                int uncovered=0;
                for(int k=0;k<8;k++){
                    int nX=i+directions[k][0];
                    int nY=j+directions[k][1];

                    if(nX < 0 || nX >=rowNum || nY < 0 || nY >=rowNum)
                        continue;

                    if(game[nX][nY]==-1)
                        covered++;
                    else if(game[nX][nY]==-2)
                        flagged++;
                    else
                        uncovered++;
                }

                if(flagged==current && current!=0){
                    //Can open every neighbour

                    for(int k=0;k<8;k++){
                        int nX=i+directions[k][0];
                        int nY=j+directions[k][1];

                        if(nX < 0 || nX >=rowNum || nY < 0 || nY >=rowNum)
                            continue;

                        if(game[nX][nY]==-1 && !result.contains(new Action(nX,nY, Action.Type.OPEN))){
                            result.add(new Action(nX,nY, Action.Type.OPEN));
//                            System.out.println("Open "+nX+","+nY);
                        }
                    }

                }else if(flagged==current-1 && flagged+covered==current){
                    //Can mark a bomb

                    for(int k=0;k<8;k++){
                        int nX=i+directions[k][0];
                        int nY=j+directions[k][1];

                        if(nX < 0 || nX >=rowNum || nY < 0 || nY >=rowNum)
                            continue;

                        if(game[nX][nY]==-1 && !result.contains(new Action(nX,nY, Action.Type.MARK))){
                            result.add(new Action(nX,nY, Action.Type.MARK));
//                            System.out.println("Mark "+nX+","+nY);
                        }
                    }
                }

            }
        }

        return result;
    }

模拟操作

模拟操作主要是使用了Java中的Robot类,我们只需要模拟两个动作,即把鼠标指针移动到指定位置,然后左键或者右键即可
这个坐标就是目标格子在截屏中的坐标,使用之前的公式就可以轻松的算出来,在实际操作中,由于需要点击,而算出来的坐标是格子左上角的坐标,所以还需要往最终结果上加上一个偏移量,这里取得偏移量是blockSize/2,即最终坐标是
( x O f f s e t + x ∗ b l o c k S i z e + b l o c k S i z e / 2 , y O f f s e t + y ∗ b l o c k S i z e + b l o c k S i z e / 2 ) (xOffset + x*blockSize + blockSize/2,yOffset+y*blockSize+blockSize/2) (xOffset+xblockSize+blockSize/2,yOffset+yblockSize+blockSize/2)
具体代码如下

                    for(Action action:actions){
                        Coordinate coordinate=ImageUtils.getCoordinate(profile,action.getX(),action.getY());
                        robot.mouseMove(coordinate.getX(),coordinate.getY());

                        if(action.getType()== Action.Type.OPEN){
                            robot.mousePress(InputEvent.BUTTON1_MASK);
                            robot.mouseRelease(InputEvent.BUTTON1_MASK);
                        }else{
                            robot.mousePress(InputEvent.BUTTON3_MASK);
                            robot.mouseRelease(InputEvent.BUTTON3_MASK);
                        }

                        Thread.sleep(100);
                    }

                    robot.mouseMove(0,0);

求解完成后,鼠标会移动到屏幕左上角来表示求解结束

其余技术细节

  1. 全局快捷键
    为了便于控制,我们设置了一个全局快捷键,这个可以参考【2】,这里不再赘述了
  2. 自动截图
    这个功能是参考了【3】,具体实现代码也很简单,代码如下
Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
Robot robot=new Robot();
BufferedImage screenshot = robot.createScreenCapture(new
                           Rectangle(0, 0, (int) d.getWidth(), (int) d.getHeight()));

实验结果

由于识别准确率的问题,所以还是存在是不是点到地雷的情况,但是较少发生
测试中最大的问题还是“死亡二选一”的问题,这使得游戏无法自动求解,需要人为干预,后续会针对这个问题进行优化
目前测试下来,这个程序可以很好的完成中等难度的扫雷,在需要人为干预死亡二选一的情况下,程序能做到最快在70秒完成中等难度的扫雷游戏
下面是程序工作时的gif图以及最终的游戏结果
(下面的计时器显示,我确实没开倍速播放,如果优化一下,程序应该是能在更短时间内解决游戏的,最后gif停下来的地方就是遇到死亡二选一了)
在这里插入图片描述
在这里插入图片描述

后续工作

下面将针对遇到的问题进行一些讨论

  1. 识别精度问题,存在误判的情况
    关于这个问题,目前有两个方向
    一个是换战场,尝试win10的扫雷,文章【1】里面也提到了,win10的扫雷方块没有渐变色,识别起来应该会更简单
    另一个就是疯狂扩大样本量,但是这样会增大图像对比时的时间成本,如果还要用差值法来进行图片对比,那可能需要再进行一些优化,例如尝试使用cuda来进行并行计算
    当然,样本量大了之后,也可以尝试使用卷积神经网络来做分类,这也是一个可选的方向
  2. 随着blockSize变小,精准切割存在困难
    当前的切割采取的是固定策略,其中的参数不一定是完美的,事实上,每个小块都可能会有1-2个像素的误差,当blockSize较大时,一般而言格子数量也较少,所以误差不那么明显;但如果blockSize较小,例如24x24的游戏中,blockSize测下来只有38,那么一方面,格子数量变多了,误差的累积效应增大,另一方面,各自本身较小,误差更明显,所以这种分割的结果就是很不理想
    目前计划采取【1】中的办法,使用边界线等其他特征来判断格子的边界,从而进行切割
  3. 如何解决死亡二选一
    这个暂时还没进行文献调研,还不能给出一个比较完美的方法,目前仅有一些符合直觉的思路:
    其一是,瞎点,但是凭借个人经验,这样挺容易点到雷的…
    其二是,找到可能的情况数量最少的地方,例如,只需要做二选一,肯定可以确认一个是雷,一个不是雷的地方,然后跑随机数。这就纯看运气了…但如果是做速通的话,这个思路或许是最高效的
  4. 算法仍然存在优化空间
    虽然这个算法是 O ( n 2 ) O(n^2) O(n2)的,但是扫雷游戏的规模一般不大,所以求解的时间开销几乎可以忽略不计,开销的大头在图像识别和游戏矩阵的构建
    在算法层面,其实是可以减少一下图像识别的次数的,例如,在标记完雷和打开完一定不是雷的格子之后,就根据程序已知的信息,再判断一下,还能不能找到可以确定是雷或者不是雷的地方,如果实在找不到了,再截图+图像识别,来获取最新的游戏状态

参考文献

【1】https://pangruitao.com/post/3058
【2】https://zhuanlan.zhihu.com/p/446086846
【3】https://cloud.tencent.com/developer/article/1669635

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/429892.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Kubernetes】 多云管理策略解析

文章目录Kubernetes 多云的实现1. 前言1.1 Kubernetes 多云的现实需求2. Kubernetes 多云的架构设计2.1 跨云 Kubernetes 的挑战2.1.1 不同云厂商的接口不兼容2.1.2 多云环境中的安全问题2.1.3 跨云环境中的网络问题2.2 Kubernetes 多云的架构设计2.2.1 统一网络管理2.2.2 使用…

新能源汽车高压配电管理(PDU/BDU)

一、概念与组成 PDU(Power Distribution Unit)&#xff0c;即高压配电单元&#xff0c;功能是负责新能源车高压系统中的电源分配与管理&#xff0c;为整车提供充放电控制、高压部件上电控制、电路过载短路保护、高压采样、低压控制等功能&#xff0c;保护和监控高压系统的运行…

MacOS系统启动React前端项目时报错Error: EMFILE: too many open files, open解决方法

错误场景 最近在开发React的前端微应用&#xff0c;启动时模块构建报错Module build failed&#xff0c; Error: EMFILE: too many open files, 如下图所示&#xff1a; Error: EMFILE: too many open files的错误&#xff0c;经排查是因为单个微应用项目较大&#xff0c;发…

【Linux安装数据库】Ubuntu安装mysql并连接navicat

Linux系统部署Django项目 文章目录Linux系统部署Django项目一、mysql安装二、mysql配置文件三、新建数据库和用户四、nivacat链接mysql一、mysql安装 linux安装mysql数据库有很多教程&#xff0c;根据安装方式不同&#xff0c;相关的步骤也不同。可以参考&#xff1a;【Linux安…

前端基础(HTML、CSS、JS、jQuery)

文章目录一、HTML基础1.1 常用标签&#xff08;表格、表单、按钮等&#xff09;1.2 其他一些标签&#xff08;书签、显示效果、缩写等&#xff09;二、CSS基础2.1 CSS引入方式2.2 CSS选择器2.3 CSS常用属性三、JavaScript3.1 JS使用方式3.2 变量和数据类型3.3 函数、作用域、条…

Unity基础框架从0到1(五)延时任务调度模块

索引 这是Unity基础框架从0到1的第五篇文章&#xff0c;前面的文章和对应的视频我一起列到这里&#xff1a; 文章 Unity基础框架从0到1 开篇 Unity游戏框架从0到1 (二) 单例模块 Unity基础框架从0到1&#xff08;三&#xff09;高效的全局消息系统 Unity基础框架从0到1&a…

CentOS 7 DNS服务器架设

CentOS 7 DNS服务器部署 项目背景和要求 要保证即能够解析内网域名linuxidc.local的解析&#xff0c;又能解析互联网的域名。 主DNS服务器&#xff1a;ZZYH1.LINUXIDC.LOCAL 辅助DNS服务器&#xff1a;ZZYH2.LINUXIDC.LOCAL 包含以下域的信息&#xff1a; 1、linuxidc.lo…

mybatis多表联查(一对一、一对多/多对一、多对多)

mybatis多表联查(一对一、一对多/多对一、多对多) 在开发过程中单表查询往往不能满足需求分析的很多功能&#xff0c;对于比较复杂业务来说&#xff0c;关联的表有几个&#xff0c;甚至是几十个并且表与表之间的关联相当复杂。为了能够实现复杂的功能业务&#xff0c;就必须进…

Java 并发工具合集 JUC 大爆发!!!

并发工具类 通常我们所说的并发包也就是 java.util.concurrent (JUC)&#xff0c;集中了 Java 并发的各种工具类&#xff0c; 合理地使用它们能帮忙我们快速地完成功能 。 1. CountDownLatch CountDownLatch 是一个同步计数器&#xff0c;初始化的时候 传入需要计数的线程等待数…

Monaco Editor编辑器教程(二七):集成多个GitLab编辑器颜色样式主题

前言 在开发编辑器时,未来满足开发者的审美需求,有时需要设计多套颜色主题,最基础的是黑色主题和白色主题。如果用户愿意出钱的话,可以加上一些其他花里胡哨的颜色主题,在vscode中是可以搜到。今天就来教大家一些,如何借助现成的资源来为自己的monaco编辑器增加丰富的颜…

JavaScript控制语句及搭建前端服务器

目录 一、for in 二、for of 三、try catch 四、搭建前端服务器 &#xff08;1&#xff09;安装nvm &#xff08;2&#xff09;检查npm &#xff08;3&#xff09;搭建前端服务器 一、for in 主要用来遍历对象 let father {name:张三, age:18, study:function(){}};f…

Linux常用的网络命令有哪些?快速入门!

在Linux系统中&#xff0c;有许多常用的网络命令可以用来进行网络配置和故障排除。这些命令可以帮助我们了解网络的状态和性能&#xff0c;并且可以快速诊断和解决网络问题。在本文中&#xff0c;我们将介绍一些常用的Linux网络命令&#xff0c;并提供一些案例来帮助您更好地理…

高数基础2

目录 函数的极限&#xff1a; 自变量趋向于有限值时函数的极限 左右极限 需要分左右极限的三种问题&#xff1a; 例题&#xff1a; 极限性质&#xff1a; 函数的保号性&#xff1a; 函数的保号性&#xff1a; 例题&#xff1a; 极限值与无穷小的关系 极限存在准则&#…

FRRoute 路由信息采集 + MPLS VPN隧道信息采集与识别

FRRoute 路由信息采集 MPLS VPN隧道信息采集与识别FRRoute数据库-表路由信息采集命令输出字段 -> 映射到 -> 数据库字段数据结构 算法show int brroute -nshow interfaces loMPLS VPN隧道信息采集与识别step 1 : 采集mpls邻居表step 2 : MPLS 隧道识别FRRoute 数据库-…

RK3568平台开发系列讲解(Linux系统篇)线程 pthread 详解

🚀返回专栏总目录 文章目录 一、POSIX 线程模型二、pthread_create()创建线程三、线程属性3.1、初始化线程对象属性3.2、销毁一个线程属性对象3.3、线程的分离状态3.4、线程的调度策略3.5、线程的优先级3.6、线程栈四、线程退出五、回收已终止线程的资源沉淀、分享、成长,让…

Word处理控件Aspose.Words功能演示:使用C#创建MS Word文档

Aspose.Words是一种高级Word文档处理API&#xff0c;用于执行各种文档管理和操作任务。API支持生成&#xff0c;修改&#xff0c;转换&#xff0c;呈现和打印文档&#xff0c;而无需在跨平台应用程序中直接使用Microsoft Word。 Aspose API支持流行文件格式处理&#xff0c;并…

苹果6信号不好的快速解决方法

许多朋友反馈&#xff0c;苹果6的信号不佳&#xff0c;建议从以下方面查找&#xff1a; 方法一&#xff1a;开启飞行模式后再关闭 有时候手机由于周围环境网络比较差&#xff0c;会导致信号处于无服务状态&#xff0c;这时后我们开启飞行模式后再关闭飞行模式&#xff0c;系统就…

反序列化漏洞及PHP魔法函数

目录 1、漏洞原理 2、序列化&#xff08;以PHP语言为例&#xff09; 3、反序列化 4、PHP魔法函数 &#xff08;1&#xff09;__wakeup() &#xff08;2&#xff09;__destruct() &#xff08;3&#xff09;__construct() &#xff08;4&#xff09;__toString() &…

Arduino UNO驱动micro SD卡读写模块

目录一、简介二、使用前准备三、测试方法四、实验现象一、简介 Micro SD卡模块TF卡读写卡器板载电平转换电路&#xff0c;即接口电平可为5V或3.3V&#xff0c;支持支持Micro SD卡(≤2G)、Micro SDHC高速卡(≤32G)。供电电源为4.5V~5.5V&#xff0c;板载3.3V稳压电路&#xff0…

甘特图控件DHTMLX Gantt入门使用教程【引入】:dhtmlxGantt与ASP.NET Core(上)

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的大部分开发需求&#xff0c;具备完善的甘特图图表库&#xff0c;功能强大&#xff0c;价格便宜&#xff0c;提供丰富而灵活的JavaScript API接口&#xff0c;与各种服务器端技术&am…