【五子棋实战】第2章 博弈树负值极大alpha-beta剪枝算法

本文详细介绍了五子棋中使用的博弈树极大极小值alpha-beta剪枝算法,包括博弈树的概念、极大极小值搜索、负值极大法Negamax以及alpha-beta剪枝的原理和实现。通过回溯和递归,结合评分标准和剪枝策略,提高搜索效率,寻找最佳落子位置。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  市面上比较常用的五子棋算法是博弈树极大极小值alpha-beta剪枝算法,该算法可以分成四个部分来讲解,它们是环环相扣的:博弈树 - 极大极小值搜索 - 负值极大法 - alpha&beta剪枝


博弈树

  博弈树(Game Tree)是博弈论中的一个概念,用于表示博弈过程中的各种可能走法和对应的结果。它是树结构,树的每个节点表示游戏的一个状态,每个节点的子节点表示在该状态下可能的下一步行动。

由于是树结构,在代码实现上会使用递归,再细分一下可以说是回溯法traceback。

  在博弈树中,根节点表示游戏的初始状态,而叶子节点则表示游戏结束的状态。通过遍历博弈树,可以找到最佳的行动策略或评估游戏的结果。

  对于两人对弈的游戏,博弈树的每一层交替表示不同的玩家的行动。在五子棋游戏中,轮到某一方行动时,会生成一层子节点,每个子节点代表下一步的落子位置。对于每个落子位置,继续生成下一层子节点,以此类推,直到游戏结束。

但是通常不可能一直类推到游戏结束,走3步的计算时间就很长很长了。

  举个例子,假设有一个2*2的五子棋棋盘,黑子先行,下在左上角,如下图:

请添加图片描述

  那么博弈树就如上图构建的这样。根节点为当前棋局,第1步是白子下棋,还剩下3个空位可以下;第2步又轮到黑子下棋,还剩2个空位;如此循环下去……

  我们发现对于2x2的棋盘,博弈树一共有3x2x1种路径。对于一个正常的15x15的棋盘,博弈树会有224层224!种路径。那么如何能确定哪一种路径是最优的呢?这就用到了极大极小值思想。


极大极小值搜索Minimax

  极大极小值思想是一种在博弈和人工智能领域中常用的方法。在博弈中,可以使用极大极小值搜索算法(如Minimax算法)来确定最佳的下一步行动。算法通过递归地模拟所有可能的游戏状态和对手的反应,然后选择最有利于自己的行动。

  在执行极大极小值搜索算法之前,我们需要给每个状态的棋盘进行打分,然后才能用极大极小值搜索算法来比较哪个状态的棋盘是优的。

  为棋盘打分 - 评分标准

  首先需要一个评分标准。对于五子棋来说,即如果出现了"空白白空空"这样的棋型我们要算多少分,如果出现了"白黑黑黑黑空"这样的棋型我们要算多少分。项目中设计的评分标准如下,并不区分黑白子,1为有子,0为空子:

# 棋型的评估分数
shape_score = [
    (50, (0, 1, 1, 0, 0)),
    (50, (0, 0, 1, 1, 0)),
    (200, (1, 1, 0, 1, 0)),
    (500, (0, 0, 1, 1, 1)),
    (500, (1, 1, 1, 0, 0)),
    (5000, (0, 1, 1, 1, 0)),
    (5000, (0, 1, 0, 1, 1, 0)),
    (5000, (0, 1, 1, 0, 1, 0)),
    (5000, (1, 1, 1, 0, 1)),
    (5000, (1, 1, 0, 1, 1)),
    (5000, (1, 0, 1, 1, 1)),
    (5000, (1, 1, 1, 1, 0)),
    (5000, (0, 1, 1, 1, 1)),
    (500000, (0, 1, 1, 1, 1, 0)),
    (99999999, (1, 1, 1, 1, 1))
]

