Python+PyQt5实现五子棋游戏(人机博弈+深搜+α-β剪枝)

Python+PyQt5实现五子棋游戏(人机博弈+深搜+α-β剪枝)

一、问题描述

1、五子棋

五子棋是全国智力运动会竞技项目之一,是一种两人对弈的纯策略型棋类游戏。

五子棋的棋具与围棋通用,是一种传统的棋种,有两种玩法。

一种是双方分别使用黑白两色的棋子,下在棋盘直线与横线的交叉点上,先形成五子连线者获胜。还有一种是自己形成五子连线就替换对方任意一枚棋子。被替换的棋子可以和对方交换棋子。最后以先出完所有棋子的一方为胜。

2、什么是Agent?

Agent的概念——1977年Carl Hewitt “Viewing Control Structures as Patterns of Passing Messages”

Agent一词最早见于M.Minsky于1986年出版的《Society of Mind》

广义的Agent包括人类、物理世界的机器人和信息世界的软件机器人。

狭义的Agent专指信息世界中的软件机器人或称软件****Agent

Wooldrige 《Intelligent Agents: Theory and Practice》:

2.1弱定义

Agent用来最一般地说明一个软硬件系统,具有四个特性:

(1)自治性。在无人或其他系统的直接干预下可自主操作,并能控制其行为和内部状态。

(2)社会性。能够通过某种通信语言与其他Agent(也可能是人)进行交互。

(3)反应性。感知所处的环境,对环境的变化做出实时的反应,并可通过行为改变环境。

(4)能动性。不仅仅简单地对环境做出反应,而且可以主动地表现出目标驱动的行为。

2.2强定义

Agent除具备弱定义中所有特性外,还应具备一些人类才具有的特性,如知识、信念、义务、意图等。

3、实验内容

2、参照象棋游戏案例,设计一个五子棋人机游戏

(1)使用 python 编写

(2)不要求智能体的棋力

二、建立模型

1、游戏规则

1、对局双方各执一色棋子。

2、空棋盘开局。

3、黑先、白后,交替下子,每次只能下一子。

4、棋子下在棋盘的空白点上,棋子下定后,不得向其它点移动,不得从棋盘上拿掉或拿起另落别处。

5、黑方的第一枚棋子必须下在天元点上,即中心交叉点

2、游戏实现

在这里插入图片描述

2.1 玩家下棋
#玩家下棋
    def mousePressEvent(self, e):
        #判断棋局状态
        if self.status == 0:
            return
        x, y = e.x(), e.y()
        #print(x,y)
        if e.button() == Qt.LeftButton:
            #校验落在位置是否在棋盘内,棋盘范围(40,40)~(960,960)
            if x >= 40 and x <= 960 and y >= 40 and y<= 960:
                #确定棋子精确位置,x,y为将要放置棋子的坐标,a为该坐标的状态值,m,n是该棋子坐标在棋盘点矩阵chessboard_position中的行和列
                x,y,a,m,n = self.position(x,y)
                #如果未能确定棋子精确位置(x == -1),就不允许落子,如果该位置已有棋子(a!=0),也不允许落子
                if x == -1 or a != 0:
                    return
                #print(x, y)
                #画出棋子
                self.draw(x,y,1)
                # 更改棋盘点状态
                self.chessboard_position[m][n][2] = 1
                #判断输赢,根据结果决定是否继续棋局
                if self.judge(m,n):
                    # 锁定棋局状态,待ai下完后再放开
                    self.status = 0
                    self.label_7.setText("让我想一下......")
                    #ai下棋
                    self.ai.set_chessboard(self.chessboard_position)
                    self.ai.start()
            else:
                print("请在棋盘内落子!")
                return
