PyQt5实现五子棋游戏(人机对弈)
这篇博客主要是为了学习python和pyqt,因为对棋类游戏比较热衷,所以从规则较简单的五子棋入手,利用pyqt5实现图形界面,做一个可以进行人机对弈的脚本,最后打包成应用程序。ai的算法打算用神经网络来完成,正在苦学tensorflow中。
本来我以为五子棋规则很简单,不就像小学时候玩的那样,五个棋子连在一起就赢了嘛,但是后来发现事情并没有那么简单,现在的五子棋有禁手这个规则 ,“三三禁手” 、“四四禁手”、“长连禁手”等等,都是为了限制现行一方必胜。我也不是职业的棋手,对吧,所以禁手什么的就不考虑了,弄个简单的成品出来就很满足了。
代码全是边学习边写的,有瑕疵的地方欢迎提出。
第一步,收集素材
主要就是棋子、棋盘的图片,还有下棋的音效
音效与代码一起在最后给出
第二步,五子棋的逻辑类
收集完素材后,不着急界面的编写,先将五子棋的逻辑写好,界面和逻辑要分开,这很重要。
先想想在五子棋的逻辑类里要有哪些东西。
首先是棋盘,棋盘用15*15的数组表示
然后是棋子,黑棋用1表示,白棋用2表示,空白就用0表示
再然后还要获取指定点的坐标,获取指定点的方向等等。
最重要的也是稍微有点难度的部分就是判断输赢。结合网上的方法和我自己的理解,下面贴出我写的代码,仅供参考。
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): # 返回数组对象 return self.__board def draw_xy(self, x, y, state): # 获取落子点坐标的状态 self.__board[x][y] = state def get_xy_on_logic_state(self, x, y): # 获取指定点坐标的状态 return self.__board[x][y] def get_next_xy(self, point, direction): # 获取指定点的指定方向的坐标 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): # 获取指定点的指定方向的状态 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) # 当前落下的棋是黑棋还是白棋,它的状态存储在state中 for directions in self.__dir: # 对米字的4个方向分别检测是否有5子相连的棋 count = 1 # 初始记录为1,因为刚落下的棋也算 for direction in directions: # 对落下的棋子的同一条线的两侧都要检测,结果累积 point = (x, y) # 每次循环前都要刷新 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实现图形界面
先想好思路。
1.目标是做一个简易的五子棋的界面,主窗口只需要一个widget就可以了
2.widget的背景设置为棋盘图片
3.鼠标每点击一次空白区域,该区域就添加一个标签,在标签中插入棋子图片
4.因为是人机对弈,玩家执黑棋,所以可以将鼠标变成黑棋图片(这一点比较复杂,需要重写标签类)
5.整体逻辑是:鼠标点击一次—->换算坐标(ui坐标到棋盘坐标)—->判断坐标是否合理—->黑棋落在棋盘上—->判断是否赢棋—->电脑思考—->电脑下白棋—->判断是否赢棋……
6.因为ai思考需要时间,所以还需要加一个线程,单独让它计算ai的走法
7.一些细节问题: 赢棋和输棋怎么处理(对话框)、和棋怎么办(这个先不考虑)、游戏后期棋子非常多的时候容易眼花,不知道ai走到哪怎么办(加一个指示箭头)、音效怎么插入(用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 # ---------------------------------------------------------------------- # 定义线程类执行ai的算法 # ---------------------------------------------------------------------- class ai(qtcore.qthread): finishsignal = qtcore.pyqtsignal(int, int) # 构造函数里增加形参 def __init__(self, board, parent=none): super(ai, self).__init__(parent) self.board = board # 重写 run() 函数 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() # 设置棋盘背景 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) # 鼠标变成手指形状 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')) # 窗口图标 # 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 # 步数 self.x, self.y = 1000, 1000 self.mouse_point = label(self) # 将鼠标图片改为棋子 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)] # 新建棋子标签,准备在棋盘上绘制棋子 for piece in self.pieces: piece.setvisible(true) # 图片可视 piece.setscaledcontents(true) #图片大小根据标签大小可变 self.mouse_point.raise_() # 鼠标始终在最上层 self.ai_down = true # ai已下棋,主要是为了加锁,当值是false的时候说明ai正在思考,这时候玩家鼠标点击失效,要忽略掉 mousepressevent self.setmousetracking(true) self.show() def paintevent(self, event): # 画出指示箭头 qp = qpainter() qp.begin(self) self.drawlines(qp) qp.end() def mousemoveevent(self, e): # 黑色棋子随鼠标移动 # 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() # 鼠标坐标 i, j = self.coordinate_transform_pixel2map(x, y) # 对应棋盘坐标 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) # 新建线程对象,传入棋盘参数 self.ai.finishsignal.connect(self.ai_draw) # 结束线程,传出参数 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 # 步数+1 winner = self.chessboard.anyone_win(i, j) # 判断输赢 if winner != empty: self.mouse_point.clear() self.gameover(winner) def drawlines(self, qp): # 指示ai当前下的棋子 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 里的逻辑坐标到 ui 上的绘制坐标的转换 return margin + j * grid - piece / 2, margin + i * grid - piece / 2 def coordinate_transform_pixel2map(self, x, y): # 从 ui 上的绘制坐标到 chessmap 里的逻辑坐标的转换 i, j = int(round((y - margin) / grid)), int(round((x - margin) / grid)) # 有magin, 排除边缘位置导致 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: # 复位 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) # 构造函数里增加形参 def __init__(self, board, parent=none): super(ai, self).__init__(parent) self.board = board # 重写 run() 函数 def run(self): self.ai = searcher() self.ai.board = self.board score, x, y = self.ai.search(2, 2) self.finishsignal.emit(x, y)
这里加了一个线程执行ai的计算,前面有个 from ai import searcher ,ai还没有写,先从网上找了一个博弈的算法。searcher()就是ai类。该线程传入参数是 board 就是棋盘状态。调用self.ai.search(2, 2),第一个2是博弈树的深度,值越大ai越聪明,但是计算时间也越长。第二个2是说电脑执白棋,如果为1则是黑棋。线程结束后传入参数 x, y 就是ai计算后线程传出的参数。
class label(qlabel): def __init__(self, parent): super().__init__(parent) self.setmousetracking(true) def enterevent(self, e): e.ignore()
重新定义label类是为了让黑棋图片随着鼠标的移动而移动。如果直接用qlabel的话不能达到预期的效果,具体为什么自己去摸索吧。
最后是所有的脚本代码,在这之后还会继续学习,将脚本打包成可执行文件,并且加入神经网络的算法。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。