这个评分标准的设计直接影响算法的效果,非常重要。而且,这里的评分标准也不是最优的,只是效果还不错,建议读者自行增加、修改去试试。

  为棋盘打分 - 如何累计分数

  累计分数也是一个大学问。

  本项目的做法是,对于每一个落白子的点A,去遍历A的横、竖、左斜、右斜的四个方向上是否有白子的形状落入评分标准,有的话就记录下最大分值和对应的形状。如果这个点A在四个方向中的某个方向已经被记录过,那么就不在纳入计算了。最后从记录的数组中找一下有没有相交的点,有的话得分加倍。

  黑子也是这样计算。

  最终棋盘的分数 = 我方颜色棋子总得分 - 敌方棋子总得分 * 权重。

所以其实白子、黑子都有参与到计算中来。权重体现了电脑的进攻性,可配置。

  极大极小值思想

  极大极小值思想的目的是为了找到博弈树最优的一条路径。

请添加图片描述

  极大极小值思想考虑的是在对手也采取最优策略的情况下,我们能够保证的最小收益或最大损失。

  在上图中,假设最后一层分数为1、2、3、4、5、6,那么它们的上一层是我方,我方取对自己有利的(取max),所以第三层还是1、2、3、4、5、6,第二层是敌方,他会取对我方最不利的分数(取min),所以是1、3、5,第一层是我方,取max=5。

  总结一下,当前黑子下完了,白子落子搜索到了第二层节点值为5的那个节点的落子坐标。

因为是回溯,所以树肯定是从下往上看。


负值极大法Negamax

  对于敌方而言,取得是最小。但是如果也让敌方取最大,只是最后加一个负号,那么不也相当于取最小吗??而且这样会简化我们的代码,使得我们的代码变得更少更优雅。于是就产生了负值极大法。

其实是使代码像天书一样抽象难看懂。。。。

  负值极大法(Negamax)是一种基于极小化搜索树的博弈算法。它基于一个基本的观察:对于一个玩家而言,最好的策略与对手的最差策略是等价的。因此,通过将对手的收益取负值,问题可以转化为一个极大化搜索问题。

  这两种算法本质上是相似的,只是表达方式不同。在Negamax算法中,通过将对手的收益取负值,将问题转化为一个极大化搜索问题;而在Minimax算法中,通过交替最大化和最小化的方式,在博弈树中搜索最佳决策。

请添加图片描述

  AI算法的厉害之处还在于你会往下递归几步,15x15的棋盘可以递归224步,有224!种路径,意味着你要从第224层往第一层回溯,估计只有超算才行了。

  通常我们递归3层就224x223x222=1108,9344个叶子节点了。于是,聪明的人对其进行了优化, 这就是alpha-beta剪枝搜索。


alpha-beta剪枝

  Alpha-beta剪枝是一种用于优化极大极小搜索(Minimax Search)算法的技术,它可以有效地减少搜索的节点数,从而提高搜索效率。Alpha-beta剪枝算法在Minimax搜索的基础上引入了两个参数:alpha和beta。

  在Minimax搜索过程中,每个玩家都追求最大化自己的收益或最小化对手的收益。在搜索的过程中,当某个节点的值超出了alpha到beta的范围时,可以通过剪枝操作来减少对该节点以及其子节点的搜索。

  具体来说,当搜索到一个节点时,会先搜索其子节点,并递归地进行下去。在递归返回之后,根据当前玩家是最大化还是最小化玩家,对alpha和beta进行更新。如果alpha的值大于等于beta的值,那么当前节点的搜索可以提前结束,不再搜索其余子节点,因为它们不会影响当前玩家的最佳选择。

  通过不断更新alpha和beta的值,可以在搜索的过程中动态地减少搜索的节点数,从而大幅度提高搜索效率。Alpha-beta剪枝算法的关键在于正确地维护alpha和beta的值,并且按照正确的顺序遍历子节点,以确保剪枝操作的正确性。

