前言
這次我們來寫個簡單支持聯機對戰的游戲,支持局域網聯機對戰的五子棋小游戲,廢話不多說,讓我們愉快地開始吧~
效果演示

開發工具
Python版本: 3.6.4
相關模塊:
pygame模塊;
PyQt5模塊;
以及一些Python自帶的模塊,
環境搭建
安裝Python并添加到環境變數,pip安裝需要的相關模塊即可,
原理簡介
這里簡單介紹下原理吧,代碼主要用PyQt5寫的,pygame只用來播放一些音效,首先,設計并實作個游戲主界面:

代碼實作如下:
'''游戲開始界面'''
class gameStartUI(QWidget):
def __init__(self, parent=None, **kwargs):
super(gameStartUI, self).__init__(parent)
self.setFixedSize(760, 650)
self.setWindowTitle('五子棋-???: ilove-python')
self.setWindowIcon(QIcon(cfg.ICON_FILEPATH))
# 背景圖片
palette = QPalette()
palette.setBrush(self.backgroundRole(), QBrush(QPixmap(cfg.BACKGROUND_IMAGEPATHS.get('bg_start'))))
self.setPalette(palette)
# 按鈕
# --人機對戰
self.ai_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('ai'), self)
self.ai_button.move(250, 200)
self.ai_button.show()
self.ai_button.click_signal.connect(self.playWithAI)
# --聯機對戰
self.online_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('online'), self)
self.online_button.move(250, 350)
self.online_button.show()
self.online_button.click_signal.connect(self.playOnline)
'''人機對戰'''
def playWithAI(self):
self.close()
self.gaming_ui = playWithAIUI(cfg)
self.gaming_ui.exit_signal.connect(lambda: sys.exit())
self.gaming_ui.back_signal.connect(self.show)
self.gaming_ui.show()
'''聯機對戰'''
def playOnline(self):
self.close()
self.gaming_ui = playOnlineUI(cfg, self)
self.gaming_ui.show()
會pyqt5的應該都可以寫出這樣的界面,沒啥特別的,記得把人機對戰和聯機對戰兩個按鈕觸發后的信號分別系結到人機對戰和聯機對戰的函式上就行,
效果大概是這樣的:

