目錄
一. 在準備
· Minesweeper Arbiter(必須使用MS-Arbiter來進行掃雷!)
二.  實現想法
1. 窗體截取
#2. 雷塊分割
3. 雷块识别
4. 扫雷算法实现
首頁 後端開發 Python教學 厲害了,用Python破個世界紀錄!

厲害了,用Python破個世界紀錄!

Apr 20, 2023 pm 01:19 PM
python 程式碼

厲害了,用Python破個世界紀錄!

大家好,

用Python OpenCV實作了自動掃雷,突破世界記錄,我們先來看看效果吧。

厲害了,用Python破個世界紀錄!

中級- 0.74秒3BV/S=60.81

#相信許多人很早就知道有掃雷這麼一款經典的遊(顯示卡測試)戲(軟體),更是有不少人曾聽過中國雷聖,也是中國掃雷第一、世界綜合排名第二的郭蔚嘉的頂頂大名。掃雷作為一款在Windows9x時代就已經誕生的經典遊戲,從過去到現在依然都有著它獨特的魅力:快節奏高精準的滑鼠操作要求、快速的反應能力、刷新紀錄的快感,這些都是掃雷給雷友們帶來的、只屬於掃雷的獨一無二的興奮點。

一. 在準備

準備動手製作一套掃雷自動化軟體之前,你需要準備以下一些工具/軟體/環境

- 開發環境

  1. Python3 環境- 推薦3.6或以上[更加推薦Anaconda3,以下很多依賴庫無需安裝]
  2. numpy依賴庫[如有Anaconda則無需安裝]
  3. #PIL依賴庫[如有Anaconda則不需要安裝]
  4. opencv-python
  5. win32gui、win32api依賴函式庫
  6. 支援Python的IDE [可選,如果你能忍受用文字編輯器寫程序也可以]​​

- 掃雷軟體

· Minesweeper Arbiter(必須使用MS-Arbiter來進行掃雷!)

好啦,那麼我們的準備工作已經全部完成了!讓我們開始吧~

二.  實現想法

在去做一件事情之前最重要的是什麼?是將要做的這件事情在心中建構一個步驟架構。只有這樣,才能保證在去做這件事的過程中,盡可能的做到深思熟慮,使得最終有個好的結果。我們寫程式也要盡可能做到在正式開始開發之前,在心中有個大致的想法。

對於本專案而言,大致的開發過程是這樣的:

  1. 完成窗體內容截取部分
  2. 完成雷塊分割部分
  3. 完成雷塊類型識別部分
  4. 完成掃雷演算法

好啦,既然我們有了個思路,那就擼起袖子大力幹!

1. 窗體截取

其實對於本項目而言,窗體截取是一個邏輯上簡單,實現起來卻相當麻煩的部分,而且還是必不可少的部分。我們透過Spy 得到了以下兩點資訊:

class_name = "TMain"
title_name = "Minesweeper Arbiter "
登入後複製
  • ms_arbiter.exe的主窗體類別為"TMain"
  • ms_arbiter.exe的主窗體名稱為"Minesweeper Arbiter "

注意到了什麼?主窗體的名稱後面有個空格。正是這個空格讓筆者困擾了一會兒,只有加上這個空格,win32gui才能夠正常的獲取到窗體的句柄。

本專案採用了win32gui來獲取窗體的位置信息,具體程式碼如下:

hwnd = win32gui.FindWindow(class_name, title_name)
if hwnd:
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
登入後複製

透過以上程式碼,我們得到了窗體相對於整塊螢幕的位置。之後我們需要透過PIL來進行掃雷介面的棋盤截取。

我們需要先導入PIL函式庫

from PIL import ImageGrab
登入後複製

然後再進行具體的操作。

left += 15
top += 101
right -= 15
bottom -= 43
rect = (left, top, right, bottom)
img = ImageGrab.grab().crop(rect)
登入後複製

聰明的你肯定一眼就發現了那些奇怪的Magic Numbers,沒錯,這的確是Magic Numbers,是我們透過一點點細微調節得到的整個棋盤相對於窗體的位置。

注意:這些資料只在Windows10下測試通過,如果在別的Windows系統下,不保證相對位置的正確性,因為舊版的系統可能有不同寬度的窗體邊框。

厲害了,用Python破個世界紀錄!橘色的區域是我們所需要的

好啦,棋盤的圖像我們有了,下一步就是對各個雷塊進行圖像分割了~

#2. 雷塊分割

厲害了,用Python破個世界紀錄!

