Hi,各位同学好呀,这里是L学长!
今天向大家分享一个今年(2022)最新完成的毕业设计项目作品
python小游戏毕设 五子棋小游戏设计与实现 (源码)
学长根据实现的难度和等级对项目进行评分(最低0分,满分5分)
难度系数:3分
工作量:3分
创新点:4分
基于python实现的支持局域网联机对战的五子棋小游戏。
游戏规则:
五子棋起源于中国,是全国智力运动会竞技项目之一,是一种两人对弈的纯策略型棋类游戏。双方分别使用黑白两色的棋子,下在棋盘直线与横线的交叉点上,先形成五子连珠者获胜。
Python版本:3.6.4
相关模块:
pygame模块;
以及一些Python自带的模块。
简介
Pygame是一系列专门为编写电子游戏而设计的Python模块(modules)。Pygame在已经非常优秀的SDL库的基础上增加了许多功能。这让你能够用Python语言编写出丰富多彩的游戏程序。
Pygame可移植性高,几乎能在任何平台和操作系统上运行。
Pygame已经被下载过数百万次。
Pygame免费开源。它在LGPL许可证(Lesser General Public License,GNU宽通用公共许可证)下发行。使用Pygame,你可以创造出免费开源,可共享,或者商业化的游戏。详情请见LGPL许可证。
优点
能够轻松使用多核CPU(multi core CPUs) :如今双核CPU很常用,8核CPU在桌面系统中也很便宜,而利用好多核系统,能让你在你的游戏中实现更多东西。特定的pygame函数能够释放令人生畏的python GIL(全局解释器锁),这几乎是你用C语言才能做的事。
核心函数用最优化的C语言或汇编语言编写:C语言代码通常比Python代码运行速度快10-20倍。而汇编语言编写的代码(assembly code)比Python甚至快到100多倍。
安装便捷:一般仅需包管理程序或二进制系统程序便能安装。
真正地可移植:支持Linux (主要发行版), Windows (95, 98, ME, 2000, XP, Vista, 64-bit Windows,), Windows CE, BeOS, MacOS, Mac OS X, FreeBSD, NetBSD, OpenBSD, BSD/OS, Solaris, IRIX, and QNX等操作系统.也能支持AmigaOS, Dreamcast, Atari, AIX, OSF/Tru64, RISC OS, SymbianOS and OS/2,但是还没有受到官方认可。你也可以在手持设备,游戏控制台, One Laptop Per Child (OLPC) computer项目的电脑等设备中使用pygame.
用法简单:无论是小孩子还是大人都能学会用pygame来制作射击类游戏。
很多Pygame游戏已发行:其中包括很多游戏大赛入围作品、非常受欢迎的开源可分享的游戏。
由你来控制主循环:由你来调用pygame的函数,pygame的函数并不需要调用你的函数。当你同时还在使用其他库来编写各种各种的程序时,这能够为你提供极大的掌控权。
不需要GUI就能使用所有函数:仅在命令行中,你就可以使用pygame的某些函数来处理图片,获取游戏杆输入,播放音乐……
对bug反应迅速:很多bug在被上报的1小时内就能被我们修复。虽然有时候我们确实会卡在某一个bug上很久,但大多数时候我们都是很不错的bug修复者。如今bug的上报已经很少了,因为许多bug早已被我们修复。
代码量少:pygame并没有数以万计的也许你永远用不到的冗杂代码。pygame的核心代码一直保持着简洁特点,其他附加物诸如GUI库等,都是在核心代码之外单独设计研发的。
模块化:你可以单独使用pygame的某个模块。想要换着使用一个别的声音处理库?没问题。pygame的很多核心模块支持独立初始化与使用。
最小开发框架
import pygame,sys #sys是python的标准库,提供Python运行时环境变量的操控
pygame.init() #内部各功能模块进行初始化创建及变量设置,默认调用
size = width,height = 800,600 #设置游戏窗口大小,分别是宽度和高度
screen = pygame.display.set_mode(size) #初始化显示窗口
pygame.display.set_caption("小游戏程序") #设置显示窗口的标题内容,是一个字符串类型
while True: #无限循环,直到Python运行时退出结束
for event in pygame.event.get(): #从Pygame的事件队列中取出事件,并从队列中删除该事件
if event.type == pygame.QUIT: #获得事件类型,并逐类响应
sys.exit() #用于退出结束游戏并退出
pygame.display.update() #对显示窗口进行更新,默认窗口全部重绘
代码执行流程
这里简单介绍下原理吧,代码主要用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重新写个游戏界面就行了,效果大概是这样的:
'''人机对战'''
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()
整个逻辑是这样的:
设计并实现游戏的基本界面之后,先默认永远是玩家先手(白子),电脑后手(黑子)。然后,当监听到玩家鼠标左键点击到棋盘网格所在的范围内的时候,捕获该位置,若该位置之前没有人落子过,则玩家成功落子,否则重新等待玩家鼠标左键点击事件。玩家成功落子后,判断是否因为玩家落子而导致游戏结束(即棋盘上有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))
当客户端连接到服务器端时,服务器端也发送服务器端的玩家基本信息给客户端:
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']
对战过程实现的基本逻辑和人机对战是一致的,只不过要考虑数据同步问题,所以看起来代码略多了一些。当然对于联机对战,我也做了一些小修改,比如必须点击开始按钮,并经过对方同意之后,才能正式开始对弈,悔棋按钮只有在对方回合才能按,对方同意悔棋后需要记得把落子方切换回自己。然后加了一个催促按钮,同样必须在对方回合才能按。其他好像也没什么特别的改动了。