2.2 AI下棋
2.2.1 AI线程
#ai线程
class AIThread(QThread):
    _signal = pyqtSignal(list)
    def __init__(self):
        super(AIThread, self).__init__()

    def set_chessboard(self,chessboard):
        self.chessboard = chessboard

    def run(self):
        ai = AI(self.chessboard)
        values = -100000000
        record = [-1, -1, 2]
        # 记录values最大的那步棋下的位置
        for i in range(15):
            for j in range(15):
                # 如果该点为空,假设下在该点,修改棋盘状态
                if self.chessboard[i][j][2] == 0:
                    # 如果该点周围米字方向上两格都为空,就跳过该点(缩小落子范围,跳过离棋盘上其他棋子较远的点)
                    if ai.judge_empty(i, j):
                        continue
                    self.chessboard[i][j][2] = 2
                    # 评估
                    evaluate = ai.ai(1,1,values)
                    # # 如果当前白子下法能完成五连,则将evaluate设一个较大的值
                    # if ai.judge(i, j):
                    #     evaluate = 10000000
                    #取评估值的最大值
                    if evaluate >= values:
                        values = evaluate
                        record = [i, j, 2]
                    # 回溯
                    self.chessboard[i][j][2] = 0
        #print("{}:{}".format(0, values))
        #print("剪枝次数:{}".format(ai.count))
        self._signal.emit(record)
2.2.2 AI推演
#color表示下棋的那一方,deep表示推演深度,pre_evaluate表示上一层的评估值
    def ai(self,color,deep,pre_evaluate):
        #递归边界
        if deep >= 2:
            temp = self.evaluateBoard(2,self.chessboard) - self.evaluateBoard(1,self.chessboard)
            #print("{}:{}".format(deep, temp))
            return temp
        #values初始值
        if color == 2:
            values = -100000000
        else:
            values = 100000000
        #记录values最大的那步棋下的位置
        for i in range(15):
            for j in range(15):

                #如果该点为空,假设下在该点,修改棋盘状态
                if self.chessboard[i][j][2] == 0:
                    #如果该点周围米字方向上两格都为空,就跳过该点
                    if self.judge_empty(i,j):
                        continue
                    self.chessboard[i][j][2] = color
                    #递归评估
                    evaluate = self.ai(3-color,deep+1,values)
                    if color == 2:
                        # 剪枝,如果当前的评估值比最小的pre_evaluate要大就跳过该情况,注意要回溯
                        if evaluate > pre_evaluate:
                            # 回溯
                            self.chessboard[i][j][2] = 0
                            self.count += 1
                            return 100000000
                    else:
                        # 剪枝,如果当前的评估值比最大的pre_evaluate要小就跳过该分支,注意要回溯
                        if evaluate < pre_evaluate:
                            # 回溯
                            self.chessboard[i][j][2] = 0
                            self.count += 1
                            return -100000000
                    #如果是白子回合,应当取评估值的最大值
                    if color == 2:
                        # #如果当前白子下法能完成五连,则将evaluate设一个较大的值
                        # if self.judge(i,j):
                        #     evaluate = 10000000
                        if evaluate >= values:
                            values = evaluate
                    # 如果是黑子回合,应当取评估值的最小值
                    else:
                        if evaluate <= values:
                            values = evaluate
                    #回溯
                    self.chessboard[i][j][2] = 0
        #print("{}:{}".format(deep,values))
        return values
2.5 画出棋子
#放置棋子
    def draw(self,i,j,a):
        if a == 1:
            self.pieces[self.step].setPixmap(self.black)  # 放置黑色棋子
        elif a == 2:
            self.pieces[self.step].setPixmap(self.white)  # 放置白色棋子
        self.pieces[self.step].setGeometry(i,j,64,64) # 设置位置,大小
        self.step += 1
        self.label_2.setText("步数:{}".format(self.step))
