源码导读
皮卡鱼由许多分工明确的模块组成。搜索决定重点算哪些变化,评估给局面打分,局面模块负责落子、撤销和规则,运行层把它们组织起来,与界面交换指令和结果。
这组文章以 official Pikafish 提交 1c66b9b 为准。每页都附对应源码;算法名称相同,不代表其他引擎或其他版本的实现细节也相同。
先按你想知道的问题读
| 想弄明白什么? | 从这里开始 |
|---|---|
| 怎样从大量变化中找出好棋? | 搜索算法:排序、缓存、剪枝、减深与延伸。 |
evaluate.cpp 做什么,NNUE 怎么打分? | 局面评估与 NNUE:输入特征、增量计算、网络输出与评估修正。 |
| 棋盘怎样存,怎么走棋、撤销和判规则? | 局面表示与规则:位棋盘、状态、哈希键、合法性与走棋历史。 |
| 界面怎样启动引擎,线程和内存怎样工作? | 引擎运行与通信:命令处理、结果输出、资源分配与诊断。 |
如果只想调整软件设置,先看 UCI 选项;如果要写界面或程序连接引擎,可结合 UCI 协议 阅读。
鳕鱼 NNUE 文档 提供更深入的通用背景;理解当前皮卡鱼的具体实现,先读上面的评估页。
从一次“开始分析”看各部分怎样配合
- 启动准备。
main.cpp初始化攻击表和局面所需的数据,再进入 UCI 命令循环。 - 收到局面。 UCI 层解析
position,运行层建立局面,并按附带的moves逐步落子、保存状态。仅有一张当前盘面的 FEN,不能恢复此前完整的走棋历史。 - 收到搜索要求。
go携带时间、深度等限制;Engine检查网络并交给搜索线程。 - 展开变化。 搜索调用走法生成与合法性检查,落子后递归;需要局面估分时调用评估。算完一个分支后撤销走法,返回上一个局面。
- 反馈结果。 运行层通过回调交出搜索信息,由 UCI 层输出
info和最终的bestmove,界面再决定怎样显示。
这里的 Engine 是负责协调的 C++ 类,不是另一个独立引擎。go 启动的是后台搜索;命令循环还需要能接收 stop 等指令。
源码:程序入口、Engine 接口、启动搜索与设置局面。更完整的命令流程见 引擎运行与通信。
文件地图:遇到问题该去哪里找
不必从第一个文件顺序读到最后。先找到职责,再沿函数调用往下看,会更容易理解。
搜索与评估
| 文件 | 主要负责什么 |
|---|---|
search.cpp | 迭代加深、PVS、剪枝、深度调整、静态搜索,以及搜索结果的处理。 |
movepick.cpp、history.h | 候选走法按什么顺序取出,以及搜索中积累的历史统计。 |
evaluate.cpp | 调用 NNUE,并把网络结果调整为搜索使用的静态评估。 |
nnue/network.cpp 与 nnue/nnue_architecture.h | 网络读写、推理入口,以及网络各层的连接方式。 |
nnue/features/、nnue/nnue_accumulator.cpp | 把局面变成网络输入,并复用相邻局面之间的计算。 |
局面、走法与规则
| 文件 | 主要负责什么 |
|---|---|
types.h、bitboard.h | 格点、棋子、走法等基础定义,以及位棋盘运算。 |
attacks.cpp、attacks.h | 攻击表的初始化和查询,处理车炮线路、马腿、象眼等攻击关系。 |
movegen.cpp | 按吃子、非吃子、应将等需求生成候选走法。 |
position.h、position.cpp | 保存与更新局面、检查将帅安全、撤销走法、估计换子,以及处理依赖历史的棋规。 |
运行、通信与工具
| 文件 | 主要负责什么 |
|---|---|
main.cpp、engine.cpp | 程序入口,以及局面、网络、线程、选项等资源的协调。 |
uci.cpp、ucioption.cpp | 接收命令、解析参数、输出结果和处理选项。 |
score.cpp | 把内部评分转换成对外使用的分数表示;不负责选择最佳走法。 |
thread.cpp、timeman.cpp、tt.cpp | 线程、时间预算和置换表存储。 |
numa.h、memory.cpp | 线程与内存的硬件布局、内存分配等配套工作。 |
perft.h、benchmark.cpp | 枚举合法走法路径,或运行预设测试任务;两者检验的内容不同。 |
Makefile、universal/ | 编译配置,以及通用包中的运行时实现选择。 |
这张地图按阅读需求归组,不表示模块之间只有单向调用。例如搜索会访问局面、评估、置换表和历史表;运行层则负责把这些资源准备好。
读代码时,先分清这几件事
搜索分数、静态评估和界面分数分属不同环节。 不能看到 evaluate() 返回一个值,就当它是界面最终显示的分数;也不能把输出的 cp 数字直接当成胜率或等级分。详见 评估流程 和 分数输出。
当前局面与历史状态也不是同一回事。 棋子摆放相同,行棋方、限招计数或前面的将捉循环可能不同。读缓存和规则代码时,要一起看它们使用了哪些状态。
名字和注释需要结合真实调用核对。 Pikafish 源码保留了 Stockfish 命名空间和部分上游术语。看到一个熟悉名字,不等于当前象棋版本使用了国际象棋的所有机制;应当继续看函数在哪些条件下调用、实际走的是哪条分支。
找到方法不等于找到固定参数。 剪枝余量、网络结构和编译选择都可能改变。本文固定提交链接就是为了让解释和代码对应;阅读新版源码时,需要重新核对。
修改以后,怎样知道改对了
不同检查回答不同问题:
- 能编译、能启动:说明这一构建路径基本可用,不能证明棋规和搜索都正确。
- Perft:适合检查候选走法、合法性过滤及落子撤销的组合是否符合预期;不能代替长将、长捉等历史规则测试。
- 规则与局面用例:使用明确的盘面和完整走棋历史,核对终局、分数或规则处理。
- Benchmark:帮助观察预设任务的节点数、用时等;比较时,除待测改动外,引擎版本、网络、线程、硬件和参数等条件应保持一致。
- 对弈测试:才直接考察修改对实战结果的影响,需要足够样本和一致的测试条件。更快、更深或某一题算对了,都不能单独证明整体棋力更强。