请添加图片描述
  特别重要 - 剪枝过程

  如上图,α为已知的最大值, β为已知的最小值, 因为还没搜索不知道是多少,保险起见,初始化为-∞ 和+∞。

  1 => 首先,深度优先遍历,会遍历到第四层第一个节点值为1,它的父节点属于Max层,那么把α置为1,说明这个父节点只接收>=1的值。

  2 => 接着回溯到了第二层第一个节点,由于每返回一层它们的α、β值是互换的,所以此时这个节点说它只接收<=1的值。

  3 => 接着遍历到第四层第二个节点值为2,它的父节点属于Max层,那么把α置为2,说明这个父节点只接收>=2的值。此时问题来了,第三层第二个节点>=2,第二层第一个节点<=1,产生冲突,把第三层第二个节点为根的子树剪掉,因为第三层第二个节点>=2表示我这个子树肯定是>=2了,但是它的父节点只要<=1的,那还要你干嘛?

  4 => 那么回溯到根节点,根节点现在变成>=1的了。

  5 => 接着遍历到第四层第三个节点值为3,它的父节点属于Max层,那么把α置为3,说明这个父节点只接收>=3的值。

  6 => 此时回溯到了第二层第二个节点,由于每返回一层它们的α、β值是互换的,所以此时这个节点说它只接收<=3的值。

  7 => 接着遍历到第四层第四个节点值为2,它的父节点属于Max层,那么把α置为2,说明这个父节点只接收>=2的值。而它的父节点是>=3,所以不剪枝,同时回溯,覆盖父节点的范围,变成<=2。

  8 => 那么又回溯到根节点,根节点之前是>=1的,现在被覆盖,变为>=2。

  9 => ……其他同理……

因为我图画的简陋,所以看不出来剪枝的妙处。但是可以想象一下上图被剪枝的部分,如果它们节点有很多,那么除了左子树,它们的右子树都不会再需要遍历了。

  值得注意的是,Alpha-beta剪枝算法适用于满足最优化原则的博弈问题,即一个玩家的收益等于另一个玩家的损失。它能够在保证找到最优解决方案的前提下,大大减少搜索的节点数,提高搜索效率。然而,如果搜索树的分支因子很高,或者搜索深度很大,仍然可能需要相当大的计算资源来完成搜索。因此,在实际应用中,仍然需要结合启发式评估函数等其他优化技术来进一步提高效率。


博弈树负值极大alpha-beta剪枝算法代码实现

## 初始化输入、确定输出

  首先明确算法的输入、输出,这其实是一个试错的过程,可能要到整体项目做的差不多才能确定,我是已经做完了,所以就直接贴出来了。

def ai(player, ratio, length, board, depth):
    """
    AI入口
    :param player: 当前玩家. 初始化ai与human的棋子列表.
    :param ratio: 进攻系数.
    :param length: 棋盘边长.
    :param board: 棋盘值. 1表示白子,-1表示黑子,0表示空.
    :param depth: 搜索深度.
    :return: 落子位置x, 落子位置y, 搜索次数.
    """
    # 设置全局变量
    global list_ai, list_human, list_ai_add_human, list_all, x, y, search_count, DEP, LEN, RAT
    # 初始化
    DEP, LEN, RAT = depth, length, ratio
    list_ai, list_human, list_ai_add_human, list_all = init_list(player, board)
    # 设置alpha和beta的初始值
    alpha = -99999999
    beta = 99999999
    # 回溯搜索
    backtrack(True, depth, alpha, beta)
    list_ai.append((x, y))
    return x, y, search_count, is_win(list_ai)

  该函数被命名为ai,也就是电脑的意思,接受以下参数:

  player: 当前玩家,用于初始化 AI 和人类玩家的棋子列表。

因为传入的只有棋盘值,棋盘值有1和-1,那电脑到底应该把1作为自己的棋子,还是-1呢?这就需要用到参数player,如果player == 'black',表示电脑是黑子,那么电脑会把-1作为自己的棋子,1作为敌方棋子。

  ratio: 进攻系数,可能是用于调整 AI 对进攻和防守的权衡。

