用途: 作为围棋打谱辅助系统对弈引擎模块的参考设计文档。 覆盖规则: 落子合法性、气与提子、劫争、终局判定(中日规则对比)。 目标语言: 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 };
};