Soma Zero Tutorials
🔍 搜索功能尚未开启,敬请期待。

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 专门解决。