在進行雷塊分割之前,我們事先需要了解雷塊的尺寸以及它的邊框大小。經過筆者的測量,在ms_arbiter下,每個雷塊的尺寸為16px*16px。

知道了雷塊的尺寸,我們就可以進行每一個雷塊的裁切了。首先我們要知道在橫和豎兩個方向上雷塊的數量。

block_width, block_height = 16, 16
blocks_x = int((right - left) / block_width)
blocks_y = int((bottom - top) / block_height)
登入後複製

之後,我們建立一個二維數組用於儲存每個雷塊的圖像,並且進行圖像分割,保存在先前建立的數組中。

def crop_block(hole_img, x, y):
x1, y1 = x * block_width, y * block_height
x2, y2 = x1 + block_width, y1 + block_height
return hole_img.crop((x1, y1, x2, y2))
blocks_img = [[0 for i in range(blocks_y)] for i in range(blocks_x)]
for y in range(blocks_y):
for x in range(blocks_x):
blocks_img[x][y] = crop_block(img, x, y)
登入後複製

將整個圖像獲取、分割的部分封裝成一個庫,隨時調用就OK啦~在筆者的實作中,我們將這一部分封裝成了imageProcess.py,其中函數get_frame()用於完成上述的影像擷取、分割過程。

3. 雷块识别

这一部分可能是整个项目里除了扫雷算法本身之外最重要的部分了。笔者在进行雷块检测的时候采用了比较简单的特征,高效并且可以满足要求。

def analyze_block(self, block, location):
block = imageProcess.pil_to_cv(block)
block_color = block[8, 8]
x, y = location[0], location[1]
# -1:Not opened
# -2:Opened but blank
# -3:Un initialized
# Opened
if self.equal(block_color, self.rgb_to_bgr((192, 192, 192))):
if not self.equal(block[8, 1], self.rgb_to_bgr((255, 255, 255))):
self.blocks_num[x][y] = -2
self.is_started = True
else:
self.blocks_num[x][y] = -1
elif self.equal(block_color, self.rgb_to_bgr((0, 0, 255))):
self.blocks_num[x][y] = 1
elif self.equal(block_color, self.rgb_to_bgr((0, 128, 0))):
self.blocks_num[x][y] = 2
elif self.equal(block_color, self.rgb_to_bgr((255, 0, 0))):
self.blocks_num[x][y] = 3
elif self.equal(block_color, self.rgb_to_bgr((0, 0, 128))):
self.blocks_num[x][y] = 4
elif self.equal(block_color, self.rgb_to_bgr((128, 0, 0))):
self.blocks_num[x][y] = 5
elif self.equal(block_color, self.rgb_to_bgr((0, 128, 128))):
self.blocks_num[x][y] = 6
elif self.equal(block_color, self.rgb_to_bgr((0, 0, 0))):
if self.equal(block[6, 6], self.rgb_to_bgr((255, 255, 255))):
# Is mine
self.blocks_num[x][y] = 9
elif self.equal(block[5, 8], self.rgb_to_bgr((255, 0, 0))):
# Is flag
self.blocks_num[x][y] = 0
else:
self.blocks_num[x][y] = 7
elif self.equal(block_color, self.rgb_to_bgr((128, 128, 128))):
self.blocks_num[x][y] = 8
else:
self.blocks_num[x][y] = -3
self.is_mine_form = False
if self.blocks_num[x][y] == -3 or not self.blocks_num[x][y] == -1:
self.is_new_start = False
登入後複製

可以看到,我们采用了读取每个雷块的中心点像素的方式来判断雷块的类别,并且针对插旗、未点开、已点开但是空白等情况进行了进一步判断。具体色值是笔者直接取色得到的,并且屏幕截图的色彩也没有经过压缩,所以通过中心像素结合其他特征点来判断类别已经足够了,并且做到了高效率。

在本项目中,我们实现的时候采用了如下标注方式:

  • 1-8:表示数字1到8
  • 9:表示是地雷
  • 0:表示插旗
  • -1:表示未打开
  • -2:表示打开但是空白
  • -3:表示不是扫雷游戏中的任何方块类型

通过这种简单快速又有效的方式,我们成功实现了高效率的图像识别。

4. 扫雷算法实现