主要的代碼實作如下:
'''人機對戰'''
class playWithAIUI(QWidget):
back_signal = pyqtSignal()
exit_signal = pyqtSignal()
send_back_signal = False
def __init__(self, cfg, parent=None, **kwargs):
super(playWithAIUI, self).__init__(parent)
self.cfg = cfg
self.setFixedSize(760, 650)
self.setWindowTitle('五子棋-???: ilove-python')
self.setWindowIcon(QIcon(cfg.ICON_FILEPATH))
# 背景圖片
palette = QPalette()
palette.setBrush(self.backgroundRole(), QBrush(QPixmap(cfg.BACKGROUND_IMAGEPATHS.get('bg_game'))))
self.setPalette(palette)
# 按鈕
self.home_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('home'), self)
self.home_button.click_signal.connect(self.goHome)
self.home_button.move(680, 10)
self.startgame_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('startgame'), self)
self.startgame_button.click_signal.connect(self.startgame)
self.startgame_button.move(640, 240)
self.regret_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('regret'), self)
self.regret_button.click_signal.connect(self.regret)
self.regret_button.move(640, 310)
self.givein_button = PushButton(cfg.BUTTON_IMAGEPATHS.get('givein'), self)
self.givein_button.click_signal.connect(self.givein)
self.givein_button.move(640, 380)
# 落子標志
self.chessman_sign = QLabel(self)
sign = QPixmap(cfg.CHESSMAN_IMAGEPATHS.get('sign'))
self.chessman_sign.setPixmap(sign)
self.chessman_sign.setFixedSize(sign.size())
self.chessman_sign.show()
self.chessman_sign.hide()
# 棋盤(19*19矩陣)
self.chessboard = [[None for i in range(19)] for _ in range(19)]
# 歷史記錄(悔棋用)
self.history_record = []
# 是否在游戲中
self.is_gaming = True
# 勝利方
self.winner = None
self.winner_info_label = None
# 顏色分配and目前輪到誰落子
self.player_color = 'white'
self.ai_color = 'black'
self.whoseround = self.player_color
# 實體化ai
self.ai_player = aiGobang(self.ai_color, self.player_color)
# 落子聲音加載
pygame.mixer.init()
self.drop_sound = pygame.mixer.Sound(cfg.SOUNDS_PATHS.get('drop'))
'''滑鼠左鍵點擊事件-玩家回合'''
def mousePressEvent(self, event):
if (event.buttons() != QtCore.Qt.LeftButton) or (self.winner is not None) or (self.whoseround != self.player_color) or (not self.is_gaming):
return
# 保證只在棋盤范圍內回應
if event.x() >= 50 and event.x() <= 50 + 30 * 18 + 14 and event.y() >= 50 and event.y() <= 50 + 30 * 18 + 14:
pos = Pixel2Chesspos(event)
# 保證落子的地方本來沒有人落子
if self.chessboard[pos[0]][pos[1]]:
return
# 實體化一個棋子并顯示
c = Chessman(self.cfg.CHESSMAN_IMAGEPATHS.get(self.whoseround), self)
c.move(event.pos())
c.show()
self.chessboard[pos[0]][pos[1]] = c
# 落子聲音響起
self.drop_sound.play()
# 最后落子位置標志對落子位置進行跟隨
self.chessman_sign.show()
self.chessman_sign.move(c.pos())
self.chessman_sign.raise_()
# 記錄這次落子
self.history_record.append([*pos, self.whoseround])
# 是否勝利了
self.winner = checkWin(self.chessboard)
if self.winner:
self.showGameEndInfo()
return
# 切換回合方(其實就是改顏色)
self.nextRound()
'''滑鼠左鍵釋放操作-呼叫電腦回合'''
def mouseReleaseEvent(self, event):
if (self.winner is not None) or (self.whoseround != self.ai_color) or (not self.is_gaming):
return
self.aiAct()
'''電腦自動下-AI回合'''
def aiAct(self):
if (self.winner is not None) or (self.whoseround == self.player_color) or (not self.is_gaming):
return
next_pos = self.ai_player.act(self.history_record)
# 實體化一個棋子并顯示
c = Chessman(self.cfg.CHESSMAN_IMAGEPATHS.get(self.whoseround), self)
c.move(QPoint(*Chesspos2Pixel(next_pos)))
c.show()
self.chessboard[next_pos[0]][next_pos[1]] = c
# 落子聲音響起
self.drop_sound.play()
# 最后落子位置標志對落子位置進行跟隨
self.chessman_sign.show()
self.chessman_sign.move(c.pos())
self.chessman_sign.raise_()
# 記錄這次落子
self.history_record.append([*next_pos, self.whoseround])
# 是否勝利了
self.winner = checkWin(self.chessboard)
if self.winner:
self.showGameEndInfo()
return
# 切換回合方(其實就是改顏色)
self.nextRound()
'''改變落子方'''
def nextRound(self):
self.whoseround = self.player_color if self.whoseround == self.ai_color else self.ai_color
'''顯示游戲結束結果'''
def showGameEndInfo(self):
self.is_gaming = False
info_img = QPixmap(self.cfg.WIN_IMAGEPATHS.get(self.winner))
self.winner_info_label = QLabel(self)
self.winner_info_label.setPixmap(info_img)
self.winner_info_label.resize(info_img.size())
self.winner_info_label.move(50, 50)
self.winner_info_label.show()
'''認輸'''
def givein(self):
if self.is_gaming and (self.winner is None) and (self.whoseround == self.player_color):
self.winner = self.ai_color
self.showGameEndInfo()
'''悔棋-只有我方回合的時候可以悔棋'''
def regret(self):
if (self.winner is not None) or (len(self.history_record) == 0) or (not self.is_gaming) and (self.whoseround != self.player_color):
return
for _ in range(2):
pre_round = self.history_record.pop(-1)
self.chessboard[pre_round[0]][pre_round[1]].close()
self.chessboard[pre_round[0]][pre_round[1]] = None
self.chessman_sign.hide()
'''開始游戲-之前的對弈必須已經結束才行'''
def startgame(self):
if self.is_gaming:
return
self.is_gaming = True
self.whoseround = self.player_color
for i, j in product(range(19), range(19)):
if self.chessboard[i][j]:
self.chessboard[i][j].close()
self.chessboard[i][j] = None
self.winner = None
self.winner_info_label.close()
self.winner_info_label = None
self.history_record.clear()
self.chessman_sign.hide()
'''關閉視窗事件'''
def closeEvent(self, event):
if not self.send_back_signal:
self.exit_signal.emit()
'''回傳游戲主頁面'''
def goHome(self):
self.send_back_signal = True
self.close()
self.back_signal.emit()
整個邏輯是這樣的:
設計并實作游戲的基本界面之后,先默認永遠是玩家先手(白子),電腦后手(黑子),然后,當監聽到玩家滑鼠左鍵點擊到棋盤網格所在的范圍內的時候,捕獲該位置,若該位置之前沒有人落子過,則玩家成功落子,否則重新等待玩家滑鼠左鍵點擊事件,玩家成功落子后,判斷是否因為玩家落子而導致游戲結束(即棋盤上有5顆同色子相連了),若游戲結束,則顯示游戲結束界面,否則輪到AI落子,AI落子和玩家落子的邏輯類似,然后又輪到玩家落子,以此類推,
需要注意的是:為保證回應的實時性,AI落子演算法應當寫到滑鼠左鍵點擊后釋放事件的回應中(感興趣的小伙伴可以試試寫到滑鼠點擊事件的回應中,這樣會導致必須在AI計算結束并落子后,才能顯示玩家上一次的落子和AI此次的落子結果),
開始按鈕就是重置游戲,沒啥可說的,這里為了避免有些人喜歡耍賴,我實作的時候代碼寫的是必須完成當前對弈才能重置游戲,
因為是和AI下,所以悔棋按鈕直接悔兩步,從歷史記錄串列里pop最后兩次落子然后從棋盤對應位置取下這兩次落子就OK了,并且保證只有我方回合可以悔棋以避免出現意料之外的邏輯出錯,
認輸按鈕也沒啥可說的,就是認輸然后提前結束游戲,
接下來我們來實作一下聯機對戰,這里我們選擇使用TCP/IP協議進行聯機通信從而實作聯機對戰,先啟動游戲的一方作為服務器端:

通過新開一個執行緒來實作監聽:
threading.Thread(target=self.startListen).start()
'''開始監聽客戶端的連接'''
def startListen(self):
while True:
self.setWindowTitle('五子棋-???: ilove-python ——> 服務器端啟動成功, 等待客戶端連接中')
self.tcp_socket, self.client_ipport = self.tcp_server.accept()
self.setWindowTitle('五子棋-???: ilove-python ——> 客戶端已連接, 點擊開始按鈕進行游戲')
后啟動方作為客戶端連接服務器端并發送客戶端玩家的基本資訊:
self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.tcp_socket.connect(self.server_ipport)
data = https://www.cnblogs.com/daimubai/p/{'type': 'nickname', 'data': self.nickname}
self.tcp_socket.sendall(packSocketData(data))
self.setWindowTitle('五子棋-???: ilove-python ——> 已經成功連接服務器, 點擊開始按鈕進行游戲')
當客戶端連接到服務器端時,服務器端也發送服務器端的玩家基本資訊給客戶端:
data = https://www.cnblogs.com/daimubai/p/{'type': 'nickname', 'data': self.nickname}
self.tcp_socket.sendall(packSocketData(data))
然后客戶端和服務器端都利用新開的執行緒來實作網路資料監聽接收:
'''接收客戶端資料'''
def receiveClientData(self):
while True:
data = https://www.cnblogs.com/daimubai/p/receiveAndReadSocketData(self.tcp_socket)
self.receive_signal.emit(data)'''接收服務器端資料'''
def receiveServerData(self):
while True:
data = https://www.cnblogs.com/daimubai/p/receiveAndReadSocketData(self.tcp_socket)
self.receive_signal.emit(data)
并根據接收到的不同資料在主行程中做成對應的回應:
'''回應接收到的資料'''
def responseForReceiveData(self, data):
if data['type'] == 'action' and data['detail'] == 'exit':
QMessageBox.information(self, '提示', '您的對手已退出游戲, 游戲將自動回傳主界面')
self.goHome()
elif data['type'] == 'action' and data['detail'] == 'startgame':
self.opponent_player_color, self.player_color = data['data']
self.whoseround = 'white'
self.whoseround2nickname_dict = {self.player_color: self.nickname, self.opponent_player_color: self.opponent_nickname}
res = QMessageBox.information(self, '提示', '對方請求(重新)開始游戲, 您為%s, 您是否同意?' % {'white': '白子', 'black': '黑子'}.get(self.player_color), QMessageBox.Yes | QMessageBox.No)
if res == QMessageBox.Yes:
data = https://www.cnblogs.com/daimubai/p/{'type': 'reply', 'detail': 'startgame', 'data': True}
self.tcp_socket.sendall(packSocketData(data))
self.is_gaming = True
self.setWindowTitle('五子棋-???: ilove-python ——> %s走棋' % self.whoseround2nickname_dict.get(self.whoseround))
for i, j in product(range(19), range(19)):
if self.chessboard[i][j]:
self.chessboard[i][j].close()
self.chessboard[i][j] = None
self.history_record.clear()
self.winner = None
if self.winner_info_label:
self.winner_info_label.close()
self.winner_info_label = None
self.chessman_sign.hide()
else:
data = https://www.cnblogs.com/daimubai/p/{'type': 'reply', 'detail': 'startgame', 'data': False}
self.tcp_socket.sendall(packSocketData(data))
elif data['type'] == 'action' and data['detail'] == 'drop':
pos = data['data']
# 實體化一個棋子并顯示
c = Chessman(self.cfg.CHESSMAN_IMAGEPATHS.get(self.whoseround), self)
c.move(QPoint(*Chesspos2Pixel(pos)))
c.show()
self.chessboard[pos[0]][pos[1]] = c
# 落子聲音響起
self.drop_sound.play()
# 最后落子位置標志對落子位置進行跟隨
self.chessman_sign.show()
self.chessman_sign.move(c.pos())
self.chessman_sign.raise_()
# 記錄這次落子
self.history_record.append([*pos, self.whoseround])
# 是否勝利了
self.winner = checkWin(self.chessboard)
if self.winner:
self.showGameEndInfo()
return
# 切換回合方(其實就是改顏色)
self.nextRound()
elif data['type'] == 'action' and data['detail'] == 'givein':
self.winner = self.player_color
self.showGameEndInfo()
elif data['type'] == 'action' and data['detail'] == 'urge':
self.urge_sound.play()
elif data['type'] == 'action' and data['detail'] == 'regret':
res = QMessageBox.information(self, '提示', '對方請求悔棋, 您是否同意?', QMessageBox.Yes | QMessageBox.No)
if res == QMessageBox.Yes:
pre_round = self.history_record.pop(-1)
self.chessboard[pre_round[0]][pre_round[1]].close()
self.chessboard[pre_round[0]][pre_round[1]] = None
self.chessman_sign.hide()
self.nextRound()
data = https://www.cnblogs.com/daimubai/p/{'type': 'reply', 'detail': 'regret', 'data': True}
self.tcp_socket.sendall(packSocketData(data))
else:
data = https://www.cnblogs.com/daimubai/p/{'type': 'reply', 'detail': 'regret', 'data': False}
self.tcp_socket.sendall(packSocketData(data))
elif data['type'] == 'reply' and data['detail'] == 'startgame':
if data['data']:
self.is_gaming = True
self.setWindowTitle('五子棋-???: ilove-python ——> %s走棋' % self.whoseround2nickname_dict.get(self.whoseround))
for i, j in product(range(19), range(19)):
if self.chessboard[i][j]:
self.chessboard[i][j].close()
self.chessboard[i][j] = None
self.history_record.clear()
self.winner = None
if self.winner_info_label:
self.winner_info_label.close()
self.winner_info_label = None
self.chessman_sign.hide()
QMessageBox.information(self, '提示', '對方同意開始游戲請求, 您為%s, 執白者先行.' % {'white': '白子', 'black': '黑子'}.get(self.player_color))
else:
QMessageBox.information(self, '提示', '對方拒絕了您開始游戲的請求.')
elif data['type'] == 'reply' and data['detail'] == 'regret':
if data['data']:
pre_round = self.history_record.pop(-1)
self.chessboard[pre_round[0]][pre_round[1]].close()
self.chessboard[pre_round[0]][pre_round[1]] = None
self.nextRound()
QMessageBox.information(self, '提示', '對方同意了您的悔棋請求.')
else:
QMessageBox.information(self, '提示', '對方拒絕了您的悔棋請求.')
elif data['type'] == 'nickname':
self.opponent_nickname = data['data']
修改的地方
必須點擊開始按鈕,并經過對方同意之后,才能正式開始對弈,悔棋按鈕只有在對方回合才能按,對方同意悔棋后需要記得把落子方切換回自己,然后加了一個催促按鈕,同樣必須在對方回合才能按,以上就是全部代碼修改的全部地方了,
文章到這里就結束了,感謝你的觀看,Python24個小游戲系列,下篇文章分享2048小游戲
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/387701.html
標籤:Python