进攻系数就是上面说的权重。

  length: 棋盘的边长。
  board: 表示棋盘状态的二维数组,其中1表示白子,-1表示黑子,0表示空。
  depth: 搜索的深度,决定了 AI 进行决策时回溯的层数。

搜索的深度就是所谓的“能看几步”,我电脑目前看3步就要算个40来秒,着实不太行了。

  该函数返回落子的位置(x, y)、搜索次数和是否获胜。

  在函数内部,使用global关键字声明一些全局变量,包括list_ailist_humanlist_ai_add_humanlist_allxysearch_countDEPLENRAT。分别表示ai的棋子、敌方的棋子、ai的棋子+敌方的棋子,所有的位置坐标、计算出来的x坐标、计算出来的y坐标、算法搜索次数(主要是debug用)、搜索深度、棋盘长度、进攻系数。

这里做成全局变量是因为就不要传参了,不然每个函数的形参都要写一大堆,挺麻烦的。

  接下来,将传入的参数初始化到对应的全局变量中。将 depth 赋值给 DEPlength 赋值给 LENratio 赋值给 RAT。然后调用 init_list 函数,使用 playerboard 参数来初始化 list_ailist_humanlist_ai_add_humanlist_all 这些列表。

  设置初始的alphabeta值,用于进行Alpha-Beta剪枝。

  调用 backtrack 函数进行回溯搜索,传入参数 True 表示当前是 AI 的回合,搜索的深度为 depth,并使用 alphabeta 进行 Alpha-Beta 剪枝。搜索完成后,将得到的落子位置 (x, y) 添加到 list_ai 列表中,然后返回 (x, y)search_count(搜索次数)和调用 is_win 函数判断 list_ai 是否包含获胜的棋型。

## 开始回溯

def backtrack(is_me, depth, alpha, beta):
    """
    博弈树极大极小值alpha-beta剪枝搜索
    :param is_me: 当前是不是轮到自己下棋.
    :param depth: 回溯深度.
    :param alpha: 
    :param beta: 
    :return: 
    """
    # 约束条件:已经有一方获胜 或者 搜索深度为0
    if is_win(list_ai) or is_win(list_human) or depth == 0:
        return evaluation(is_me)

    # 生成当前局面下所有的候选步
    blank_list = list(set(list_all).difference(set(list_ai_add_human)))
    # 启发式搜索, 优先剪枝
    order(blank_list)  # 搜索顺序排序  提高剪枝效率

    # 遍历每一个候选步
    for next_step in blank_list:

        global search_count
        search_count += 1

        # 如果要评估的位置没有相邻的子, 则不去评估  减少计算
        if not has_neightnor(next_step):
            continue

        if is_me:
            list_ai.append(next_step)
        else:
            list_human.append(next_step)

        list_ai_add_human.append(next_step)

        value = -backtrack(not is_me, depth - 1, -beta, -alpha)

        if is_me:
            list_ai.remove(next_step)
        else:
            list_human.remove(next_step)

        list_ai_add_human.remove(next_step)

        if value > alpha:
            if depth == DEP:
                global x, y
                x = next_step[0]
                y = next_step[1]
            # alpha + beta剪枝点
            if value >= beta:
                return beta
            alpha = value
    return alpha

  上面的代码是用博弈树极大极小值算法和alpha-beta剪枝搜索实现五子棋最优点搜索的函数。

  函数名为backtrack,接受四个参数:

  is_me:表示当前轮到的是自己还是对手下棋,是一个布尔值。
  depth:回溯的深度,即搜索树的层数。
  alpha:alpha剪枝的初始值,表示极大值。
  beta:beta剪枝的初始值,表示极小值。

  函数的作用是在博弈树中进行搜索,并返回评估值。

  函数的执行流程如下:

  1. 约束条件检查:如果已经有一方获胜或者搜索深度为0,则返回当前局面的评估值,表示当前局面的好坏程度。

  2. 生成当前局面下所有的候选步:通过计算当前局面中空白位置和已下棋位置的差集,得到所有可下棋的位置。

  3. 启发式搜索:对候选步进行排序,以提高剪枝效率。还对周围没有棋子的点位进行搜索优化,没有的话就不去遍历它。

  4. 遍历每一个候选步:

    对于每个候选步,将其添加到当前下棋方的位置列表中。

    调用递归函数backtrack,以更新搜索树,并返回一个评估值。

    通过alpha-beta剪枝进行优化:

      如果当前轮到自己下棋(is_me为True),更新alpha值为评估值value和alpha的较大值。
      如果轮到对手下棋(is_me为False),更新beta值为评估值value和beta的较小值。
      如果评估值value大于alpha,则更新alpha为value
      如果depth等于最大搜索深度(DEP),记录当前位置xy
      如果评估值value大于等于beta,则进行剪枝,直接返回beta。

  5. 返回alpha作为当前局面的评估值。

