PyQt5實(shí)現(xiàn)五子棋游戲(人機(jī)對(duì)弈)
這篇博客主要是為了學(xué)習(xí)Python和PyQt,因?yàn)閷?duì)棋類游戲比較熱衷,所以從規(guī)則較簡單的五子棋入手,利用PyQt5實(shí)現(xiàn)圖形界面,做一個(gè)可以進(jìn)行人機(jī)對(duì)弈的腳本,最后打包成應(yīng)用程序。AI的算法打算用神經(jīng)網(wǎng)絡(luò)來完成,正在苦學(xué)TensorFlow中。
本來我以為五子棋規(guī)則很簡單,不就像小學(xué)時(shí)候玩的那樣,五個(gè)棋子連在一起就贏了嘛,但是后來發(fā)現(xiàn)事情并沒有那么簡單,現(xiàn)在的五子棋有禁手這個(gè)規(guī)則 ,“三三禁手” 、“四四禁手”、“長連禁手”等等,都是為了限制現(xiàn)行一方必勝。我也不是職業(yè)的棋手,對(duì)吧,所以禁手什么的就不考慮了,弄個(gè)簡單的成品出來就很滿足了。
代碼全是邊學(xué)習(xí)邊寫的,有瑕疵的地方歡迎提出。
第一步,收集素材
主要就是棋子、棋盤的圖片,還有下棋的音效



