当前位置 博文首页 > Janbar:用算法去扫雷(go语言)

    Janbar:用算法去扫雷(go语言)

    作者:[db:作者] 时间:2021-07-05 16:25

    1. 最初的准备
      首先得完成数据的录入,及从扫雷的程序读取界面数据成为我的算法可识别的数据
      其次是设计扫雷的算法,及如何才能判断格子是雷或者可以点击鼠标左键和中键。
      然后将步骤2的到的结果通过我的程序实现鼠标的点击动作
      下面是一个成功的gif图片,放在前面容易吸引人啊,哈哈。
      21秒高级扫雷
    2. 首先实现第一步
      将扫雷程序界面数据读取并保存为我的代码可识别的数据。我们知道程序界面的各个数字都有不同的颜色,那么我们可以通过这些颜色得到每个数字的特征码,及我的程序可以通过这些特征码识别这些数据。
      什么是图片的特征码,及图片和特征码为一对一的关系,知道特征码就能确定这个图片是啥,因此我的程序内置了特征码,然后获取雷区截图,通过分析rgb值得到特征码和已有的特征码比较,相同则确定了该位置是个啥。我借鉴了该文章图像识别技术的图像灰化,及将一张彩色图片灰化为两个不同的值,及图片二值化。使用如下算法
      灰化图像
      经过我的处理可以得到下面的值,我只拿其中一个数字说明,下面是数字2做图像灰化后得到的二值图像码
      1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
      1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
      1,1,1,0,0,0,0,0,0,0,0,1,1,1,1,1,
      1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,
      1,1,0,0,0,1,1,1,1,0,0,0,1,1,1,1,
      1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,
      1,1,1,1,1,1,1,0,0,0,0,1,1,1,1,1,
      1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,
      1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,
      1,1,0,0,0,0,1,1,1,1,1,1,1,1,1,1,
      1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,
      1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,
      1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
      1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
      1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
      0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,
      得到所有图像的特征码如下,我用go语言做了处理
    GridNumbers = map[GridDefine][]int{  // 每个标识的特殊位置的颜色值,用于区分这些位置的值
    		DefOne:     {0, 1, 0, 1, 1, 1, 1, 1, 1, 1}, // 1
    		DefTwo:     {0, 0, 0, 0, 0, 0, 0, 1, 0, 1}, // 2
    		DefThree:   {0, 1, 0, 0, 0, 0, 0, 1, 0, 1}, // 3
    		DefFour:    {0, 1, 0, 1, 0, 1, 0, 1, 1, 1}, // 4
    		DefFive:    {0, 1, 0, 0, 1, 0, 1, 1, 0, 1}, // 5
    		DefSix:     {0, 0, 0, 0, 1, 1, 1, 1, 1, 1}, // 6
    		DefSeven:   {1, 1, 0, 1, 0, 1, 0, 1, 0, 1}, // 7
    		DefEight:   {0, 0, 0, 0, 0, 1, 0, 1, 0, 1}, // 8
    		DefFlag:    {0, 1, 1, 1, 1, 1, 1, 1, 1, 0}, // 红旗
    		DefMine:    {0, 1, 0, 1, 0, 1, 0, 1, 0, 1}, // 地雷
    		DefRedMine: {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // 标红地雷,表示输了
    		DefClick:   {1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, // 可点击白板
    		DefNotNeed: {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, // 不可点击
    	}
    
    1. 找到图像识别方案,下面就说说算法吧
      第一步,找到必定为雷的位置,如下图所示,之所以能得出标旗位置,是因为周围有数字可以确定这是一个雷。因此有下面的准则
      可点击数 + 已经标旗数 = 本格子雷数,右键把可点击位置全部标雷
      此时这些点已经没有意义,可以不考虑了
      必定为雷
      第二步,找到有不确定的位置,但是标旗数已经等于本格雷数,所以该格子周围可点击处一定不是雷。所以还有一下准则
      标旗数 = 本格子雷数,鼠标中键点击格子,点开可点击位置
      此时对于左边的3右边的2来说雷的数量已经够了,可以中键点击这两个格子。此时这些点已经没有意义,可以不考虑了
      一定不是雷
      第三步,如果遍历所有数字点,没有满足第一和第二步的情况,那么将除了第一步和第二步排除后的数字点列为不确定的点。此时需要相邻2个点同时做判断。下面介绍一种不确定状态下标雷技巧
      对于中间的2上面3个点有二个雷,2右边的1表示上面三个点中右边二个点只能有一个雷,因此对于中间的2来说剩余的一个点一定是雷。下面是准则。
      两个相邻格子待标雷数多的为A,待标雷数少的为B
      A待标雷数 - B待标雷数 = A可点击数 - AB相交位置数
      表示A除了相交位置以外的可点击位置一定全是雷
      不确定标雷
      第四步,下面的情况我一定知道红色位置不是雷,因为对于4而言下面二个点只能有一个雷,那么4左边的2的已经把雷确认完了,所以红色位置肯定不是雷,下面是准则。
      两个相邻格子A和B待标雷数相同,A的可点击数为a,格子B的可点击数为b,两个格子相交数为c
      如果a = c,则格子B除开相交位置的可点击位置一定不是雷
      如果b = c,则格子A除开相交位置的可点击位置一定不是雷
      不确定点一定不是雷
    2. 根据上面四个准则,扫雷一定能进行到除了猜雷就没辙的情况。意思是按照上面的准则,全都没法判定是雷不是雷的情况,那么只能乖乖的猜雷了。然而我多出查询资料得到猜雷最多凭概率,没有办法做到一定成功。因此我的扫雷算法的猜雷只是简单的在不确定点周围随机点一个左键。所以用我的程序自动猜雷是会出现猜错的情况。
    3. 下面公布我的代码吧
    package main
    
    import (
    	"log"
    	"math/rand"
    	"reflect"
    	"time"
    	"unsafe"
    
    	"fmt"
    
    	"github.com/lxn/win" // 另外的源码,github.com/CodyGuo/win
    )
    
    type Pos struct {
    	x int /* 表示一个点的横坐标,纵坐标 */
    	y int
    }
    
    /* 定义格子内容的类型,不直接用int避免使用时混乱 */
    type GridDefine int
    
    const ( /* 枚举类型,标记格子内容 */
    	DefClick   GridDefine = iota // 可点击白板
    	DefOne                       // 1
    	DefTwo                       // 2
    	DefThree                     // 3
    	DefFour                      // 4
    	DefFive                      // 5
    	DefSix                       // 6
    	DefSeven                     // 7
    	DefEight                     // 8
    	DefFlag                      // 红旗
    	DefNotNeed                   // 不可点击的空白,以及标识无用的数字位置
    	DefMine                      // 地雷
    	DefRedMine                   // 标红地雷,表示输了
    )
    
    const (
    	GameName = "扫雷" // 游戏窗体名称
    	GameHigh = 16   // 雷区高度
    	GameWide = 30   // 雷区宽度
    	GridLen  = 16   // 雷区每个格子长宽
    	//GameMine = 99   // 存在雷的个数,貌似用不上了
    )
    
    var (
    	StartBtn    win.POINT                // 笑脸的位置,点击可以开始游戏
    	StartNum    win.POINT                // 标记剩余雷数的位置
    	StartMine   win.POINT                // 雷区起始位置
    	GameHwnd    win.HWND                 // 扫雷窗体对象
    	GuessMineOk int                      // 1表示启动启动猜雷,0表示玩家自己猜雷
    	TeachModel  int                      // 1表示每一步都显示操作,0表示不显示每一步的操作
    	CntNotSure  int                      // 标记不确定点个数
    	flagStart   int                      // 标记是否已经开局,如果开局则永远不会等于0
    	NotSurePos  [GameHigh * GameWide]Pos // 缓存一次扫描中所有不确定点位置
    	GridSave    [][]GridDefine           // 保存数据的二维数组
    	GridNumbers = map[GridDefine][]int{  // 每个标识的特殊位置的颜色值,用于区分这些位置的值
    		DefOne:     {0, 1, 0, 1, 1, 1, 1, 1, 1, 1}, // 1
    		DefTwo:     {0, 0, 0, 0, 0, 0, 0, 1, 0, 1}, // 2
    		DefThree:   {0, 1, 0, 0, 0, 0, 0, 1, 0, 1}, // 3
    		DefFour:    {0, 1, 0, 1, 0, 1, 0, 1, 1, 1}, // 4
    		DefFive:    {0, 1, 0, 0, 1, 0, 1, 1, 0, 1}, // 5
    		DefSix:     {0, 0, 0, 0, 1, 1, 1, 1, 1, 1}, // 6
    		DefSeven:   {1, 1, 0, 1, 0, 1, 0, 1, 0, 1}, // 7
    		DefEight:   {0, 0, 0, 0, 0, 1, 0, 1, 0, 1}, // 8
    		DefFlag:    {0, 1, 1, 1, 1, 1, 1, 1, 1, 0}, // 红旗
    		DefMine:    {0, 1, 0, 1, 0, 1, 0, 1, 0, 1}, // 地雷
    		DefRedMine: {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // 标红地雷,表示输了
    		DefClick:   {1, 1, 1, 1, 1, 1, 1, 1, 1, 0}, // 可点击白板
    		DefNotNeed: {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, // 不可点击
    	}
    	GridSlice = make([]byte, GameWide*GridLen*GameHigh*GridLen*4) // 缓存截屏数据,全局初始化,避免重复申请内存
    )
    
    /**
    * 初始化数据
    * 做准备工作
    **/
    func init() {
    	var RectPos, ClientPos win.RECT // 找出窗体左上,右下的坐标,以及窗体除标题栏的宽高
    	GameHwnd = win.FindWindow(win.StringToBSTR(GameName), win.StringToBSTR(GameName))
    
    	if !win.ShowWindow(GameHwnd, win.SW_RESTORE) { // 激活窗口,如果当前为最小化则还原窗体
    		log.Fatal("请运行扫雷游戏...") /* 找不到游戏窗体,直接退出 */
    	}
    	win.UpdateWindow(GameHwnd)              // 更新窗体
    	win.GetWindowRect(GameHwnd, &RectPos)   /* 得到窗体左上右下的坐标 */
    	win.GetClientRect(GameHwnd, &ClientPos) /* 得到窗体处标题栏以外的长和宽 */
    	var leftX, topY = RectPos.Right - ClientPos.Right, RectPos.Bottom - ClientPos.Bottom
    
    	StartBtn.X, StartBtn.Y = (RectPos.Left+RectPos.Right)/2, topY+25 // 初始化开始游戏按钮
    	StartMine.X, StartMine.Y = leftX+10, topY+53                     // 初始化雷区起始位置
    	StartNum.X, StartNum.Y = leftX+15, topY+14                       // 初始化数字区位置
    
    	GridSave = make([][]GridDefine, GameHigh) // 产生二维数组保存雷区数据
    	for i := 0; i < GameHigh; i++ {           // 遍历高
    		GridSave[i] = make([]GridDefine, GameWide)
    	}
    	fmt.Print("1 [教学模式],0 [自动模式],请输入:")
    	fmt.Scanln(&TeachModel)
    	if TeachModel == 0 { /* 如果是自动模式则需要选择猜雷方法 */
    		fmt.Print("1 [自动猜雷],0 [人工猜雷],请输入:")
    		fmt.Scanln(&GuessMineOk)
    	} /* 如果是教学模式,则默认人工猜雷 */
    }
    
    /**
    * 开始执行
    **/
    func main() {
    	var (
    		i, j, flagSure, flagAuto int
    	)
    
    	win.SetCursorPos(StartBtn.X, StartBtn.Y) // 妈逼的只能鼠标点击把窗口激活了,试过win32 api不行啊
    	win.MouseEvent(win.MOUSEEVENTF_LEFTDOWN|win.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
    	time.Sleep(time.Millisecond * 200) // 双击开始的笑脸
    	win.MouseEvent(win.MOUSEEVENTF_LEFTDOWN|win.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
    
    	rand.Seed(time.Now().UnixNano()) //初始化随机数种子
    	log.Println("游戏开始!")
    
    	for RefreshGrid() && GetRemainMineCnt() > 0 { /* 当遇到红色地雷 或 地雷 以及剩余雷数为0 */
    		CntNotSure, flagSure = 0, 0
    		for i = 0; i < GameHigh; i++ { // 遍历高度
    			for j = 0; j < GameWide; j++ { //遍历宽度
    				if GridSave[i][j] >= DefOne && GridSave[i][j] <= DefEight { /* 代表数字的位置 */
    					switch flagPos, style := GetAroundCount(i, j, 0); style {
    					case 1: /* 类型1表示周围可点击格子一定是雷 */
    						for _, v := range flagPos {
    							ClickPos(v, "right")
    						}
    						GridSave[i][j] = DefNotNeed /* 标完雷,此位置已无效 */
    						flagSure = 1                // 表示本次遍历出现可点右键标雷的点
    					case 2: /* 类型2表示本格可以点鼠标中键了 */
    						ClickPos(Pos{x: j, y: i}, "center")
    						GridSave[i][j] = DefNotNeed /* 点开一片区域,此位置已无效 */
    						flagSure = 2                // 表示本次出现可点中键的点
    						if RefreshGrid() == false { /* 点中键后需要刷新当前界面避免重复点击中键 */
    							goto EndLoop // 当然如果遇到地雷则退出循环
    						}
    					case 3: /* 周围地雷已经标完,且不需要点击中键 */
    						GridSave[i][j] = DefNotNeed /* 点开一片区域,此位置已无效 */
    						flagSure = 3                // 表示本次出现可点中键的点
    					default:
    						NotSurePos[CntNotSure].x = i
    						NotSurePos[CntNotSure].y = j
    						CntNotSure++ // 记录本次遍历所有无法确定雷的点
    					}
    				}
    			} //GameWide
    		} // GameHigh
    
    		if flagSure == 0 { /* 本次遍历全是不确定的点 */
    			if flagStart == 0 { /* 开局,如果没有出现必定能判断的点,则一直随机点鼠标左键 */
    				ClickPos(Pos{x: rand.Intn(GameWide), y: rand.Intn(GameHigh)}, "left")
    				continue // 开局点击后如无法出现必定可以继续的点,直接进入下一个循环
    			}
    
    			for i = 0; i < CntNotSure; i++ { // 遍历不确定的点
    				if GetAroundNotSureCount(NotSurePos[i].x, NotSurePos[i].y) {
    					break // 如果对雷区有操作则需要重新整个界面扫雷
    				}
    				flagSure++ // 如果把不确定点全部遍历则表示需要猜雷了
    			}
    
    			if flagSure == CntNotSure {
    				/** 猜测某个点没有雷,运气成分,如需提高胜率可优化下面代码,下面是关于猜雷的思路
    				 * 1.(https://tieba.baidu.com/p/1761431400?red_tag=3267954760)
    				 *   上面是我看到比较靠谱的理论,由于计算机虽然笨但运算快,因此我打算实现这个方案
    				 * 2.死猜,及无论如何都不可能判定哪个是雷,那就只能随机猜一个了
    				 * 3.这里还要注意一点,及剩余雷数,有时候根据剩余雷数可以提高胜率
    				 * 按照上面3个步骤,还没发确定是不是雷,妈逼只能靠运气了
    				 * 到处找教程最终没能找到一个好点的方案,还是随机点击一个位置
    				 **/
    				if GuessMineOk == 1 { /* 自动猜雷是随机点一个 */
    					i = rand.Intn(CntNotSure) // 下面是最挫的猜雷方案,随机找一个不确定点,在该点周围随机点一个点
    					var tPos, _ = GetAroundCount(NotSurePos[i].x, NotSurePos[i].y, 1)
    					i = rand.Intn(len(tPos)) // 在不确定列表中随机找一个点,随机点这个点周围的一个可点击点
    					ClickPos(Pos{x: tPos[i].x, y: tPos[i].y}, "left")
    				} else {
    					if flagAuto == 0 {
    						flagAuto = 1
    						log.Println("请你猜雷吧,确认后按空格键继续...")
    						WaitKeyboard(win.VK_SPACE) // 等待空格键按下并松开
    					}
    					time.Sleep(time.Millisecond * 500) // 玩家猜雷,可以允许延时
    				}
    			}
    		} else { /* 将开局标志赋值,此时flagStart代表已经不是开局了 */
    			flagStart = flagSure
    			flagAuto = 0 // 经历了猜雷到不猜
    		}
    	}
    EndLoop:
    
    	if GetRemainMineCnt() == 0 {
    		log.Println("你赢了比赛!")
    	} else {
    		log.Println("你输了比赛!")
    	}
    	fmt.Println("请按回车退出...")
    	fmt.Scanln() // 双击打开时避免最终一闪而逝!
    }
    
    /**
    * 找到不可确定点
    * 与周围的不可确定点一起
    * 看能否确定一些雷和可点击位置
    * return,返回true表示一定点了数字或标了雷
    **/
    func GetAroundNotSureCount(x, y int) bool {
    	var (
    		i, j, cntMix       int
    		clickPos, needMine = GetAroundCount(x, y, 1) /* 得到本点可点击位置,以及剩余雷数 */
    		clickCnt           = len(clickPos)           // 可点击数个数
    		MorePos            = [2]struct {
    			//* 记录在点a不在点b周围可点击的位置 */
    			pos [8]Pos //最大也就8个点,数组完全够用
    			cnt int    // 记录点的个数,用于遍历时使用
    		}{} // 多于点的位置,以及点的个数
    		endClick = struct {
    			k   int    // 最终需要点击MorePos数组的哪一个
    			key string // 鼠标键值,因为有时候需要左键数字,有时候需要右键标雷
    		}{}
    		posInNotSure = func(x1, y1 int) bool { //闭包,确定这个点在不确定列表中
    			for i1 := 0; i1 < CntNotSure; i1++ {
    				if NotSurePos[i1].x == x1 && NotSurePos[i1].y == y1 {
    					return true
    				}
    			}
    			return false
    		}
    	)
    
    	/* 只找x相同或y相同的点 */
    	for i = x - 1; i <= x+1; i++ { // 以下双层循环遍历本点四周的点
    		for j = y - 1; j <= y+1; j++ { /* 只会找上下左右4个点,并且本点是一个数字点,且必须在不确定列表中 */
    			if i >= 0 && j >= 0 && i < GameHigh && j < GameWide && (i == x && j != y || i != x && j == y) && (GridSave[i][j] >= DefOne && GridSave[i][j] <= DefEight) && posInNotSure(i, j) {
    				cntMix = 0 // 记录两个点重合可点击位置个数
    				/* 根据(x,y),(i,j)这两个不确定点找还能标雷或点击的位置 */
    				var nowPos, nowMine = GetAroundCount(i, j, 1) // 得到周围的这个不确定点,周围可点击位置和待标雷数
    				MorePos[0].cnt = 0
    				for _, v1 := range clickPos {
    					endClick.k = 0 // 这里该变量作为标记使用,避免定义太多变量了
    					for _, v2 := range nowPos {
    						if v1 == v2 {
    							endClick.k = 1
    							cntMix++ // 记录相交的点个数
    							break
    						}
    					}
    					if endClick.k == 0 {
    						MorePos[0].pos[MorePos[0].cnt] = v1
    						MorePos[0].cnt++ // 记录在clickPos中且不在nowPos中的点
    					}
    				} // range clickPos
    
    				MorePos[1].cnt = 0
    				for _, v1 := range nowPos {
    					endClick.k = 0
    					for _, v2 := range clickPos {
    						if v1 == v2 {
    							endClick.k = 1
    							break
    						}
    					}
    					if endClick.k == 0 {
    						MorePos[1].pos[MorePos[1].cnt] = v1
    						MorePos[1].cnt++ // 记录在clickPos中且不在nowPos中的点
    					}
    				} // range nowPos
    
    				endClick.k = -1          // 当赋值其他数据时,表示一定需要点击
    				endClick.key = "right"   // 因为只有nowMine == needMine才为left,设置默认值
    				if nowMine == needMine { // 两个点待标雷个数相同
    					if cntMix == clickCnt { // 表示一个点全部在相交位置,此时另一个点附近不相交的点只能是数字
    						endClick.k = 1
    					} else if cntMix == len(nowPos) { // 同上,只是换了一个点而已
    						endClick.k = 0
    					}
    					endClick.key = "left"
    				} else if nowMine > needMine { // 待标雷个数大的一方
    					if nowMine-needMine == len(nowPos)-cntMix {
    						endClick.k = 1 // 2个点待标雷数相减 = 可点击数大的点减去重合点的个数,表示可点击数多的点多出的位置一定全是雷
    					}
    				} else {
    					if needMine-nowMine == clickCnt-cntMix {
    						endClick.k = 0 // 同上,只是点不一样而已
    					}
    				}
    
    				if endClick.k >= 0 && MorePos[endClick.k].cnt > 0 {
    					for i = 0; i < MorePos[endClick.k].cnt; i++ {
    						ClickPos(MorePos[endClick.k].pos[i], endClick.key) // 需要操作的格子不是地雷则随便搞
    					}
    					return true // 已经点击数字或标雷,整个界面需要重新判定
    				}
    			} // end if
    		} // end j
    	} // end i
    	return false // 当前点没有合适的判定点
    }
    
    /**
    * 找到x,y周围8个点中
    * 可点击的点个数,已经标为红旗的个数
    * 返回可点击的坐标位置,且返回当前这个点的类型
    * 类型有2=>需要点击鼠标中键,1=>需要把周边的可点击点标小旗
    * 剩下的类型需要更深层次的计算了
    **/
    func GetAroundCount(x, y, inTpye int) (flagPos []Pos, status int) {
    	var (
    		i, j              int
    		cntClick, cntFlag GridDefine /* 标记可点击,标记红旗 */
    	)
    	for i = x - 1; i <= x+1; i++ { // 以下双层循环遍历本点四周的点
    		for j = y - 1; j <= y+1; j++ {
    			if i >= 0 && j >= 0 && i < GameHigh && j < GameWide && (i != x || j != y) { // 剔除超过边界点,以及x,y所在点
    				if DefClick == GridSave[i][j] {
    					flagPos = append(flagPos, Pos{x: j, y: i})
    					cntClick++
    				} else if DefFlag == GridSave[i][j] {
    					cntFlag++
    				}
    			}
    		} // y
    	} // x
    
    	if 1 == inTpye { /* 返回周围可点击点位置,并且返回当前点剩余雷的个数 */
    		return flagPos, int(GridSave[x][y] - cntFlag)
    	}
    
    	if GridSave[x][y] == cntFlag {
    		if cntClick == 0 { /* 如果可点击数量为空,则当前位置不需要点鼠标中键 */
    			return flagPos, 3
    		}
    		return flagPos, 2 /* 小旗个数等于本格子雷数,点击鼠标中键 */
    	}
    
    	if cntClick+cntFlag == GridSave[x][y] {
    		return flagPos, 1 /* 可点击 + 小旗 = 本格子雷数,表示可点击一定全是雷 */
    	}
    
    	return flagPos, 0 /* 剩下的情况一定是可点击格数大于本格剩余雷数 */
    }
    
    /**
    * 传入ClickTask对象
    * 模拟鼠标点击某个位置
    * 单击左键中键右键
    **/
    func ClickPos(pos Pos, key string) {
    	var NowPos = uintptr((pos.x*GridLen + 21) | (pos.y*GridLen+64)<<16)
    	switch key {
    	case "left": // 确定不是雷,则随便点左键
    		win.SendMessage(GameHwnd, win.WM_LBUTTONDOWN, 0, NowPos)
    		win.SendMessage(GameHwnd, win.WM_LBUTTONUP, 0, NowPos)
    	case "right":
    		if GridSave[pos.y][pos.x] == DefClick { /* 右键位置为可点击才点,否则不点,避免重复标雷 */
    			win.SendMessage(GameHwnd, win.WM_RBUTTONDOWN, 0, NowPos)
    			win.SendMessage(GameHwnd, win.WM_RBUTTONUP, 0, NowPos)
    			GridSave[pos.y][pos.x] = DefFlag // 并且此处标记为地雷
    		}
    	case "center": // 中键点开一片区域
    		win.SendMessage(GameHwnd, win.WM_MBUTTONDOWN, 0, NowPos)
    		win.SendMessage(GameHwnd, win.WM_MBUTTONUP, 0, NowPos)
    	}
    
    	if TeachModel == 1 && flagStart != 0 { /* 开局以后的操作才显示 */
    		var tmpPos = win.POINT{X: int32(pos.x*GridLen + 21), Y: int32(pos.y*GridLen + 64)}
    		win.ClientToScreen(GameHwnd, &tmpPos) /* 将相对窗体位置转化为相对整个屏幕的位置 */
    		win.SetCursorPos(tmpPos.X, tmpPos.Y)
    		log.Printf("点击鼠标按键:%6s,请按空格键继续...\n", key)
    		WaitKeyboard(win.VK_SPACE) // 等待空格键按下并松开
    	}
    }
    
    /**
    * 等待一个按键按下并松开
    **/
    func WaitKeyboard(key int32) {
    	for win.GetKeyState(key) >= 0 { /* 有按键退出循环 */
    		time.Sleep(time.Millisecond * 100)
    	}
    	for win.GetKeyState(key) < 0 { /* 松开按键退出循环 */
    		time.Sleep(time.Millisecond * 100)
    	}
    }
    
    /**
    * 用到获取屏幕截图代码
    * 已经去掉robotgo,让程序没有dll依赖
    * 截屏代码摘自https://github.com/vova616/screenshot
    * 这里需要学习指针转换的操作,以及内存拷贝的操作
    **/
    func RefreshGrid() bool {
    	var (
    		w, h              int32 = GameWide * GridLen, GameHigh * GridLen
    		a, b, c, d, e     int   // 只是作为循环变量而已
    		screen, screenMem win.HDC
    		dib               win.HBITMAP
    		bi                win.BITMAPINFO
    		ptr               = unsafe.Pointer(uintptr(0))
    		obj               win.HGDIOBJ
    		tmpArr            [10]int                    // 缓存那几个点的值
    		GrayControl       = func(r, g, b byte) int { /* 灰化图像,阈值为150 */
    			if float32(r)*0.11+float32(g)*0.59+float32(b)*0.3 >= 150 {
    				return 1
    			}
    			return 0
    		}
    	)
    	bi.BmiHeader.BiSize = uint32(reflect.TypeOf(bi.BmiHeader).Size())
    	bi.BmiHeader.BiWidth = w
    	bi.BmiHeader.BiHeight = -h /* Non-cartesian, please */
    	bi.BmiHeader.BiPlanes = 1
    	bi.BmiHeader.BiBitCount = 32
    	bi.BmiHeader.BiCompression = win.BI_RGB
    	bi.BmiHeader.BiSizeImage = uint32(4 * w * h)
    	bi.BmiHeader.BiXPelsPerMeter = 0
    	bi.BmiHeader.BiYPelsPerMeter = 0
    	bi.BmiHeader.BiClrUsed = 0
    	bi.BmiHeader.BiClrImportant = 0
    
    	if screen = win.GetDC(0); screen == 0 {
    		return false
    	}
    	defer win.ReleaseDC(0, screen)
    
    	dib = win.CreateDIBSection(screen, &bi.BmiHeader, win.DIB_RGB_COLORS, &ptr, 0, 0)
    	if dib == 0 || win.GpStatus(dib) == win.InvalidParameter {
    		return false
    	}
    	defer win.DeleteObject(win.HGDIOBJ(dib))
    
    	if screenMem = win.CreateCompatibleDC(screen); screenMem == 0 {
    		return false
    	}
    	defer win.DeleteDC(screenMem)
    
    	if obj = win.SelectObject(screenMem, win.HGDIOBJ(dib)); obj == 0 || obj == 0xffffffff {
    		return false
    	}
    	defer win.DeleteObject(obj)
    
    	if !win.BitBlt(screenMem, 0, 0, w, h, screen, StartMine.X, StartMine.Y, win.SRCCOPY) {
    		return false // 截屏
    	}
    
    	hDrp := (*reflect.SliceHeader)(unsafe.Pointer(&GridSlice))
    	hDrp.Data = uintptr(ptr) /* 将指针中的数据映射到[]byte中 */
    	//hDrp.Len = int(w * h * 4)
    	//hDrp.Cap = int(w * h * 4)
    
    	for a = 0; a < GameHigh; a++ { // 遍历高度
    		for b = 0; b < GameWide; b++ { //遍历宽度
    			if DefNotNeed == GridSave[a][b] || DefFlag == GridSave[a][b] {
    				continue /* 该点已没意义 或 该点已经标记为地雷,所以不用计算 */
    			}
    
    			for c, e = 0, 0; c < 5; c++ { // 找到那几个特殊的点,备注找到更少点确定值则可以越快得到数据
    				d = (a*GridLen+7-c)*4*GameWide*GridLen + 4*(b*GridLen+c+7)
    				tmpArr[e] = GrayControl(GridSlice[d+2], GridSlice[d+1], GridSlice[d])
    				e++
    				/* 注意本处的特征码是根据灰化后的图像得出,我也是花了九牛二虎之力才搞到的额 */
    				d = (a*GridLen+c+9)*4*GameWide*GridLen + 4*(b*GridLen+2)
    				tmpArr[e] = GrayControl(GridSlice[d+2], GridSlice[d+1], GridSlice[d])
    				e++
    			}
    
    			for k, v := range GridNumbers { // 遍历map,得到本格子的实际信息
    				for c = 0; c < 10; c++ {
    					if v[c] != tmpArr[c] {
    						c = -1 // 标识该位置与当前v不匹配
    						break
    					}
    				}
    
    				if c != -1 { /* 如果z=-1表示没有匹配到当前v的特征值 */
    					if k == DefMine || k == DefRedMine {
    						return false /* 遇到标红的雷或者黑色的雷,游戏结束 */
    					}
    					GridSave[a][b] = k
    					break
    				}
    			}
    		} // 内层for结束
    	} // 外层for结束
    	return true
    }
    
    /**
    * 从界面得到剩余雷的个数
    **/
    func GetRemainMineCnt() int {
    	var (
    		i, x, y int
    		Num     [3]int
    		FlagCnt [7]int
    		/*   __0__
    		 * 5|     |1
    		 *  |__6__|
    		 * 4|     |2
    		 *  |__3__|
    		 * 按照上面的顺序标记一个数字
    		 * 相邻颜色值相同则赋值为1,不同则赋值为0
    		 * 根据对应的map匹配得到该位置具体数字
    		 **/
    		FlagDot = map[int][]int{
    			0: {1, 1, 1, 1, 1, 1, 0},
    			1: {0, 1, 1, 0, 0, 0, 0},
    			2: {1, 1, 0, 1, 1, 0, 1},
    			3: {1, 1, 1, 1, 0, 0, 1},
    			4: {0, 1, 1, 0, 0, 1, 1},
    			5: {1, 0, 1, 1, 0, 1, 1},
    			6: {1, 0, 1, 1, 1, 1, 1},
    			7: {1, 1, 1, 0, 0, 0, 0},
    			8: {1, 1, 1, 1, 1, 1, 1},
    			9: {1, 1, 1, 1, 0, 1, 1},
    		}
    		NumPosX = []int32{StartNum.X, StartNum.X + 13, StartNum.X + 26} /* 三个数字左上角x坐标 */
    		hdc     = win.CreateDC(win.StringToBSTR("DISPLAY"), nil, nil, nil)
    	)
    	defer win.DeleteDC(hdc) // 用完hdc对象要释放
    
    	for i = 0; i < 3; i++ {
    		if win.GetPixel(hdc, NumPosX[i]+5, StartNum.Y) == win.GetPixel(hdc, NumPosX[i]+5, StartNum.Y+1) {
    			FlagCnt[0] = 1
    		} else {
    			FlagCnt[0] = 0
    		}
    		if win.GetPixel(hdc, NumPosX[i]+9, StartNum.Y+4) == win.GetPixel(hdc, NumPosX[i]+9, StartNum.Y+5) {
    			FlagCnt[1] = 1
    		} else {
    			FlagCnt[1] = 0
    		}
    		if win.GetPixel(hdc, NumPosX[i]+9, StartNum.Y+14) == win.GetPixel(hdc, NumPosX[i]+9, StartNum.Y+15) {
    			FlagCnt[2] = 1
    		} else {
    			FlagCnt[2] = 0
    		}
    		if win.GetPixel(hdc, NumPosX[i]+5, StartNum.Y+18) == win.GetPixel(hdc, NumPosX[i]+5, StartNum.Y+19) {
    			FlagCnt[3] = 1
    		} else {
    			FlagCnt[3] = 0
    		}
    		if win.GetPixel(hdc, NumPosX[i]+1, StartNum.Y+14) == win.GetPixel(hdc, NumPosX[i]+1, StartNum.Y+15) {
    			FlagCnt[4] = 1
    		} else {
    			FlagCnt[4] = 0
    		}
    		if win.GetPixel(hdc, NumPosX[i]+1, StartNum.Y+4) == win.GetPixel(hdc, NumPosX[i]+1, StartNum.Y+5) {
    			FlagCnt[5] = 1
    		} else {
    			FlagCnt[5] = 0
    		}
    		if win.GetPixel(hdc, NumPosX[i]+5, StartNum.Y+9) == win.GetPixel(hdc, NumPosX[i]+5, StartNum.Y+10) {
    			FlagCnt[6] = 1
    		} else {
    			FlagCnt[6] = 0
    		}
    
    		for x = 0; x < 10; x++ {
    			for y = 0; y < 7; y++ {
    				if FlagCnt[y] != FlagDot[x][y] {
    					y = -1 // 不匹配当前数字,不必遍历所有值
    					break
    				}
    			}
    
    			if -1 != y { // 如果全部匹配则就是这个数字了
    				Num[i] = x
    				break /* 找到数字值,不必再循环 */
    			}
    		}
    	}
    
    	return Num[0]*100 + Num[1]*10 + Num[2] /* 转换为剩余雷数 */
    }
    
    
    1. 这里我要说一下,我的程序可以教你扫雷。如下图输入1或0回车即可
      执行模式
      如果选择了教学模式,那么只能用人工猜雷。此时教学模式会把每一步鼠标操作都展现出来,方便大家思考和学习。这时候按一下空格键才会自动进行下一步,而且会显示鼠标位置到底是右键标雷还是左键点击还是中键操作。你可以根据当前操作分析周围的点,看看我的程序是如何做到判断一个位置是否有雷的。
      提示

    2. 剩下的自动模式无非就程序自动扫雷,此时会选择自动猜雷还是人工猜雷,如果人工猜雷则需要等程序无法判定能否操作时由你自己去猜雷。自动猜雷就是随便点了一个点而已。综上所述,我的扫雷代码算是完成了,除了猜雷没有好的方案完成以外,其他都做的很不错。希望给大家一些启发。
      这里是程序哈扫雷游戏下载程序

    当然如果你没有csdn积分可以用百度云,密码49nt

    cs
    下一篇:没有了