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

2.5 威胁空间搜索与 VCF/VCT

这一节是五子棋引擎最有特色、也最“解气”的一块。我们要造一把专门“算杀”的利器,把那些藏在深处、常规搜索看不到的强制必胜线一网打尽。它背后有个绝妙的洞察:当你不停制造威胁时,对手的回应是被逼死的——而被逼死的分支,可以搜得又深又快。这块也是 1994 年 Allis 证明“五子棋先手必胜”所用的核心技术。

一、动机:再快的常规搜索,也够不到深处的杀

接着上一节末尾的问题。设想这样一条线:我下一个冲四,逼你挡;你一挡,我又得到一个新的冲四,再逼你挡……如此连续逼下去,七八手之后,我突然下出一个两头皆空的活四——你挡无可挡,我赢定了。

这条线客观上是必胜的,可我们的 α-β 多半发现不了:它分支太多,撑死搜十来层,而这条杀可能藏在第 15、20 手。于是引擎对着一个其实已经赢定的局面,却浑然不觉。问题不在评估,在深度——常规搜索“铺得太宽,钻得不够深”。我们需要换一种只往深里钻的搜索。

二、威胁空间搜索:让对手“没得选”

突破口是一个关键观察:当我下一个冲四时,对手几乎没有选择——他只能去堵那唯一能成五的点,否则下一手我就连五赢了。换句话说,我制造威胁,就把对手的分支因子从几十压成了 1。

这就是威胁空间搜索(Threat-Space Search, TSS)的灵魂:

普通搜索之所以宽,是因为双方都有几十种选择。但如果我只走制造威胁的棋(冲四、活三),对手就被迫应招、几乎只有一种回法。于是整棵搜索树从“宽而浅”变成了“窄而深”——分支因子接近 1,我就能毫不费力地往下钻 20、30 层,把深处的杀看个通透。

代价是它只看“我一直进攻”的线,不是通用搜索——但这恰恰是它的用途:专门用来回答“我现在有没有一条强制取胜的连续进攻”

三、VCF 解算器:连续冲四胜

最纯粹的威胁空间搜索叫 VCF(Victory by Continuous Fours,连续冲四胜):我每一步都下冲四,对手每一步都被逼着挡那个唯一点,直到我成五,或者再也凑不出新的冲四为止。因为对手的回应完全是强制的,递归写起来出奇地干净:

def vcf(board, me):
    """me 能否靠连续冲四强制取胜?能则返回取胜的走法序列,否则返回 None。"""
    for m in moves_that_make_four(board, me):   # 只考虑'能形成冲四'的走法
        board.play(m, me)
        if has_five(board, me):                 # 这一手直接成五 → 赢了
            board.undo(); return [m]

        block = forced_block(board, me)          # 对手被冲四逼着,只能挡唯一点
        board.play(block, -me)
        line = vcf(board, me)                    # 递归:接着找下一个冲四
        board.undo(); board.undo()
        if line is not None:
            return [m, block] + line             # 把这条必胜线一路拼回来
    return None                                  # 凑不出连续冲四 → 此路不通

注意循环只遍历 moves_that_make_four——只走冲四,这就是“威胁空间”收窄分支的体现;而 forced_block 直接给出对手唯一的应手,对手一方根本不分叉。正因如此,这段递归能轻松钻到很深,几毫秒就判出一条十几手的杀。这是常规 α-β 给不了的能力。

四、VCT,与把必胜线“讲”给 ANIMA 听

VCT(Victory by Continuous Threats,连续威胁胜)是 VCF 的升级:除了冲四,还允许用活三来制造威胁。活三虽不像冲四那样把对手逼到唯一一点(对手挡法可能不止一种),但威胁同样强,于是 VCT 能找到更多、更隐蔽的杀;代价是对手分支略多、搜索更复杂。实现思路和 VCF 一脉相承,只是“威胁走法”和“对手应法”的集合都放宽了一些。

最后,回到贯穿全课的那条暗线——开放信息接口。VCF/VCT 解出的不只是“能不能赢”,而是一条具体的取胜着法序列。这正是 1.8 接口契约里预留forced_win 字段该填的内容。我们把它接上:

def info(self):
    line = vcf(self.board, self.to_move)         # 先算一下有没有连续冲四杀
    return {
        # …… board / legal_moves / evaluation / best_move 等照旧 ……
        "forced_win": {                          # ★ 兑现 1.8 预留的字段
            "exists": line is not None,
            "winning_line": line,                # 具体的必杀着法序列
            "depth": len(line) if line else None # 几手之内能杀
        }
    }

这一步意义不小:引擎从此不只是“给出一步好棋”,而是能解释“我看到了一条 N 步内的强制杀,路线是这样”。将来 Anima 认知框架把五子棋当工具调用时,问一句 info() 就能拿到这条必胜线——这正是“开放信息接口”最初想要的、AlphaZero 那种黑盒给不出的可读信息。

五、小结与下一节

  • 动机:常规 α-β 铺得宽、钻不深,会漏掉藏在深处的连续强制杀。
  • 威胁空间搜索:只走威胁棋 → 对手被迫应招、分支接近 1 → 搜索从“宽而浅”变“窄而深”,能钻很深。
  • VCF / VCT:连续冲四(对手唯一应)/ 连续威胁(加入活三)专用算杀器;递归干净、极快。
  • ANIMA 接口:把必胜线填进 info()forced_win,让引擎能“解释”自己看到的杀——可读信息正是黑盒模型的短板。

现在引擎又会评估、又能算杀,已经相当强了。但它有个效率问题:搜索中同一个局面会被反复算很多遍,浪费惊人。下一节 2.6 Zobrist 哈希与置换表,我们给它装一个“记忆”,把算过的局面缓存下来,让搜索再快一截。