用途: 作为围棋打谱辅助系统对弈引擎模块的参考设计文档。 覆盖规则: 落子合法性、气与提子、劫争、终局判定(中日规则对比)。 目标语言: C++(可移植至任何语言)。
1. 数据结构:棋盘表示
1.1 核心设计
class GoBoard {
public:
static constexpr int SIZE = 19; // 19×19
static constexpr int TOTAL = SIZE * SIZE; // 361
enum Stone { EMPTY = 0, BLACK = 1, WHITE = 2 };
Stone at(int row, int col) const;
void place(int row, int col, Stone color);
void remove(int row, int col);
static bool onBoard(int row, int col); // 边界检查
static int toIndex(int row, int col); // (row,col) → 361索引
static void toCoord(int index, int &row, int &col);
private:
Stone m_grid[TOTAL] = { EMPTY };
};
1.2 邻接方向
// 4 邻接方向(上下左右)
constexpr int DIRS[4][2] = {
{-1, 0}, {1, 0}, {0, -1}, {0, 1}
};
1.3 数据结构选择考量
| 方案 | 优点 | 缺点 |
|---|---|---|
Stone[361] 一维数组 |
缓存友好、简单 | 需坐标转换 |
Stone[19][19] 二维 |
直观 | 某些遍历麻烦 |
std::bitset<361> x2 (黑白各一) |
极省内存 | 无法表示 EMPTY |
选用: Stone[361] |
简洁高效 | — |
建议: 一维数组 +
toIndex()/toCoord()工具函数。性能最优,代码量最少。
2. 气的计算
2.1 定义
气 (Liberty): 与一颗棋子(或一组同色连通棋子)相邻的空交叉点数量。
· · · ← 4 气 (四颗子各有一口) · ● ·
· ● · ● ○ ● ← 只剩 1 气 (被围)
· · · · ● ·
2.2 连通组 (Group)
一组同色、通过 4-邻接连通的棋子构成一个"组”。组共享气。
● ● ● ● ● ● · ●
● ○ → 1组 ● · ● → 1组 ● ● ● → 边缘组
● ● ●
2.3 算法:洪泛 (Flood Fill)
// 计算棋盘上某个位置所在连通组的"气集合"
struct Group {
std::vector<int> stones; // 组内棋子索引
std::vector<int> liberties; // 气点索引(去重)
};
Group GoBoard::computeGroup(int row, int col) const
{
Group g;
Stone color = at(row, col);
if (color == EMPTY) return g;
bool visited[TOTAL] = { false };
bool libAdded[TOTAL] = { false }; // 气点去重
std::vector<int> stack;
stack.push_back(toIndex(row, col));
visited[toIndex(row, col)] = true;
while (!stack.empty()) {
int idx = stack.back(); stack.pop_back();
int r, c; toCoord(idx, r, c);
g.stones.push_back(idx);
for (auto &d : DIRS) {
int nr = r + d[0], nc = c + d[1];
if (!onBoard(nr, nc)) continue;
int ni = toIndex(nr, nc);
Stone s = at(nr, nc);
if (s == EMPTY) {
if (!libAdded[ni]) {
g.liberties.push_back(ni);
libAdded[ni] = true;
}
} else if (s == color && !visited[ni]) {
visited[ni] = true;
stack.push_back(ni);
}
}
}
return g;
}
// 便捷方法
int GoBoard::countLiberties(int row, int col) const {
return (int)computeGroup(row, col).liberties.size();
}
flowchart TD
A["从 (row,col) 开始<br/>标记为 visited"] --> B["取栈顶 → 检查 4 邻域"]
B --> C{"邻格是什么?"}
C -->|"EMPTY"| D["加入 liberties 集合<br/>(libAdded 去重)"]
C -->|"同色 & 未访问"| E["标记 visited<br/>入栈"]
C -->|"异色 / 出界"| F["跳过"]
D --> B
E --> B
F --> B
B -->|"栈空"| G["返回 Group<br/>{stones, liberties}"]
3. 提子逻辑
3.1 规则
提子 (Capture): 落子后,检查对方所有邻接组。若任一组的气数为 0,移除该组所有棋子。
落子前: 白落 ● 后: 提子后:
○ ○ ● ○ ○ ● · · ●
○ · ● → ○ ● ● → · ● ●
○ ○ ● ○ ○ ● · · ●
3.2 算法
// 落子后执行: 移除所有气数为 0 的对方组
// 返回: 被提掉的棋子数量
int GoBoard::captureOpponent(int row, int col, Stone myColor)
{
Stone oppColor = (myColor == BLACK) ? WHITE : BLACK;
std::vector<int> deadStones;
for (auto &d : DIRS) {
int nr = row + d[0], nc = col + d[1];
if (!onBoard(nr, nc)) continue;
if (at(nr, nc) != oppColor) continue;
// 检查是否已被记录(避免同一个组被重复处理)
int ni = toIndex(nr, nc);
bool alreadyDead = false;
for (int ds : deadStones) {
if (ds == ni) { alreadyDead = true; break; }
}
if (alreadyDead) continue;
Group g = computeGroup(nr, nc);
if (g.liberties.empty()) {
// 气为 0 → 提掉整个组
for (int si : g.stones) {
deadStones.push_back(si);
}
}
}
// 执行提子
for (int si : deadStones) {
int r, c; toCoord(si, r, c);
m_grid[si] = EMPTY;
}
return (int)deadStones.size();
}
注意: 必须先捕获对方,再检查自己是否自杀。顺序错了会导致错误判定。
4. 落子合法性判定
4.1 判定流程
flowchart TD
MOVE["拟落子 (row,col,color)"] --> A{"交叉点为空?"}
A -->|"❌ 已有子"| ILLEGAL["❌ 非法:交叉点被占"]
A -->|"✅"| B["临时放置棋子"]
B --> C["captureOpponent()<br/>提走所有气=0的对方组"]
C --> D["computeGroup(row,col)<br/>检查自己的气"]
D --> E{"自己的气 > 0?"}
E -->|"✅ 有气"| LEGAL["✅ 合法<br/>落子生效"]
E -->|"❌ 气 = 0"| F["❌ 非法:自杀<br/>回退棋子 + 回退提子"]
4.2 实现
enum class MoveResult { LEGAL, OCCUPIED, SUICIDE, KO };
struct MoveInfo {
MoveResult result;
int captured; // 被提棋子数
uint64_t nextHash; // 用于劫争检测的局面对比哈希
};
MoveInfo GoBoard::tryMove(int row, int col, Stone color)
{
MoveInfo info{};
info.result = MoveResult::LEGAL;
// 1. 交叉点为空?
if (at(row, col) != EMPTY) {
info.result = MoveResult::OCCUPIED;
return info;
}
// 2. 备份局面(用于回退)
Stone backup[TOTAL];
std::copy(m_grid, m_grid + TOTAL, backup);
// 3. 临时落子
place(row, col, color);
// 4. 先捕获对方(规则:先杀对方,再判自杀)
info.captured = captureOpponent(row, col, color);
// 5. 检查自己
Group myGroup = computeGroup(row, col);
if (myGroup.liberties.empty()) {
// 自杀:回退
std::copy(backup, backup + TOTAL, m_grid);
info.result = MoveResult::SUICIDE;
info.captured = 0;
return info;
}
// 6. 检查劫争
uint64_t newHash = computeHash();
if (newHash == m_prevHash) {
std::copy(backup, backup + TOTAL, m_grid);
info.result = MoveResult::KO;
info.captured = 0;
return info;
}
m_prevHash = newHash;
info.nextHash = newHash;
return info;
}
4.3 自杀特例
○ ○ ○
○ ● ○ ← 黑落 ●
○ ○ ○
落子后: 黑吃掉全部白棋 → 黑自己仍有气 → ✅ 合法!
规则: 如果落子能提掉对方棋子,使得自己获得气,则不算自杀。只有"放下去后自己和对方都没气"才算自杀。
5. 劫争
5.1 什么是劫
局面 A → 黑提一子 → 局面 B → 白立即提回 → 局面 A → 无限循环 ❌
劫前: 黑提 ●: 如果白立即提回 ●:
○ ● · ○ ● ● ○ · ● ← 回到劫前! 禁止!
○ · ● ○ · ● ○ ● ●
○ ○ ○ ○ ○ ○ ○ ○ ○
基础劫规则: 禁止立即将局面恢复到上一手之前的状态。
5.2 Zobrist Hashing
用 Zobrist 哈希快速对比局面。对每个 (position, color) 预生成随机 64-bit 数,局面哈希 = XOR 所有棋盘上的子的哈希。
// 预计算随机表
uint64_t ZOBRIST[361][3]; // [position][EMPTY/BLACK/WHITE]
void initZobrist() {
std::mt19937_64 rng(42);
for (int i = 0; i < TOTAL; i++)
for (int c = 0; c < 3; c++)
ZOBRIST[i][c] = rng();
}
// 增量更新哈希(O(1),不需要遍历全盘)
void GoBoard::updateHash(uint64_t &hash, int idx, Stone oldColor, Stone newColor) {
hash ^= ZOBRIST[idx][(int)oldColor]; // 移除旧
hash ^= ZOBRIST[idx][(int)newColor]; // 加入新
}
5.3 超级劫 (Superko)
Positional Superko(局面超级劫): 禁止重复任何曾经出现过的局面(不仅是上一步)。
class GoBoard {
std::unordered_set<uint64_t> m_history; // 历史局面哈希集合
uint64_t m_prevHash = 0;
bool isKo(uint64_t newHash) const {
if (newHash == m_prevHash) return true; // 简单劫
if (m_history.count(newHash)) return true; // 超级劫
return false;
}
};
| 劫规则 | 检测内容 | 存储成本 | 一般采用 |
|---|---|---|---|
| 简单劫 | newHash == prevHash |
O(1) | 初学者 / 简版 |
| 超级劫 | history.contains(newHash) |
O(N) | 正式引擎 |
6. 终局判定与计目
6.1 何时终局
双方连续 Pass(虚手)→ 终局。
class GoEngine {
int m_consecutivePasses = 0;
void pass() {
m_consecutivePasses++;
if (m_consecutivePasses >= 2) endGame();
switchTurn();
}
void placeStone(int row, int col) {
m_consecutivePasses = 0; // 落子 → 重置
}
};
6.2 终局算法:洪泛填充
终局后需要区分:活子、死子、空(领地)。
flowchart TD
END["终局: 双方 Pass"] --> FIND["洪泛法找出所有空区域<br/>(connected components of EMPTY)"]
FIND --> CHECK{"每个空区域的<br/>邻接颜色?"}
CHECK -->|"仅黑"| BLACK["→ 黑领地"]
CHECK -->|"仅白"| WHITE["→ 白领地"]
CHECK -->|"黑白混合"| DAME["→ 双活/公气<br/>不计数"]
6.3 实现
Territory GoBoard::scoreTerritory() const
{
Territory t;
bool visited[TOTAL] = { false };
for (int i = 0; i < TOTAL; i++) {
if (m_grid[i] != EMPTY || visited[i]) continue;
std::vector<int> region;
bool touchesBlack = false;
bool touchesWhite = false;
std::vector<int> stack = { i };
visited[i] = true;
while (!stack.empty()) {
int idx = stack.back(); stack.pop_back();
region.push_back(idx);
int r, c; toCoord(idx, r, c);
for (auto &d : DIRS) {
int nr = r + d[0], nc = c + d[1];
if (!onBoard(nr, nc)) continue;
int ni = toIndex(nr, nc);
Stone s = at(nr, nc);
if (s == EMPTY && !visited[ni]) {
visited[ni] = true;
stack.push_back(ni);
} else if (s == BLACK) touchesBlack = true;
else if (s == WHITE) touchesWhite = true;
}
}
if (touchesBlack && !touchesWhite)
t.blackTerritory += (int)region.size();
else if (touchesWhite && !touchesBlack)
t.whiteTerritory += (int)region.size();
}
return t;
}
6.4 分数计算
struct Score {
double black, white, margin;
std::string winner;
};
Score GoBoard::computeFinalScore(const Territory &t, double komi = 6.5) const
{
Score s;
int blackStones = 0, whiteStones = 0;
for (int i = 0; i < TOTAL; i++) {
if (m_grid[i] == BLACK) blackStones++;
else if (m_grid[i] == WHITE) whiteStones++;
}
// 数子法(中国规则)
s.black = blackStones + t.blackTerritory;
s.white = whiteStones + t.whiteTerritory + komi;
s.margin = s.black - s.white;
s.winner = (s.margin > 0) ? "Black" : "White";
return s;
}
7. 中国规则 vs 日本规则
| 维度 | 中国规则 (数子法) | 日本规则 (数目法) |
|---|---|---|
| 黑方得分 | 黑子数 + 黑领地 | 黑领地 - 黑被提子 |
| 白方得分 | 白子数 + 白领地 + 贴目 | 白领地 - 白被提子 + 贴目 |
| 活子算分? | ✅ 算 | ❌ 不算 |
| 死子处理 | 终局后人工指出移除 | 终局后人工指出移除 |
| 实战中 | 中国大陆 | 日本/韩国/欧美 |
| Komi 贴目 | 7.5 子 | 6.5 目 |
7.1 中日规则切换
enum class RuleSet { CHINESE, JAPANESE };
Score GoBoard::scoreGame(RuleSet rule, double komi) const
{
Territory t = scoreTerritory();
if (rule == RuleSet::CHINESE) {
// 数子法: 子 + 空
int black = 0, white = 0;
for (int i = 0; i < TOTAL; i++) {
if (m_grid[i] == BLACK) black++;
else if (m_grid[i] == WHITE) white++;
}
return {
(double)(black + t.blackTerritory),
(double)(white + t.whiteTerritory + komi)
};
} else {
// 数目法: 空 - 死子
return {
(double)(t.blackTerritory - m_capturedBlack),
(double)(t.whiteTerritory - m_capturedWhite + komi)
};
}
}
8. 完整引擎类设计
8.1 API
class GoEngine {
public:
void reset();
void pass();
MoveInfo play(int row, int col); // 落子 → 返回合法性+提子信息
const GoBoard& board() const;
Stone currentPlayer() const;
int moveNumber() const;
uint64_t boardHash() const;
bool isGameOver() const;
Score finalScore(RuleSet rule = RuleSet::CHINESE, double komi = 7.5) const;
std::string toSGF() const; // 导出 SGF 格式
std::string toGTP(const std::string &cmd) const; // GTP 协议格式化
};
8.2 落子全流程
flowchart TD
INPUT["play(row, col, color)"] --> EMPTY{"交叉点空?"}
EMPTY -->|"❌"| RET_OCC["返回 OCCUPIED"]
EMPTY -->|"✅"| BACKUP["备份局面"]
BACKUP --> PLACE["临时放子"]
PLACE --> CAPTURE["captureOpponent()"]
CAPTURE --> CHECK{"自己气 > 0?"}
CHECK -->|"❌"| RESTORE["回退→ SUICIDE"]
CHECK -->|"✅"| KO{"劫? hash in history"}
KO -->|"❌ 重复"| RESTORE2["回退→ KO"]
KO -->|"✅ 新"| COMMIT["记录hash → LEGAL<br/>reset pass counter<br/>switch turn"]
9. 边界情况全集
9.1 边角落子
边: 角:
· · · · · ·
· ● · · ● · ← 边角气更少,算法自动处理
○ ○ ○ · ○ ○ (onBoard 排除越界)
9.2 自杀合法(提子后获得气)
黑落 ● 前: 黑落 ● 后:
○ ○ ○ 边 ○ ○ ○ 边
○ ● ○ ○ ● ○ ← 黑子气=0...
○ · ● ○ ● ● ← 但提掉白子后气=2 ✅
9.3 打二还一(不是劫)
局面 A: 黑提 ●: 白提回: 局面 D:
○ ● · ○ ● ● ○ · ● ○ ● ·
○ · ● → ○ · ● → ○ ● ● ≠ ○ · ● ← 不同! 允许!
○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○ ○
白提回的不是"同一子所在位置”,局面不同 → 不是劫。
9.4 循环劫与长生劫
| 情况 | 描述 | 处理 |
|---|---|---|
| 三劫循环 | 三个劫交替提,永不重复同一局面 | 超级劫禁止 → 无胜负 |
| 长生劫 | 特殊形状反复出现,周期 > 2 | 超级劫禁止 → 无胜负 |
10. 参考实现量级
| 模块 | 行数估计 | 复杂度 |
|---|---|---|
| GoBoard (棋盘点查/组计算/气/提子) | ~200 行 | 中等 |
| GoEngine (落子/劫/Pass/终局) | ~250 行 | 中等 |
| 计目器 (scoreTerritory) | ~80 行 | 低 |
| Zobrist 哈希 | ~30 行 | 低 |
| SGF 解析/生成 | ~150 行 | 低 |
| GTP 客户端 | ~200 行 | 低 |
| 总计 | ~900 行 | — |
围棋规则引擎本身并不大——900 行 C++ 即可覆盖全部核心逻辑。复杂度在 AI(KataGo),不在规则。
附录: GoBoard 完整头文件
// GoBoard.h
#pragma once
#include <vector>
#include <unordered_set>
#include <random>
#include <cstring>
class GoBoard {
public:
static constexpr int SIZE = 19;
static constexpr int TOTAL = SIZE * SIZE;
static constexpr int DIRS[4][2] = {{-1,0},{1,0},{0,-1},{0,1}};
enum Stone { EMPTY = 0, BLACK = 1, WHITE = 2 };
GoBoard();
Stone at(int row, int col) const { return m_grid[toIndex(row,col)]; }
void place(int r, int c, Stone s) { m_grid[toIndex(r,c)] = s; }
void remove(int r, int c) { m_grid[toIndex(r,c)] = EMPTY; }
static bool onBoard(int r, int c) {
return r >= 0 && r < SIZE && c >= 0 && c < SIZE;
}
static int toIndex(int r, int c) { return r * SIZE + c; }
static void toCoord(int i, int &r, int &c) { r = i / SIZE; c = i % SIZE; }
struct Group { std::vector<int> stones, liberties; };
Group computeGroup(int row, int col) const;
int countLiberties(int row, int col) const;
int captureOpponent(int row, int col, Stone myColor);
uint64_t computeHash() const;
struct Territory {
int blackTerritory = 0, whiteTerritory = 0;
std::vector<int> deadBlack, deadWhite;
};
Territory scoreTerritory() const;
private:
Stone m_grid[TOTAL] = { EMPTY };
};