这可能是本篇文章最激动人心的部分了。在这里我们需要先说明一下具体的扫雷算法思路:

  1. 遍历每一个已经有数字的雷块,判断在它周围的九宫格内未被打开的雷块数量是否和本身数字相同,如果相同则表明周围九宫格内全部都是地雷,进行标记。
  2. 再次遍历每一个有数字的雷块,取九宫格范围内所有未被打开的雷块,去除已经被上一次遍历标记为地雷的雷块,记录并且点开。
  3. 如果以上方式无法继续进行,那么说明遇到了死局,选择在当前所有未打开的雷块中随机点击。(当然这个方法不是最优的,有更加优秀的解决方案,但是实现相对麻烦)

基本的扫雷流程就是这样,那么让我们来亲手实现它吧~

首先我们需要一个能够找出一个雷块的九宫格范围的所有方块位置的方法。因为扫雷游戏的特殊性,在棋盘的四边是没有九宫格的边缘部分的,所以我们需要筛选来排除掉可能超过边界的访问。

def generate_kernel(k, k_width, k_height, block_location):
 ls = []
 loc_x, loc_y = block_location[0], block_location[1]
for now_y in range(k_height):
for now_x in range(k_width):
if k[now_y][now_x]:
 rel_x, rel_y = now_x - 1, now_y - 1
 ls.append((loc_y + rel_y, loc_x + rel_x))
return ls
 kernel_width, kernel_height = 3, 3
# Kernel mode:[Row][Col]
 kernel = [[1, 1, 1], [1, 1, 1], [1, 1, 1]]
# Left border
if x == 0:
for i in range(kernel_height):
 kernel[i][0] = 0
# Right border
if x == self.blocks_x - 1:
for i in range(kernel_height):
 kernel[i][kernel_width - 1] = 0
# Top border
if y == 0:
for i in range(kernel_width):
 kernel[0][i] = 0
# Bottom border
if y == self.blocks_y - 1:
for i in range(kernel_width):
 kernel[kernel_height - 1][i] = 0
# Generate the search map
 to_visit = generate_kernel(kernel, kernel_width, kernel_height, location)
登入後複製

我们在这一部分通过检测当前雷块是否在棋盘的各个边缘来进行核的删除(在核中,1为保留,0为舍弃),之后通过generate_kernel函数来进行最终坐标的生成。

def count_unopen_blocks(blocks):
count = 0
for single_block in blocks:
if self.blocks_num[single_block[1]][single_block[0]] == -1:
count += 1
return count
def mark_as_mine(blocks):
for single_block in blocks:
if self.blocks_num[single_block[1]][single_block[0]] == -1:
self.blocks_is_mine[single_block[1]][single_block[0]] = 1
unopen_blocks = count_unopen_blocks(to_visit)
if unopen_blocks == self.blocks_num[x][y]:
 mark_as_mine(to_visit)
登入後複製

在完成核的生成之后,我们有了一个需要去检测的雷块“地址簿”:to_visit。之后,我们通过count_unopen_blocks函数来统计周围九宫格范围的未打开数量,并且和当前雷块的数字进行比对,如果相等则将所有九宫格内雷块通过mark_as_mine函数来标注为地雷。

def mark_to_click_block(blocks):
for single_block in blocks:
# Not Mine
if not self.blocks_is_mine[single_block[1]][single_block[0]] == 1:
# Click-able
if self.blocks_num[single_block[1]][single_block[0]] == -1:
# Source Syntax: [y][x] - Converted
if not (single_block[1], single_block[0]) in self.next_steps:
self.next_steps.append((single_block[1], single_block[0]))
def count_mines(blocks):
count = 0
for single_block in blocks:
if self.blocks_is_mine[single_block[1]][single_block[0]] == 1:
count += 1
return count
mines_count = count_mines(to_visit)
if mines_count == block:
mark_to_click_block(to_visit)
登入後複製

扫雷流程中的第二步我们也采用了和第一步相近的方法来实现。先用和第一步完全一样的方法来生成需要访问的雷块的核,之后生成具体的雷块位置,通过count_mines函数来获取九宫格范围内所有雷块的数量,并且判断当前九宫格内所有雷块是否已经被检测出来。

如果是,则通过mark_to_click_block函数来排除九宫格内已经被标记为地雷的雷块,并且将剩余的安全雷块加入next_steps数组内。

# Analyze the number of blocks
self.iterate_blocks_image(BoomMine.analyze_block)
# Mark all mines
self.iterate_blocks_number(BoomMine.detect_mine)
# Calculate where to click
self.iterate_blocks_number(BoomMine.detect_to_click_block)
if self.is_in_form(mouseOperation.get_mouse_point()):
for to_click in self.next_steps:
 on_screen_location = self.rel_loc_to_real(to_click)
 mouseOperation.mouse_move(on_screen_location[0], on_screen_location[1])
 mouseOperation.mouse_click()
