>  기사  >  백엔드 개발  >  대단해요, Python을 사용하여 세계 기록을 경신했어요!

대단해요, Python을 사용하여 세계 기록을 경신했어요!

王林
王林앞으로
2023-04-20 13:19:061237검색

대단해요, Python을 사용하여 세계 기록을 경신했어요!

안녕하세요 여러분,

Python+OpenCV를 사용하여 자동 지뢰 제거를 구현하고 세계 기록을 깨뜨렸습니다. 먼저 그 효과를 살펴보겠습니다.

대단해요, Python을 사용하여 세계 기록을 경신했어요!

중급 - 0.74초 3BV/S=60.81

고전 게임(그래픽 카드 테스트) 게임(소프트웨어)인 지뢰찾기에 대해 오랫동안 많은 분들이 알고 계시고, 많은 분들이 들어보셨으리라 믿습니다. 중국의 썬더 세인트(Thunder Saint)는 중국 지뢰 제거 1위, 세계 2위인 궈웨이가(Guo Weijia)의 별명이기도 하다. Windows 9x 시대에 탄생한 고전 게임인 지뢰찾기는 빠른 속도와 고정밀 마우스 조작 요구 사항, 빠른 응답 기능, 기록을 세우는 스릴 등 과거부터 현재까지 여전히 고유한 매력을 갖고 있습니다. 지뢰찾기가 가져오는 모든 것 지뢰찾기 친구들이 가져다주는 독특한 즐거움은 지뢰찾기만의 고유한 특징입니다.

1. 준비

지뢰제거 자동화 소프트웨어를 준비하기 전에 다음 도구/소프트웨어/환경을 준비해야 합니다.

- 개발 환경

  1. Python3 환경- 권장 3.6 이상 [Anaconda3가 더 권장됩니다. 아래 종속성 라이브러리는 설치할 필요가 없습니다]
  2. Numpy 종속 라이브러리 [Anaconda가 있는 경우 설치 필요 없음]
  3. PIL 종속 라이브러리 [Anaconda가 있는 경우 설치할 필요 없음]
  4. opencv-python
  5. win32gui, win32api 종속 라이브러리
  6. Python을 지원하는 IDE [텍스트 편집기를 사용하여 프로그램을 작성할 수 있는 경우 선택할 수 있음]

- 지뢰 찾기 소프트웨어

· 지뢰 찾기 Arbiter(지뢰 찾기에는 MS-Arbiter를 사용해야 합니다!)

알겠습니다. , 그러면 준비가 완료되었습니다. 시작해 보세요~

2. 구현 아이디어

어떤 일을 하기 전에 가장 중요한 것은 무엇인가요? 그것은 당신이 하려는 일에 대한 단계별 틀을 마음속에 구축하는 것입니다. 그래야만 이 일을 하는 과정이 최대한 신중하게 이루어지므로 결국 좋은 결과를 얻을 수 있습니다. 프로그램을 작성할 때 공식적으로 개발을 시작하기 전에 일반적인 아이디어를 염두에 두도록 최선을 다해야 합니다.

본 프로젝트의 일반적인 개발 과정은 다음과 같습니다.

  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)

당신이 똑똑하다면, 그 이상한 매직 넘버를 한 눈에 발견했을 것입니다. 그렇습니다. 이것은 우리가 약간의 미묘한 조정을 통해 얻은 형태에 대한 전체 체스판의 위치입니다. .

참고: 이 데이터는 Windows 10에서만 테스트되었습니다. 다른 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)

이후에는 각 천둥 블록의 이미지를 저장할 2차원 배열을 생성하고, 이미지를 분할하여 이전에 생성된 배열에 저장합니다.

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)

전체 이미지 획득 및 분할 부분을 언제든지 호출할 수 있는 라이브러리로 캡슐화합니다~ 작성자 구현에서는 이 부분을 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 51cto.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제