2.3 启发式评估函数
上一节我们一直假装有个
evaluate(),现在来兑现它。评估函数是五子棋引擎的灵魂——它决定了引擎到底“懂不懂棋”。而五子棋的棋感,几乎全部浓缩在几个棋型里:活四、冲四、活三……这一节我们认识它们,再把它们变成分数。
一、动机:评估函数就是引擎的“棋感”
回顾困境:五子棋搜不到终局,所以搜索到深度上限时,必须靠 evaluate() 给这个“半截局面”估个分。这个估分准不准,直接等于引擎棋力的上限——搜索负责“往前看”,评估负责“看懂眼前”,两者缺一不可。
那五子棋的“看懂”是什么?是一眼认出棋盘上的威胁:谁连了一长串、哪串两头是活的、哪串已经被堵死。把这些威胁分门别类、各给一个分,就是评估函数要干的事。所以我们先得有一套“威胁的词汇”——棋型。
二、棋型:把“威胁”分门别类
下面这几个棋型是五子棋的核心词汇。我们用 ● 表示某一方的子、_ 表示空点、× 表示被对方堵住或到了棋盘边。请仔细体会每种棋型“有多可怕”:
连五 ● ● ● ● ● 直接赢了,游戏结束
活四 _ ● ● ● ● _ 两头都空 → 下一手两边都能成五,对手只能挡一头,必胜!
冲四 × ● ● ● ● _ 一头被堵 → 只威胁右边那一个空点,对手挡住就化解
活三 _ ● ● ● _ _ 下一手能长成“活四” → 严重威胁,必须理会
眠三 × ● ● ● _ 一头被堵的三,威胁小得多
活二 _ ● ● _ _ 还早,但有潜力
关键的分水岭是 活四 和 冲四 的区别,这是初学者最容易混的:活四两头都空,对手挡哪头都没用,是必胜的“绝杀”;冲四只差一个特定点成五,对手只要堵住那个点就化解了。同样道理,活三 之所以可怕,是因为放任不管,它下一手就变成挡不住的活四——所以实战里“见活三就要挡”。把这套“可怕程度”量化,就是下一步。
三、把棋型变成分数:评分函数
思路很直接:给每种棋型定一个权重,然后数一数局面里我方有多少个、对方有多少个,加权相减。写成公式就是(从“我”的视角看):
局面分 = ∑p w(p)·N我(p) − ∑p w(p)·N敌(p)
这里 p 跑遍所有棋型,w(p) 是该棋型的权重,N我(p)、N敌(p) 分别是我方、对方拥有该棋型的数量。权重大致这样定——注意各档之间要拉开数量级,让高级棋型彻底压过低级的:
# 棋型权重(示意值,实战需要反复调参)
PATTERN_SCORES = {
"FIVE": 10_000_000, # 连五:已经赢了
"OPEN_FOUR": 100_000, # 活四:挡不住,几乎等于赢
"FOUR": 10_000, # 冲四:逼对手挡唯一点
"OPEN_THREE": 1_000, # 活三:放任就成活四
"SLEEP_THREE": 100, # 眠三:一头被堵
"OPEN_TWO": 100,
"SLEEP_TWO": 10,
}
def evaluate(board, me):
"""从 me 的视角给局面打分:我方棋型得分 − 对方棋型得分。"""
return score_side(board, me) - score_side(board, -me)
def score_side(board, player):
"""扫描 player 的所有连子,识别棋型并把权重加起来。"""
total = 0
for 每条过 player 棋子的方向线 in board:
kind = classify(连子数, 两端开放数) # 比如 (4, 2)→活四,(4, 1)→冲四
total += PATTERN_SCORES.get(kind, 0)
return total
那个 classify 是核心:它沿着上一节学的四个方向扫描,数出“连了几个子、两端各开不开放”,再据此判定棋型——比如“连 4 个、两端都空”就是活四 (4, 2),“连 4 个、只有一端空”就是冲四 (4, 1)。真正写它要小心不少边界情况(跳着连的、被自己另一条线复用的),这里先把骨架讲清,细节留给你在代码里打磨。
调参的要点只有一句话:各档权重必须拉开数量级。因为一个活四的价值应当压倒任意多个活三——如果权重定得太接近,引擎可能会为了凑几个活三,而对眼前的活四视而不见,酿成大错。“宁可少算几个小威胁,也绝不能看漏一个大威胁”,这就是数量级拉开的意义。
四、反思:再好的评估,也是一张“静态快照”
现在引擎有棋感了,棋力会大涨。但它有个根子上的局限:评估函数看的是当下这一张静止的棋盘,它数得清此刻有几个活三冲四,却算不出“一连串强制进攻最终会不会赢”。
举个例子:五子棋里有种杀法叫连续冲四——我先冲四逼你挡,挡完我又冲四再逼你挡,一路逼下去,几手之后突然成一个活四,必胜。这种胜利可能藏在五六手之外,而我们的搜索因为分支太多只能搜到有限的深度,很可能根本没搜到那一步。于是评估函数会把一个“其实已经必胜”的局面,估成“大致均势”——这就漏算了。
静态评估 + 有限深度搜索,对付这种强制连续威胁力不从心。要可靠地算清这类杀棋,需要一种专门盯着“威胁”往深里钻的搜索。这正是下面要讲的内容。
五、小结与下一节
- 评估 = 棋感:搜不到底时给中间局面估分,准不准决定棋力上限。
- 棋型词汇:连五 / 活四(绝杀)/ 冲四(可挡)/ 活三(不挡就成活四)/ 眠三 / 活二;分清“活”与“冲/眠”是关键。
- 评分公式:局面分 = ∑ w(p)·N我(p) − ∑ w(p)·N敌(p);权重务必拉开数量级,大威胁压倒小威胁。
- 局限:静态评估 + 有限搜索会漏算“连续冲四”这类强制杀。
我们刚定义的这套棋型,马上还有一个大用处。下一节 2.4 走子排序与候选生成,就用棋型来给走法排序——把“能成冲四、活三”的强手提到最前面,让 α-β 剪枝发挥到极致。至于本节末尾那个“漏算强制杀”的硬骨头,我们留到 2.5 威胁空间搜索与 VCF/VCT 专门解决。