SGF(Smart Game Format)是围棋棋谱的通用交换格式,所有主流围棋软件和 AI 都认它。这篇文章把 SGF 的树形结构、核心属性、解析策略讲清楚,并给出一个 ~200 行的 C++ 读写实现。


1. SGF 到底是什么

SGF 最早由 Anders Kierulf 的 Smart Go 程序定义,后来被广泛采纳为围棋棋谱标准。本质上是一个树形结构的纯文本格式

(;  ← 根节点开始
    GM[1]           ← Game: Go
    SZ[19]          ← Size: 19×19
    KM[6.5]         ← Komi: 6.5目
    PB[柯洁]        ← Player Black
    PW[AlphaGo]     ← Player White
    RE[B+Resign]    ← Result: 黑中盘胜
    ;B[pd]          ← 黑第1手: Q16 位
    ;W[dp]          ← 白第2手: D4 位
    ;B[pp]          ← 黑第3手...
    ;W[dd]
)

一个 .sgf 文件就是一个根节点 (),里面可以嵌套任意深度的子节点,形成棋谱树。主线是最左侧分支,其他分支是变化图/参考图。


2. 格式规则

2.1 基本结构

Collection  = GameTree { GameTree }
GameTree    = "(" Sequence { GameTree } ")"
Sequence    = Node { Node }
Node        = ";" { Property }
Property    = PropIdent PropValue { PropValue }
PropIdent   = UcLetter [ UcLetter ]   ← 大写字母标识 (1-2字符)
PropValue   = "[" Text "]"

关键规则:

  • 属性名是 1-2 个大写字母:B(黑)、W(白)、SZ(尺寸)、KM(贴目)…
  • 属性值用 [] 包裹,一个属性可以有多个值:AB[aa][bb][cc]
  • ; 开始一个新节点
  • 嵌套 () 表示变化分支
  • \ 是转义符,\] 表示字面 ]

2.2 棋子坐标

SGF 用字母对表示棋盘交叉点:

列: a b c d e f g h i j k l m n o p q r s  (跳过 I)
行: a b c d e f g h i j k l m n o p q r s  (同样跳过 I)

示例:
  aa = 左上角 (0,0)  ← 不是 A1! SGF 的 origin 在左上角
  ss = 右下角 (18,18)
  pd = Q4  ← p列第16 p=16th, d=4th → (3,15)
  dp = D16
  jj = 天元 (9,9)

转换公式:

// SGF坐标 → 0-indexed (row, col)
int col = sgfCoord[0] - 'a';
int row = sgfCoord[1] - 'a';

// 0-indexed (row, col) → SGF坐标
char colChar = 'a' + col;
char rowChar = 'a' + row;

2.3 核心属性速查

属性 含义 示例
GM[1] 游戏类型 (1=围棋) GM[1]
SZ[19] 棋盘尺寸 SZ[19] / SZ[9] / SZ[13]
KM[6.5] 贴目 KM[6.5] / KM[7.5]
PB[柯洁] 黑方棋手 PB[AlphaGo]
PW[AlphaGo] 白方棋手 PW[Lee Sedol]
RE[B+Resign] 结果 RE[W+1.5] RE[B+R]
DT[2026-05-11] 日期 DT[2026-05-11]
HA[2] 让子数 HA[0] 平分
RU[Chinese] 规则 RU[Japanese]
B[pd] 黑落子 坐标格式
W[dp] 白落子 坐标格式
AB[dd][pp] 初始黑子(让子) 可多值
AW[jj] 初始白子 可多值
C[comment] 注释 C[这手不太好]
N[节点名] 节点标签 N[Figure 1]

3. 棋谱树

真正的棋谱不是线性序列——它是一棵树:

(;  ← 根节点 (元信息)
    ;B[pd]  ← 第1手
    ;W[dp]  ← 第2手
    (;B[pp] ← 变化A: 黑走P17
     ;W[dc]
    )
    (;B[dd] ← 变化B: 黑走D4
     ;W[pp]
    )
    ;B[fc]  ← 主线下第3手
    ;W...
)

遍历主线的算法: DFS 优先走第一个子节点(最左侧分支)。

// 获取主线所有着法
std::vector<Move> extractMainLine(const SGFNode* root) {
    std::vector<Move> moves;
    const SGFNode* node = root->firstChild;
    while (node) {
        if (node->hasProperty("B"))
            moves.push_back({BLACK, node->getCoord("B")});
        else if (node->hasProperty("W"))
            moves.push_back({WHITE, node->getCoord("W")});
        node = node->firstChild;  // ← 始终走第一个子节点
    }
    return moves;
}

