效果
?
閑來無事,打開虛擬機上的掃雷玩了玩,覺得自己計算很浪費時間,還容易遺漏,就做了個自動掃雷,
簡單模式下很容易通關,困難的就看臉了,感興趣的可以拿去運行一下,
自動化處理核心代碼段在 168~273行,
很多人學習蟒蛇,不知道從何學起, 很多人學習python,掌握了基本語法之后,不知道在哪里尋找案例上手, 很多已經可能案例的人,卻不知道如何去學習更多高深的知識, 那么針對這三類人,我給大家提供一個好的學習平臺,免費獲取視頻教程,電子書,以及課程的源代碼! QQ群:101677771 歡迎加入,一起討論一起學習
次日,發現自動掃雷演算法并不完整,上次的代碼僅對單個數字周圍進行判斷,
但在一些情況下,單個數字無法判斷,要綜合一片小區域確定某些方塊是否一定是炸彈,或者一定安全,
暫且稱為高級演算法,(并不是演算法有多高級,本質上還是取集合(區域),進行大量的判斷,難在復雜判斷的邏輯關系以及集合、字典的操作上),
通過半天的歸納總結,找到規律后開始設計代碼,
**本次修改還優化了輸出格式,使得在大區域下更容易確定方塊的坐標,**
- 【代碼中一些重復的地方可以提取出來作為單獨的方法,通過改變引數位置來實作相同的功能,讓代碼看上去更精簡】
- 【但長時間的修改代碼,隨著變數、變數型別、資料結構嵌套和邏輯關系的不斷增加,有點被搞得頭暈了】
- 【所以既然運行上沒有錯誤,多次試驗也沒有發現毛病,就不去管它了,說不定也是靠著什么bug運行起來了呢】
- 【事實上最初寫出來的高級演算法代碼還多了一個子模塊,這個模塊在一次高級演算法結束之后進行進一步處理】
- 【在設計演算法的時候,考慮到這樣能減少遍歷游戲視窗的次數,加快運行速度,并且可以確定更多的更復雜的坐標及操作】
- 【雖然寫好代碼之后第一次運行全自動困難模式順利通關,但在后來的幾次測驗中總會錯把炸彈點開??】
- 【而且計算出的坐標我實在是看不懂根據什么條件算出來的,修改多次無果,想著是不是演算法最初處理的時候就是錯誤的】
- 【把這段代碼注釋掉之后,多次測驗,沒有任何錯誤,而且計算出來的坐標和操作都是正確的!計算不出來的時候,我看著也是無法確定哪個方塊安全】
- 【這種情況下只能靠運氣隨機選擇,所以上文說到,說不定是靠著什么bug運行起來的呢,】
教學時間:
咱都上榜一了,咋能不出個教學呢,
(綠色底為演算法講解,藍色底為舉例說明,無色底、算是旁白或設計演算法的程序吧,)
首先介紹下掃雷的冷知識:
掃雷上面數字的意思是該方塊周圍八格方塊中的雷的個數,
有時候點一下會開一大片,是因為點到了數字0,0周圍沒有方塊,游戲會自動向四周擴散,直到有數字的方塊停止,(0向四周擴散是不可能遇到雷的)
這段“點到0向四周擴散”的代碼在141~150行,通過126-128行進入,通過非常簡單的遞回實作的,遍歷一遍0方塊周圍的方塊,是數字就顯示數字,是0就再次呼叫該方法,傳入該位置,這段代碼可以優化,但重點是自動掃雷,
生成一局掃雷的方法:(對應代碼43 ~ 63行)
先生成指定大小(m×n)的空間,我在這里用了二維陣串列示,初始化全為0,程式中變數名為game_space, 然后隨機k(k個雷)個不同坐標,用’*‘表示雷,記錄在陣列中,
然后遍歷一下每個雷的周圍八個格子,如果不是字符’*‘,則+1,生成完成,(可以驗證,數字與周圍雷的數量都是匹配的)
可能有同學會質疑,既然game_space串列已經記錄了雷的位置,那還有啥掃的?
實際上,掃雷游戲就是提前生成好的游戲棋盤資料,然后在每個格子上都蓋上蓋子而已,這里表示頂層蓋子的串列是show_list,
所有的操作都在show_list串列上,game_space串列僅用于點擊show_list串列時進行資料補充,相當于掀開了外層的蓋子,露出了里面的內容,
自動掃雷:
自動掃雷就是一個模擬玩家掃雷的程序,你開一局掃雷,第一個肯定是要隨便點的,如果運氣爆棚,是可以直接點到雷結束該局游戲的,
點到數字一般也不會是8,一般都是從點到0開始才能判斷哪里是雷,哪里是安全的,
這里開一局“簡單”模式的掃雷,8*8大小,10個雷,用于舉例說明,(順帶一提,我寫的輸出格式在pycharm里輸出是整齊的,沒有在IDLE里測驗過,這里粘貼過來■就比數字寬些)
隨機選擇【4, 3】點開
Game Over!
1│ ■ ■ ■ ■ ■ ■ ■ ■
2│ ■ ■ ■ ■ ■ ■ ■ ■
3│ ■ ■ ■ ■ ■ ■ ■ ■
4│ ■ ■ * ■ ■ ■ ■ ■
5│ ■ ■ ■ ■ ■ ■ ■ ■
6│ ■ ■ ■ ■ ■ ■ ■ ■
7│ ■ ■ ■ ■ ■ ■ ■ ■
8│ ■ ■ ■ ■ ■ ■ ■ ■
運氣爆棚,一發入魂, 再開一局,
再開N局,
無法確定位置,隨機選擇【2, 1】點開
無法確定位置,隨機選擇【4, 8】點開
1│ ■ ■ ■ ■ ■ ■ 1 0
2│ 1 ■ ■ ■ ■ ■ 1 0
3│ ■ ■ ■ ■ ■ 1 1 0
4│ ■ ■ ■ ■ ■ 1 0 0
5│ ■ ■ ■ ■ ■ 3 1 1
6│ ■ ■ ■ ■ ■ ■ ■ ■
7│ ■ ■ ■ ■ ■ ■ ■ ■
8│ ■ ■ ■ ■ ■ ■ ■ ■
這里我們可以通過【行3列7】(以下使用形如【3,7】表示)確定【2,6】是雷,因為【3,7】周圍只有【2,6】沒有點開,而【3,7】周圍只有一個雷,
給【2,6】插上旗子之后,又可以通過【1,7】判斷【1,6】是安全的,可以點開;可以通過【3,6】判斷【2,5】【3,5】【4,5】是安全的,
【2,5】【3,5】【4,5】點開后 又可以通過【4,6】判斷【5,5】是雷,
自動演算法說明:(該段代碼在174-198行)
掃雷棋盤使用二維陣串列示,首先通過兩層回圈遍歷,找到一個大于0的數字,使用一個變數’z‘記錄這個數字,
再遍歷一下該數字周圍的八格格子,這里我又使用了兩層回圈實作,(代碼在178-180行,要確保坐標不超界),
在遍歷程序中,若發現插了旗子‘□’的格子,則讓z - 1,表示z周圍已經確定了1個雷,所以還剩z - 1個雷,
如果發現了還沒有點開過的格子’■‘,則記錄該坐標,(代碼中用類變數coordinate_list串列記錄)
八個格子遍歷完成后,若 z 等于 0, 則說明coordinate_list串列中記錄的’■‘格子都是安全的,可以點擊,于是將coordinate_list串列中的每個坐標后記錄可以點擊(即操作1),回傳,
若z 等于 coordinate_list串列的長度,則可以確定coordinate_list串列中記錄的’■‘格子都是雷 ,給這些坐標記錄上插旗操作(即操作0),回傳,
剩下的情況就是coordinate_list串列長度 大于 z了,這種情況無法確定記錄的格子是什么東西,則清空coordinate_list串列, 繼續回圈,尋找下個數字進行這套操作,
注意到z = 0時,coordinate_list串列可能為空,所以要在回傳前判斷coordinate_list串列非空,(代碼194行)
回傳的只是串列中的一個坐標及操作,若串列中有多個坐標,要全部進行相應操作,對應代碼(168-172行)
還可以通過綜合一個區域判斷【6,5】【6,7】一定是雷,因為【5,6】周圍有3個雷,【5,5】是1個,剩下兩個在【6,5】【6,6】【6,7】中;
而通過【5,7】可知【6,6】【6,7】【6,8】中只有1個雷,所以【6,5】一定是雷,又由【5,8】可知【6,7】【6,8】中有1個雷,
所以【6,7】一定是雷,【6,6】【6,8】安全,若存在疑惑,可通過假設法假設【6,6】或【6,8】是雷,通過推匯出雷周圍的數字與數字周圍的雷數不符合,得到驗證,
這里就是上面提到的“高級演算法”,會在下文中詳細介紹演算法(就是一堆復雜的資料結構和邏輯判斷),
這里只是說明了通過現在的局面可以判斷到的格子,在自動掃雷程式中是按行先遍歷的,不一定就是按我說的這些去依次點開,可能點開【1,6】后又能判斷出【1,6】周圍的格子是安全的了,
(順帶一提,有??圖文字,但是寬度和方塊、數字不一樣,為了輸出排版,就換成了白色方塊□ 代替??,大家也可以修改代碼,將所有字符、數字換成全角,)
讓我們繼續游戲:(操作0是插旗的意思,操作1是點開的意思)
選定位置【2, 6】, 操作為:0
選定位置【1, 6】, 操作為:1
1│ ■ ■ ■ ■ ■ 1 1 0
2│ 1 ■ ■ ■ ■ □ 1 0
3│ ■ ■ ■ ■ ■ 1 1 0
4│ ■ ■ ■ ■ ■ 1 0 0
5│ ■ ■ ■ ■ ■ 3 1 1
6│ ■ ■ ■ ■ ■ ■ ■ ■
7│ ■ ■ ■ ■ ■ ■ ■ ■
8│ ■ ■ ■ ■ ■ ■ ■ ■
在這里插入掃雷串列,是因為點開【1,6】后可通過【1,6】【2,6】判斷【1,5】【2,5】安全,已經和我上面所述的操作路徑不同了,
下面我不在詳細展示,讓我們直接快進到需要重要介紹的位置,
選定位置【2, 5】, 操作為:1
選定位置【1, 5】, 操作為:1
……
……
1│ 0 0 1 □ 2 1 1 0
2│ 1 1 2 1 2 □ 1 0
3│ 2 □ 2 0 1 1 1 0
4│ 2 □ 2 1 1 1 0 0
5│ 2 2 1 2 □ 3 1 1
6│ □ 1 0 3 □ ■ ■ ■
7│ 2 2 1 2 □ 3 1 1
8│ 1 □ 1 1 1 1 0 0
呼叫高級計算處理,請等候...
計算完成!生成操作:
坐標【6,8】,操作:1
坐標【6,6】,操作:1
========================
選定位置【6, 6】, 操作為:1
選定位置【6, 8】, 操作為:1
選定位置【6, 7】, 操作為:0
可以看到,通過上面簡單的計算,已經快將游戲解完了,在簡單模式下,多數情況根本用不上高級計算,這局游戲是開了許多局才呼叫到了高級演算法代碼,就算沒有高級演算法,也可以通過隨機方法隨機一個坐標點開,有2/3的概率是數字,只要點開一個是數字,就可以確定最后一個雷的位置了,
可由【5,8】知道【6,7】【6,8】中有1個雷,由【5,7】知道 【6,6】【6,7】【6,8】中有1個雷,所以【6,6】一定是安全的,可以點開,
可由【5,6】得知【6,6】【6,7】中有1個雷,綜合【5,7】,可以確定【6,8】是安全的,可以點開,
這就是需要通過綜合一片區域才能確定的,用代碼實作確實比較復雜,
高級演算法說明:(代碼在228~273行,通過202行呼叫)
要將這種思想轉化為代碼,就要先總結他們共有的特征,就是找規律,規律是:求差集,
這里要用到python的 字典、集合、元組、串列,
首先,我將游戲輸出串列show_list做了一個處理,把所有>0的數字減去他們周圍已經插旗的個數,結果保存在了processed_list串列中,(代碼229~247行)
在處理之前 要先創建一個空的集合c(代碼238行),在遍歷數字周圍八個方格時,如果遇到沒有打開的方格’■‘,則在c中記錄該方格坐標,這里,我們相當于得到了一個區域,
遍歷完八個方格之后,如果集合c非空,那么處理的數字一定還>0,將集合c轉變為元組,作為字典的鍵,處理后的數字做為該鍵對應的值,(字典名為set_dic)
對上面的示例,該操作能得到:
1│ 0 0 0 □ 0 0 0 0
2│ 0 0 0 0 0 □ 0 0
3│ 0 □ 0 0 0 0 0 0
4│ 0 □ 0 0 0 0 0 0
5│ 0 0 0 0 □ 1 1 1
6│ □ 0 0 0 □ ■ ■ ■
7│ 0 0 0 0 □ 1 1 1
8│ 0 □ 0 0 0 0 0 0
并得到字典set_dic = {((6, 6), (6, 7)) : 1, ((6, 6), (6, 7), (6, 8)) : 1, ((6, 7), (6, 8)) : 1} 【解釋下為什么要將集合c轉化為元組,因為集合是可變資料型別,屬于非可哈希型別,無法作為字典的鍵,】
之后就是對字典的兩層回圈:取其中一個鍵,(取完鍵之后首先轉變為集合型別,通過集合型別自帶的判斷子集的方法),與其他鍵做判斷是否為子集,
如果是子集,即可求差集,并求這兩個鍵對應值的差,如果值的差為0,說名求得的差集中的坐標是安全的,可以打開;如果值的差非零,如果值得差等于差集的長度,說明差集中的坐標一定是雷,需要插旗,(代碼249~273)
對應上面的示例,集合a = {(6, 6), (6, 7)} 是 集合b = {(6, 6), (6, 7), (6, 8)}的子集, 求差集 b - a 得到差集{(6, 8)}, 差值為0,說明坐標(6,8)的方格’■‘是安全的,
之后就是將元組(6,8)轉換為串列[6, 8],加入操作碼得到[6, 8, 1],加入到操作串列 coordinate_list 中,
講解結束,這也不算難,也得益于python本身的優勢,如果用其他語言,還要定義大量結構體,或無休止的取值賦值,
這里順便說個bug,代碼中在得到差集和差值后,加入操作串列的時候沒有判斷要插入的資料是否已經存在,因為可能通過不同的集合判斷得到同一個差集,如果是點擊操作還好,點擊過后該方塊不能再進行其他操作,但如果是插旗,那么插旗之后再插旗就是取消插旗,
而操作完成之后,下次高級判斷可能還會得到兩次相同坐標的插旗操作,那么程式將進入無線回圈,所以可以將 coordinate_list 串列變成集合型別,集合本身不允許重復,或者在向 coordinate_list 串列加入資料前前進行一個重復判斷,
這個bug是在測驗 行100,列200,雷3000的游戲中發現的bug,小規模一般不會遇到,
【可以將帶有注釋的time.sleep代碼給打開,在括號內設定合適的時長,可以看到自動掃雷的程序】
python代碼
import random
import time
class SaoLei:
m: int
n: int
k: int
mode: int
coordinate_list = []
game_space: list[list[int or str]]
def __init__(self, m: int, n: int, k: int, mode: int):
"""
初始化游戲
:param m: m行, m >= 8
:param n: n列, n >= 8
:param k: k個雷, 10 <= k <= m*n*0.3
:param mode: 0:手動模式,1:全自動模式 2:半自動模式
"""
if m >= 8:
self.m = m
else:
print("row Error!")
exit(0)
if n >= 8:
self.n = n
else:
print("col Error!")
exit(0)
if 10 <= k <= m * n * 0.3:
self.k = k
self.flag = self.k
else:
print("k Error!")
exit(0)
if mode in (0, 1, 2):
self.mode = mode
else:
print("Mode Error!")
exit(0)
# 生成區域
self.game_space = [[0 for _ in range(n)] for _ in range(m)]
print("game_space create success!")
# 隨機雷的位置
i = 0
while i < self.k:
a = random.randint(0, m - 1)
b = random.randint(0, n - 1)
if self.game_space[a][b] != '*':
i += 1
# 產生資料
self.game_space[a][b] = '*'
c = [-1, 0, 1]
for x in c:
if 0 <= a + x < m:
for y in c:
if 0 <= b + y < n:
if self.game_space[a + x][b + y] != '*':
self.game_space[a + x][b + y] += 1
# 呼叫游戲行程
self.game_window()
def game_window(self):
show_list = [['■' for _ in range(self.n)] for _ in range(self.m)]
text = ''
while True:
# 輸出畫面
for i in range(10):
print()
print(text)
print("+++" * self.n, end='+\n')
print(' │ ', end='')
for k in range(1, self.n + 1):
print('%d' % (k % 10), end=' ')
print('\n─┼─', end='')
print('───' * (self.n - 1), end='─\n')
k = 1
for i in range(self.m):
print(k, end='│ ')
for j in range(self.n):
print(show_list[i][j], end=' ')
print()
k = (k + 1) % 10
if text == 'Game Over!':
exit(0)
text = ''
# 檢測是否結束
row = self.m
for i in show_list:
if '■' not in i:
row -= 1
if row == 0:
print('Victory!')
exit(1)
# 輸入坐標及操作
if self.mode == 0:
a, b, c = self.input_set()
else:
a, b, c = self.automatic_input(show_list)
# 進行處理
if c == 0:
if self.flag > 0:
if show_list[a][b] == '■':
show_list[a][b] = '□'
self.flag -= 1
elif show_list[a][b] == '□':
show_list[a][b] = '■'
self.flag += 1
else:
text = 'Error! Is number'
else:
text = 'Error! No flag'
elif c == 1:
if show_list[a][b] == '■':
if self.game_space[a][b] == '*':
show_list[a][b] = '*'
text = 'Game Over!'
elif self.game_space[a][b] == 0:
show_list[a][b] = self.game_space[a][b]
self.look_zero(a, b, show_list)
else:
show_list[a][b] = self.game_space[a][b]
else:
text = 'Error! No Click'
elif c == 2:
if show_list[a][b] == '■':
show_list[a][b] = '?'
elif show_list[a][b] == '?':
show_list[a][b] = '■'
else:
text = 'Error! Open'
def look_zero(self, a: int, b: int, show_list):
for i in range(a - 1, a + 2):
for j in range(b - 1, b + 2):
if 0 <= i < self.m and 0 <= j < self.n:
if show_list[i][j] == 