音效與代碼一起在最后給出
第二步,五子棋的邏輯類
收集完素材后,不著急界面的編寫,先將五子棋的邏輯寫好,界面和邏輯要分開,這很重要。
先想想在五子棋的邏輯類里要有哪些東西。
首先是棋盤,棋盤用15*15的數(shù)組表示
然后是棋子,黑棋用1表示,白棋用2表示,空白就用0表示
再然后還要獲取指定點(diǎn)的坐標(biāo),獲取指定點(diǎn)的方向等等。
最重要的也是稍微有點(diǎn)難度的部分就是判斷輸贏。結(jié)合網(wǎng)上的方法和我自己的理解,下面貼出我寫的代碼,僅供參考。
chessboard.py
# ---------------------------------------------------------------------- # 定義棋子類型,輸贏情況 # ---------------------------------------------------------------------- EMPTY = 0 BLACK = 1 WHITE = 2 # ---------------------------------------------------------------------- # 定義棋盤類,繪制棋盤的形狀,切換先后手,判斷輸贏等 # ---------------------------------------------------------------------- class ChessBoard(object): def __init__(self): self.__board = [[EMPTY for n in range(15)] for m in range(15)] self.__dir = [[(-1, 0), (1, 0)], [(0, -1), (0, 1)], [(-1, 1), (1, -1)], [(-1, -1), (1, 1)]] # (左 右) (上 下) (左下 右上) (左上 右下) def board(self): # 返回?cái)?shù)組對(duì)象 return self.__board def draw_xy(self, x, y, state): # 獲取落子點(diǎn)坐標(biāo)的狀態(tài) self.__board[x][y] = state def get_xy_on_logic_state(self, x, y): # 獲取指定點(diǎn)坐標(biāo)的狀態(tài) return self.__board[x][y] def get_next_xy(self, point, direction): # 獲取指定點(diǎn)的指定方向的坐標(biāo) x = point[0] + direction[0] y = point[1] + direction[1] if x < 0 or x >= 15 or y < 0 or y >= 15: return False else: return x, y def get_xy_on_direction_state(self, point, direction): # 獲取指定點(diǎn)的指定方向的狀態(tài) if point is not False: xy = self.get_next_xy(point, direction) if xy is not False: x, y = xy return self.__board[x][y] return False def anyone_win(self, x, y): state = self.get_xy_on_logic_state(x, y) # 當(dāng)前落下的棋是黑棋還是白棋,它的狀態(tài)存儲(chǔ)在state中 for directions in self.__dir: # 對(duì)米字的4個(gè)方向分別檢測(cè)是否有5子相連的棋 count = 1 # 初始記錄為1,因?yàn)閯偮湎碌钠逡菜? for direction in directions: # 對(duì)落下的棋子的同一條線的兩側(cè)都要檢測(cè),結(jié)果累積 point = (x, y) # 每次循環(huán)前都要刷新 while True: if self.get_xy_on_direction_state(point, direction) == state: count += 1 point = self.get_next_xy(point, direction) else: break if count >= 5: return state return EMPTY def reset(self): # 重置 self.__board = [[EMPTY for n in range(15)] for m in range(15)]
將上面的代碼放在chessboard.py里面就完成了最基本的操作了。
第三步,利用PyQt5實(shí)現(xiàn)圖形界面
先想好思路。
1.目標(biāo)是做一個(gè)簡易的五子棋的界面,主窗口只需要一個(gè)Widget就可以了
2.Widget的背景設(shè)置為棋盤圖片
3.鼠標(biāo)每點(diǎn)擊一次空白區(qū)域,該區(qū)域就添加一個(gè)標(biāo)簽,在標(biāo)簽中插入棋子圖片
4.因?yàn)槭侨藱C(jī)對(duì)弈,玩家執(zhí)黑棋,所以可以將鼠標(biāo)變成黑棋圖片(這一點(diǎn)比較復(fù)雜,需要重寫標(biāo)簽類)
5.整體邏輯是:鼠標(biāo)點(diǎn)擊一次—->換算坐標(biāo)(UI坐標(biāo)到棋盤坐標(biāo))—->判斷坐標(biāo)是否合理—->黑棋落在棋盤上—->判斷是否贏棋—->電腦思考—->電腦下白棋—->判斷是否贏棋……
6.因?yàn)锳I思考需要時(shí)間,所以還需要加一個(gè)線程,單獨(dú)讓它計(jì)算AI的走法
7.一些細(xì)節(jié)問題: 贏棋和輸棋怎么處理(對(duì)話框)、和棋怎么辦(這個(gè)先不考慮)、游戲后期棋子非常多的時(shí)候容易眼花,不知道AI走到哪怎么辦(加一個(gè)指示箭頭)、音效怎么插入(用QSound)等等
下面給出整體代碼:
gobangGUI.py
from chessboard import ChessBoard
from ai import searcher
WIDTH = 540
HEIGHT = 540
MARGIN = 22
GRID = (WIDTH - 2 * MARGIN) / (15 - 1)
PIECE = 34
EMPTY = 0
BLACK = 1
WHITE = 2
import sys
from PyQt5 import QtCore, QtGui
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QMessageBox
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap, QIcon, QPalette, QPainter
from PyQt5.QtMultimedia import QSound
# ----------------------------------------------------------------------
# 定義線程類執(zhí)行AI的算法
# ----------------------------------------------------------------------
class AI(QtCore.QThread):
finishSignal = QtCore.pyqtSignal(int, int)
# 構(gòu)造函數(shù)里增加形參
def __init__(self, board, parent=None):
super(AI, self).__init__(parent)
self.board = board
# 重寫 run() 函數(shù)
def run(self):
self.ai = searcher()
self.ai.board = self.board
score, x, y = self.ai.search(2, 2)
self.finishSignal.emit(x, y)
# ----------------------------------------------------------------------
# 重新定義Label類
# ----------------------------------------------------------------------
class LaBel(QLabel):
def __init__(self, parent):
super().__init__(parent)
self.setMouseTracking(True)
def enterEvent(self, e):
e.ignore()
class GoBang(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.chessboard = ChessBoard() # 棋盤類
palette1 = QPalette() # 設(shè)置棋盤背景
palette1.setBrush(self.backgroundRole(), QtGui.QBrush(QtGui.QPixmap('img/chessboard.jpg')))
self.setPalette(palette1)
# self.setStyleSheet("board-image:url(img/chessboard.jpg)") # 不知道這為什么不行
self.setCursor(Qt.PointingHandCursor) # 鼠標(biāo)變成手指形狀
self.sound_piece = QSound("sound/luozi.wav") # 加載落子音效
self.sound_win = QSound("sound/win.wav") # 加載勝利音效
self.sound_defeated = QSound("sound/defeated.wav") # 加載失敗音效
self.resize(WIDTH, HEIGHT) # 固定大小 540*540
self.setMinimumSize(QtCore.QSize(WIDTH, HEIGHT))
self.setMaximumSize(QtCore.QSize(WIDTH, HEIGHT))
self.setWindowTitle("GoBang") # 窗口名稱
self.setWindowIcon(QIcon('img/black.png')) # 窗口圖標(biāo)
# self.lb1 = QLabel(' ', self)
# self.lb1.move(20, 10)
self.black = QPixmap('img/black.png')
self.white = QPixmap('img/white.png')
self.piece_now = BLACK # 黑棋先行
self.my_turn = True # 玩家先行
self.step = 0 # 步數(shù)
self.x, self.y = 1000, 1000
self.mouse_point = LaBel(self) # 將鼠標(biāo)圖片改為棋子
self.mouse_point.setScaledContents(True)
self.mouse_point.setPixmap(self.black) #加載黑棋
self.mouse_point.setGeometry(270, 270, PIECE, PIECE)
self.pieces = [LaBel(self) for i in range(225)] # 新建棋子標(biāo)簽,準(zhǔn)備在棋盤上繪制棋子
for piece in self.pieces:
piece.setVisible(True) # 圖片可視
piece.setScaledContents(True) #圖片大小根據(jù)標(biāo)簽大小可變
self.mouse_point.raise_() # 鼠標(biāo)始終在最上層
self.ai_down = True # AI已下棋,主要是為了加鎖,當(dāng)值是False的時(shí)候說明AI正在思考,這時(shí)候玩家鼠標(biāo)點(diǎn)擊失效,要忽略掉 mousePressEvent
self.setMouseTracking(True)
self.show()
def paintEvent(self, event): # 畫出指示箭頭
qp = QPainter()
qp.begin(self)
self.drawLines(qp)
qp.end()
def mouseMoveEvent(self, e): # 黑色棋子隨鼠標(biāo)移動(dòng)
# self.lb1.setText(str(e.x()) + ' ' + str(e.y()))
self.mouse_point.move(e.x() - 16, e.y() - 16)
def mousePressEvent(self, e): # 玩家下棋
if e.button() == Qt.LeftButton and self.ai_down == True:
x, y = e.x(), e.y() # 鼠標(biāo)坐標(biāo)
i, j = self.coordinate_transform_pixel2map(x, y) # 對(duì)應(yīng)棋盤坐標(biāo)
if not i is None and not j is None: # 棋子落在棋盤上,排除邊緣
if self.chessboard.get_xy_on_logic_state(i, j) == EMPTY: # 棋子落在空白處
self.draw(i, j)
self.ai_down = False
board = self.chessboard.board()
self.AI = AI(board) # 新建線程對(duì)象,傳入棋盤參數(shù)
self.AI.finishSignal.connect(self.AI_draw) # 結(jié)束線程,傳出參數(shù)
self.AI.start() # run
def AI_draw(self, i, j):
if self.step != 0:
self.draw(i, j) # AI
self.x, self.y = self.coordinate_transform_map2pixel(i, j)
self.ai_down = True
self.update()
def draw(self, i, j):
x, y = self.coordinate_transform_map2pixel(i, j)
if self.piece_now == BLACK:
self.pieces[self.step].setPixmap(self.black) # 放置黑色棋子
self.piece_now = WHITE
self.chessboard.draw_xy(i, j, BLACK)
else:
self.pieces[self.step].setPixmap(self.white) # 放置白色棋子
self.piece_now = BLACK
self.chessboard.draw_xy(i, j, WHITE)
self.pieces[self.step].setGeometry(x, y, PIECE, PIECE) # 畫出棋子
self.sound_piece.play() # 落子音效
self.step += 1 # 步數(shù)+1
winner = self.chessboard.anyone_win(i, j) # 判斷輸贏
if winner != EMPTY:
self.mouse_point.clear()
self.gameover(winner)
def drawLines(self, qp): # 指示AI當(dāng)前下的棋子
if self.step != 0:
pen = QtGui.QPen(QtCore.Qt.black, 2, QtCore.Qt.SolidLine)
qp.setPen(pen)
qp.drawLine(self.x - 5, self.y - 5, self.x + 3, self.y + 3)
qp.drawLine(self.x + 3, self.y, self.x + 3, self.y + 3)
qp.drawLine(self.x, self.y + 3, self.x + 3, self.y + 3)
def coordinate_transform_map2pixel(self, i, j):
# 從 chessMap 里的邏輯坐標(biāo)到 UI 上的繪制坐標(biāo)的轉(zhuǎn)換
return MARGIN + j * GRID - PIECE / 2, MARGIN + i * GRID - PIECE / 2
def coordinate_transform_pixel2map(self, x, y):
# 從 UI 上的繪制坐標(biāo)到 chessMap 里的邏輯坐標(biāo)的轉(zhuǎn)換
i, j = int(round((y - MARGIN) / GRID)), int(round((x - MARGIN) / GRID))
# 有MAGIN, 排除邊緣位置導(dǎo)致 i,j 越界
if i < 0 or i >= 15 or j < 0 or j >= 15:
return None, None
else:
return i, j
def gameover(self, winner):
if winner == BLACK:
self.sound_win.play()
reply = QMessageBox.question(self, 'You Win!', 'Continue?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
else:
self.sound_defeated.play()
reply = QMessageBox.question(self, 'You Lost!', 'Continue?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes: # 復(fù)位
self.piece_now = BLACK
self.mouse_point.setPixmap(self.black)
self.step = 0
for piece in self.pieces:
piece.clear()
self.chessboard.reset()
self.update()
else:
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = GoBang()
sys.exit(app.exec_())
簡要說明一下
class AI(QtCore.QThread): finishSignal = QtCore.pyqtSignal(int, int) # 構(gòu)造函數(shù)里增加形參 def __init__(self, board, parent=None): super(AI, self).__init__(parent) self.board = board # 重寫 run() 函數(shù) def run(self): self.ai = searcher() self.ai.board = self.board score, x, y = self.ai.search(2, 2) self.finishSignal.emit(x, y)
這里加了一個(gè)線程執(zhí)行AI的計(jì)算,前面有個(gè) from ai import searcher ,ai還沒有寫,先從網(wǎng)上找了一個(gè)博弈的算法。searcher()就是AI類。該線程傳入?yún)?shù)是 board 就是棋盤狀態(tài)。調(diào)用self.ai.search(2, 2),第一個(gè)2是博弈樹的深度,值越大AI越聰明,但是計(jì)算時(shí)間也越長。第二個(gè)2是說電腦執(zhí)白棋,如果為1則是黑棋。線程結(jié)束后傳入?yún)?shù) x, y 就是AI計(jì)算后線程傳出的參數(shù)。
class LaBel(QLabel): def __init__(self, parent): super().__init__(parent) self.setMouseTracking(True) def enterEvent(self, e): e.ignore()
重新定義Label類是為了讓黑棋圖片隨著鼠標(biāo)的移動(dòng)而移動(dòng)。如果直接用QLabel的話不能達(dá)到預(yù)期的效果,具體為什么自己去摸索吧。
最后是所有的腳本代碼,在這之后還會(huì)繼續(xù)學(xué)習(xí),將腳本打包成可執(zhí)行文件,并且加入神經(jīng)網(wǎng)絡(luò)的算法。
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Python使用__new__()方法為對(duì)象分配內(nèi)存及返回對(duì)象的引用示例
這篇文章主要介紹了Python使用__new__()方法為對(duì)象分配內(nèi)存及返回對(duì)象的引用,結(jié)合實(shí)例形式分析了Python對(duì)象初始化及內(nèi)存操作相關(guān)使用技巧,需要的朋友可以參考下2019-09-09
在MAC上搭建python數(shù)據(jù)分析開發(fā)環(huán)境
這篇文章主要介紹了在MAC上搭建python數(shù)據(jù)分析開發(fā)環(huán)境的相關(guān)資料,需要的朋友可以參考下2016-01-01
Python中reduce()函數(shù)的用法詳細(xì)解讀
這篇文章主要介紹了Python中reduce()函數(shù)的用法詳細(xì)解讀,reduce函數(shù)是通過函數(shù)對(duì)迭代器對(duì)象中的元素進(jìn)行遍歷操作,但需要注意的是?reduce?函數(shù)返回的是計(jì)算的結(jié)果,而?map/filter?返回的是作用后的迭代器對(duì)象,需要的朋友可以參考下2023-08-08
Python+Scipy實(shí)現(xiàn)自定義任意的概率分布
Scipy自帶了多種常見的分布,如正態(tài)分布、均勻分布、二項(xiàng)分布、多項(xiàng)分布、伽馬分布等等,還可以自定義任意的概率分布。本文將為大家介紹如何利用Scipy自定義任意的概率分布,感興趣的可以了解下2022-08-08
python實(shí)現(xiàn)字符串字母大小寫轉(zhuǎn)換的幾種方法
本文主要介紹了python實(shí)現(xiàn)字符串字母大小寫轉(zhuǎn)換的幾種方法,包括islower()、isupper()、istitle()、lower()、casefold()、upper()、capitalize()、title()和swapcase(),具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03
Pycharm 如何連接遠(yuǎn)程服務(wù)器并debug調(diào)試
本文主要介紹了Pycharm 如何連接遠(yuǎn)程服務(wù)器并debug調(diào)試,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
pycharm不以pytest方式運(yùn)行,想要切換回普通模式運(yùn)行的操作
這篇文章主要介紹了pycharm不以pytest方式運(yùn)行,想要切換回普通模式運(yùn)行的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-09-09