## 判赢

  判赢函数有2个用法:一个是在回溯里面作为约束条件,还有一个是在得到计算出的落子之后,判断这样下棋有没有赢。

def is_win(L):
    """
    判断输赢
    :param L: 电脑或玩家的落子列表
    :return: True or False.
    """
    def check_five_in_a_row(start_row, start_col, row_delta, col_delta):
        for i in range(5):
            if (start_row + i * row_delta, start_col + i * col_delta) not in L:
                return False
        return True

    for m in range(LEN):
        for n in range(LEN):
            # 判断横向是否有五子连珠
            if n < LEN - 4 and check_five_in_a_row(m, n, 0, 1):
                return True
            # 判断纵向是否有五子连珠
            if m < LEN - 4 and check_five_in_a_row(m, n, 1, 0):
                return True
            # 判断右上斜向是否有五子连珠
            if m < LEN - 4 and n < LEN - 4 and check_five_in_a_row(m, n, 1, 1):
                return True
            # 判断右下斜向是否有五子连珠
            if m < LEN - 4 and n > 3 and check_five_in_a_row(m, n, 1, -1):
                return True
    return False

  判断输赢函数is_win,接受一个落子列表L作为参数,并返回一个布尔值表示是否有一方获胜。

  函数的执行流程如下:

  1. 定义了一个内部函数check_five_in_a_row,用于检查指定起始位置和方向上是否存在五子连珠。

  · 函数接受四个参数:起始行start_row、起始列start_col、行的增量row_delta和列的增量col_delta

  · 使用循环遍历五个位置,检查每个位置是否存在于落子列表L中,如果有任何一个位置不在列表中,则返回False。

  · 如果所有位置都在列表中,则返回True,表示存在五子连珠。

  2. 使用两层嵌套的循环遍历棋盘上的每个位置,对于每个位置,分别进行以下判断:

   - 判断横向是否有五子连珠:如果当前位置的列小于LEN-4(棋盘边界判断)且调用check_five_in_a_row函数返回True,则表示存在横向五子连珠,返回True。

   - 判断纵向是否有五子连珠:如果当前位置的行小于LEN-4且调用check_five_in_a_row函数返回True,则表示存在纵向五子连珠,返回True。

   - 判断右上斜向是否有五子连珠:如果当前位置的行小于LEN-4且列小于LEN-4且调用check_five_in_a_row函数返回True,则表示存在右上斜向五子连珠,返回True。

   - 判断右下斜向是否有五子连珠:如果当前位置的行小于LEN-4且列大于3且调用check_five_in_a_row函数返回True,则表示存在右下斜向五子连珠,返回True。

  3. 如果在遍历完所有位置后仍未返回True,则表示不存在五子连珠,返回False。

