本文程式碼基於 python3.6 和 pygame1.9.4。
五子棋比起我之前寫的幾款遊戲來說,難度提高了不少。如果是人與人對戰,那麼,電腦只需要判斷是否贏了就可以。如果是人機對戰,那你還得讓電腦知道怎麼下。
我們先從簡單的問題來看。
開端
畫棋盤
首先肯定是要畫出棋盤來,用 pygame 畫出一個 19 × 19 或 15 × 15 的棋盤並不是什麼難事,這在之前的文章中已經多次用到,就不贅述了。
畫棋子
需要說一下的是畫棋子,因為沒找到什麼合適的棋子圖片,所以只要自己來畫棋子。
我們用 pygame.draw.circle
畫出來的圓形是這樣的:
pygame.draw
中有畫抗鋸齒直線的函式 aaline
,但是並沒有 aacircle
這樣的函式來畫一個抗鋸齒的圓。
這裡就需要用到 pygame.gfxdraw
啦。pygame.gfxdraw
目前還僅是實驗版本,這意味著這個 API 可能會在以後的 pygame 版本中發生變化或消失。
要繪製抗鋸齒和填充形狀,請首先使用函式的aa *版本,然後使用填充版本。例如:
col = (255, 0, 0)
surf.fill((255, 255, 255))
pygame.gfxdraw.aacircle(surf, x, y, 30, col)
pygame.gfxdraw.filled_circle(surf, x, y, 30, col)
複製程式碼
我們用這個方法在棋盤上畫一個棋子試試看。
可以看到效果已明顯改善。落子
落子需要判斷滑鼠事件,當滑鼠左鍵點選,獲取滑鼠點選的位置,然後根據棋盤的位置,計算出棋子落在棋盤的位置。
while True:
for event in pygame.event.get():
if event.type == QUIT:
sys.exit()
elif event.type == MOUSEBUTTONDOWN:
pressed_array = pygame.mouse.get_pressed()
if pressed_array[0]: # 滑鼠左鍵點選
mouse_pos = pygame.mouse.get_pos()
click_point = _get_clickpoint(mouse_pos)
複製程式碼
勝利判定
當一子落下,如何判定是否勝利?
可以肯定的是,當某一子落下的時候,如果出現了 5 連,那麼落下的這顆子必定在這條 5 連線上。那麼這個問題就可以簡化了,我們無需全盤掃描,只需要在落子位置上橫豎撇捺掃描一下,判斷是否出現 5 連即可。
我們定義一個棋盤類,類中例項化一個 19 × 19 的二維陣列,初始值皆為 0,表示空,用 1 表示黑子,2 表示白子。這個類對外提供一個落子方法 drop
,接收引數落子方和落子座標,如果落子後勝利,則返回勝利者,否則返回 None。
Chessman = namedtuple('Chessman', 'Name Value Color')
Point = namedtuple('Point', 'X Y')
BLACK_CHESSMAN = Chessman('黑子', 1, (45, 45, 45))
WHITE_CHESSMAN = Chessman('白子', 2, (219, 219, 219))
offset = [(1, 0), (0, 1), (1, 1), (1, -1)]
class Checkerboard:
def __init__(self, line_points):
self._line_points = line_points
self._checkerboard = [[0] * line_points for _ in range(line_points)]
def _get_checkerboard(self):
return self._checkerboard
checkerboard = property(_get_checkerboard)
# 判斷是否可落子
def can_drop(self, point):
return self._checkerboard[point.Y][point.X] == 0
def drop(self, chessman, point):
"""
落子
:param chessman: 黑子/白子
:param point:落子位置
:return:若該子落下之後即可獲勝,則返回獲勝方,否則返回 None
"""
print(f'{chessman.Name} ({point.X}, {point.Y})')
self._checkerboard[point.Y][point.X] = chessman.Value
if self._win(point):
print(f'{chessman.Name}獲勝')
return chessman
# 判斷是否贏了
def _win(self, point):
cur_value = self._checkerboard[point.Y][point.X]
for os in offset:
if self._get_count_on_direction(point, cur_value, os[0], os[1]):
return True
def _get_count_on_direction(self, point, value, x_offset, y_offset):
count = 1
for step in range(1, 5):
x = point.X + step * x_offset
y = point.Y + step * y_offset
if 0 <= x < self._line_points and 0 <= y < self._line_points and self._checkerboard[y][x] == value:
count += 1
else:
break
for step in range(1, 5):
x = point.X - step * x_offset
y = point.Y - step * y_offset
if 0 <= x < self._line_points and 0 <= y < self._line_points and self._checkerboard[y][x] == value:
count += 1
else:
break
return count >= 5
複製程式碼
這裡我定義了一個偏移量,我們一共要計算橫豎撇捺 4 條線,任意一條線出現 5 連就算獲勝。計算方法實際上是一樣的,只是方向不同,所以定義一個偏移量陣列,不同的偏移量表示不同的方向,這樣就可以利用迴圈來實現了,節省了很多程式碼。
電腦落子
這就是全篇的重頭戲了,要怎麼教電腦下五子棋。首先宣告,我用的是相對傳統的方式,不是真正意義上的深度學習。
五子棋就是要實現 5 連,所以,一開始,我的想法是:將所有連線儲存在一個陣列中,落子的時候選擇最長的連線落子。但這樣有個問題解決不掉,如何讓電腦識別“三三”呢?
後來網上看到篇文章,使用的方法是:遍歷棋盤上的空位,計算每一個位置其橫豎撇捺 8 個方向上是否有己方的子,有一個就加 10 分,最後選得分最高的位置落子。
這樣不太嚴謹,寫出來的電腦估計水平很菜,但是這個思路卻是對的,落子就是要找到最值得的地方,那麼我們乾脆對每一個可落子的地方來做一個評估,選出最優解。
棋形術語
這裡我們需要了解一下五子棋的幾種基本棋形:連五,活四,衝四,活三,眠三,活二,眠二。
連五
顧名思義,五顆同色棋子連在一起,贏了。
活四
四顆同色棋子連在一起,並且左右兩邊都沒有對方棋子阻擋,有兩個連五點。
衝四
四顆同色棋子連在一起,並且一邊有對方棋子阻擋,或者四顆棋子不是連的,當中有個空擋,這時只有一個連五點。
活三、跳活三
活三:三顆同色棋子連在一起。
跳活三:中間隔了一個空格的活三。眠三
只能夠形成衝四的三,無外乎兩種情況,一是一邊被擋住了,一是當中有 2 個空格。(其實我在程式碼中僅考慮了第一種情況,即便形成衝四,也不是什麼危險局面。)
活二和眠二
活二,能夠形成活三的二;眠二,能夠形成眠三的二。這裡就不放圖了,參考活三眠三。
打分機制
理解了這些棋形,那麼按我們之前的思路,就是如何打分了。
- 首先,連五肯定是不存在的,出現連五勝負已分,所以只要棋局還在進行中,就不會出現連五。那麼,什麼優先順序最高?自然就是活四了。
- 其次是對方的“四”,對方活四,你防不防都一樣輸了,對方衝四,你就必須防守。
- 再次是我方的活三或衝四,活三跟衝四其實是一個級別的,對方必須防守。
- 再次是對方的活三或衝四。
以此類推下去。我們可以總結一點規律:
- 相同的棋形,我方優於對方。
- 衝四跟活三一個級別,眠三跟活二一個級別。
- 如果中間有空格的話,肯定是要比沒空格的略微低階一點,但不至於降級。
基本邏輯就是這樣,這一塊的程式碼我寫得也不好,整個判斷寫了100多行,就不貼程式碼了,大家可以直接下原始碼看。
五子棋執黑是必贏的,程式碼中,玩家就是執黑先手,電腦執白後手,所以,下的好是完全可以贏電腦的,不過一個小小失誤也很可能被電腦翻盤。
相關博文推薦