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

时间:2022-10-27 21:51:37

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

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

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

第一步,收集素材

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

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

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

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

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

第二步,五子棋的逻辑类

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

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

首先是棋盘,棋盘用15*15的数组表示
然后是棋子,黑棋用1表示,白棋用2表示,空白就用0表示
再然后还要获取指定点的坐标,获取指定点的方向等等。
最重要的也是稍微有点难度的部分就是判断输赢。结合网上的方法和我自己的理解,下面贴出我写的代码,仅供参考。

chessboard.py

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# ----------------------------------------------------------------------
# 定义棋子类型,输赢情况
# ----------------------------------------------------------------------
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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
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_())

简要说明一下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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计算后线程传出的参数。

?
1
2
3
4
5
6
7
class label(qlabel):
  def __init__(self, parent):
    super().__init__(parent)
    self.setmousetracking(true)
 
  def enterevent(self, e):
    e.ignore()

重新定义label类是为了让黑棋图片随着鼠标的移动而移动。如果直接用qlabel的话不能达到预期的效果,具体为什么自己去摸索吧。

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://blog.csdn.net/windowsyun/article/details/78877939