## 评估-计算分数

  计算分数分2个函数,一个是计算整体的分数evaluation,另外一个是针对某个点计算某个方向上的分数cal_score

# 评估函数
def evaluation(is_me):
    if is_me:
        my_list = list_ai
        enemy_list = list_human
    else:
        my_list = list_human
        enemy_list = list_ai

    # 算自己的得分
    score_all_arr = []  # 得分形状的位置 用于计算如果有相交 得分翻倍
    my_score = 0
    for pt in my_list:
        m = pt[0]
        n = pt[1]
        my_score += cal_score(m, n, 0, 1, enemy_list, my_list, score_all_arr)
        my_score += cal_score(m, n, 1, 0, enemy_list, my_list, score_all_arr)
        my_score += cal_score(m, n, 1, 1, enemy_list, my_list, score_all_arr)
        my_score += cal_score(m, n, -1, 1, enemy_list, my_list, score_all_arr)

    #  算敌人的得分, 并减去
    score_all_arr_enemy = []
    enemy_score = 0
    for pt in enemy_list:
        m = pt[0]
        n = pt[1]
        enemy_score += cal_score(m, n, 0, 1, my_list, enemy_list, score_all_arr_enemy)
        enemy_score += cal_score(m, n, 1, 0, my_list, enemy_list, score_all_arr_enemy)
        enemy_score += cal_score(m, n, 1, 1, my_list, enemy_list, score_all_arr_enemy)
        enemy_score += cal_score(m, n, -1, 1, my_list, enemy_list, score_all_arr_enemy)

    total_score = my_score - enemy_score * RAT * 0.1

    return total_score

  上述代码是一个评估函数evaluation,用于评估当前局面的得分。函数根据当前轮到的是自己还是对手,计算出相应的得分。

  函数的执行流程如下:

  1. 根据参数is_me判断当前轮到的是自己还是对手,将对应的落子列表赋值给my_listenemy_list

  2. 初始化变量:

  score_all_arr:用于存储得分形状的位置,用于计算如果有相交则得分翻倍。
  my_score:自己的得分,初始值为0。

  3. 遍历自己的落子列表my_list,对于每个位置(m, n),分别进行以下操作:

  调用cal_score函数计算在水平、垂直、右斜和左斜四个方向上的得分,并累加到my_score中。
  在计算得分的过程中,使用enemy_listmy_list作为参数,以判断对手的棋子是否存在,同时将得分形状的位置存储到score_all_arr中。

  4. 初始化变量:

  score_all_arr_enemy:用于存储对手的得分形状的位置。
  enemy_score:对手的得分,初始值为0。

  5. 遍历对手的落子列表enemy_list,对于每个位置(m, n),分别进行以下操作:

  调用cal_score函数计算对手在水平、垂直、右斜和左斜四个方向上的得分,并累加到enemy_score中。
  在计算得分的过程中,使用my_listenemy_list作为参数,以判断自己的棋子是否存在,同时将得分形状的位置存储到score_all_arr_enemy中。

  6. 计算总得分:

  将自己的得分my_score减去对手的得分enemy_score乘以一个权重因子RAT和0.1的结果,得到总得分total_score

  7. 返回总得分total_score作为当前局面的评估值。