2.4 评估函数
# 这个函数是评价当前棋盘上仅考虑某一种颜色的得分
    # 想要得到考虑双方棋子的得分,就是自己得分减去对方得分即可evaluateBoard(1) - evaluateBoard(2)
    # color 1-black 2-white
    def evaluateBoard(self,color,chessboard):
        values = 0
        directions = [(-1, 0), (1, 0), (-1, 1), (1, -1), (0, 1), (0, -1), (1, 1), (-1, -1)]
        directions_2 = [(1, 0), (1, -1), (0, 1), (1, 1)]
        for row in range(self.size):
            for col in range(self.size):
                #如果当前棋子不是color对应的棋子就跳过
                if chessboard[row][col][2] != color:
                    continue
                #五个棋子,每一个都会被计算一次,所以如果出现五连,那么最后的values相当于加上5*200000
                j = 0
                while j < len(directions):
                    count = 1
                    a = 0
                    #记录中止原因
                    record = []
                    # 循环两次,分别判断两个相对的方向
                    while a <= 1:
                        x, y = row, col
                        a += 1
                        for i in range(4):
                            if x + directions[j][0] < 0 or x + directions[j][0] > self.size - 1 or y + directions[j][1] < 0 or y + directions[j][1] > self.size - 1:
                                #超过边界相当于被另一棋子堵住
                                record.append(3-color)
                                break
                            x += directions[j][0]
                            y += directions[j][1]
                            if chessboard[x][y][2] == chessboard[row][col][2]:
                                count += 1
                            else:
                                # 若当前方向上出现另一颜色棋子或者没有棋子,中止该方向的判断
                                #记录该次中止原因
                                record.append(chessboard[x][y][2])
                                break
                        j += 1
                    # 如果在米子方向上有连续5个子,则
                    # values += 200000;
                    if count >= 5:
                        values += 200000
                    elif count == 4:
                        #print("4中止原因:{}".format(record))
                        # 如果有连续4个子并且两边都没有堵住,则
                        # values += 70000;
                        if record[0] == record[1] == 0:
                            values += 70000
                        # 如果同一个方向有连续4个子并且仅有一边被堵住,则
                        # values += 4000;
                        elif (record[0] == 0 and record[1] == (3-color)) or (record[0] == (3-color) and record[1] == 0):
                            values += 1000
                    elif count == 3:
                        #print("3中止原因:{}".format(record))
                        # 如果是“活三”的情况,则values += 3000
                        if record[0] == record[1] == 0:
                            values += 1000
                        # 如果是“活三”被堵住了一边,则values += 500;
                        elif (record[0] == 0 and record[1] == (3-color)) or (record[0] == (3-color) and record[1] == 0):
                            values += 150
                    elif count == 2:
                        #print("2中止原因:{}".format(record))
                        #如果连续两个子且两边没有被堵住,values += 2000;
                        if record[0] == record[1] == 0:
                            values += 1000
                        # 如果连续两个子被堵住一边,values += 300;
                        elif (record[0] == 0 and record[1] == (3-color)) or (record[0] == (3-color) and record[1]== 0):
                            values += 150
                    # 如果是 ** * 0 * 的情况,则values += 3000;
                    # 如果是 ** 0 ** 的情况,则values += 2600;

                    k = 0
                    while k < len(directions_2):
                        x,y = row,col
                        record = []
                        record.append(chessboard[x][y][2])
                        #向下,右,左下,右下,四个方向搜索4个格子
                        for i in range(4):
                            # 搜索一个格子,如果白棋和黑棋相邻,values += 10确保白棋第一棋下在黑棋旁边
                            if i == 1 and len(record) == 2:
                                if record[0] != record[1] and record[0] and record[1]:
                                    values += 10
                            if x + directions_2[k][0] < 0 or x + directions_2[k][0] > self.size - 1 or y + directions_2[k][
                                1] < 0 or y + directions_2[k][1] > self.size - 1:
                                break
                            x += directions_2[k][0]
                            y += directions_2[k][1]
                            record.append(chessboard[x][y][2])
                        if len(record) == 5:
                            count = record.count(0)
                            # 如果是 *** 0 * 或* 0 * **的情况,则values += 3000;
                            # 即record中0的个数为1,且record[1]或record[3]是0,record.count(color) == 4
                            if (count == 1 and record[1] == 0 and record.count(color) == 4) or (count == 1 and record[3] == 0 and record.count(color) == 4):
                                values += 3000
                                #print("*** 0 * 或* 0 * **:{}".format(record))
                            # 如果是 ** 0 ** 的情况,则values += 2600;
                            if count == 1 and record[2] == 0 and record.count(color) == 4:
                                values += 2600
                                #print("** 0 **:{}".format(record))
                        k += 1
        return values
