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~完整源代码详见相关文件~