# 每个方向上的分值计算
def cal_score(m, n, x_decrict, y_derice, enemy_list, my_list, score_all_arr):
    add_score = 0  # 加分项
    # 在一个方向上, 只取最大的得分项
    max_score_shape = (0, None)

    # 如果此方向上,该点已经有得分形状,不重复计算
    for item in score_all_arr:
        for pt in item[1]:
            if m == pt[0] and n == pt[1] and x_decrict == item[2][0] and y_derice == item[2][1]:
                return 0

    # 在落子点方向上循环查找得分形状
    for offset in range(-5, 1):
        pos = []
        for i in range(0, 6):
            if (m + (i + offset) * x_decrict, n + (i + offset) * y_derice) in enemy_list:
                pos.append(2)
            elif (m + (i + offset) * x_decrict, n + (i + offset) * y_derice) in my_list:
                pos.append(1)
            else:
                pos.append(0)
        tmp_shap5 = (pos[0], pos[1], pos[2], pos[3], pos[4])
        tmp_shap6 = (pos[0], pos[1], pos[2], pos[3], pos[4], pos[5])

        for (score, shape) in shape_score:
            # 命中一个得分形状
            if tmp_shap5 == shape or tmp_shap6 == shape:
                if score > max_score_shape[0]:
                    max_score_shape = (score, ((m + (0+offset) * x_decrict, n + (0+offset) * y_derice),
                                               (m + (1+offset) * x_decrict, n + (1+offset) * y_derice),
                                               (m + (2+offset) * x_decrict, n + (2+offset) * y_derice),
                                               (m + (3+offset) * x_decrict, n + (3+offset) * y_derice),
                                               (m + (4+offset) * x_decrict, n + (4+offset) * y_derice)), (x_decrict, y_derice))

    # 计算两个形状相交, 如两个3活 相交, 得分增加 一个子的除外
    if max_score_shape[1] is not None:
        for item in score_all_arr:
            for pt1 in item[1]:
                for pt2 in max_score_shape[1]:
                    if pt1 == pt2 and max_score_shape[0] > 10 and item[0] > 10:
                        add_score += item[0] + max_score_shape[0]

        score_all_arr.append(max_score_shape)

    return add_score + max_score_shape[0]

  上述代码是计算在指定方向上的得分函数cal_score,用于评估棋局中某个位置在特定方向上的得分情况。

  函数的执行流程如下:

  1. 初始化变量:
   - add_score:加分项,用于记录在当前方向上的额外得分。
   - max_score_shape:最大得分形状,初始值为(0, None),用于记录在当前方向上的最高得分形状及其分值。

  2. 检查当前方向上是否已经有得分形状,如果有,则不进行重复计算,直接返回0。

  3. 在落子点的方向上循环遍历,查找可能的得分形状。遍历范围为从偏移-5到1,共6个位置。

  4. 对于每个位置,判断该位置在落子列表中的状态:
   - 如果在对手的落子列表中,将状态设置为2。
   - 如果在自己的落子列表中,将状态设置为1。
   - 如果为空位,将状态设置为0。

  5. 根据当前位置及其相邻位置的状态,形成长度为5或6的形状。

  6. 遍历预定义的得分形状和对应的分值:
   - 如果当前形状与预定义的形状匹配,命中得分形状。
   - 如果当前得分高于max_score_shape中记录的最高得分,则更新max_score_shape为当前得分形状。

  7. 判断两个形状是否相交:
   - 如果max_score_shape不为None,表示存在最高得分形状。
   - 遍历已记录的得分形状,检查是否与最高得分形状相交:
   - 如果相交且两个形状的分值都大于10,则将相交的得分形状的分值加到add_score中。

  8. 将最高得分形状记录到score_all_arr中。

  9. 返回加分项add_score与最高得分形状的分值之和作为当前方向上的总得分。

  通过遍历落子点的特定方向上的位置,并判断形成的形状是否与预定义的得分形状匹配,从而计算出该方向上的得分情况。同时,如果存在相交的得分形状,则额外增加得分。最终返回当前方向上的总得分。


总结

  ai 调用 backtrace进行回溯。

   backtrace里面有约束条件:判赢is_win、depth是否为0。

  如果满足约束条件,那么计算总棋局分数evaluationevaluation会调用cal_scorecal_score计算某个点在某个方向的分数。

  如果不满足约束条件,做一下启发(剪枝),然后接着递归。

  最后返回四个参数:x、y坐标,搜索次数、是否赢了。


继续学习下一篇实战!

  【五子棋实战】第3章 算法包装成第三方接口

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

征途黯然.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值