2.5 判断输赢
    #在每次落子后判断输赢,x,y为落子的棋盘点矩阵行列
    def judge(self,m,n):
        #print(m,n)
        #print(self.chessboard_position[m][n])
        #判断chessboard_position[m][n]在米字方向上是否有五个连续的棋子
        directions = [(-1,0),(1,0),(-1,1),(1,-1),(0,1),(0,-1),(1,1),(-1,-1)]
        #上下方向,统计向上和向下连续的棋子个数,大于等于五则胜利
        j = 0
        while j < len(directions):
            count = 1
            a = 0
            #循环两次,分别判断两个相对的方向
            while a <= 1:
                x, y = m, n
                a += 1
                for i in range(4):
                    if x + directions[j][0] < 0 or x + directions[j][0] > self.size - 1 or y + directions[j][1] < 0 or y + directions[j][1] > self.size - 1:
                        break
                    x += directions[j][0]
                    y += directions[j][1]
                    if self.chessboard_position[x][y][2] == self.chessboard_position[m][n][2]:
                        count += 1
                    else:
                        #若当前方向上出现另一颜色棋子,中止该方向的判断
                        break
                j += 1

            if count >= 5:
                result_label = LaBel(self)
                result_label.setVisible(True)  # 图片可视
                result_label.setScaledContents(True)  # 图片大小根据标签大小可变
                if self.chessboard_position[m][n][2] == 1:
                    print("黑子胜出")
                    #计时停止
                    self.game_time.set_status(0)
                    #设置棋局状态为0
                    self.status = 0
                    #显示相关信息
                    #self.winner = LaBel(self)
                    win = QPixmap('./designer/image/win-removebg-preview.png')
                    result_label.setPixmap(win)
                    result_label.setGeometry(140, 232, 700, 150)
                    #结束棋局
                    return 0
                else:
                    print("白子胜出")
                    # 计时停止
                    self.game_time.set_status(0)
                    # 设置棋局状态为0
                    self.status = 0
                    win = QPixmap('./designer/image/lost-removebg-preview.png')
                    result_label.setPixmap(win)
                    result_label.setGeometry(140, 232, 700, 150)
                    # 结束棋局
                    return 0
        #继续棋局
        return 1

三、模型实现

1、图形界面

QQ截图20211201132728

2、完整代码:

2.1 Github

该项目使用Pycharm 2021.2.3 + Python3.8编写

完整项目已上传github:https://github.com/sgsx11/Gobang

2.2 start.py

carbon (6)

2.3 AI.py

carbon (7)

2.4 gobang_ui.py

carbon (8)

四、总结

该五子棋游戏棋盘大小n = 15*15=255,假设搜索深度为d,使用深度优先搜索进行推演的时间复杂度为
O ( n d ) O(n^d) O(nd)
在未使用剪枝前,当d = 2时,ai思考的时间为9到10s,使用剪枝后时间缩短到3到4s,当d = 3时,ai思考的时间为3~4分钟,d = 4时,ai思考时间已经不能忍受,使得游戏体验大大下降

在后续可以通过启发式搜索来优化ai,或者改用人工神经网络来训练ai

五、参考文章

五子棋AI算法(一)https://blog.csdn.net/qq_44732921/article/details/102620510

五子棋AI算法(二)https://blog.csdn.net/qq_44732921/article/details/102648408

五子棋AI算法(三)https://blog.csdn.net/qq_44732921/article/details/104068832

PyQt5实现五子棋游戏(人机对弈)https://www.jb51.net/article/155340.htm

猜你喜欢

转载自blog.csdn.net/sgsx11/article/details/121655234