https://mp.weixin.qq.com/s/79...git
每次都寫單機遊戲自嗨好像沒啥意思,此次咱們來寫個支持聯機對戰的遊戲吧,省的有人在issue裏說:github
好吧,聯機和對手比賽輸了總不能怪我了吧算法
OK,跑題了,這明明是一個學習用的公衆號。由於我以前也沒寫過能夠聯機對戰的遊戲,因此先整個簡單的遊戲試試吧,支持局域網聯機對戰的五子棋小遊戲。廢話很少說,讓咱們愉快地開始吧~服務器
這裏簡單介紹下原理吧,代碼主要用PyQt5寫的,pygame只用來播放一些音效。首先,設計並實現個遊戲主界面:微信
代碼實現以下:網絡
'''遊戲開始界面''' class gameStartUI(QWidget): def __init__(self, parent=None, **kwargs): super(gameStartUI, self).__init__(parent) self.setFixedSize(760, 650) self.setWindowTitle('五子棋-微信公衆號: Charles的皮卡丘') 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的應該均可以寫出這樣的界面,沒啥特別的,記得把人機對戰和聯機對戰兩個按鈕觸發後的信號分別綁定到人機對戰和聯機對戰的函數上就行。併發
而後分別來實現人機對戰和聯機對戰就好了。這裏人機對戰的算法抄的公衆號以前發的那篇AI五子棋的文章裏用的算法,因此只要花點心思用PyQt5從新寫個遊戲界面就好了,效果大概是這樣的:app
主要的代碼實現以下:socket
'''人機對戰''' 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('五子棋-微信公衆號: Charles的皮卡丘') 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()
整個邏輯是這樣的:tcp
設計並實現遊戲的基本界面以後,先默認永遠是玩家先手(白子),電腦後手(黑子)。而後,當監聽到玩家鼠標左鍵點擊到棋盤網格所在的範圍內的時候,捕獲該位置,若該位置以前沒有人落子過,則玩家成功落子,不然從新等待玩家鼠標左鍵點擊事件。玩家成功落子後,判斷是否由於玩家落子而致使遊戲結束(即棋盤上有5顆同色子相連了),若遊戲結束,則顯示遊戲結束界面,不然輪到AI落子。AI落子和玩家落子的邏輯相似,而後又輪到玩家落子,以此類推。
須要注意的是:爲保證響應的實時性,AI落子算法應當寫到鼠標左鍵點擊後釋放事件的響應中(感興趣的小夥伴能夠試試寫到鼠標點擊事件的響應中,這樣會致使必須在AI計算結束並落子後,才能顯示玩家上一次的落子和AI這次的落子結果)。
開始按鈕就是重置遊戲,沒啥可說的,這裏爲了不有些人喜歡耍賴,我實現的時候代碼寫的是必須完成當前對弈才能重置遊戲(畢竟小夥子小姑娘們要學會有耐心地下完一盤棋呀)。
由於是和AI下,因此悔棋按鈕直接悔兩步,從歷史記錄列表裏pop最後兩次落子而後從棋盤對應位置取下這兩次落子就OK了,而且保證只有我方回合能夠悔棋以免出現意料以外的邏輯出錯。
認輸按鈕也沒啥可說的,就是認輸而後提早結束遊戲。
接下來咱們來實現一下聯機對戰,這裏咱們選擇使用TCP/IP協議進行聯機通訊從而實現聯機對戰。先啓動遊戲的一方做爲服務器端:
經過新開一個線程來實現監聽:
threading.Thread(target=self.startListen).start() '''開始監聽客戶端的鏈接''' def startListen(self): while True: self.setWindowTitle('五子棋-微信公衆號: Charles的皮卡丘 ——> 服務器端啓動成功, 等待客戶端鏈接中') self.tcp_socket, self.client_ipport = self.tcp_server.accept() self.setWindowTitle('五子棋-微信公衆號: Charles的皮卡丘 ——> 客戶端已鏈接, 點擊開始按鈕進行遊戲')
後啓動方做爲客戶端鏈接服務器端併發送客戶端玩家的基本信息:
self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.tcp_socket.connect(self.server_ipport) data = {'type': 'nickname', 'data': self.nickname} self.tcp_socket.sendall(packSocketData(data)) self.setWindowTitle('五子棋-微信公衆號: Charles的皮卡丘 ——> 已經成功鏈接服務器, 點擊開始按鈕進行遊戲')
當客戶端鏈接到服務器端時,服務器端也發送服務器端的玩家基本信息給客戶端:
data = {'type': 'nickname', 'data': self.nickname} self.tcp_socket.sendall(packSocketData(data))
而後客戶端和服務器端都利用新開的線程來實現網絡數據監聽接收:
'''接收客戶端數據''' def receiveClientData(self): while True: data = receiveAndReadSocketData(self.tcp_socket) self.receive_signal.emit(data) '''接收服務器端數據''' def receiveServerData(self): while True: data = 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 = {'type': 'reply', 'detail': 'startgame', 'data': True} self.tcp_socket.sendall(packSocketData(data)) self.is_gaming = True self.setWindowTitle('五子棋-微信公衆號: Charles的皮卡丘 ——> %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 = {'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 = {'type': 'reply', 'detail': 'regret', 'data': True} self.tcp_socket.sendall(packSocketData(data)) else: data = {'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('五子棋-微信公衆號: Charles的皮卡丘 ——> %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']
對戰過程實現的基本邏輯和人機對戰是一致的,只不過要考慮數據同步問題,因此看起來代碼略多了一些。固然對於聯機對戰,我也作了一些小修改,好比必須點擊開始按鈕,並通過對方贊成以後,才能正式開始對弈,悔棋按鈕只有在對方回合才能按,對方贊成悔棋後須要記得把落子方切換回本身。而後加了一個催促按鈕,一樣必須在對方回合才能按。其餘好像也沒什麼特別的改動了。
All done~完整源代碼詳見相關文件~