登入後複製

在最终的实现内,笔者将几个过程都封装成为了函数,并且可以通过iterate_blocks_number方法来对所有雷块都使用传入的函数来进行处理,这有点类似Python中Filter的作用。

之后笔者做的工作就是判断当前鼠标位置是否在棋盘之内,如果是,就会自动开始识别并且点击。具体的点击部分,笔者采用了作者为"wp"的一份代码(从互联网搜集而得),里面实现了基于win32api的窗体消息发送工作,进而完成了鼠标移动和点击的操作。具体实现封装在mouseOperation.py中,可以在查看完整代码:

https://www.php.cn/link/b8a6550662b363eb34145965d64d0cfb

厲害了,用Python破個世界紀錄!

以上是厲害了,用Python破個世界紀錄!的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

PHP和Python:解釋了不同的範例 PHP和Python:解釋了不同的範例 Apr 18, 2025 am 12:26 AM

PHP主要是過程式編程,但也支持面向對象編程(OOP);Python支持多種範式,包括OOP、函數式和過程式編程。 PHP適合web開發,Python適用於多種應用,如數據分析和機器學習。

在PHP和Python之間進行選擇:指南 在PHP和Python之間進行選擇:指南 Apr 18, 2025 am 12:24 AM

PHP適合網頁開發和快速原型開發,Python適用於數據科學和機器學習。 1.PHP用於動態網頁開發,語法簡單,適合快速開發。 2.Python語法簡潔,適用於多領域,庫生態系統強大。

Python vs. JavaScript:學習曲線和易用性 Python vs. JavaScript:學習曲線和易用性 Apr 16, 2025 am 12:12 AM

Python更適合初學者,學習曲線平緩,語法簡潔;JavaScript適合前端開發,學習曲線較陡,語法靈活。 1.Python語法直觀,適用於數據科學和後端開發。 2.JavaScript靈活,廣泛用於前端和服務器端編程。

PHP和Python:深入了解他們的歷史 PHP和Python:深入了解他們的歷史 Apr 18, 2025 am 12:25 AM

PHP起源於1994年,由RasmusLerdorf開發,最初用於跟踪網站訪問者,逐漸演變為服務器端腳本語言,廣泛應用於網頁開發。 Python由GuidovanRossum於1980年代末開發,1991年首次發布,強調代碼可讀性和簡潔性,適用於科學計算、數據分析等領域。

vs code 可以在 Windows 8 中運行嗎 vs code 可以在 Windows 8 中運行嗎 Apr 15, 2025 pm 07:24 PM

VS Code可以在Windows 8上運行,但體驗可能不佳。首先確保系統已更新到最新補丁,然後下載與系統架構匹配的VS Code安裝包,按照提示安裝。安裝後,注意某些擴展程序可能與Windows 8不兼容,需要尋找替代擴展或在虛擬機中使用更新的Windows系統。安裝必要的擴展,檢查是否正常工作。儘管VS Code在Windows 8上可行,但建議升級到更新的Windows系統以獲得更好的開發體驗和安全保障。

visual studio code 可以用於 python 嗎 visual studio code 可以用於 python 嗎 Apr 15, 2025 pm 08:18 PM

VS Code 可用於編寫 Python,並提供許多功能,使其成為開發 Python 應用程序的理想工具。它允許用戶:安裝 Python 擴展,以獲得代碼補全、語法高亮和調試等功能。使用調試器逐步跟踪代碼,查找和修復錯誤。集成 Git,進行版本控制。使用代碼格式化工具,保持代碼一致性。使用 Linting 工具,提前發現潛在問題。

notepad 怎麼運行python notepad 怎麼運行python Apr 16, 2025 pm 07:33 PM

在 Notepad 中運行 Python 代碼需要安裝 Python 可執行文件和 NppExec 插件。安裝 Python 並為其添加 PATH 後,在 NppExec 插件中配置命令為“python”、參數為“{CURRENT_DIRECTORY}{FILE_NAME}”,即可在 Notepad 中通過快捷鍵“F6”運行 Python 代碼。

sublime怎麼運行代碼python sublime怎麼運行代碼python Apr 16, 2025 am 08:48 AM

在 Sublime Text 中運行 Python 代碼,需先安裝 Python 插件,再創建 .py 文件並編寫代碼,最後按 Ctrl B 運行代碼,輸出會在控制台中顯示。

See all articles