4. C++ 解析器实现

4.1 数据结构

// SGF Node
struct SGFNode {
    std::map<std::string, std::vector<std::string>> props;
    std::vector<SGFNode*> children;
    SGFNode* parent = nullptr;

    bool hasProperty(const std::string& key) const {
        return props.count(key) > 0;
    }

    std::string getString(const std::string& key,
                          const std::string& def = "") const {
        auto it = props.find(key);
        return (it != props.end() && !it->second.empty())
            ? it->second[0] : def;
    }

    int getInt(const std::string& key, int def = 0) const {
        std::string s = getString(key);
        return s.empty() ? def : std::stoi(s);
    }

    std::pair<int,int> getCoord(const std::string& key) const {
        std::string s = getString(key);
        if (s.size() >= 2)
            return {s[0] - 'a', s[1] - 'a'};
        return {-1, -1};
    }
};

4.2 解析器

class SGFParser {
public:
    SGFNode* parse(const std::string& input) {
        m_pos = 0;
        m_input = input;
        return parseGameTree(nullptr);
    }

private:
    std::string m_input;
    size_t m_pos = 0;

    // 消费空白和换行
    void skipWS() {
        while (m_pos < m_input.size() &&
               (m_input[m_pos] == ' ' || m_input[m_pos] == '\n'
                || m_input[m_pos] == '\r' || m_input[m_pos] == '\t'))
            m_pos++;
    }

    // 解析 GameTree := "(" Sequence { GameTree } ")"
    SGFNode* parseGameTree(SGFNode* parent) {
        if (m_pos >= m_input.size() || m_input[m_pos] != '(')
            return nullptr;
        m_pos++; // 跳过 '('

        SGFNode* node = parseSequence(parent);
        if (!node) { m_pos++; return nullptr; }

        // 递归解析嵌套子 GameTree (变化分支)
        skipWS();
        while (m_pos < m_input.size() && m_input[m_pos] == '(') {
            SGFNode* child = parseGameTree(node);
            if (child) node->children.push_back(child);
            skipWS();
        }

        // 期待 ')'
        if (m_pos < m_input.size() && m_input[m_pos] == ')')
            m_pos++;

        return node;
    }

    // 解析 Sequence := Node { Node }
    SGFNode* parseSequence(SGFNode* parent) {
        skipWS();
        SGFNode* first = nullptr;
        SGFNode* prev = parent;

        while (m_pos < m_input.size() && m_input[m_pos] == ';') {
            m_pos++; // 跳过 ';'
            SGFNode* node = new SGFNode;
            node->parent = prev;
            parseProperties(node);

            if (prev && prev != parent)
                prev->children.push_back(node);
            if (!first) first = node;
            prev = node;
        }

        return first;
    }

    // 解析 Property := PropIdent PropValue { PropValue }
    void parseProperties(SGFNode* node) {
        skipWS();
        while (m_pos < m_input.size() &&
               std::isupper(m_input[m_pos])) {

            // PropIdent: 1-2 个大写字母
            std::string ident;
            ident += m_input[m_pos++];
            if (m_pos < m_input.size() && std::isupper(m_input[m_pos]))
                ident += m_input[m_pos++];

            // PropValue := "[" Text "]"
            std::vector<std::string> values;
            while (m_pos < m_input.size() && m_input[m_pos] == '[') {
                m_pos++; // 跳过 '['
                std::string value;
                while (m_pos < m_input.size() && m_input[m_pos] != ']') {
                    if (m_input[m_pos] == '\\') {
                        m_pos++; // 跳过转义符
                        if (m_pos < m_input.size())
                            value += m_input[m_pos++];
                    } else {
                        value += m_input[m_pos++];
                    }
                }
                if (m_pos < m_input.size()) m_pos++; // 跳过 ']'
                values.push_back(value);
            }

            node->props[ident] = values;
            skipWS();
        }
    }
};

4.3 使用示例

std::string sgf = R"(
(;GM[1]SZ[19]KM[6.5]PB[Black]PW[White]
 ;B[pd];W[dp];B[pp];W[dd]
 ;B[fq];W[cn];B[qn])
)";

SGFParser parser;
SGFNode* root = parser.parse(sgf);

