一个使用 Go 语言实现的五子棋(Gomoku)AI 引擎,遵循 Gomocup/Piskvork 协议,通过标准输入输出与比赛管理器(Manager)进行文本交互。内置基于 Minimax + 棋型评分 + Alpha-Beta 剪枝 的 AI 算法,并支持迭代加深与时间控制。
本项目是 Gomocup 五子棋 AI 竞赛中的「Brain(引擎)」端实现。其设计目标是将 UI 界面、对战调度与 AI 逻辑彻底分离,让开发者能够专注于 AI 算法本身。
- 语言:Go 1.21(见
go.mod) - 模块名:
gomoku - 可执行文件:
pbrain-bango(按协议要求,引擎可执行文件名必须以pbrain-开头) - 通信方式:基于 stdin/stdout 的文本协议,每行一条命令
gomoku/
├── main.go # 程序入口:flag 解析、web(TCP)/管道双模式、双线程主循环与命令分发;
│ # 组合根——import 两个算法包完成注册,BANGO_ALGO 选择算法
├── engine.go # 引擎核心:状态管理、棋盘操作、前一手(最近落子)追踪、输出控制;
│ # 只依赖 algorithm.Algorithm 接口,aiMove 把棋盘快照为 Request 后委托 Think
├── protocol.go # 协议数据结构:Command 定义与坐标解析
├── engine_ai_test.go # 引擎×算法集成测试:aiMove 合法性、开局库协议端到端
├── engine_test.go # 引擎状态测试:START 前棋盘分配、渲染、满盘兜底
├── protocol_test.go # 协议黑盒测试:管道与 TCP web 模式的一致性、命令错误路径
├── arena_test.go # arena 冒烟测试(-short 跳过)
├── algorithm/ # ★ 算法集:统一接口 + 注册表 + 各算法实现
│ ├── algorithm.go # Algorithm 接口(Name/Think/Reset/EndSession)、Request 上下文、
│ │ # 注册表(Register/New/Names,默认 alphabeta,BANGO_ALGO 可选)
│ ├── alphabeta/ # 原 alphabeta 搜索算法(原根目录 algorithm/rules/opening/tt/vcx/book*.go)
│ │ ├── adapter.go # 算法适配器:置换表/开局库生命周期,Think=原 aiMove
│ │ ├── search.go # 搜索核心:评估(按线缓存)、negamax+αβ+PVS、quiescence、
│ │ │ # 迭代加深、杀手/历史启发、着法生成
│ │ ├── rules.go # 规则判定:胜负分派与连珠禁手检测
│ │ ├── opening.go # 经典 26 式开局:模式枚举、识别、理论区投影
│ │ ├── tt.go # 置换表:Zobrist 哈希与缓存读写
│ │ ├── vcx.go # 算杀:VCF/VCT 连续威胁搜索
│ │ ├── bookcmd.go # 开局库维护子命令:validate / build(RunBookCommand)
│ │ ├── swap2.go # SWAP2 开局决策(评估黑方形势→换先或执白回手)
│ │ └── *_test.go # 搜索/规则/置换表/算杀/开局库/时间控制/自对弈等全套测试
│ └── random/ # random 算法:空盘天元;己方成五点优先(自胜检查);前一手杀局
│ # 判断(跳五/冲四成五点、活三四两端加权随机、跳活三取空隙);否则在
│ # 全部子的切比雪夫距离<3 邻域内取距前一切比雪夫距离最小的点,
│ # 仅平局时按权重随机
├── book/ # ★ 开局库功能模块:数据源解析、8 对称规范化编译、查询(引擎与算法共用)
├── cmd/arena/ # 对弈 harness:双引擎管道对弈、随机开局、比分汇总(第八节验收用)
├── gui/ # 浏览器对弈前端(ES module)+ TCP 桥接服务;test/ 下三个 node 测试套件
├── openbook/ # 开局库源数据(默认启用 gomocup-2026-15x15.json + classic-26-15x15.json)
├── protol.md # Gomocup/Piskvork 协议中文说明文档
├── go.mod # Go 模块定义
└── .gitignore # 忽略 pbrain-* 编译产物
编译产物
pbrain-bango(协议要求引擎可执行文件以pbrain-开头)不入版本库,按第六节自行编译生成。 分层约定:根目录只保留协议会话层(package main),算法一律放algorithm/下的子包并通过algorithm.Algorithm接口接入;book/是无搜索依赖的纯数据模块,供会话层、算法与工具共用。
协议明确要求避免单线程下的读写死锁(详见 protol.md 第 26-29 行)。本项目采用双线程架构,在 main.go 中实现:
| 线程 | 职责 | 实现位置 |
|---|---|---|
| 线程 1(读线程) | 唯一消费 stdin,解析命令并转发到通道 | readLoop() |
| 线程 2(思考线程) | 从通道取出命令,执行思考并写入回复 | thinkLoop() |
两个线程通过带缓冲的 cmdChan(容量 16)协调。读线程是 stdin 的唯一消费者,思考线程无需在思考时读取输入,从结构上规避了引擎与管理器互相等待的死锁。
引擎状态由 Engine 结构体持有,所有对棋盘和状态的访问均通过内嵌的 sync.Mutex 保护:
resetBoard():重置/分配指定大小的空棋盘,并调用algo.Reset让算法的跨手缓存(置换表)跟随对局生命周期place():在合法坐标处落子(越界自动忽略),同时维护前一手(最近落子)锚点,供算法做威胁分析与邻近加权;TAKEBACK 撤掉锚点子时自动失效aiMove():计算落子位置——持锁快照棋盘/规则/时限/前一手为algorithm.Request,解锁后委托algo.Think,返回盘外或满盘时兜底首个空点
协议强调必须及时刷新输出缓冲区(详见 protol.md 第 23-24 行)。引擎使用 bufio.Writer 包装 stdout,writeLine() 在每行写入后立即调用 flushOut() 刷新,确保管理器不会因收不到数据而阻塞等待。所有回复以 CRLF(\r\n)结尾。
引擎在 thinkLoop() 中分发处理以下命令,完整覆盖协议要求的标准命令集:
| 命令 | 格式 | 说明 | 处理函数 |
|---|---|---|---|
START |
START [size] |
初始化游戏并创建空棋盘(size 须 > 5,默认 15) | cmdStart() |
BEGIN |
BEGIN |
引擎作为先手,立即输出第一步落子 | cmdBegin() |
TURN |
TURN X,Y |
告知对手落子,引擎分析后返回己方落子 | cmdTurn() |
BOARD |
BOARD + 多行 X,Y,ID + DONE |
设置完整棋盘状态(恢复对局) | cmdBoard() |
INFO |
INFO key value |
接收管理器推送的时间控制等配置 | cmdInfo() |
END |
END |
结束对局,回复 OK |
thinkLoop() |
RESTART |
RESTART |
重置棋盘并重新开始,回复 OK |
thinkLoop() |
ABOUT |
ABOUT |
返回引擎信息 | thinkLoop() |
PRINT |
PRINT |
打印当前棋盘(调试用) | cmdPrint() |
TAKEBACK |
TAKEBACK X,Y |
可选命令:悔棋,移除指定棋子,回复 OK |
cmdTakeback() |
PLAY |
PLAY X,Y |
可选命令:管理器强制落子(SUGGEST 的应答),回显坐标 | cmdPlay() |
RECTSTART |
RECTSTART W,H |
可选命令:矩形棋盘初始化;仅支持方形(W==H),否则 ERROR |
cmdRectStart() |
SWAP2BOARD |
SWAP2BOARD + 多行 X,Y + DONE |
可选命令:Swap2 开局(摆三子 / SWAP 或续着) | cmdSwap2() |
| 未知命令 | - | 回复 UNKNOWN |
thinkLoop() |
BOARD 是协议中唯一的例外——单条命令跨多行。读线程在 readBoardBody() 中逐行累积 X,Y,ID 三元组,直到遇到 DONE 才组装成完整的 Command 发往通道,保证思考线程收到的是已解析完毕的数据。
Info 结构体存储管理器推送的配置,同时保留原始键值对(raw)。当前仅存储不做事前规则判定(如连珠禁手检查),支持的键包括:
| 键 | 字段 | 含义 |
|---|---|---|
timeout_turn |
TimeoutTurn |
每步限时(毫秒,0 = 尽快落子) |
timeout_match |
TimeoutMatch |
整局限时(毫秒) |
max_memory |
MaxMemory |
内存上限(字节) |
time_left |
TimeLeft |
剩余时间(毫秒) |
game_type |
GameType |
游戏类型码 |
rule |
Rule |
规则码:1 自由、2 连珠、4 连珠、9 Caro |
fast |
Fast |
快速模式 |
落子决策被抽象为 algorithm.Algorithm 接口,会话层(package main)只依赖接口:
type Algorithm interface {
Name() string // 注册名:"alphabeta"、"random"
Think(req Request) (x, y int) // 一步决策:Request 是棋盘快照+规则+时限+前一手
Reset(size int, maxMemory int64) // 新对局/换盘面:兼容的置换表等缓存可跨 RESTART 存活
EndSession() // 会话结束:丢弃全部跨对局状态
}- Request:
Size+ 行主序棋盘(0 空 / 1 己方 / 2 对手 / 3 标记)+OwnIsBlack+ 规则码 + 时限/深度/内存预算 + INFO folder + 前一手坐标LastX/LastY(HasLast=false表示空盘)。 - 注册表:各算法子包在
init()中algorithm.Register(name, factory);main.goimport 即注册。运行时用环境变量BANGO_ALGO选择(默认alphabeta),未知名字在 stderr 告警后回退默认。 - 新增算法:建
algorithm/<name>/子包实现接口并注册,不改动引擎核心。
内置算法:
| 算法 | 路径 | 说明 |
|---|---|---|
alphabeta |
algorithm/alphabeta/ |
完整搜索 AI(5.1-5.9),adapter.go 持有置换表与开局库生命周期 |
random |
algorithm/random/ |
加权随机基线,带前一手杀局判断(见下) |
random 算法决策流:① 空盘直接返回天元;② 自胜检查(两层搜索·第一层)——己方已有四(冲四/活四/跳四)时立即取成五点取胜,优先级高于一切防守;③ 杀局判断——沿前一手四个方向滑动 5 格窗口,「同一方 4 子 + 有且仅有一个空位」即跳连成五点(含冲四/活四的成五点),堵唯一空点(多个窗口命中时按距前一手加权随机);连珠长 3/4 且两端皆空判活三/活四,在两端点中按距前一手远近加权随机选一;跳活三(6 格窗口内 3 子 + 1 内部空隙、两外侧皆空)由专门检查识别,直接取空隙点,把跳型威胁拦在升级成跳四之前(两层搜索·第二层,优先于跳四阶段的防守);④ 常规落子:收集距棋盘所有子切比雪夫距离 < 3 的空白交点,取其中距前一手切比雪夫距离最小的点,仅当最小距离点多于一个时在组内加权随机(同距权重相同,即均匀随机;无前一手信息时以距最近棋子的距离为排序键)。
搜索在棋盘的扁平副本 b[y*size+x](0 空 / 1 己方 / 2 对手)上进行,落子/撤子即写入/清零。胜负判定由 winsMove() 按当前规则分派(见 5.8 节)。
genMoves() 只考虑与已有棋子切比雪夫距离 ≤ 2 的空点,避免全盘枚举导致分支爆炸。
候选数据增量化:落子/提子唯一原语 setStone 在评估线缓存之外再维护三份增量状态——子数计数器、半径 2 / 半径 1 两张邻域引用计数表(取代每空格的 5×5 / 3×3 扫描)、以及逐格逐向棋型值缓存:pointScore 的单方向体(双向数子 + 开端 + 跳形折半)按 (格 × 4 向 × 2 方) 缓存,setStone 对 4 个方向上 ±9 步依赖窗口内的格子打脏标记(按方向×方共 8 位),读取时惰性重算。正确性门槛:TestShapeCacheExact 在随机 make/undo 序列上逐步断言缓存值与独立 oracle 逐位相等(含子数与两张邻域表)。搜索树与逐空格扫描版本逐位一致。
威胁梯子双保留不变量(2026-08 修复「活三不防」):genMovesRanked() 的每一级威胁梯子都同时保留双方的格子——行棋方自己的获胜点在前、对方的紧随其后(参考实现 eval.js bySide/orderedSet 同款结构)。成五级合并 fivesMe+fivesOpp、活四级合并 liveFourMe+liveFourOpp 并让双方冲四随行;单边提前返回只在引擎行棋节点语义成立("我方活四下一手必胜"),放到对方行棋节点会把对方自己的反击点整体隐藏——搜索据此构造出幻影必胜分(例如对方节点候选只剩挡点时,己方活四威胁被评成 winScore−6),根节点于是放弃挡对方的活三转头"进攻"(TestGenMovesKeepsOpponentCounterThreats / TestGenMovesKeepsOpponentFive / TestAnswersLiveThreeWithOwnFourThreat 回归)。附带收益:活四级随行对方的冲四点,x.ooo 跳四的缺口封堵点重新进入候选集。
evaluate() 逐行扫描全部行、列、对角线,由 scoreLineFor() 识别棋型并累计分值,最终评分 = 己方总分 − 对方总分。评分表:
| 棋型 | 分数 | 棋型 | 分数 |
|---|---|---|---|
| 五连 | 10,000,000 | 眠三 | 5,000 |
| 活四 | 1,000,000 | 跳活三 | 15,000 |
| 冲四 | 100,000 | 活二 | 2,000 |
| 活三 | 30,000 | 眠二 | 500 |
评分同时处理跳棋型(如 oo.oo 为冲四、.oo.o. 为跳活三),并要求活三具备延伸空间(x.ooo.x 记为眠三)。
全盘扫描的结果按线缓存(见 5.14):落子/撤子只重扫穿过的 4 条线,evaluate() 本身 O(1) 运行总和;evaluateFull() 保留原扫描逻辑作为位级对照基准。
搜索采用教程(guidebook)第 9 章的 negamax 形式:单一角色,每个节点的分值都从行棋方视角出发(stmEvaluate),父节点把子节点结果取负——score = -negamax(depth-1, -beta, -alpha)——不再有最大化/最小化两份分支,PVS 窗口、胜负分、置换表分值全部统一为一种记法。alpha/beta 约束行棋方视角的分值,剪掉不可能影响决策的分支。搜索中还做了几个关键优化:
- 即时胜负检测:每层落子后先看
winsMove,能成五直接返回winScore - ply(行棋方赢为正;取负后的父节点天然看到"越晚输越好",靠近根的胜利分更高,AI 优先选最快的赢法); - 偶数层迭代:从深度 2 开始按偶数加深,保证叶子局面总是对手应对之后的局面,规避水平线效应;
- PVS 主变例搜索:见 5.12;
- LMR 后期走法降深:安静尾巴(
quietFrom之后)第 4 手起、深度 ≥3 的着法先按 depth−2 零窗侦察,越窗才全深重搜。与剪枝不同,LMR 会改变搜索值(故以lmrEnabled门控,位等价测试套件在无 LMR 栈上运行),强度收益由战术套件 + arena 对弈验收:固定深度 12 节点数 34,858 → 25,047(−28%)。
pointScore() 对每个候选点做快速的「落子后四方向棋型」预估,以 进攻分 + 防守分 降序排序。好的排序让 Alpha-Beta 剪枝接近最优效率,是搜索深度达到 6~8 层的关键。
run() 从深度 2 逐层加深,上一轮最佳着法在下一轮优先搜索;预算由 INFO timeout_turn / time_left(毫秒)换算(默认 1 秒,取 timeout_turn 的 90%,并保留整局时钟 3/4 的余量),超时的层直接放弃、沿用上一层结果。根节点 aspiration 窗口:以上一层分值 ±3 万(一个活三量级,按局面波动自适应放大)为窗口搜索下一层,落窗外围绕观测边界几何加宽重搜——最终重搜保证根分值与全窗逐位一致,仅在分值平稳时省节点。15 路中局实测:深度 12 累计 ~0.55s,深度 14 于 ~2.8s 健全完成(maxSearchDepth 上限即 14)。
不同的走法顺序可能到达同一局面("置换",如先下 A 再下 B ≡ 先下 B 再下 A)。tt.go 为每个 (盘面, 行棋方) 节点维护 64 位 Zobrist 指纹,缓存搜索结果:
- 哈希:每个交叉点 × 双方各分配一个固定种子(splitmix64)生成的 64 位随机数,局面哈希为全部棋子随机数的异或;落子/撤子时只需增量 XOR 一次。行棋方也参与键值(双方角色搜索中"我走"与"对方走"是不同节点)。
- 查询:进入
minimax节点先查表。表内深度足够时:EXACT 直接返回;LOWER(下界,来自 β 截断)满足score ≥ β时返回;UPPER(上界)满足score ≤ α时返回;深度不足则仅复用表内最佳着法置前排序。 - 存储:节点搜索完成后按结果分类——β 截断记 LOWER、值不超过 α 记 UPPER、否则 EXACT(均为行棋方视角的 negamax 分值)。表为两路组相联(guidebook §7.2 同款):"浅而新"与"深而旧"共存一桶,替换选较浅者、严格更深者绝不因更浅新条目被逐(§7.4 深度优先语义的两路版);默认 2¹⁸ 桶 ×2 路,内存维持 ~12.6MB。每路槽位为
atomic.Pointer[ttEntry](2026-08 为 lazy SMP 并发化):写入构造新条目后一次原子替换,读取零锁无撕裂。 - 正确性保障:胜负分
±(winScore − ply)是相对根节点的值,永不作为表内截断返回(其最佳着法仍用于排序);超时中断的层不写表。表长默认 2¹⁹ 条(约 12.6MB),按INFO max_memory自动收缩(变化即时重建)。 - 跨手保留(2026-08):表由
Engine持有而非每次思考新建——Zobrist 种子固定,跨手哈希天然一致,上一手的深条目下一手直接命中。START/RESTART同尺寸沿用;换棋盘尺寸或max_memory变化时重建;新对局(resetSession)丢弃。实测同预算下跨手复用可将转置局面的重搜降到 1%(12 节点 vs 1864),arena 50 局 28:22 优胜。 - 验收测试:固定深度下开启/关闭置换表必须得到完全相同的根分值与着法(
tt_test.goTestTTSearchEquivalence),同深度 8 节点数降至原来的 ~83%,迭代加深整流程耗时约减半。
rules.go 按 INFO rule 代码实现各规则的胜负判定与连珠禁手:
| 代码 | 规则 | 胜负与禁手 |
|---|---|---|
| 0 | Freestyle | 5 个或以上同色连子获胜,无禁手(默认) |
| 1 | Standard | 恰好 5 个获胜,长连不算赢 |
| 4 | Renju | 黑方禁手(三三、四四、长连),白方无禁手且长连算赢 |
| 8/9 | Caro | 恰好 5 个获胜,但两端同时被对方堵住则不算赢(边界算开放端) |
- 胜负判定统一走
winsMove():搜索、算杀、战术捷径全部按当前规则判定,不再使用固定的>=5。 - 禁手在着法生成阶段过滤(
genMoves与算杀的forcingMoves):黑方的候选列表永不包含禁手点,搜索树不会进入非法分支,评估函数与剪枝不受扰动。禁手检测在四个方向上统计「填一格可成五」的四与「填一格可成活四」的三,按成 virtually 同一棋型去重(活四两端/活三两端只算一个,oo_P_oo同线双三正确算两个);五子优先原则(RIF):成五的落点永不禁手。三的枚举要求补填格真正加入连内(k ∈ [−a, b]),避免 P 自身已成活四时线旁旁观空格让 c==4 成立、凭空多出假三把「四三」误判成 3-3 禁手(TestFourThreeIsLegal回归)。 - 黑方归属:引擎可能执黑或执白。
BEGIN(引擎先行)、首着TURN(对手先行)与BOARD首子(连珠按落子顺序)自动判定黑方归属,禁手只作用于黑方。 - 已知简化:三的「该三的成四点本身是否禁手」递归判定未实现(RIF 完整规则),极端局面下可能保守地避开一个合法点,不会走出非法着法。
vcx.go 实现教程第八章的「算杀」:只考虑连续威胁(冲四/活三)的证明搜索。正常迭代加深没有找到必胜解时,先做 12 层 VCF(仅冲四),再做 10 层 VCT(冲四+活三)。时间预算(2026-09 修复):杀棋搜索与主梯子共享同一绝对截止时间(runSingle 按整手预算设定,杀棋分得梯子用剩的部分),不再各阶段「从现在起」追加 1/4、1/8 切片——旧做法叠加后无上界,中盘强制树爆炸时一手可超预算十余倍(arena 实测 5.3s/预算 360ms,TestThinkHoldsWholeMoveBudget 回归)。超时语义:deadline 每节点检查、触发即置 timedOut 并沿递归立即上抛,超时的证明一律视为未定——不报告杀棋、更不得当作已验证的防守(TestKillSearchPastDeadlineInconclusive 回归):
- MAX 层(进攻方):只走己方的四(VCT 再加活三),任一分支成五即证明杀棋;
- MIN 层(防守方):走双方所有的四/活三(防守含反击)与挡成五点、挡活三的成活四点,有一种防住即证明不成杀。防守列表按 quiescence
fourThreats同款双视角构建——若只列防守方自己的四/活三,「没有反四」会被误读成「无法防守」,单纯冲四(挡一格即化解)就被当成必胜返回;成五点入防守列表(key 略低于己方成五,有五先赢)。2026-08 二次修复:VCT 模式下防守层再加「对方活三的成活四点」(key=冲四−1)——否则进攻方把活二长成新活三时,该三尚无成五点可挡、防守集合为空,连一个裸活二都被「证明」必胜并绕过主搜索直接落子(TestKillSearchLiveTwoNotAKill/TestKillSearchFreshThreeDefended回归,真杀测试 7/7 保全;修复后 arena 50 局 26:24 优胜)。活四仍不可防、含强制挡四的真杀链不受影响。防守列表不截断(2026-09):nodeMoveLimit=12的截断只作用于进攻层(漏杀可接受),防守列表被截断等于把成立的防守藏起来、凭空造杀(TestDefenceForcingMovesUncapped回归); - 无假阳性:搜索超出深度视作防守成功,绝不误报杀棋。
- 已证胜局护栏(2026-08):主搜索已证明己方必胜(
prevScore ≥ winScore−64)时整个杀棋搜索跳过——只看连续威胁的窄证明不得用未证明的替代着法覆盖主搜索的全宽胜解。 - 对方杀棋探测(2026-08,参考实现 candidateMinmax 防线):己方 VCF/VCT 均未证明时,以对方为进攻方跑一次 VCT——若对方(假如下一手轮到它)存在连续威胁必胜,则占据其主威胁点,且落子后重探验证封锁确实瓦解证明才采纳(连珠下黑方引擎落封锁点前查禁手)。2026-09 修复:验证不再只试杀线首着一个点,而是枚举己方必答点(成五点封堵 + 对方活三成活四点,上限 6 个)逐个重探,找到任一瓦解证明的点即采纳;封不掉的多点叉威胁交回主搜索着法。超时一律视为未定:探测或验证中途超时都返回 -1,绝不用没搜完的搜索充当「已验证防守」。触发门槛(2026-09):主梯子完成迭代且评分 ≥ 0(不认为有危险)时探测不得覆盖梯子着法——窄证明在安静/占优局面下噪声过大,实测天元主线探测的封堵 (8,6) 黑胜仅 14%、被它覆盖的梯子着法 (6,5) 黑胜 93%,门槛后天元局黑胜率 33.5% → 85.7%(
TestLadderMoveKeptWhenNoDanger/TestOppKillProbeBlocksOpponentKill/TestOppKillProbeLiveTwoNotAKill/TestOppKillProbeRejectsUnblockableFork/TestKillSearchPastDeadlineInconclusive/TestOppKillProbeBoundedOnOverrunPosition回归)。
叶节点采用参考实现同款的 quiescence search,取代教程第九章的「冲四延伸」(落子成四的分支额外 +2 层)——延伸会让强制链子树越搜越深,而 PVS 的零窗口(β=α+1)无法廉价证伪这些链:
- 门控:
quiescence()入口检查「最后一手是否刚成四」(复用extendsOn)。不变量:quiescence 自身展开的每一手都是成四点或挡四点,未决的四只可能来自刚刚那一手——不在门内直接返回静态评估,安静叶节点零额外开销; - 展开集合:
fourThreats()列出双方的成四/成五点(活四两端、跳四缺口均由pointScore识别),双方同权、强者先行,连珠禁手照常过滤;深度上限 4 层(恰好「四→挡→反四→挡」一个来回); - stand-pat:轮走方若已在 (α, β) 之外则直接截断(negamax 视角:行棋方分值 ≥ β 返回、子节点取负递归
-quiescence(3-side, -β, -α)); - 盲区与兜底:双四被挡其一后遗留的四由静态评估的量级兜底(盘面四型已按 −100 万/−10 万 记分);
- 活三扩展(2026-08,默认开):
qThreeEnabled把门控放宽到「末手刚成(含跳)活三」——按穿过该子的实际连子判定(连三须两端皆空;跳三按 ±4 窗口内同色 3 子、跨度 ≤5),展开集合同步把「对方活三的成活四点」纳入挡点(与算杀防守层同款双视角修正,否则门一开对方活三无人能挡)。验收:arena A/B 50 局、400ms/手、seed 11,开 28:22 胜关,默认开、BANGO_QTHREE=0可关;属值改变特性,黄金基线与 LMR 一同排除。
效果对比(对手冲四在手、TestQuiescenceSeesFive 实测,行棋方视角):静态评估 1,000,600(只见四型),quiescence 9,999,998(看到五)。
book/book.go 移植教程原生项目(gobang)的开局库模块,数据格式互通:
- 数据源:JSON(
id/rules/size/coordinateSystem/openings[].coordinates),支持board-row-column与center-relative-x-y(Gomocup 官方开局页格式)两种坐标系;rules支持freestyle/renju,规则或棋盘尺寸不匹配的书整本禁用 - 经典 26 式开局(
opening.go,2026-08):黑1 天元 + 白1 直指/斜指 + 黑2 在天元 5×5 盒内的对称类,规范化去重后恰好 13 直指 + 13 斜指 = 26 式,与经典计数吻合。book gen-classic枚举种子线,book build --seeds以固定深度搜索生长续着(成五点终止行、绝不入书),产出openbook/classic-26-15x15.json。定位为模式引导书(modeBook):其续着来自深度 8 自搜索、弱于实战搜索,arena 实测直接跟走会输棋(11:19),故只作理论区来源、绝不直接采纳;直接采纳仍只用赛事级 gomocup 书 - 模式识别与理论区(P2/P3):
openingModeOf按同一规范化机器识别前三子的模式(黑1 须天元、白1 须贴身直/斜、黑2 须在盒内,4 子时忽略偏离的白2;非天元/远距应答 → 非经典模式,干净降级);modeTheoryZone把模式引导书在该模式规范前缀下的应答投影回棋盘框架——作为软先验:3-4 子时理论格始终保留并排根列表最前,其余安静候选仍须贴身/马步;搜索仍自行裁决 - 编译:每条开局序列按前缀展开成「局面 → 下一手候选」映射,局面经 8 对称规范化(4 旋转 × 2 镜像,字典序最小 key,并记录自同构群);同局面多来源/多对称命中时权重累加
- 查询键为石子集合(颜色相对黑方编码)——引擎无需着法历史,
BOARD恢复的局面同样可命中;首着泛化:单子局面未直接命中时,把书里所有「第 1→2 手」位移平移到实际首子位置,按贴身/马步准则过滤(与开局先验同判据——远距位移对随机首子是几何上的任意点,自对弈实测 42 局随机开局中 38 局产生远距首应答,过滤后归零),且只偏置排序、不直接采纳(arena 实测翻译候选直接采纳对基线 26:34 落败,降级为排序后 29:31 平手;精确命中的书着不受影响照常采纳)(TestTranslatedFallbackClassicOnly回归) - 使用决策(
run(),与原生实现一致):当前局面无强制性威胁(hasOpenThreat:任一方有成四/成五点,或按精确棋型——连三两端空 / ±4 窗口 3 子跳三——存在活三制造点)时直接采纳权重最高的书着法;战斗局面只把书权重注入根节点排序(applyBookOrdering),搜索仍自行裁决。2026-08 门槛重写:旧hasThreatAtLeast(活三)用四方向pointScore求和,而dirShape对跳形打半价(15000),两个互不相干的跳形在不同方向求和 30100 即冒充活三——加上贴身接触局面从第二手起必然存在真实 30000 点,"直接采纳"路径实际从未生效;新判定逐方向精确分类(TestHasOpenThreatShapes回归)。颜色映射修复:书键按"相对黑方"编码颜色,aiMove原本只在rule != 0时调用setRule,freestyle(默认规则)下执白时blackSide停留在默认playerMe,查询键颜色全部颠倒、第二手起永远匹配失败(只有不校验颜色的单子平移兜底幸存)——现无条件设置映射(TestBookWhiteFreestyleAdoption回归) - 护栏:书着法先过
isForbidden(连珠)/占位过滤;书文件缺失或损坏静默降级为纯搜索 - 加载:
INFO folder指定的持久目录下pbrain-bango/book.json优先,其次引擎可执行文件同目录book.json(协议提供即独占);否则装载工作目录/可执行目录openbook/下的默认书——gomocup-2026-15x15.json(直接采纳书)与classic-26-15x15.json(模式引导书),工作目录兜底覆盖go run .的临时可执行文件场景;规则或尺寸不匹配时静默禁用 - 无库开局先验(2026-08,默认开,
BANGO_OPENPRIOR=0关):书未命中/未采纳时,前 4 子内根节点的安静候选(落子后不成活三及以上,精确棋型判定)必须与任意棋子贴身(切比雪夫 ≤1)或成马步(曼哈顿 3)——远距斜二、带缺口的分裂跳二等非经典发展被过滤;强制着法(成三/四/五点)与书采纳路径完全不受影响,过滤后为空则回退原列表
维护子命令:
pbrain-bango book validate book.json # 六项校验:越界/重复手/成五/连珠禁手/行棋方/局面重复
pbrain-bango book build --out book.json --depth 8 --ply 4 --width 2 --time 60000
# 自搜索增长开局树(确定性深度搜索评分,逐位置保留
# margin 内的最优候选,按规范键去重置换)PVS 的依据是:主要变例通常只有一条,找到第一个能产生截断的走法后,其余走法大概率不比它好。minimax() 与根节点 searchRoot() 中:第一个走法全窗口搜索;其后走法先以零窗口(己方 (α, α+1)、对方 (β−1, β))侦察,只证明该走法落在窗口外;侦察值仍落在 (α, β) 内才以全窗口重搜定值。winsMove 与叶子分支本就是精确值,不侦察不重搜。
PVS 是值等价变换——固定深度下根分值与着法与普通 Alpha-Beta 完全一致(TestPVSSearchEquivalence),节省的时间转化为同预算下更深的迭代。收益高度依赖排序质量:威胁阶梯 + TT 手置首 + 杀手走法使「第一手即最佳」的假设大体成立(实测 16 万次侦察仅 246 次落窗重搜)。
排序信号在威胁阶梯与 TT 手之上再加两级统计(教程第九章链接项,参考实现同款机制):
- 杀手走法:每个 ply 两槽记录「刚在本层引发 β 截断的着法」(去重移位),排序时提到 TT 手之后的 1、2 号位——它们是本层被验证过的反驳着法,且紧随其后的零窗口侦察让失败成本近零;
- 历史启发:按角色分表的
history[2][n×n],截断时+= depth²(越深的子树被截断越值得早试);每次迭代加深前全表减半老化。应用范围严格分段:genMovesRanked()返回安静尾巴起点,成五/冲四/双三的威胁阶梯原序保留(TestGenMovesPriorityLadder金丝雀),仅活三及更弱的尾巴按统计稳定重排,slot 0 永不重排。
实测(中局基准深度 12):杀手走法把节点数砍半(73,717 → 35,296);历史启发在杀手之后边际收益 ~0.5%(杀手指槽已覆盖大部分截断场景),保留因其成本近零、且安静尾巴更宽的实战局面中收益更大。
全盘 6n−2 条线(n 行、n 列、两族对角各 2n−1)各自缓存双方棋型分,evalTotal 为运行总和。落子/撤子统一走 setStone():增量 XOR 哈希 + 只重扫穿过该点的 4 条线(O(n)),evaluate() O(1) 返回。选线缓存而非教程的逐点缓存,是为了位级等价——评估语义一字不改,全部开/关等价测试自动成为回归覆盖。正确性门槛:TestEvalIncrementalExact 在 9/15 路、freestyle/renju 四种模式下随机走子+悔子 3,000 步,每步断言缓存总和与全盘扫描逐位相等。基准耗时再降 3~4%(杀手剪枝后叶节点占比已低;叶评估 O(1) 化为后续「位置加成」类改进提供增量维护的基础)。
两个 env 门控特性,代码就绪但默认关闭:
- 位置加成
BANGO_POS=1:中心金字塔项(天元 7 环、每环 ×10、至多 70 分,压在眠二 500 之下)按子增量维护(posTotal运行总和),evaluateFulloracle 同步扩展——位级等价传统保留(TestEvalIncrementalExact断言语义evaluate()对 oracle)。arena 验收未通过(50 局、400ms/手、seed 7:关 30:20 胜开),判定每节点成本与中心化平手判定得不偿失,默认关;量级调小或限定开局阶段后可重验。 - lazy SMP
BANGO_SMP=N(N>1 个 worker):run()分裂 N 个私有 searcher(私有棋盘/历史/杀手,共享一张 Engine 级 TT),各自完整跑迭代加深、根着法按 worker 序号轮转(搜索树互补填表),聚合取最深完成迭代、同深 worker 0 优先。前提是 TT 并发安全(每路atomic.Pointer,见 5.7)。测试语义:TestSMPWorkersAgreeOnScore断言各 worker 分值与单线程全等(表共享只返回合法条目——硬门槛),着法不作断言(轮转下并列最优可合法漂移);TestSMPTTRaceClean以 4 worker 压小表过-race。SMP 天然非确定,默认关,收益由 arena 让时对弈评估。
negamax_test.go:13 组固定局面固定深度的根分值/着法黄金基线,保值搜索栈(LMR 关闭)必须逐位复现预置数值(根节点行棋方恒为引擎,分值语义不变);algorithm_test.go:棋型评分表逐例校验;「一步成五必取」「对方四必挡」「活三必防」等战术断言;评估函数换边反对称校验;着法生成优先级阶梯(成五/活四级双方合并、行棋方在前,TestGenMovesKeepsOpponentCounterThreats回归「活三不防」根因)、Chebyshev-2 候选窗的边界裁剪与去重、quietFrom强制段/安静尾巴排序契约;门控 quiescence 的门控等价、五威胁识别、lastP 未知保守展开、双活四/遗留四的静态兜底(TestQuiescenceGateQuietPosition/TestQuiescenceSeesFive/TestQuiescenceLeftoverFourFallsBackToStatic等);tt_test.go:Zobrist 增量哈希一致性、置换表读写/替换策略/同槽异锁碰撞保护、固定深度与整轮迭代加深的开/关等价性、max_memory表收缩、剪枝安全性(开/关 Alpha-Beta 结果必须一致)与提速统计;PVS、杀手走法、历史启发各自的开/关等价性与提速统计;TT 胜利分区间守卫的正/反对照(区外深度条目直接复用、区内绝不作 cutoff,TestTTWinScoreGuard);评估线缓存的位级属性测试与线 id 编解码契约(TestEvalIncrementalExact/TestLineGatherRoundTrip),含悔棋-重放型序列的TestEvalIncrementalWithTakebacks;shape_cache_test.go:增量候选状态(子数/邻域引用计数/逐向棋型值缓存)的正确性门槛——随机 make/undo 序列逐步断言缓存与独立 oracle、计数器与全盘重扫逐位相等;time_test.go:thinkBudget预算公式表驱动校验;极小预算下战术捷径必命中、真实思考必返回合法着法;deadline 过期后run回退且置换表零写入;中止节点(入口/移循环中)不入表;小预算中断后续搜满深度必须收敛到全新搜索的同分同着(TestInterruptedThenResumedConsistent);search_bench_test.go:中局/战术两个固定图面的确定性搜索基准(go test -run '^$' -bench BenchmarkSearch -benchtime 1x -v),输出节点数与截断数供优化对比;vcx_test.go:双三/活三必胜杀棋必须找到、安静局面不得误报、对方冲四可破解杀棋、可挡冲四不得误报必杀、经过强制挡四的真杀链必须找到;rules_test.go:三三/四四/长连禁手、四三合法、跳四交叉 4-4、白棋无禁手、五子优先覆盖禁手、着法生成过滤(黑滤/白留)、各规则(0/1/4/8/9)胜负语义(含 caro 边缘端点、standard 长连不胜)、连珠自对弈完局;禁手枚举后增量评估/哈希不被扰动;book_test.go/book_engine_test.go/bookcmd_test.go:开局库编译与对称查询、权重累积、首着平移、规则/尺寸护栏、禁手候选过滤、协议端到端命中、validate/build 闭环;执白 freestyle 颜色映射采纳、精确威胁门槛六例(安静/双方向跳二伪威胁/连三/跳三/x.x.x/成四点)、openbook 默认书路径与装载;ai_selfplay_test.go:完整自对弈回归(合法性、能分出胜负)与搜索深度/节点数统计(go test -v -run TestSearchStats查看);protocol_test.go:黑盒协议一致性(管道/TCP)、START 尺寸边界(5 OK / 4 与非法输入 ERROR)、TURN/TAKEBACK 错误路径与越界坐标宽容语义、BOARD 回着、field-3 标记格、可选命令、连珠避禁手、未知命令保活;engine_test.go:START 前棋盘已分配(回归)、渲染内容、满盘run返回 (−1,−1) 与aiMove盘内兜底;arena_test.go:cmd/arena冒烟测试(-short跳过),两局快棋比分汇总校验;gui/test/:三个 node 测试套件(npm test)——forbidden.test.mjs前端规则 12 例、protocol.test.mjsEngineManager 应答配对/MESSAGE 剥离/超时/断连/TAKEBACK·BOARD 分型(FakeLink 注入,EngineLink的 fetch/EventSource 浏览器路径不测)、board.test.mjs棋盘坐标映射与cellAt命中判定(Proxy 伪 ctx 桩掉 canvas)。
BANGO_ALGO=alphabeta ./pbrain-bango # 默认:完整搜索 AI
BANGO_ALGO=random ./pbrain-bango # 加权随机基线(带前一手杀局判断)BANGO_ALGO 支持任意已注册算法名(algorithm.Names():alphabeta、random);未知名字在 stderr 告警后回退 alphabeta,引擎始终可对弈。
# 日常开发
go build -o pbrain-bango .
# 发布构建(优化参数:-s -w 去除符号表与 DWARF 调试信息,
# -trimpath 抹除本地路径,产物更小且不含构建机信息)
go build -trimpath -ldflags="-s -w" -o pbrain-bango .引擎作为控制台程序,通过 stdin/stdout 与管理器交互。可手动测试:
echo -e "START 15\nBEGIN\nTURN 7,8\nEND" | ./pbrain-bango预期输出示例:
OK
7,7
6,6
OK
除 stdin/stdout 外,引擎可通过 web 子命令把协议迁移到 TCP socket 上:
./pbrain-bango web # 监听 0.0.0.0:9527
./pbrain-bango web --port 8080 # 指定端口
./pbrain-bango web --port 8080 --addr 127.0.0.1 # 指定端口与绑定地址| 子命令/参数 | 默认值 | 说明 |
|---|---|---|
web |
— | 子命令:启用 TCP socket 模式,替代 stdin/stdout |
--port |
9527 | 监听端口(仅在 web 子命令下有效) |
--addr |
0.0.0.0 | 绑定地址(仅在 web 子命令下有效) |
不带子命令直接运行(./pbrain-bango)即管道模式;在管道模式下传 --port/--addr 会得到 unknown command 错误并退出。
行为说明:
- 客户端连上后,协议命令从 socket 按行读取、回复按行写回,行格式与命令语义和管道模式完全一致(telnet / nc 可直接调试)。
- 串行单会话:同一时刻只服务一个连接;客户端断开或发送
END后连接关闭、棋盘重置,服务进程回到 accept 等待下一个连接(第二个客户端在 backlog 中排队)。 - web 模式下
END表示对局结束:无应答、关闭当前连接、等待新会话;进程常驻。 - 监听地址打印到 stderr。
将编译产物 pbrain-bango 配置到 Gomocup 竞赛管理器(如 Piskvork)中即可参与对局。引擎会响应 ABOUT 命令返回,其中 ai 字段为当前选中的算法(BANGO_ALGO,默认 alphabeta):
name="pbrain-bango", version="0.1", author="cale && GLM5.3", ai="alphabeta"
以引擎先手为例(详见 protol.md 第 198-211 行):
管理器 → 引擎:START 15 引擎 → 管理器:OK
管理器 → 引擎:BEGIN 引擎 → 管理器:7,7 (引擎第一步)
管理器 → 引擎:TURN 8,8 引擎 → 管理器:9,8 (引擎落子)
...(对局继续)...
管理器 → 引擎:END 引擎 → 管理器:OK
搜索优化(PVS、门控 quiescence + 活三扩展、杀手/历史、置换表、评估缓存)、VCF/VCT 算杀、连珠禁手与开局库均已落地(见第五节)。2026-08 第二轮优化已完成以下各项(各自带验收,详见对应小节):
- 首着泛化贴身过滤 + 降级排序(5.11,2026-08 第四轮):平移兜底候选按贴身/马步准则过滤(远距首应答 38/42 → 0)并降级为仅排序——arena 实测直接采纳 26:34 回归,排序化后 29:31 与基线平手
- 经典 26 式开局:枚举书 + 模式识别 + 理论区先验(5.11,2026-08 第四轮):对称枚举独立复现 26 式计数(13 直 + 13 斜),
book build --seeds自搜索生长经典书并入默认装载;模式分类器与理论区投影给偏离局面软引导;顺手修复book validate多行间不悔棋的潜伏缺陷 - 开局库三层修复 + 无库开局先验(5.11 / 5.2,2026-08 第三轮):freestyle 执白颜色映射修复、
hasOpenThreat精确门槛重写、默认装载openbook/gomocup-2026-15x15.json(Gomocup 2026 官方开局);前 4 子安静候选贴身/马步先验(BANGO_OPENPRIOR可关),协议端到端验证白棋对天元/跳形开局全部按书应答 - 威胁梯子双保留 + 对方杀棋探测(5.2 / 5.9,2026-08 第三轮):修复「活三不防」正确性缺陷——对方行棋节点上单边提前返回隐藏对方反击点、搜索构造幻影必胜并放弃防守;照参考实现 eval.js 把成五/活四级改为双方合并、行棋方在前,并新增对方杀棋探测(验证封锁才采纳)与已证胜局护栏。arena 对旧引擎 50 局 50:0(seed 7/13 各 30/20 局、300ms/手),回归测试 6 例
- VCT 防守层补挡活三(5.9):修复「活二级假杀」正确性缺陷,arena 26:24 优胜
- TT 跨手保留:表由
Engine持有、START/RESTART同尺寸沿用、换尺寸/换预算重建;newSearcherWithTT借用注入,跨手命中复用实测 12 节点 vs 1864 节点(1%),arena 28:22 优胜 - 协议参数补齐:
INFO max_depth(钳制迭代加深上限)/max_node(limitHit()周期检查、走既有中断路径)已解析并挂钩,端到端测试覆盖 - quiescence 活三扩展(5.10,默认开):arena 28:22 优胜
- lazy SMP(默认关):TT 每路
atomic.Pointer化,run()分裂多 worker 共享表、根着法轮转散开搜索树、取最深完成迭代;go test -race全绿(含顺手修复的存量 stderr 竞争),BANGO_SMP=N开启。arena 验收:4 worker 31:19 优胜单线程(50 局、400ms/手、seed 13,单侧 p≈0.04)——收益明确;默认暂关以保持引擎着法可复现,竞赛场景建议BANGO_SMP=4启动 - 位置加成(默认关):中心金字塔项按子增量维护、oracle 同步扩展(位级等价保留),但 arena 50 局关 30:20 胜开——每节点成本与中心化平手判定得不偿失,代码留
BANGO_POS=1开关供后续调参(缩小量级或仅限前 N 手时再验收)
按预期收益排序的后续方向:
-
SMP 默认值评估:首轮 arena 数据见下文,样本量与显著性不足时保持默认关
-
位置加成调参再验收:量级减半(≤35)或仅开放局前 10 手,重跑 arena
-
棋力验收:值等价或位级等价的优化(固定深度棋力不变、速度提升)由开/关等价测试守护;战术视野类改动(如 quiescence)最终需同预算、≥50 局、开局多样化的自对弈对局集确认
-
对弈 harness:上述验收固化为可复现命令(
cmd/arena,管道对弈、随机首着开局(PLAY强制,对手经首个TURN获知;两颗预置子会让后手引擎盲视,不可行)、颜色各半、非法着/超时/崩溃判负,比分逐局打印,-verbose打印每手、-json落盘,-time-turn-b支持让时对弈):go run ./cmd/arena -games 40 -time-turn 400 ./pbrain-a ./pbrain-b go run ./cmd/arena -games 30 -time-turn 200 -time-turn-b 400 ./pbrain-new ./pbrain-old
关于教程第九章列出但不实现的两项,理由备查:**TSS(威胁空间搜索)**已由 genMoves 威胁分级(防守点与攻击点同权分类)+ VCF/VCT 的防守层覆盖;米字进攻路径教程作者自认存在未解决的误判情形,威胁分级生成已达成同类剪枝效果。