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

3.3 搜索:迁移到国象

好消息:搜索这块你大半已经会了。第一、二章造的 α-β、迭代加深、置换表,到国象几乎原样能用。这一节先确认哪些直接迁移,再补两样国象(以及所有“能吃子”的棋)特别需要的东西:静态搜索MVV-LVA 排序

一、直接搬过来:α-β、迭代加深、置换表

搜索框架是通用的——它只通过统一接口问引擎“能走哪、走完啥样、终局没”,并不关心是五子棋还是国象。所以前两章这几件主力可以原样迁移:

  • α-β 剪枝(1.4):极小化极大 + 剪枝,内核完全不变。
  • 迭代加深(2.2):逐层加深、超时即返回,配上时间预算。
  • 置换表 + Zobrist(2.6):国象的换位更普遍(不同顺序常到同一局面),置换表收益甚至更大;python-chess 还自带局面哈希,省得自己写。

换句话说,把第二章那台搜索机器接上国象的规则引擎,它立刻就能下国象了。真正要新加的,是下面两样针对“吃子”的东西。

二、静态搜索:别在“打了一半”的时候停手

这是国象(和一切能吃子的棋)特有的一个坑,叫地平线效应(horizon effect)。设想搜索到了深度上限,正要用评估函数收尾,可此刻棋盘上双方正杀得难解难分——我刚用后吃了你的车,评估函数一看“我多一个车”,给了高分就返回了;可它没看到:你下一手就把我的后吃回去了!评估在一个“打到一半”的局面上停手,得出的判断完全是假象。

解法叫静态搜索(Quiescence Search):到达深度上限后,先别急着评估,继续把所有“吃子”走完,直到局面“安静”下来(没有立即的吃子了),再评估。这样就避免在一片厮杀中误判:

def quiescence(engine, alpha, beta):
    stand_pat = evaluate(engine)              # 当前局面的“静态分”
    if stand_pat >= beta:
        return beta                           # 已经够好,剪枝
    alpha = max(alpha, stand_pat)
    for move in engine.capture_moves():       # ★ 只继续搜“吃子”着法
        engine.make_move(move)
        score = -quiescence(engine, -beta, -alpha)
        engine.unmake_move()
        alpha = max(alpha, score)
        if alpha >= beta:
            break
    return alpha

关键就在 capture_moves()——静态搜索只追着吃子走(有时加上将军),把那段“你来我往的兑子”算到尽头再打分。这和第二章 VCF 的思路异曲同工:都是只盯着一类关键着法往深里钻,把会扭曲评估的战术序列算清楚。

三、走子排序:MVV-LVA

2.4 讲过 α-β 高度依赖走子顺序,国象也一样,而且它有个专属的好排序法叫 MVV-LVA(Most Valuable Victim − Least Valuable Aggressor,“吃大子、用小子”):优先尝试“用价值低的子吃价值高的子”的着法——比如用兵吃后,这种几乎稳赚的好棋当然要第一个搜。

除此之外,2.4 的杀手启发、历史启发,2.6 置换表里存的最佳着法,全都原样适用。把它们叠加起来排序,国象的 α-β 同样能剪得很狠。你看,整套排序经验是跨棋种通用的,只是 MVV-LVA 这一味是国象“能吃子”才有的特色。

四、小结与下一节

  • 直接迁移:α-β、迭代加深、置换表(Zobrist) 内核通用,接上国象规则即可下棋;国象换位多,置换表收益更大。
  • 静态搜索:到深度上限后继续把吃子序列走完再评估,消除“打到一半误判”的地平线效应;思路同 VCF。
  • MVV-LVA:优先“小子吃大子”,配合杀手/历史/置换表着法,让 α-β 剪枝高效。

搜索齐活,但它要靠 evaluate() 给局面打分——而国象的评估比五子棋的棋型复杂得多。下一节 3.4 传统手工评估函数,我们来写国象的“棋感”:子力价值 + 子力位置表 + 王安全。