Java 实现扫雷与高胜率低耗时自动扫雷 AI (下)
上一篇博客介紹了本項目總體情況, 這一篇來介紹一下我實現(xiàn)的自動掃雷 AI 算法. 本 AI 勝率比網上最高勝率的 AI 差 0.5% 左右. 不過本 AI 也不是沒有優(yōu)勢, 它運算速度很快 (強行有優(yōu)勢 (ˉ▽ ̄~)), 平均 42 毫秒可以掃完一局 Win XP 規(guī)則下的專家難度.
這篇博客會介紹一下我的思路和踩過的坑, 也會列出一些關于勝率的數據. 希望能夠幫助其他萌新入個門. 項目已經開源, 代碼也寫了注釋, 鏈接放在文章最后.
先再次把最終成品的 AI 勝率等指標羅列一下:
| 測試版本 | Win XP 規(guī)則, 專家難度 | Win 7 規(guī)則, 專家難度 |
| 測試局數 | 50,0000 局 | 50,0000 局 |
| 勝率 | 39.68% | 52.45% |
| 運行總耗時 | 21275 秒 | 33136 秒 |
| 每局平均耗時 | 42 毫秒 | 66 毫秒 |
| 勝局的每局平均耗時 | 57 毫秒 | 68 毫秒 |
注1: 測試 Win XP 規(guī)則是在半夜運行的, 電腦除了掃雷沒有在運行其他進程; 而測試 Win 7 規(guī)則時我同時還在拿電腦辦公, 導致 Win 7 規(guī)則下耗時比 XP 多. 而實際上 Win XP 與 Win 7 應該運行時間差不多.
注2: 測試使用的掃雷程序是自己復現(xiàn)的 Win XP 與 Win 7 規(guī)則, 而非模擬鼠標點擊原版掃雷窗口. 因為那樣太慢了. 網上有大佬已經測試過, Win XP, Win 7 原版掃雷并無影響地雷分布的隱藏規(guī)則. 所以若真要在原版上測試, 結果應該不會有較大偏差.
我在網上找到的勝率最高的掃雷 AI 是 ztxz16 大佬寫的, Win XP 版本勝率 40.07%, Win 7 版本勝率 52.98%. 我的做法也是參考了 ztxz16 大佬的算法 (最強掃雷 AI 算法詳解 + 源碼分享 - ztxz16). B 站的 _黃歪歪 應該也是他, 也發(fā)了些關于掃雷 AI 的視頻. 膜一波.
不過 ztxz16 大佬的這個項目偏試驗性, 我頂著沒有注釋的壓力把大佬開源的源碼看了一遍, 看得出來有些地方他應該是懶得優(yōu)化, 最終 AI 速度不是很快. 我本來想自己測試一遍他的 AI, 但跑了一晚上也只跑出來了幾千局還是幾萬局 (捂臉).
該交代的差不多都交代完了, 下面開始講我的做法.
高勝率低耗時自動掃雷 AI 算法
術語與定義
統(tǒng)一一下一些操作的術語方便后續(xù)描述. 我不是掃雷圈的, 很多術語可能不知道, 所以如果以下操作本來就有自己的中文名稱, 提醒我我會改過來的!
| 鼠標左鍵, 那個點開格子的操作 | 挖掘 |
| 鼠標右鍵, 那個放置小紅旗的操作 | 插旗 |
| 鼠標雙鍵或中鍵, 那個自動檢測周圍八格是否可挖掘的操作 | 檢查周圍 |
| 空白的, 沒點過的格子 | 未知格 |
| 已知的有數字 (包括 0) 的格子 (已挖開且不是雷的格子) | 數字格 |
另外,
第一步: 開局挖哪
開局第一步點擊不同的地方也是會影響勝率的. 根據 ztxz16 大佬的測試 (地表最強掃雷AI ? 編程探索掃雷極限勝率 - _黃歪歪), 開局 Win XP 挖掘 ( 0 , 0 ) (0, 0) (0,0), Win 7 挖掘 ( 2 , 2 ) (2, 2) (2,2) (或對稱的另外三個角落) 勝率最高.
我自己也對 Win XP 與 Win7 大致測了一下 ( 0 , 1 ) (0, 1) (0,1), ( 1 , 0 ) (1, 0) (1,0), ( 1 , 1 ) (1, 1) (1,1) 等幾個位置, 確實如此, 勝率會下降 1% ~ 2% 左右.
| Win XP 開局 ( 0 , 0 ) (0, 0) (0,0) | Win 7 開局 ( 2 , 2 ) (2, 2) (2,2) |
不過至于為什么是角落勝率比中心高, 我猜可能是角落有更多可套用減法公式的場景 (減法公式在下一節(jié)有講). 減法公式本質就是根據相鄰兩個數字格的一側去推斷另一側. 而邊緣的格子天然已知靠邊的一側無雷, 因而可以更容易地推斷出另一側雷的個數.
第二步: 基于定義與定式
用到了兩個基本公式或定式:
第一, 僅基于一格判斷: 即根據目標數字格的數字與其周圍八格 (角落的話不滿八格) 的狀態(tài), 判斷該數字格周圍八格的未知格是不是全為雷或全不為雷. 這是掃雷最基本的公式. 其邏輯其實就是鼠標左右雙鍵檢查周圍的邏輯.
第二, 基于相鄰兩格: 即使用減法公式.
如圖, M M M, N N N 為兩個相鄰數字格, 兩者上下各有一塊互相影響的公共區(qū)域 P P P, Q Q Q, 左右則分別有互不影響的兩翼 A A A, B B B. 記地雷數量為 C C C, 顯然地雷數量 C C C 符合如下等式:
{ C A r o u n d M = C A + C P + C Q C A r o u n d N = C B + C P + C Q \begin{cases} C_{AroundM} = C_A + C_P + C_Q \\ C_{AroundN} = C_B + C_P + C_Q \end{cases} {CAroundM?=CA?+CP?+CQ?CAroundN?=CB?+CP?+CQ??
兩式相減即得減法公式:
C A r o u n d M ? C A r o u n d N = C A ? C B C_{AroundM} - C_{AroundN} = C_A - C_B CAroundM??CAroundN?=CA??CB?
即 M M M 與 N N N 的數字之差就是 A A A 與 B B B 雷數量之差.
舉個簡單的例子: 如下方案例圖 1 兩個數字格 2 ? 1 = 1 2 - 1 = 1 2?1=1, 即藍色區(qū)域比綠色區(qū)域多一個雷, 而藍色區(qū)域三個格子有兩個已知不是雷, 故而可推得藍色區(qū)域有且只有一個雷, 這個雷只能在 * 處. 既然藍區(qū)有一個雷, 綠區(qū)就有 1 ? 1 = 0 1 - 1 = 0 1?1=0 個雷, 故而 N 處必為一個數字格.
| 案例圖 1 | 案例圖 2 |
而如果相鄰兩格的一側在棋盤邊緣, 如案例圖 2 所示, 藍區(qū)在棋盤外, 即藍區(qū)無雷, 而 M ? N = 0 M - N = 0 M?N=0, 故綠區(qū)均不為雷.
可見減法公式在邊緣有更多的使用場景, 這可能也是開局要挖角落的原因之一.
關于減法公式更詳細的內容可以參考這篇專欄: 掃雷新手判雷上路(一)——初步認識減法公式及其衍生的最簡單定式 - MsPVZ.ZSW.
截至這一步的勝率與耗時
如果僅僅使用基本定式, 遇到定式無法解決的就投降判負, 勝率在 5% 左右 (畢竟如果開局就點了個 ‘1’, 那就直接無了).
而如果僅使用基本定式, 遇到定式無法解決的就瞎猜, 勝率就已經有 23% 了.
平均每局耗時在 4ms 以下, 沒精確測過. 基于定式的算法雖然使用場景較為受限, 但效率非常高, 掃完一整局的時間復雜度也就 O ( S ) O(S) O(S).
第三步: 基于每個格子可能有雷的概率
針對某一棋局局面, 每個未知格有雷的概率當然是可以計算出來的, 即
目 標 未 知 格 含 雷 概 率 = 目 標 未 知 格 含 雷 的 方 案 數 量 局 面 所 有 可 行 方 案 數 量 目標未知格含雷概率 = \frac{目標未知格含雷的方案數量}{局面所有可行方案數量} 目標未知格含雷概率=局面所有可行方案數量目標未知格含雷的方案數量?
掃的時候誰有雷的概率最低就掃誰.
局面所有可行方案的計算方法用回溯法即可. 當然直接就對所有未知格進行回溯的話復雜度有 O ( 2 S ) O(2^S) O(2S) 自然受不了, 所以這一步的算法主要難點是剪枝.
如何剪枝, 可以用下面這張圖很直觀地展現(xiàn)出來:
在上圖中, 每個未知格上都被打上了該格子的非雷概率 (注意, 這張圖上展示的是 “非雷概率”, 也就是 1 ? P 有 雷 1 - P_{有雷} 1?P有雷?), 且與數字格相鄰的未知格均被用彩色高亮了出來; 而不與數字格相鄰的未知格則均用灰色標識.
不知道怎么取名, 這里我姑且將所有相近的 (不一定緊挨)、相同顏色的未知格集合稱作一個 “連通分量”, 而剩下所有灰色格子姑且稱之為 “孤立格”. 而本小節(jié)最重要的兩個步驟就是①計算出所有連通分量, ②基于連通分量計算出每個未知格的有雷概率.
計算連通分量
回溯法剪枝其實就是要分隔出盡可能短的連通分量, 然后在每個連通分量上回溯出所有可能方案, 最后合并所有連通分量的方案.
首先不太嚴謹地定義一下 “連通分量”. 在不考慮剩余雷數制約的前提下, 如果一個未知格 A 最終挖開的不同狀態(tài) (數字或雷) 會影響另一個未知格 B 不同狀態(tài)的概率分布 (反過來 B 的狀態(tài)也會影響 A), 則認為 A、B 同屬一個連通分量. 且若 A、B 同屬一個連通分量, B、C 同屬一個連通分量, 則 A、B、C 同屬一個連通分量.
放到實際計算中, 其實就很簡單:
用上述方法計算得的連通分量內部其實依然可能存在互不影響的部分, 可以進一步優(yōu)化, 將一個連通分量拆成多個更短的連通分量.
優(yōu)化方法也很簡單, 如果已經能確定某個未知格是雷, 請把他插上小紅旗 (網上看了很多 AI 不喜歡插旗). 如下兩張圖所示.
| 不插旗時找出的連通分量 (深紅) | 插了旗后找出的連通分量 (青色、深紅) |
計算有雷概率
(公式敲的我頭昏腦脹, 如有錯誤請指正)
回溯枚舉所有 K K K 個連通分量, 可以得到如下結果:
對于 ? c ≥ 0 \forall c \ge 0 ?c≥0, ? i ∈ [ 1 , K ] \forall i \in [1, K] ?i∈[1,K], ? j ∈ [ 1 , L e n ( C C i ) ] \forall j \in [1, Len(CC_i)] ?j∈[1,Len(CCi?)],
- T o t a l C n t ( i , c ) TotalCnt_{(i, c)} TotalCnt(i,c)? - 當連通分量 C C i CC_i CCi? 中一共有 c c c 個雷時的所有可行方案個數.
- M i n e C n t ( i , j , c ) MineCnt_{(i, j, c)} MineCnt(i,j,c)? - 當連通分量 C C i CC_i CCi? 中一共有 c c c 個雷時, 屬于 C C i CC_i CCi? 的格子 C e l l ( i , j ) Cell_{(i, j)} Cell(i,j)? 在所有 T o t a l C n t ( i , c ) TotalCnt_{(i, c)} TotalCnt(i,c)? 個可行方案中有雷的次數.
由上述信息可以計算, 前 k k k 個連通分量 C C 1 ~ C C k CC_1 \sim CC_k CC1?~CCk? 作為一個整體一共有 c c c 個雷時的方案數:
M u l t i C n t ( 1 ~ k , c ) = ∑ s = 0 c M u l t i C n t ( 1 ~ k ? 1 , s ) × T o t a l C n t ( k , c ? s ) ( 1 ) = ∑ s = 0 c M u l t i C n t ( 1 ~ i ? 1 , s ) × M u l t i C n t ( i ~ k , c ? s ) , i ∈ [ 2 , k ] ( 2 ) \begin{aligned} MultiCnt_{(1 \sim k, c)} & = \sum_{s = 0}^c MultiCnt_{(1 \sim k - 1, s)} \times TotalCnt_{(k, c - s)} & (1) \\ & = \sum_{s = 0}^c MultiCnt_{(1 \sim i - 1, s)} \times MultiCnt_{(i \sim k, c - s)}, i \in [2, k] & (2) \end{aligned} MultiCnt(1~k,c)??=s=0∑c?MultiCnt(1~k?1,s)?×TotalCnt(k,c?s)?=s=0∑c?MultiCnt(1~i?1,s)?×MultiCnt(i~k,c?s)?,i∈[2,k]?(1)(2)?
在實際編程中, 會涉及大量計算 M u l t i C n t ( except? i , c ) MultiCnt_{(\text{except }i, c)} MultiCnt(except?i,c)?. 使用上面 ( 1 ) (1) (1) 動態(tài)規(guī)劃, 使用 ( 2 ) (2) (2) 從左右雙向記憶化, 能節(jié)約點計算.
然后對于所有不屬于任何連通分量的孤立格, 我們也要計算它們的可行方案數. 若 b b b 個孤立格中有 c c c 個雷, 則這些孤立格的可行方案數為:
I s o l C n t ( b , c ) = I s o l C n t ( b ? 1 , c ) + I s o l C n t ( b ? 1 , c ? 1 ) IsolCnt_{(b, c)} = IsolCnt_{(b - 1, c)} + IsolCnt_{(b - 1, c - 1)} IsolCnt(b,c)?=IsolCnt(b?1,c)?+IsolCnt(b?1,c?1)?
實際實現(xiàn)中我直接打了個 16 × 30 × 99 16 \times 30 \times 99 16×30×99 的表.
于是可以計算, 所有 b b b 個孤立格與指定連通分量集合 T T T 在有 c c c 個雷的情況下的可行方案數為:
C n t ( T , c ) = ∑ s = 0 c M u l t i C n t ( T , s ) × I s o l C n t ( b , c ? s ) Cnt_{(T, c)} = \sum_{s = 0}^c MultiCnt_{(T, s)} \times IsolCnt_{(b, c - s)} Cnt(T,c)?=s=0∑c?MultiCnt(T,s)?×IsolCnt(b,c?s)?
于是, 對于任意連通分量的任意格子 C e l l ( i , j ) Cell_{(i, j)} Cell(i,j)?, 一共 c c c 個雷, K K K 個連通分量, 其有雷概率為:
M i n e P r o b ( i , j ) = ∑ s = 0 c C n t ( except? i , s ) × M i n e C n t ( i , j , c ? s ) C n t ( 1 ~ K , c ) MineProb_{(i, j)} = \frac{\sum_{s = 0}^c Cnt_{(\text{except }i, s)} \times MineCnt_{(i, j, c - s)}}{Cnt_{(1 \sim K, c)}} MineProb(i,j)?=Cnt(1~K,c)?∑s=0c?Cnt(except?i,s)?×MineCnt(i,j,c?s)??
有了每個連通分量的格子的概率, 孤立格子的概率就很好算了:
M i n e P r o b isol = c ? ∑ i ∑ j M i n e P r o b ( i , j ) b MineProb_{\text{isol}} = \frac{c - \sum_{i}\sum_{j}MineProb_{(i, j)}}{b} MineProbisol?=bc?∑i?∑j?MineProb(i,j)??
由此我們就計算出了所有未知格的有雷概率.
附加小策略小技巧
當有雷概率最低的未知格有不止一個時, 也有些小策略可以增加勝率. 經過實際測試, 目前我采用了以下策略, 能提升 2% 左右:
截至這一步的勝率與耗時
這一步的算法的使用場景是覆蓋了基本定式那一步的, 即定式能做的概率也能做. 但定式速度快, 所以仍有存在意義. 測了十來萬盤, “定式 + 概率” 策略的勝率在 36% ~ 37% 左右, 平均每局耗時 6ms.
(一開始我沒有將連通分量的不同雷數分開考慮, 而是根據連通分量的平均雷數來計算, 導致勝率只有 32%, 可見近似算法差距還是很大的.)
而加上了那些小策略后, 勝率提升到了 38.5%.
極端情況:
到這里你可能有疑問, 剪枝確實很高效, 但如果遇到極端情況不是依然得爆炸?
答案是肯定的. 但是經過我這么多次的測試, 基本上 1,0000 局里也很難遇上極端情況.
但如果您真的很迫切地想看我的 AI 出丑也沒關系, 我親自在項目根目錄放了一個難以剪枝的殘局案例, 保證讓您風扇起飛.
第四步: 基于對下一局面的有限預測
這一步是我瞎想的, 沒想到真的有用. 想到這一步的主要原因是, 無雷概率高不完全等于獲勝概率高; 而計算勝率又絕大部分情況下不可行. 于是就想著整個折中方案.
這一步是針對上一步有雷概率計算出現(xiàn)多個格子有最低有雷概率的情況. 本策略會覆蓋上面說的小策略小技巧, 但又由于本策略復雜度不低, 所以當判斷發(fā)現(xiàn)計算量過大時還是采用之前的小策略小技巧.
原理是, 對于每個有雷概率最低的格子, 計算: 選擇該格子后, 可被百分百確定的其他未知格的期望數量. 方法是, 枚舉格子的所有可能性 (0 ~ 8, 雷), 使用上一步的概率計算這一假設的新局面中所有格子的非雷概率. 最后統(tǒng)計每個新局面必然非雷的格子個數, 計算一下它們的平均值.
最后我們選擇期望值最高的格子.
截至這一步的勝率與耗時
測了萬把盤, 勝率達到 39.3%, 平均每局耗 8ms 多.
這一步我做的比較保守, 因為我不知道有沒有可能出現(xiàn) “非雷概率 A > B” 但 “勝率 A < B” 的情況.
第五步: 基于勝率 (最終殺招)
最后一招, 直接把每個未知格的勝率算出來.
這里區(qū)分一下 “獲勝概率” 與第三步的 “有雷概率”. 勝率是指挖掘某個未知格的最終獲勝概率; 有雷概率是指當前局面下未知格是雷的概率. 顯然, 當前局面所有未知格中最高的勝率代表了這個殘局的整體勝率.
這世上沒有什么策略比直接算勝率更準了, 但可惜它的復雜度過于恐怖, 所以我只在當殘余未知格少于等于 12 時才會啟用這個策略.
算法原理還是動態(tài)規(guī)劃. 設 b b b 為當前局面, b ( x , y ) b_{(x, y)} b(x,y)? 為將未知格 x x x 設為數字 y y y 的下一局面. 另設 C e l l W i n R a t e ( ) CellWinRate() CellWinRate() 為某格子的勝率, B o a r d W i n R a t e ( ) BoardWinRate() BoardWinRate() 為某局面的勝率, C n t ( ) Cnt() Cnt() 為某局面的所有可行方案個數.
B o a r d W i n R a t e ( b ) = Max x ( C e l l W i n R a t e ( b , x ) ) C e l l W i n R a t e ( b , x ) = ∑ y = 0 8 B o a r d W i n R a t e ( b ( x , y ) ) × C n t ( b ( x , y ) ) C n t ( b ) \begin{aligned} BoardWinRate(b) & = \underset{x}{\text{Max}}(CellWinRate(b, x)) \\ CellWinRate(b, x) & = \sum_{y=0}^{8}BoardWinRate(b_{(x, y)}) \times \frac{Cnt(b_{(x, y)})}{Cnt(b)} \end{aligned} BoardWinRate(b)CellWinRate(b,x)?=xMax?(CellWinRate(b,x))=y=0∑8?BoardWinRate(b(x,y)?)×Cnt(b)Cnt(b(x,y)?)??
每個局面 b b b 可以狀態(tài)壓縮, 用一個 String 表示, 方便記憶化搜索.
勝率算法計算完后就很簡單了, 勝率最高的格子隨便挑一個挖. 而且所有后續(xù)局面的勝率也在之前的計算中緩存過了, 從緩存里掏出來一路掃到結束即可.
我自己整理了幾個 “無雷概率” 與 “獲勝概率” 區(qū)別的例子 (這里是無雷概率, 為了方便與勝率對比), 可以幫助理解一下:
更多對比案例可以在我的 Github 項目根目錄找到.
衍生
最后我稍微衍生了一下使用場景. 如果某個區(qū)域只可能有 c c c 個雷 (雷數可確定, 比如幸福三選一), 那這片區(qū)域也能套用勝率算法直接掃掉, 姑且叫之局部勝率. 這個操作不會影響總體勝率, 但能稍微提一丟丟速 (指爆雷重開的速度… 早發(fā)現(xiàn)早治療, 早暴斃早投胎).
最終勝率與概率
已經寫在博文最上面了. 50 萬局測試, 勝率 39.68%, 平均每局 42ms.
附: 探索百分比
測試 50 萬盤的時候順便記錄了一下每一局不論輸贏都探索到了什么程度 (游戲結束前被掃開的格子占所有格子的百分比). 雖然不知道有什么用但也列一下 (柱狀圖是我在命令行里畫的, 有點寫意):
Win XP 規(guī)則專家難度 50,0000 局: A 占比 | | | | | | M M | M M M | M M M | M M M M M M M M M M M +---------------------------------------------> 探索程度0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100% Win 7 規(guī)則專家難度 50,0000 局: A 占比 | | | | M | M | M | M M | M M | M M M M M M M M M M M +---------------------------------------------> 探索程度0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%這么看確實 Win XP 下開局勸退得有點離譜.
源碼
本博客僅發(fā)布于 Github IO: https://xienaoban.github.io/posts/62679.html
和 CSDN: https://blog.csdn.net/XieNaoban/article/details/112424633
其他都是盜的.
項目源碼 Github: https://github.com/XieNaoban/Minesweeper
(喜歡的話 Star 一下呀 (づ ̄3 ̄)づ╭?~)
不會用 Github 的萌新也可以在這里下載: https://download.csdn.net/download/XieNaoban/14090898
主要有以下幾個文件:
AutoSweeper.java // 自動掃雷 AI Gui.java // 掃雷 GUI 界面 Main.java // 執(zhí)行入口 MineSweeper.java // 掃雷游戲規(guī)則的實現(xiàn) WinXpSweeper.java // 通過監(jiān)視 Win XP 原版掃雷完成的規(guī)則實現(xiàn)其中 AutoSweeper.java 和 MineSweeper.java 寫的比較上心, 有較為詳細的注釋, 代碼結構也相對清晰. 別的幾個文件就寫的比較寫意, 酌情觀看.
總結
以上是生活随笔為你收集整理的Java 实现扫雷与高胜率低耗时自动扫雷 AI (下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ubuntu16.04下运行Drcom客
- 下一篇: -webkit-touch-callou