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 是一个简洁的树形文本格式。核心就三条:
- 树结构 —
()嵌套 = 变化分支,;分隔节点 - 属性系统 — 大写字母键 +
[]值,可多值 - 坐标编码 —
a-s(跳过I)对应 0-18
200 行 C++ 就能写出完整解析+生成。把它插进 GoBoard / GoGame,棋谱的存储和交换就打通了——从摄像头识别到 .sgf 保存,从 .sgf 加载到复盘回放,一条链路完整闭合。