零、简介

本文主要为本人初次接触CSDN,尝试着自己去创作一些东西,虽然可能看来很简单,但这也是我学习的过程。
本次我想实现的主要是实现自动化win7扫雷的过程,我玩win7版本的扫雷也已经有上千局了,在游玩的过程中,我发现我的大部分操作和逻辑判断过程都是重复的,所以我产生了让电脑自己玩扫雷的想法,也是在此过程中来精进我的代码水平,向大神学习。

一、逻辑框架

1.1 与windows交互 包括获取截图 鼠标点击

1.2 扫雷内部算法实现

二、代码实现

2.1 windows下获取屏幕截图

在这里,我在网上首先搜索了C++的截图代码,太过繁长,并且经过我的测试,由于我的屏幕开启了文字大小缩放,所以其截图大小是有问题的,原先是1920*1080的屏幕,我的文字缩放是125%,也就是说其在系统中生成的原始图像是1536*864,所以C++代码截取了屏幕左上角开始1536*864的内容,不完整,所以弃用C++,改用Java,我发现Java的代码相当的简单,看来Java对这些接口做了相当好的封装,以下是实现屏幕获取的Java代码。

import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;public class GetScreen {public void getScreenShot() throws Exception{Robot robot = new Robot();BufferedImage screenShot = robot.createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));ImageIO.write(screenShot, "JPG", new File("screen_shot.jpg"));}public static void main(String[] args) throws Exception{GetScreen getScreen = new GetScreen();getScreen.getScreenShot();}
}

获取屏幕截图的主要实现原理是用java.awt包下的Robot类的createScreenCapture方法。
awt(Abstract Window Toolkit)抽象视窗工具组,是Java的平台独立的视窗系统, 图形和使用者界面器件工具包。AWT是Java基础类(JFC-Java Foundation Classes)的一部分,为Java程序提供图形使用者界面(GUI)的标准API。
Robot类用于生成本机系统输入事件,主要目的是促进Java平台的自动化测试。
createScreenCapture的参数为createScreenCapture(Rectangle screenRect),表明截取screenRect大小的屏幕,在这里我们用Toolkit.getDefaultToolkit().getScreenSize()来返回屏幕的大小。
BufferedImage是一个承载图像数据的数据流
ImageIO. write方法用来写入图片,其有三个参数,第一个是图像数据的数据流,第二个是文件格式,第三个是文件名。

2.2 对图像进行预处理

2.2.1 图像切分

图像切分的基本思路就是先把上一步得到的屏幕截图切分出雷区,再根据雷区的行数和列数来单独切分出每一小块,基本实现如下:

