原始碼導讀
皮卡魚由許多分工明確的模組組成。搜尋決定重點算哪些變化,評估給局面打分,局面模組負責落子、撤銷和規則,執行層把它們組織起來,與介面交換指令和結果。
這組文章以 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:幫助觀察預設任務的節點數、用時等;比較時,除待測改動外,引擎版本、網路、執行緒、硬體和參數等條件應保持一致。
- 對弈測試:才直接考察修改對實戰結果的影響,需要足夠樣本和一致的測試條件。更快、更深或某一題算對了,都不能單獨證明整體棋力更強。
