【五子棋实战】第2章 博弈树极大极小值alpha-beta剪枝算法
- 博弈树
- 极大极小值搜索Minimax
- 负值极大法Negamax
- alpha-beta剪枝
- 博弈树负值极大alpha-beta剪枝算法代码实现
- ## 初始化输入、确定输出
- ## 开始回溯
- ## 判赢
- ## 评估-计算分数
- 总结
- 继续学习下一篇实战!
市面上比较常用的五子棋算法是博弈树极大极小值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_ai
、list_human
、list_ai_add_human
、list_all
、x
、y
、search_count
、DEP
、LEN
和RAT
。分别表示ai的棋子、敌方的棋子、ai的棋子+敌方的棋子,所有的位置坐标、计算出来的x坐标、计算出来的y坐标、算法搜索次数(主要是debug用)、搜索深度、棋盘长度、进攻系数。
这里做成全局变量是因为就不要传参了,不然每个函数的形参都要写一大堆,挺麻烦的。
接下来,将传入的参数初始化到对应的全局变量中。将 depth
赋值给 DEP
,length
赋值给 LEN
,ratio
赋值给 RAT
。然后调用 init_list
函数,使用 player
和 board
参数来初始化 list_ai
、list_human
、list_ai_add_human
和 list_all
这些列表。
设置初始的alpha
和beta
值,用于进行Alpha-Beta
剪枝。
调用 backtrack
函数进行回溯搜索,传入参数 True
表示当前是 AI
的回合,搜索的深度为 depth
,并使用 alpha
和 beta
进行 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
),记录当前位置x
和y
。
如果评估值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_list
和enemy_list
。
2. 初始化变量:
score_all_arr
:用于存储得分形状的位置,用于计算如果有相交则得分翻倍。
my_score
:自己的得分,初始值为0。
3. 遍历自己的落子列表my_list
,对于每个位置(m, n)
,分别进行以下操作:
调用cal_score
函数计算在水平、垂直、右斜和左斜四个方向上的得分,并累加到my_score
中。
在计算得分的过程中,使用enemy_list
和my_list
作为参数,以判断对手的棋子是否存在,同时将得分形状的位置存储到score_all_arr
中。
4. 初始化变量:
score_all_arr_enemy
:用于存储对手的得分形状的位置。
enemy_score
:对手的得分,初始值为0。
5. 遍历对手的落子列表enemy_list
,对于每个位置(m, n)
,分别进行以下操作:
调用cal_score
函数计算对手在水平、垂直、右斜和左斜四个方向上的得分,并累加到enemy_score
中。
在计算得分的过程中,使用my_list
和enemy_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。
如果满足约束条件,那么计算总棋局分数evaluation
,evaluation
会调用cal_score
,cal_score
计算某个点在某个方向的分数。
如果不满足约束条件,做一下启发(剪枝),然后接着递归。
最后返回四个参数:x、y坐标,搜索次数、是否赢了。
继续学习下一篇实战!
【五子棋实战】第3章 算法包装成第三方接口