PyQt5实现五子棋游戏(人机对弈)


Posted in Python onMarch 24, 2020

这篇博客主要是为了学习Python和PyQt,因为对棋类游戏比较热衷,所以从规则较简单的五子棋入手,利用PyQt5实现图形界面,做一个可以进行人机对弈的脚本,最后打包成应用程序。AI的算法打算用神经网络来完成,正在苦学TensorFlow中。

本来我以为五子棋规则很简单,不就像小学时候玩的那样,五个棋子连在一起就赢了嘛,但是后来发现事情并没有那么简单,现在的五子棋有禁手这个规则 ,“三三禁手” 、“四四禁手”、“长连禁手”等等,都是为了限制现行一方必胜。我也不是职业的棋手,对吧,所以禁手什么的就不考虑了,弄个简单的成品出来就很满足了。

代码全是边学习边写的,有瑕疵的地方欢迎提出。

第一步,收集素材

主要就是棋子、棋盘的图片,还有下棋的音效

PyQt5实现五子棋游戏(人机对弈)

PyQt5实现五子棋游戏(人机对弈)

PyQt5实现五子棋游戏(人机对弈)

音效与代码一起在最后给出

第二步,五子棋的逻辑类

收集完素材后,不着急界面的编写,先将五子棋的逻辑写好,界面和逻辑要分开,这很重要。

先想想在五子棋的逻辑类里要有哪些东西。

首先是棋盘,棋盘用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的话不能达到预期的效果,具体为什么自己去摸索吧。

最后是所有的脚本代码,在这之后还会继续学习,将脚本打包成可执行文件,并且加入神经网络的算法。

基于PyQt5的五子棋编程(人机对弈)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python计算N天之后日期的方法
Mar 31 Python
Python实现螺旋矩阵的填充算法示例
Dec 28 Python
Python切片操作深入详解
Jul 27 Python
Python3最长回文子串算法示例
Mar 04 Python
Python Matplotlib实现三维数据的散点图绘制
Mar 19 Python
使用python实现mqtt的发布和订阅
May 05 Python
Pycharm使用之设置代码字体大小和颜色主题的教程
Jul 12 Python
Python实现ElGamal加密算法的示例代码
Jun 19 Python
django rest framework 自定义返回方式
Jul 12 Python
python opencv pytesseract 验证码识别的实现
Aug 28 Python
使用pandas读取表格数据并进行单行数据拼接的详细教程
Mar 03 Python
pandas时间序列之pd.to_datetime()的实现
Jun 16 Python
Python制作exe文件简单流程
Jan 24 #Python
PyQt5实现类似别踩白块游戏
Jan 24 #Python
实例讲解Python脚本成为Windows中运行的exe文件
Jan 24 #Python
python随机在一张图像上截取任意大小图片的方法
Jan 24 #Python
Python爬虫实战之12306抢票开源
Jan 24 #Python
python+pyqt5实现24点小游戏
Jan 24 #Python
python中实现控制小数点位数的方法
Jan 24 #Python
You might like
php文档更新介绍
2011/07/22 PHP
自己写的php中文截取函数mb_strlen和mb_substr
2015/02/09 PHP
php中return的用法实例分析
2015/02/28 PHP
php获取汉字拼音首字母的方法
2015/10/21 PHP
PHP中STDCLASS用法实例分析
2016/11/11 PHP
filemanage功能中用到的lib.js
2007/04/08 Javascript
精通JavaScript 纠正 cleanWhitespace函数
2010/03/11 Javascript
js编码之encodeURIComponent使用介绍(asp,php)
2012/03/01 Javascript
JQuery 操作/获取table具体代码
2013/06/13 Javascript
JQuery触发事件例如click
2013/09/11 Javascript
DOM基础教程之使用DOM + Css
2015/01/20 Javascript
JS的数组迭代方法
2015/02/05 Javascript
nodejs爬虫抓取数据之编码问题
2015/07/03 NodeJs
JavaScript编程中的Promise使用大全
2015/07/28 Javascript
解析Node.js基于模块和包的代码部署方式
2016/02/16 Javascript
js事件源window.event.srcElement兼容性写法(详解)
2016/11/25 Javascript
BootStrap栅格系统、表单样式与按钮样式源码解析
2017/01/20 Javascript
angular-cli修改端口号【angular2】
2017/04/19 Javascript
JavaScript队列函数和异步执行详解
2017/06/19 Javascript
使用MUI框架模拟手机端的下拉刷新和上拉加载功能
2017/09/04 Javascript
JS document form表单元素操作完整示例
2020/01/13 Javascript
JavaScript数组去重实现方法小结
2020/01/17 Javascript
iview实现图片上传功能
2020/06/29 Javascript
vue组件实现移动端九宫格转盘抽奖
2020/10/16 Javascript
python利用hook技术破解https的实例代码
2013/03/25 Python
Python 输出时去掉列表元组外面的方括号与圆括号的方法
2018/12/24 Python
使用python执行shell脚本 并动态传参 及subprocess的使用详解
2020/03/06 Python
html5构建触屏网站之网站尺寸探讨
2013/01/07 HTML / CSS
Tostadora意大利:定制T恤
2019/04/08 全球购物
师说教学反思
2014/02/07 职场文书
数学教研活动总结
2014/07/02 职场文书
索赔员岗位职责
2015/02/15 职场文书
详解JavaScript中Arguments对象用途
2021/08/30 Javascript
golang中的struct操作
2021/11/11 Golang
为自由献出你的心脏!「进击的巨人展 FINAL」2022年6月在台开展
2022/04/13 日漫
SQL Server中搜索特定的对象
2022/05/25 SQL Server