public static int[][] splitCells(int startX, int startY, int width, int height, int hori, int verti) throws IOException {BufferedImage image = ImageIO.read(new File("screen_shot.jpg"));BufferedImage croppedImage = image.getSubimage(startX, startY, width, height);int[][] ans = new int[hori][verti];for (int i = 0; i < hori; i++) {for (int j = 0; j < verti; j++) {int x0 = (int) Math.round(i * (width / (double) hori));int x1 = (int) Math.round((i + 1) * (width / (double) hori));int y0 = (int) Math.round(j * (height / (double) verti));int y1 = (int) Math.round((j + 1) * (height / (double) verti));BufferedImage cellImage = croppedImage.getSubimage(x0 + 5, y0 + 5, x1 - x0 - 10, y1 - y0 - 10);ans[i][j] = recognize(cellImage);//ImageIO.write(cellImage,"JPG","img_set\\crop_test_"+Integer.toString(i*16+j)+".jpg");}}return ans;
}

splitCells函数接收六个参数,前两个分别是雷区左上角的坐标,中间两个是雷区的宽度和高度,后两个是雷区每一行的雷数和每一列的雷数,函数返回一个二维数组,表明这个雷区的状态,或者说每一个格子的数字,这就要看下一个小节,讲上面的代码中recognize函数的实现了。
在这里主要需要知道的就是BufferedImage的getSubimage方法了,该方法接收四个参数,分别是起始点的横纵坐标和图片的宽和高,根据这些参数返回原图像的一个子图像。

2.2.2 图像小块识别

下面就要讲这个程序比较难的一部分,也就是我们怎么把一个小块的图片转变成一个电脑可以识别的状态,众所周知,扫雷中的格子有这些状态,未点开的格子,点开的格子,其中有一个1~8的数字或者没有数字(这里因为是电脑玩游戏的原因,我们不需要标出雷),当然还有可能点开是雷,但这就不在我们考虑范围之内了(毕竟这就意味这游戏失败了)

一开始我会以为这很容易,毕竟我们面对的都是固定的图像,不需要像识别手写数字那样使用神经网络之类的方法,但我在写算法的时候发现了问题,首先,每个格子的颜色不尽相同,这里可以看到win7扫雷在实现的时候为图像添加了光效,这就使得格子的亮度从左上角向右下角递减,我本来的想法是对格子的每个状态截几张图求其平均值生成一张模板图然后进行对比,现在看来可能不大行,首先是有一个亮度的区别,然后因为这个图片比较小,所以如果有一两个像素的位移会对结果有较大的影响。

我的想法是首先把数字和非数字的区别出来,我们可以观察到,数字图像和非数字图像的一个最明显的区别是非数字图像的整体颜色较为平均,而数字图像因为有色块,颜色有较大差异,所以我考虑用方差的思路,这里不是把颜色求平均再求方差,而是求这个像素与上一个像素之间的差异,因为这样可以使得差异更明显,而且我们不用选取所有的点,这样可以减小计算量,代码如下:

public static int imageVarience(BufferedImage image){int height=image.getHeight();int width=image.getWidth();int varience=0;int color = image.getRGB(2, 2);int preB = color & 0xff;int preG = (color & 0xff00) >> 8;int preR = (color & 0xff0000) >> 16;for(int i=2;i<width-2;i++){for (int j=2;j<height-2;j++){color=image.getRGB(i,j);int curB = color & 0xff;int curG = (color & 0xff00) >> 8;int curR = (color & 0xff0000) >> 16;varience+=(curB-preB)*(curB-preB)+(curG-preG)*(curG-preG)+(curR-preR)*(curR-preR);j+=2;}i+=2;}return varience;
}

在这里的代码中涉及到了BufferedImage的数据存储模式,我们可以将一个BufferedImage image数据其看作为是一个unsigned int image[width][height]数据,也就是无符号二维数组,每一个像素点BufferedImage用32bits来存储,分别为AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB,其中A为Alpha,为不透明度,RGB为红绿蓝,BufferedImage的getRGB方法会返回这32bits中的低24位,然后稍加位运算即可获得RGB值。
这些图片的方差值如下:

我们可以看到,非数字图片和数字图片的值还是有很大差别的,所以我选取了600000作为了两者的分界线。
再然后就是区分未点开的格子和点开的空白格子了,观察不难发现,它们很明显的区别就是一个呈现明显的蓝色 而另一个呈现白色,也就是说一个的Blue值会明显偏高,而另一个会比较相同,另外经测试发现,再两者中Red的值都是三原色中最低的,所以我们将Blue-Red值作为区分其是白还是蓝的标准。经测试图片数据如下,每一行都是一张图片的数据,取其平均值。

左边是蓝色的,右边是白色的,可以看到区别还是不小,我们取60作为其的临界值。
计算其图片色差的代码如下:

 public static int calColorDiff(BufferedImage image){int height = image.getHeight();int width = image.getWidth();int diff = 0;int cnt=0;for (int i = 2; i < width - 2; i++) {for (int j = 2; j < height - 2; j++) {int color = image.getRGB(i, j);int Blue = color & 0xff;int Red = (color & 0xff0000) >> 16;diff+=Blue-Red;cnt++;j += 2;}i += 2;}return diff/cnt;
}

接下来就是处理数字的部分了,我的想法和上面处理判断图像是否均匀的思路一致,因为每个数字都有自己的形状,我们可以对每一个数字设定一个其自己的点集,对于这个点集,只有正确的数字才能匹配上,判断是否匹配上的方法还是使用方差,如果匹配上,因为这个数字在它的点集上的颜色基本一致,所以其方差会很小,而别的数字方差会很大,经测试,效果还不错,唯一的缺陷就是我们需要自己去慢慢弄点集。在这里我也学习了java里ArrayList的使用,其使用方法类似于C++STL库中的vector,一样需要在尖括号中输入这个List里元素的类别,ArrayList里add方法类似于vector的push_back方法,其他方法暂时没有用到。

2.2.3 代码汇总

下面是2.2部分的代码汇总:

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;public class ImageProcess {private static final ArrayList<ArrayList<Point>> AAP =initAAP();public static int[][] splitCells(int startX, int startY, int width, int height, int hori, int verti) throws IOException {BufferedImage image = ImageIO.read(new File("screen_shot.jpg"));BufferedImage croppedImage = image.getSubimage(startX, startY, width, height);int[][] ans = new int[verti][hori];for (int i = 0; i < verti; i++) {for (int j = 0; j < hori; j++) {int x0 = (int) Math.round(j * (width / (double) hori));int x1 = (int) Math.round((j + 1) * (width / (double) hori));int y0 = (int) Math.round(i * (height / (double) verti));int y1 = (int) Math.round((i + 1) * (height / (double) verti));BufferedImage cellImage = croppedImage.getSubimage(x0 + 5, y0 + 5, x1 - x0 - 10, y1 - y0 - 10);System.out.print(recognize(cellImage)+" ");ans[i][j] = recognize(cellImage);}System.out.println();}return ans;}public static int imageVarience(BufferedImage image) {int height = image.getHeight();int width = image.getWidth();int varience = 0;int color = image.getRGB(2, 2);int preB = color & 0xff;int preG = (color & 0xff00) >> 8;int preR = (color & 0xff0000) >> 16;for (int i = 2; i < width - 2; i++) {for (int j = 2; j < height - 2; j++) {color = image.getRGB(i, j);int curB = color & 0xff;int curG = (color & 0xff00) >> 8;int curR = (color & 0xff0000) >> 16;varience += (curB - preB) * (curB - preB) + (curG - preG) * (curG - preG) + (curR - preR) * (curR - preR);j += 2;}i += 2;}return varience;}public static int calColorDiff(BufferedImage image) {int height = image.getHeight();int width = image.getWidth();int diff = 0;int cnt = 0;for (int i = 2; i < width - 2; i++) {for (int j = 2; j < height - 2; j++) {int color = image.getRGB(i, j);int Blue = color & 0xff;int Red = (color & 0xff0000) >> 16;diff += Blue - Red;cnt++;j += 2;}i += 2;}return diff / cnt;}public static int calDigSimi(BufferedImage image,int dig){ArrayList<Point> points=AAP.get(dig);int pointNum= points.size();int varience=0;int color = image.getRGB(points.get(0).x,points.get(0).y);int preB = color & 0xff;int preG = (color & 0xff00) >> 8;int preR = (color & 0xff0000) >> 16;for(Point point:points){color=image.getRGB(point.x,point.y);int curB = color & 0xff;int curG = (color & 0xff00) >> 8;int curR = (color & 0xff0000) >> 16;varience += (curB - preB) * (curB - preB) + (curG - preG) * (curG - preG) + (curR - preR) * (curR - preR);}return varience/pointNum;}/*The return value of function recognize()is specified as follows:number 0 stands for no numbers but opened;number 1~8 stands for number 1~8;number 9 stands for cell not opened;*/public static int recognize(BufferedImage image) {int varience = imageVarience(image);if (varience < 600000) {if (calColorDiff(image) < 60) return 0;else return 9;} else {int maxDig=0,minSimi=Integer.MAX_VALUE;for (int i=0;i<8;i++) {int tempSimi=calDigSimi(image,i);if (minSimi>tempSimi){minSimi=tempSimi;maxDig=i+1;}}return maxDig;}}public static void addLine(ArrayList<Point> AP, int x0, int y0, int x1, int y1) {if (Math.abs(x0 - x1) > Math.abs(y0 - y1)) {if (x0 >= x1) {int temp = x1;x1 = x0;x0 = temp;}for (int i = x0; i <= x1; i++) {int j = (int) Math.round((i - x0) / (double) (x1 - x0) * (y1 - y0) + y0);AP.add(new Point(i, j));}} else {if (y0 >= y1) {int temp = y1;y1 = y0;y0 = temp;}for (int j = y0; j <= y1; j++) {int i = (int) Math.round((j - y0) / (double) (y1 - y0) * (x1 - x0) + x0);AP.add(new Point(i, j));}}}public static ArrayList<ArrayList<Point>> initAAP() {ArrayList<ArrayList<Point>> myAAP = new ArrayList<>();ArrayList<Point> AP1 = new ArrayList<>();addLine(AP1, 9, 5, 16, 3);addLine(AP1, 16, 3, 16, 27);addLine(AP1, 16, 4, 16, 27);addLine(AP1, 10, 28, 22, 28);myAAP.add(AP1);ArrayList<Point> AP2 = new ArrayList<>();addLine(AP2,11,4,16,2);addLine(AP2,17,2,24,8);addLine(AP2,24,9,21,14);addLine(AP2,20,15,10,26);addLine(AP2,10,27,23,27);addLine(AP2,11,28,22,28);myAAP.add(AP2);ArrayList<Point> AP3 = new ArrayList<>();addLine(AP3,10,4,15,2);addLine(AP3,16,2,22,7);addLine(AP3,22,8,17,14);addLine(AP3,17,15,13,15);addLine(AP3,18,15,23,21);addLine(AP3,23,22,17,28);addLine(AP3,16,28,10,26);myAAP.add(AP3);ArrayList<Point> AP4 = new ArrayList<>();addLine(AP4,25,21,10,21);addLine(AP4,9,19,20,4);addLine(AP4,19,4,22,4);addLine(AP4,22,4,22,27);myAAP.add(AP4);ArrayList<Point> AP5 = new ArrayList<>();addLine(AP5,22,3,12,3);addLine(AP5,12,3,12,13);addLine(AP5,12,14,19,15);addLine(AP5,19,15,23,19);addLine(AP5,23,20,17,28);addLine(AP5,16,28,11,27);myAAP.add(AP5);ArrayList<Point> AP6 = new ArrayList<>();addLine(AP6,23,3,14,5);addLine(AP6,14,6,10,17);addLine(AP6,10,18,16,28);addLine(AP6,18,28,25,22);addLine(AP6,25,20,21,14);addLine(AP6,18,13,10,17);myAAP.add(AP6);ArrayList<Point> AP7 = new ArrayList<>();addLine(AP7,10,4,24,4);addLine(AP7,24,5,15,27);myAAP.add(AP7);ArrayList<Point> AP8 = new ArrayList<>();addLine(AP8,11,8,16,3);addLine(AP8,18,3,24,7);addLine(AP8,24,9,19,15);addLine(AP8,16,15,10,21);addLine(AP8,10,23,16,28);addLine(AP8,19,28,25,23);addLine(AP8,25,21,19,15);addLine(AP8,16,15,11,10);myAAP.add(AP8);return myAAP;}public static int[][] func() throws Exception {GetScreen.getScreenShot();int x0 = 167, y0 = 110, x1 = 1369, y1 = 752;int[][] ans = ImageProcess.splitCells(x0, y0, x1 - x0, y1 - y0, 30, 16);return ans;}public static void main (String[] args) throws Exception {long startTime = System.currentTimeMillis();func();long endTime = System.currentTimeMillis();System.out.println("Processing time: " + (endTime - startTime) + "ms");}
}

2.3 模拟点击

下面我们需要模拟实际的点击操作,这里我们创建了一个Simuclick类用于实现,因为实际屏幕尺寸是有差别的,所以我把其设定为了实例对象而不是静态对象,接着我们将实际屏幕的参数传递进去,为了简化逻辑,我们对外封装的方法只传递了格子的横纵坐标。这里我们还是使用2.1节给我们的awt包下的Robot类,这里我们使用三个方法,第一个是mouseMove,第二个是mousePress,第三个是mouseRelease,方法名称还是比较直观的,mouseMove就是传递横纵坐标并且把鼠标移动到横纵坐标的位置上,Press和Release分别是按压和松开,而代表鼠标左键的值是MouseEvent.BUTTON1_DOWN_MASK,但这里有一个需要注意的地方就是由于我是在副屏幕上写程序和调试,在主屏幕上运行我的扫雷程序,所以在运行程序之初我们需要在扫雷游戏的窗口内点击一下以便于把焦点转移到扫雷程序上,这我们可以在构造函数里实现,该段内容的程序代码如下所示:

import java.awt.*;
import java.awt.event.MouseEvent;public class SimuClick {private static Robot robot =null;int x0,y0,width,height,hori,verti;SimuClick(int x0, int y0, int width, int height, int hori, int verti){this.x0=x0;this.y0=y0;this.width=width;this.height=height;this.hori=hori;this.verti=verti;try {robot=new Robot();}catch (AWTException e) {e.printStackTrace();}Click(-1,-1);}public void Click(int x,int y){int cx=(int)(x0+width* (x+0.5)/hori);int cy=(int)(y0+height*(y+0.5)/verti);robot.mouseMove(cx,cy);robot.mousePress(MouseEvent.BUTTON1_DOWN_MASK);robot.mouseRelease(MouseEvent.BUTTON1_DOWN_MASK);}public static void main(String[] args) {int x0 = 167, y0 = 110, x1 = 1369, y1 = 752;SimuClick simuClick =new SimuClick(x0, y0, x1 - x0, y1 - y0, 30, 16);simuClick.Click(29,8);}
}

2.4 扫雷逻辑

这可以说是整个程序最难的部分,因为在测试中可能会有各种各样的bug,并且在这里我们是要模拟人脑的思维逻辑,以我的经验,当我在思考的时候,有的判断逻辑是比较简单的,有的是要依据推断的,有的时候就是不知道为什么这样做,看到那些数字组合就下意识地认为那里没有雷,这里有一些是根据严谨的逻辑推理可以得出来的,有的时候就是因为大部分这种情况都是对的,但少部分情况是不对的,但这种判断模式不能被我们写到程序中,因为我们的推断一定要严谨,那么,让我们开始吧。

2.4.1 最简单的逻辑

首先,我们要明确我们的程序的实现原理,扫雷游戏不是通过标出所有的雷获胜,而是通过点开来所有不是雷的区域获得胜利,所以我们在实际操作中,也不需要表出雷,因为电脑可以记忆下来那边是雷,我们只需要点开所有的空格就好,而众所周知,扫雷中的数字就代表着其周围八个格子所拥有的雷的数量,所以我们可以找到那些周围位置格子的数量和当前所标数字相等的格子,那么这些格子中一定全部是雷,而知道了那些格子是雷了以后,我们就可以继续进行判断,如果这个格子中的数字和周围已知的雷数相同,那么这个格子周围剩下未知的格子便全部不是雷,通过这个判断雷和点开空格的来回往复,我们便可以写出初级的扫雷逻辑,代码如下:

import java.awt.*;
import java.util.ArrayList;public class MineSweeper {private static int[][] map;private static boolean[][] mine;private static boolean[][] verify;static int hori=30,verti=16;private static SimuClick simuClick=null;public static void printMap(){int height=map.length;int width=map[0].length;for(int i=0;i<height;i++){for (int j=0;j<width;j++){System.out.print(map[i][j]+" ");}System.out.println();}}public static void checkMine(){for(int i=0;i<verti;i++){for (int j=0;j<hori;j++){int tempval=map[i][j];if (tempval==9||tempval==0||verify[i][j])continue;int cnt=0;ArrayList<Point> AP =new ArrayList<>();for(int x=-1;x<=1;x++){for(int y=-1;y<=1;y++){if (i+x>=0&&i+x<verti&&j+y>=0&&j+y<hori&&map[i+x][j+y]==9) {cnt++;AP.add(new Point(i + x, j + y));}}}if(cnt==tempval){for(Point point:AP){mine[point.x][point.y]=true;}verify[i][j]=true;return;}}}}public static void clickEmpty(){for(int i=0;i<verti;i++){for(int j=0;j<hori;j++){int tempval=map[i][j];if (tempval==9||tempval==0)continue;int cnt=0;ArrayList<Point> AP =new ArrayList<>();for(int x= -1;x<=1;x++) {for (int y = -1; y <= 1; y++) {if (i + x >= 0 && i + x < verti && j + y >= 0 && j + y < hori && mine[i+x][j+y]) cnt++;}}if (tempval==cnt){for(int x= -1;x<=1;x++) {for (int y = -1; y <= 1; y++) {if (i + x >= 0 && i + x < verti && j + y >= 0 && j + y < hori && map[i+x][j+y]==9&& !mine[i + x][j + y]){simuClick.Click(j+y,i+x);return;}}}}}}}public static void Logic() throws Exception {while(true){checkMine();clickEmpty();map= ImageProcess.func();}}private static void initClick() {simuClick.Click(15,7);}public static void main(String[] args) throws Exception {int x0 = 167, y0 = 110, x1 = 1369, y1 = 752;simuClick =new SimuClick(x0, y0, x1 - x0, y1 - y0, hori,verti);initClick();Thread.sleep(1000);map= ImageProcess.func();mine =new boolean[verti][hori];verify =new boolean[verti][hori];for(int i=0;i<verti;i++)for(int j=0;j<hori;j++)mine[i][j]=false;for(int i=0;i<verti;i++)for(int j=0;j<hori;j++)verify[i][j]=false;initClick();Logic();}
}

对于这个初代程序,我们可以看到,在这个程序中,我设定了两个布尔数组用来存储状态,第一个mine是雷的意思,用在表示这个格子是不是雷,第二个是verify,用来表明这个格子是否被验证过,验证过的意思是这个格子周围只有等同于其数字的格子并且全部是雷。
下面是某次运行的结果:

从这里我们可以看到,最简单的逻辑在初始状态比较好的情况下是可以打开大部分板块的(这里因为win7版本扫雷的特性,初始点到的方块一定是空白的,所以在运气不好的情况下,第一次点击会打开一个3*3的矩形,如果数字出现的不好那么是无法继续往下推理下去的)但是基本不可能完成解题,因为逻辑太过简单,这里我们通过一个例子来感受一下,我们定义一下的一个局部区域中左上角坐标为(1,1),右下角坐标为(5,5)。不难发现(2,2),(3,3)都是雷,那么我们观察在(3,1)格中的数字2,可以明确格(4,1),(4,2)中有一个雷,然后我们观察(3,2)的数字3,就可以推断出格子(4,3)一定不是雷,那么对整体的求解就会带来进展。

2.4.2 改进的逻辑判断

我在这部分建立了一个新函数叫做intelCheck用来进行相互判断,并且在主逻辑体内写了一个逻辑,就是说当2.4.1的逻辑判断带不来进展的时候才会去调用这个逻辑。下面我将会用两张图来说明这个逻辑的判断过程:

左图是第一种情况,我称为判雷,我们观察比较(2,2)的1和(3,2)的3,可以发现,相比1,3的判定范围多了两个格子,少了一个格子,雷的个数增加了2,所以我们可以肯定,多出来的两个一定都是雷。
右图是第二种情况,我称为判空,我们观察(1,2)的1和(1,3)的1,可以发现在这过程中格子数多出了一个,但是数字并没有变化,这就说明多出来的格子一定是空的。
我们的代码具体逻辑就是这样,由于在这过程中并没有用到新的语法和技术,我就直接在下面贴出我的代码:

public static void intelCheck(){for(int i=0;i<verti;i++){for(int j=0;j<hori;j++){int tempval=map[i][j];if (tempval==0||tempval==9||verify[i][j])continue;ArrayList<Point> APtemp=new ArrayList<>();for(int p=-1;p<=1;p++){for(int q=-1;q<=1;q++){if (i+p<0||i+p>=verti||j+q<0||j+q>=hori)continue;if (map[i+p][j+q]!=9)continue;if (mine[i + p][j + q])tempval--;else APtemp.add(new Point(i+p,j+q));}}for(int x=-1;x<=1;x++){for(int y=-1;y<=1;y++){if (i+x<0||i+x>=verti||j+y<0||j+y>=hori)continue;int curi=i+x,curj=j+y;int ttval=map[curi][curj];if(ttval==0||ttval==9||verify[curi][curj])continue;ArrayList<Point> APtt=new ArrayList<>();for(int p=-1;p<=1;p++){for(int q=-1;q<=1;q++){if (curi+p<0||curi+p>=verti||curj+q<0||curj+q>=hori)continue;if (map[curi+p][curj+q]!=9)continue;if (mine[curi + p][curj + q])ttval--;else APtt.add(new Point(curi+p,curj+q));}}if (ttval<tempval)continue;ArrayList<Point> APsame=getIntersect(APtemp,APtt);ArrayList<Point> APUtemp=getUnique(APtemp,APtt);ArrayList<Point> APUtt=getUnique(APtt,APtemp);int diff=ttval-tempval;if (diff==0){if (APUtemp.size()==0){if(APUtt.size()!=0){for(Point point:APUtt){simuClick.Click(point.y, point.x);}return;}}}else{if (diff==APUtt.size()){for(Point point:APUtt){mine[point.x][point.y]=true;}return;}}}}}}
}
public static void Logic() throws Exception {while(true){boolean check1=checkMine();boolean check2=clickEmpty();if(!check1 && !check2){intelCheck();}map= ImageProcess.func();}
}

下面是某次运行的结果,可以看到相比较之前有了很大的改善,但还是会有不足的地方,我们来分析一下原因:

对于下面这块雷区,这是一个常见的“死局”,我们可以看到(4,4),(5,4)中有一个雷,(5,4),(7,4)中有一个雷,那么我们的程序目前就无法判断应该选什么,所以我将在下一步的操作中引入随机数来往下拓展。

2.4.3 随机数与错误检测

在上面两步的逻辑中,我们的程序是不会产生错误,即点到雷的,但是在大多数时候,这种纯粹靠逻辑的判断是完不成一句游戏的,我们需要靠一些运气来帮助我们,同时,如果点击错误,我们也需要进行判断。
首先我们来进行错误判断,实际上我么可以在这一步直接同时进行完成游戏和游戏失败的判断,这部分的逻辑也比较简单,通过成功的截图和失败的截图进行对比可以发现,这两块的弹窗在一定的区域是有重合的,并且弹窗是没有颜色变化的,所以我们可以借用前面算图像方差的方法对其进行判断,并且我们可以发现,错误的弹窗要比正确的弹窗大一块,所以我们可以用这个多出的部分来进行正确与错误的区分。


判断的代码如下:

public static int checkOK() throws IOException {BufferedImage image = ImageIO.read(new File("screen_shot.jpg"));BufferedImage croppedImage1 = image.getSubimage(850,360,100,30);BufferedImage croppedImage2 = image.getSubimage(850,360,140,120);if (imageVarience(croppedImage1)>1000)return 0;else if (imageVarience(croppedImage2)>1000) return 2;return 1;
}

接下来是随机数的部分,我一开始的想法比较复杂,是选取一个数字,其周围的雷未知,并随机点一个格子,然后我就遇到了下图的情况,这种情况下,最右上角的格子无法被触碰到,所以没法被选到,所以我就直接遍历,将自己遇到的第一个未定的格子点开,然而我发现这种方法也就用不到随机数了23333

代码如下,非常简单:

public static void randomClick(){for(int i=0;i<verti;i++){for(int j=0;j<hori;j++){if (map[i][j]==9&&!mine[i][j]){simuClick.Click(j,i);return;}}}
}

三、算法优化

经过几次实验发现,我们的方法每次的用时大概在80秒左右,而世界冠军的水平大概在30秒,所以还有很大的优化空间。

3.1 耗时观察

我通过加上断点检测时间,在一局游戏中得到了如下结果:

通过这个不难发现,checkOK和imageProcess操作花费了大量时间,究其原因是因为其中涉及到了大量的文件IO操作,在截屏过程中我们是把图片先保存到本地,在进行逻辑判断的,这个可以删去,变为直接传递BufferedImage,这样就不用走文件而是直接在内存中操作,我们来试一试。
首先修改代码:

public static BufferedImage getScreenShot() throws Exception
{Robot robot = new Robot();//BufferedImage screenShot = robot.createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));//ImageIO.write(screenShot, "JPG", new File("screen_shot.jpg"));return robot.createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));
}

将存储图片改为直接传递BufferedImage,我们来看一下效果:

我们发现用时有了很大的进步,那么还能不能再次改进呢,我们发现,checkOK花费的时间较上一步有所上涨,这是因为我们在这一步也需要去获取截屏而不是去读入本地文件,但是checkOK仅仅需要在比较少的地方用到,就是在点击格子的时候,并且因为我们前面两段的算法是保证不会出错的,所以我们仅仅需要在随机点格子的时候加入判断,并且在已经搜索出99个雷的时候进行判断就可以了,我们进一步修改代码,得到结果如下:

这时候的效果已经比较好了,要想速度更快只能去优化截屏时间了,鉴于笔者期中考试在即,暂时无法继续优化,故暂此搁笔,有空再写,主代码如下所示:

import java.awt.*;
import java.net.PortUnreachableException;
import java.util.ArrayList;public class MineSweeper {private static int[][] map;private static boolean[][] mine;private static boolean[][] verify;static int hori=30,verti=16;private static SimuClick simuClick=null;private static int game_status=0;private static int mine_number=99;public static void printMap(){int height=map.length;int width=map[0].length;for(int i=0;i<height;i++){for (int j=0;j<width;j++){System.out.print(map[i][j]+" ");}System.out.println();}}public static ArrayList<Point> getIntersect(ArrayList<Point> AP1,ArrayList<Point> AP2){ArrayList<Point> AP=new ArrayList<>();for(Point point:AP1){if (AP2.contains(point)) AP.add(point);}return AP;}public static ArrayList<Point> getUnique(ArrayList<Point> AP1,ArrayList<Point> AP2){ArrayList<Point> AP=new ArrayList<>();for(Point point:AP1){if (!AP2.contains(point)) AP.add(point);}return AP;}public static boolean checkMine(){for(int i=0;i<verti;i++){for (int j=0;j<hori;j++){int tempval=map[i][j];if (tempval==9||tempval==0||verify[i][j])continue;int cnt=0;ArrayList<Point> AP =new ArrayList<>();for(int x=-1;x<=1;x++){for(int y=-1;y<=1;y++){if (i+x>=0&&i+x<verti&&j+y>=0&&j+y<hori&&map[i+x][j+y]==9) {cnt++;AP.add(new Point(i + x, j + y));}}}if(cnt==tempval){for(Point point:AP){mine[point.x][point.y]=true;mine_number--;}verify[i][j]=true;return true;}}}return false;}public static boolean clickEmpty(){for(int i=0;i<verti;i++){for(int j=0;j<hori;j++){int tempval=map[i][j];if (tempval==9||tempval==0)continue;int cnt=0;ArrayList<Point> AP =new ArrayList<>();for(int x= -1;x<=1;x++) {for (int y = -1; y <= 1; y++) {if (i + x >= 0 && i + x < verti && j + y >= 0 && j + y < hori && mine[i+x][j+y]) cnt++;}}if (tempval==cnt){for(int x= -1;x<=1;x++) {for (int y = -1; y <= 1; y++) {if (i + x >= 0 && i + x < verti && j + y >= 0 && j + y < hori && map[i+x][j+y]==9&& !mine[i + x][j + y]){simuClick.Click(j+y,i+x);return true;}}}}}}return false;}public static boolean intelCheck(){for(int i=0;i<verti;i++){for(int j=0;j<hori;j++){int tempval=map[i][j];if (tempval==0||tempval==9||verify[i][j])continue;ArrayList<Point> APtemp=new ArrayList<>();for(int p=-1;p<=1;p++){for(int q=-1;q<=1;q++){if (i+p<0||i+p>=verti||j+q<0||j+q>=hori)continue;if (map[i+p][j+q]!=9)continue;if (mine[i + p][j + q])tempval--;else APtemp.add(new Point(i+p,j+q));}}for(int x=-1;x<=1;x++){for(int y=-1;y<=1;y++){if (i+x<0||i+x>=verti||j+y<0||j+y>=hori)continue;int curi=i+x,curj=j+y;int ttval=map[curi][curj];if(ttval==0||ttval==9||verify[curi][curj])continue;ArrayList<Point> APtt=new ArrayList<>();for(int p=-1;p<=1;p++){for(int q=-1;q<=1;q++){if (curi+p<0||curi+p>=verti||curj+q<0||curj+q>=hori)continue;if (map[curi+p][curj+q]!=9)continue;if (mine[curi + p][curj + q])ttval--;else APtt.add(new Point(curi+p,curj+q));}}if (ttval<tempval)continue;ArrayList<Point> APsame=getIntersect(APtemp,APtt);ArrayList<Point> APUtemp=getUnique(APtemp,APtt);ArrayList<Point> APUtt=getUnique(APtt,APtemp);int diff=ttval-tempval;if (diff==0){if (APUtemp.size()==0){if(APUtt.size()!=0){for(Point point:APUtt){simuClick.Click(point.y, point.x);}return true;}}}else{if (diff==APUtt.size()){for(Point point:APUtt){mine[point.x][point.y]=true;mine_number--;}return true;}}}}}}return false;}public static void randomClick(){for(int i=0;i<verti;i++){for(int j=0;j<hori;j++){if (map[i][j]==9&&!mine[i][j]){simuClick.Click(j,i);return;}}}}public static void Logic() throws Exception {int cnt=0,cnt1=0,cnt2=0;int cmtime=0,cetime=0,ictime=0,cotime=0,iptime=0;while(true){cnt++;long time1=System.currentTimeMillis();boolean check1=checkMine();long time2=System.currentTimeMillis();System.out.println("checkMine costs: "+(time2-time1)+" ms");cmtime+=time2-time1;boolean check2=clickEmpty();long time3=System.currentTimeMillis();System.out.println("clickEmpty costs: "+(time3-time2)+" ms");cetime+=time3-time2;if(!check1 && !check2){long time4=System.currentTimeMillis();boolean check3=intelCheck();long time5=System.currentTimeMillis();cnt1++;System.out.println("intelCheck costs: "+(time5-time4)+" ms");ictime+=time5-time4;if (!check3){randomClick();long time6=System.currentTimeMillis();game_status=ImageProcess.checkOK();long time7=System.currentTimeMillis();System.out.println("checkOK costs: "+(time7-time6)+" ms");cotime+=time7-time6;cnt2++;}}if (mine_number==0){long time9=System.currentTimeMillis();game_status=ImageProcess.checkOK();long time10=System.currentTimeMillis();System.out.println("checkOK costs: "+(time10-time9)+" ms");cotime+=time10-time9;cnt2++;}long time7=System.currentTimeMillis();if (game_status>0)break;map= ImageProcess.func();long time8=System.currentTimeMillis();System.out.println("imageProcess costs: "+(time8-time7)+" ms");iptime+=time8-time7;}System.out.println("checkMine costs: "+cmtime+" ms in "+cnt+" times. avg time is "+cmtime/cnt+" ms");System.out.println("clickEmpty costs: "+cetime+" ms in "+cnt+" times. avg time is "+cetime/cnt+" ms");System.out.println("intelCheck costs: "+ictime+" ms in "+cnt1+" times. avg time is "+ictime/cnt1+" ms");System.out.println("checkOK costs: "+cotime+" ms in "+cnt2+" times. avg time is "+cotime/cnt2+" ms");System.out.println("imageProcess costs: "+iptime+" ms in "+cnt+" times. avg time is "+iptime/cnt+" ms");}private static void initClick() {simuClick.Click(15,7);}public static void main(String[] args) throws Exception {int x0 = 167, y0 = 110, x1 = 1369, y1 = 752;simuClick =new SimuClick(x0, y0, x1 - x0, y1 - y0, hori,verti);initClick();Thread.sleep(1000);map= ImageProcess.func();mine =new boolean[verti][hori];verify =new boolean[verti][hori];for(int i=0;i<verti;i++)for(int j=0;j<hori;j++)mine[i][j]=false;for(int i=0;i<verti;i++)for(int j=0;j<hori;j++)verify[i][j]=false;initClick();Logic();}
}

CSDN初体验,尝试完成一个自动扫雷程序相关推荐

  1. 基友扫雷通关跟我炫耀!于是用Python自动扫雷程序十秒通关

    起因是这样的,基友和我一起玩扫雷高难度,今天他来告诉我他的通关了! 各种炫耀,如下图! 于是,我用就用Python开发了个自动扫雷程序!跟他装了逼,瞬间喊我哥,要我教他 好了,不废话了!本文用于娱乐, ...

  2. python编写木马攻击_用Python写一个自动木马程序

    电脑作为大家日常办公的工具,最怕的一件事情之一就是被偷,当我们的电脑被盗的时候,不仅仅是电脑本身,更重要的是电脑存储的资料都会丢失.如何尽快的找回电脑需要我们想点办法,今天就教大家一个好的技巧,虽说不 ...

  3. 当我尝试写一个自动写小说的AI,长路漫漫的踩坑之路 ToT

    起因 事情是这样的,前几天我在刷B站的时候看到一个大佬用训练了一个自动写高考作文的AI 链接: https://www.bilibili.com/video/BV1pr4y1w7uM 那我就想既然别人 ...

  4. 我的Go+语言初体验--Go+之环境安装与程序编码初体验

    一.Go+ 简介 对于 Go+ 工程而言: Go+ 将支持所有 Go 功能(包括部分支持 cgo): Go+ 提供了更简单优雅的语法,比 Go 更接近自然语言: Go+ 易于学习,不必在一开始就处理工 ...

  5. python自动卸载win程序_利用python实现自动扫雷程序

    自动扫雷一般分为两种,一种是读取内存数据,而另一种是通过分析图片获得数据,并通过模拟鼠标操作,这里我用的是第二种方式. 一.准备工作 1.扫雷游戏 我是win10,没有默认的扫雷,所以去扫雷网下载 h ...

  6. 用python做一个简单的投票程序_如何编写一个自动投票程序

    展开全部 此文章为ocean所有32313133353236313431303231363533e59b9ee7ad9431333335346138,版权归ocean所有 如何编写投票程序,大致分为这 ...

  7. java自动投票软件_如何编写一个自动投票程序

    *********************************************************************************** *                ...

  8. 如何编写一个自动投票程序

    *********************************************************************************** *                ...

  9. 如何编写一个自动投票程序 1

    ***********************************************************************************  *               ...

最新文章

  1. 大数据面试题及答案 100道 (2021最新版)
  2. 怎么把html4换文件夹打不开,HTML4
  3. 蓝桥杯java第七届决赛第三题--打靶
  4. 访问GitHub超慢的解决办法
  5. html5中本地存储概念是什么?
  6. Bailian2931 期末考试第二题——比较数字个数【文本】
  7. JPGPNG图片压缩java实现
  8. python爬取起点中文网小说
  9. 巧用变量代换求极限 高数
  10. 从word中无损批量导出图片
  11. 使用Route报错:A <Route> is only ever to be used as the child of <Routes> element, never rendered directl
  12. 最短路径之佛洛伊德算法
  13. 使用Python获取股市融资融券数据并绘制曲线
  14. .Net发布到IIS服务器,IIS服务器配置
  15. Ubuntu系统下C语言编译以及Makefile编译C语言程序
  16. 有事您Q我,qq在线离线状态
  17. 第023、024讲:递归:这帮小兔崽子、汉诺塔
  18. 2018年4月16日微众银行 INT数据挖掘笔试
  19. Android语音识别——谷歌语音识别与百度语音识别
  20. 秒杀所有区间相关问题

热门文章

  1. html更改纵坐标数值,教大家Excel中图表坐标轴的数值怎么修改
  2. HadoopMapReduce寻找共同好友
  3. 8155/8255/8295参数对比
  4. 市场营销问题 (三):机票的销售策略
  5. 基于JSP网上机票销售系统的设计与实现
  6. nginx查看配置文件
  7. 解决PPTP客户端拨号不成功
  8. 7-2 简单计算器 分数 13分
  9. html字数检测,检测已经输入字数.html
  10. vue echarts调整图表和标题的距离