// 读取元信息
int boardSize = root->getInt("SZ", 19);
double komi = std::stod(root->getString("KM", "6.5"));
std::string black = root->getString("PB");
std::string white = root->getString("PW");

// 遍历主线着法
int moveNum = 0;
SGFNode* node = root->children.empty() ? nullptr : root->children[0];
while (node) {
    if (node->hasProperty("B")) {
        auto [row, col] = node->getCoord("B");
        printf("Move %d: Black (%d,%d)\n", ++moveNum, row, col);
    } else if (node->hasProperty("W")) {
        auto [row, col] = node->getCoord("W");
        printf("Move %d: White (%d,%d)\n", ++moveNum, row, col);
    }
    node = node->children.empty() ? nullptr : node->children[0];
}

5. SGF 生成

class SGFWriter {
public:
    std::string write(const SGFNode* root) {
        m_result.clear();
        writeNode(root, 0);
        return m_result;
    }

private:
    std::string m_result;

    void writeNode(const SGFNode* node, int indent) {
        if (!node) return;

        m_result += "\n" + std::string(indent, ' ') + ";";

        for (auto& [key, values] : node->props) {
            m_result += key;
            for (auto& v : values)
                m_result += "[" + escape(v) + "]";
        }

        for (auto* child : node->children)
            writeNode(child, indent + 1);
    }

    std::string escape(const std::string& s) {
        std::string r;
        for (char c : s) {
            if (c == ']' || c == '\\') r += '\\';
            r += c;
        }
        return r;
    }
};

生成示例:

SGFNode* root = new SGFNode;
root->props["GM"] = {"1"};
root->props["SZ"] = {"19"};
root->props["PB"] = {"柯洁"};
root->props["PW"] = {"AlphaGo"};

auto* moveNode = new SGFNode;
moveNode->props["B"] = {"pd"};
root->children.push_back(moveNode);

SGFWriter writer;
std::string sgf = writer.write(root);
// 输出: (;GM[1]SZ[19]PB[柯洁]PW[AlphaGo];B[pd])

6. 实战集成:与 GoBoard 联动

// 保存棋局为 SGF
std::string GoBoard::toSGF() const {
    SGFNode root;
    root.props["GM"] = {"1"};
    root.props["SZ"] = {std::to_string(SIZE)};
    root.props["KM"] = {std::to_string(m_komi)};

    SGFNode* prev = &root;
    for (auto& move : m_moves) {
        auto* node = new SGFNode;
        char col = 'a' + move.col;
        char row = 'a' + move.row;
        node->props[move.color == BLACK ? "B" : "W"] = {
            std::string(1, col) + std::string(1, row)
        };
        prev->children.push_back(node);
        prev = node;
    }

    SGFWriter w;
    return "(" + w.write(&root) + "\n)";
}

// 从 SGF 加载棋局
void GoBoard::loadSGF(const std::string& sgf) {
    SGFParser parser;
    SGFNode* root = parser.parse(sgf);
    if (!root) return;

    // 读取元信息
    m_komi = root->getInt("KM", 65) / 10.0;  // KM[65] → 6.5

    // 逐手落子
    SGFNode* node = root->children.empty() ? nullptr
                                           : root->children[0];
    while (node) {
        if (node->hasProperty("B")) {
            auto [r, c] = node->getCoord("B");
            playMove(r, c, BLACK);
        } else if (node->hasProperty("W")) {
            auto [r, c] = node->getCoord("W");
            playMove(r, c, WHITE);
        }
        node = node->children.empty() ? nullptr
                                      : node->children[0];
    }
}

7. 待扩展

特性 说明 优先级
变化图 () 嵌套解析多分支(已支持解析,待棋谱树 UI 展示)
注释 C[comment] 解析和展示
标记 TR[aa](三角) SQ[bb](方框) 等图形标记
多游戏 单文件多个 ()
UTF-8 棋手名 中文/日文/韩文(SGF 标准支持 UTF-8) ✅ 已支持

8. 小结

SGF 是一个简洁的树形文本格式。核心就三条:

  1. 树结构() 嵌套 = 变化分支,; 分隔节点
  2. 属性系统 — 大写字母键 + [] 值,可多值
  3. 坐标编码a-s(跳过 I)对应 0-18

200 行 C++ 就能写出完整解析+生成。把它插进 GoBoard / GoGame,棋谱的存储和交换就打通了——从摄像头识别到 .sgf 保存,从 .sgf 加载到复盘回放,一条链路完整闭合。