本文从架构师视角,用 4+1 视图完整描述围棋打谱辅助系统的顶层设计。核心决策:引入命令模式解耦输入源(人类 / 摄像头 / AI / SGF),统一 GoBoard 不可变状态机,为后续扩展到双人对弈、网络对弈预留接口。


1. 场景视图(Scenarios)— 用例驱动

graph TB
    subgraph 主用例
        HUMAN["🧑 人类对弈<br/>点击棋盘落子"]
        AI_GAME["🤖 人机对弈<br/>AI 自动回应"]
        CAMERA["📷 摄像头监测<br/>识别真实落子"]
        KIFU["📜 SGF 打谱<br/>加载棋谱回放"]
        ANALYSIS["🔍 局面分析<br/>终局计目/复盘"]
    end

    subgraph 支撑
        TIMER["⏱ 读秒计时"]
        SOUND["🔊 音效反馈"]
        CONFIG["⚙ 对局配置"]
    end

    HUMAN --> TIMER
    HUMAN --> SOUND
    AI_GAME --> TIMER
    CAMERA --> SOUND
    KIFU --> CONFIG
用例 输入源 触发方式 输出
人类对弈 鼠标/触屏点击 UI 事件 棋盘更新 + 计时切换 + 音效
人机对弈 人类点击 + AI GTP AI 异步回调 棋盘更新 + 光标指示
摄像头监测 ESP32-CAM 视频帧 定时器轮询/帧差检测 棋盘更新 + 音效
SGF 打谱 .sgf 文件 用户加载/自动播放 棋盘逐步还原
局面分析 当前棋局 用户点击计目 领地着色 + 分数弹窗

2. 逻辑视图(Logical)— 核心领域模型

2.1 领域层(不依赖 Qt)

classDiagram
    class GoBoard {
        -Stone[361] m_grid
        -unordered_set~uint64_t~ m_history
        +at(row, col) Stone
        +computeGroup(idx) Group
        +captureOpponent(idx, color) vector~int~
        +playMove(row, col, color) vector~int~
        +scoreTerritory() Territory
        +computeHash() uint64_t
        +toSGF() string
        +loadSGF(sgf) void
    }

    class GoGame {
        -GoBoard m_board
        -GoTimer m_blackTimer
        -GoTimer m_whiteTimer
        -Stack~Snapshot~ m_undoStack
        -Stone m_turn
        -int m_passes
        -bool m_gameOver
        +play(row, col) MoveResult
        +pass() void
        +undo() bool
        +currentPlayer() Stone
        +isGameOver() bool
        +finalScore() Score
    }

    class GoTimer {
        -int m_mainRemaining
        -int m_byoRemainingP
        -int m_byoRemainingS
        -bool m_inByoyomi
        +start() void
        +stop() void
        +pause() void
        +resume() void
        +tick() void
        +isTimeUp() bool
    }

    class IGoCommand {
        <<interface>>
        +execute(game) MoveResult
        +undo(game) void
        +description() string
    }

    class PlaceStoneCommand {
        -int m_row, m_col
        -Stone m_color
        +execute(game) MoveResult
        +undo(game) void
    }

    class PassCommand {
        -Stone m_color
        +execute(game) MoveResult
        +undo(game) void
    }

    class SGFReplayCommand {
        -SGFNode* m_node
        +execute(game) MoveResult
        +undo(game) void
    }

    GoGame --> GoBoard : has-a
    GoGame --> GoTimer : has-a x2
    IGoCommand <|.. PlaceStoneCommand
    IGoCommand <|.. PassCommand
    IGoCommand <|.. SGFReplayCommand
    GoGame --> IGoCommand : executes

2.2 命令模式设计理由

传统方案:                     命令模式方案:
  UI click → game.play()        UI click → command.execute(game)
  Camera detect → game.play()   Camera detect → command.execute(game)
  AI genmove → game.play()      AI genmove → command.execute(game)
  SGF replay → game.play()      SGF replay → batch(commands).execute(game)

问题: 每个输入源都直接调用 game.play(),   解决: 所有输入源通过 command 统一接口,
      参数校验/日志/undo 逻辑重复          校验+日志+undo 集中在一处
// 核心接口
class IGoCommand {
public:
    virtual ~IGoCommand() = default;

    // 执行命令,返回操作结果
    virtual MoveResult execute(GoGame* game) = 0;

    // 撤销命令
    virtual void undo(GoGame* game) = 0;

    // 用于日志/显示
    virtual std::string description() const = 0;

    // 用于 SGF 序列化
    virtual std::string toSGF() const { return ""; }
};

2.3 命令实现

class PlaceStoneCommand : public IGoCommand {
    int m_row, m_col;
    GoBoard::Stone m_color;
    std::vector<int> m_captured;  // 用于 undo

public:
    PlaceStoneCommand(int r, int c, GoBoard::Stone color)
        : m_row(r), m_col(c), m_color(color) {}

    MoveResult execute(GoGame* game) override {
        auto captured = game->play(m_row, m_col);
        if (captured.empty() && game->board().at(m_row, m_col) == GoBoard::EMPTY)
            return MoveResult::ILLEGAL;
        m_captured = captured;
        return MoveResult::OK;
    }

    void undo(GoGame* game) override {
        game->undo();  // GoGame 的 undo 基于快照栈
    }

    std::string description() const override {
        char buf[64];
        snprintf(buf, sizeof(buf), "%s %c%d",
                 m_color == GoBoard::BLACK ? "B" : "W",
                 'A' + m_col - (m_col >= 8 ? 1 : 0),  // 跳过 I
                 19 - m_row);
        return buf;
    }

    std::string toSGF() const override {
        char buf[8];
        snprintf(buf, sizeof(buf), "%c%c",
                 'a' + m_col, 'a' + m_row);
        return buf;
    }
};

3. 进程视图(Process)— 关键运行时流程

3.1 人机对弈主流程

sequenceDiagram
    actor Human as 🧑 人类
    participant UI as GoBoardWidget
    participant Cmd as PlaceStoneCommand
    participant Game as GoGame
    participant Timer as GoTimer
    participant AI as KataGo(GTP)
    participant Snd as GoSound

    Human->>UI: 点击交叉点
    UI->>UI: widgetToBoard() 转换坐标
    UI->>Cmd: new PlaceStoneCommand(r,c,BLACK)
    Cmd->>Game: execute(game)
    Game->>Game: playMove() → 规则校验
    alt 合法
        Game->>Timer: blackTimer.stop() / whiteTimer.start()
        Game->>Snd: playStone()
        Game-->>UI: boardChanged()
        UI->>UI: repaint() ← 显示黑子+动画

        Note over Game,AI: === AI 回合 ===
        Game->>AI: genmove W (GTP)
        AI-->>Game: "= D16" (异步回调)
        Game->>Cmd: new PlaceStoneCommand(3,15,WHITE)
        Cmd->>Game: execute(game)
        Game->>Timer: whiteTimer.stop() / blackTimer.start()
        Game->>Snd: playStone()
        Game-->>UI: boardChanged()
        UI->>UI: repaint() ← 显示白子+光标动画
    else 非法
        Game-->>UI: movePlayed(非法)
        UI->>UI: showMessage("禁着点")
    end

3.2 摄像头落子检测流程

sequenceDiagram
    participant Cam as ESP32-CAM
    participant Stream as MJPEG Stream
    participant Detector as BoardDetector
    participant Cmd as PlaceStoneCommand
    participant Game as GoGame
    participant UI as GoBoardWidget

    loop 每 500ms
        Cam->>Stream: JPEG 帧
        Stream->>Detector: QImage frame
        Detector->>Detector: classifyStones() → 当前局面
        Detector->>Detector: diff(prev, curr) → 检测变化
        alt 检测到新落子
            Detector->>Cmd: new PlaceStoneCommand(r,c,detectedColor)
            Cmd->>Game: execute(game)
            Game-->>UI: boardChanged()
        else 无变化
            Detector->>Detector: prev = curr
        end
    end

3.3 SGF 打谱流程

sequenceDiagram
    actor User as 用户
    participant UI as 控制面板
    participant Parser as SGFParser
    participant CmdFactory as CommandFactory
    participant Game as GoGame
    participant Widget as GoBoardWidget

    User->>UI: 加载 .sgf 文件
    UI->>Parser: parse(contents)
    Parser-->>UI: SGFNode* root + meta
    UI->>UI: 显示 PB/PW/RE/DT 信息

    User->>UI: 点击「▶ 播放」
    loop 逐手
        UI->>CmdFactory: createCommands(node)
        CmdFactory-->>UI: vector<IGoCommand*>
        UI->>Cmd: cmd->execute(game)
        Cmd->>Game: play(row, col)
        Game-->>Widget: boardChanged()
        Note over UI: 延迟 500ms → 下一手
    end

4. 开发视图(Development)— 模块分层

graph TB
    subgraph Input["输入层 (Input Adapters)"]
        UI_INPUT["HumanInput<br/>鼠标/触屏 → Command"]
        CAM_INPUT["CameraInput<br/>帧差检测 → Command"]
        AI_INPUT["AIInput<br/>GTP genmove → Command"]
        SGF_INPUT["SGFInput<br/>棋谱解析 → Command[]"]
    end

    subgraph Command["命令层 (Command Bus)"]
        CMD_IFACE["IGoCommand"]
        CMD_IMPL["PlaceStone / Pass / Undo"]
        CMD_FACTORY["CommandFactory"]
        CMD_HISTORY["CommandHistory<br/>undo/redo 栈"]
    end

    subgraph Domain["领域层 (Domain Core)"]
        BOARD["GoBoard<br/>棋盘状态 + 规则"]
        GAME["GoGame<br/>游戏控制器"]
        TIMER["GoTimer<br/>读秒计时"]
        SOUND["GoSound<br/>音效队列"]
    end

    subgraph Output["输出层 (Output Adapters)"]
        RENDER["GoBoardWidget<br/>Qt6 渲染"]
        STATUS["StatusPanel<br/>计时/提子/手数"]
        SGF_OUT["SGFWriter<br/>棋谱导出"]
    end

    Input --> Command
    Command --> Domain
    Domain --> Output

4.1 模块清单

模块 文件 职责
输入 HumanInput InputAdapter.h/cpp UI 事件 → Command
CameraInput CameraDetector.h/cpp 帧差检测 → Command
AIInput AIGTPClient.h/cpp GTP 协议 → Command
SGFInput SGFReplay.h/cpp .sgf 解析 → Command[]
命令 IGoCommand IGoCommand.h 命令接口
PlaceStoneCommand Commands.h/cpp 落子命令
PassCommand 同上 虚手命令
CommandHistory CommandHistory.h/cpp undo/redo 栈
领域 GoBoard GoBoard.h/cpp 纯 C++ 规则引擎
GoGame GoGame.h/cpp 游戏状态机
GoTimer GoTimer.h/cpp 读秒计时
GoSound GoSound.h/cpp 音效队列
输出 GoBoardWidget GoBoardWidget.h/cpp Qt6 棋盘渲染
StatusPanel StatusPanel.h/cpp 状态栏
SGFWriter SGFWriter.h/cpp .sgf 导出

4.2 CommandHistory 设计

class CommandHistory {
    std::vector<std::unique_ptr<IGoCommand>> m_commands;
    size_t m_position = 0;  // 当前位置(redo 边界)

public:
    void execute(std::unique_ptr<IGoCommand> cmd, GoGame* game) {
        // 截断 redo 分支
        m_commands.resize(m_position);
        cmd->execute(game);
        m_commands.push_back(std::move(cmd));
        m_position = m_commands.size();
    }

    bool canUndo() const { return m_position > 0; }
    bool canRedo() const { return m_position < m_commands.size(); }

    void undo(GoGame* game) {
        if (!canUndo()) return;
        m_commands[--m_position]->undo(game);
    }

    void redo(GoGame* game) {
        if (!canRedo()) return;
        m_commands[m_position++]->execute(game);
    }

    // 导出为完整 SGF
    std::string toSGF() const;
};

5. 物理视图(Physical)— 部署拓扑

graph TB
    subgraph 硬件["🖥 硬件设备"]
        CAM["📷 ESP32-CAM<br/>WiFi 视频流"]
        SCREEN["🖥 主屏 + 副屏<br/>副屏平放模拟棋盘"]
    end

    subgraph 主机["💻 PC 主机 (Ubuntu 20.04)"]
        subgraph 进程["go-board 进程"]
            STREAM["MjpegStream<br/>HTTP 解码"]
            DETECTOR["BoardDetector<br/>OpenCV 视觉"]
            ENGINE["GoGame<br/>规则引擎"]
            AI_PROC["KataGo<br/>子进程 (GTP)"]
            UI_PROC["Qt6 GUI<br/>MainWindow"]
        end
    end

    CAM -->|"WiFi /stream"| STREAM
    STREAM -->|"QImage 帧"| DETECTOR
    DETECTOR -->|"Command"| ENGINE
    UI_PROC -->|"Command"| ENGINE
    AI_PROC -->|"GTP stdin/stdout"| ENGINE
    ENGINE -->|"boardChanged()"| UI_PROC
    UI_PROC -->|"主屏: 控制面板"| SCREEN
    UI_PROC -->|"副屏: 棋盘光标"| SCREEN

进程通信:

通道 协议 方向 用途
ESP32-CAM → PC HTTP MJPEG 单向推 视频帧
UI → GoGame 信号槽 (Qt DirectConnection) 双向 人类落子 + 状态更新
CameraDetector → GoGame 信号槽 (Qt QueuedConnection) 单向 摄像头检测落子
GoGame ↔ KataGo stdin/stdout (GTP 文本) 双向 AI 着法生成
GoGame → SGFWriter 直接调用 单向 棋谱导出

6. 接口契约

6.1 IGoCommand(核心抽象)

enum class MoveResult { OK, OCCUPIED, SUICIDE, KO, GAME_OVER };

class IGoCommand {
public:
    virtual ~IGoCommand() = default;
    virtual MoveResult execute(GoGame* game) = 0;
    virtual void undo(GoGame* game) = 0;
    virtual std::string description() const = 0;
    virtual std::string toSGF() const { return ""; }

    // 来源标记(用于日志/音效选择)
    enum Source { HUMAN, CAMERA, AI, SGF };
    virtual Source source() const { return HUMAN; }
};

6.2 IAIPlayer(AI 适配器)

class IAIPlayer {
public:
    virtual ~IAIPlayer() = default;

    // 异步请求 AI 落子,结果通过回调返回
    virtual void requestMove(const GoBoard& board, GoBoard::Stone color,
                             std::function<void(int row, int col)> callback) = 0;

    // 同步请求(用于测试)
    virtual std::pair<int,int> requestMoveSync(const GoBoard& board,
                                                GoBoard::Stone color) = 0;

    // AI 信息
    virtual std::string name() const = 0;
    virtual std::string version() const = 0;
};

6.3 IDetector(视觉检测适配器)

class IDetector {
public:
    virtual ~IDetector() = default;

    // 处理一帧图像,返回检测到的命令(可能为空)
    virtual std::unique_ptr<IGoCommand> processFrame(const QImage& frame) = 0;

    // 棋盘是否已标定
    virtual bool isCalibrated() const = 0;

    // 获取当前已识别局面(19×19 矩阵)
    virtual const GoBoard& detectedBoard() const = 0;
};

7. 扩展性分析

7.1 新增输入源

只需要实现 IDetector 或直接创建 IGoCommand

新输入源 → 实现对应 Adapter → 产出 IGoCommand → 注入 GoGame → OK
扩展场景 新增模块 改动范围
网络对弈 NetworkAdapter (WebSocket ↔ IGoCommand) 新增 1 模块
语音落子 VoiceAdapter (ASR → 坐标 → IGoCommand) 新增 1 模块
双摄像头 MultiCamDetector (组合 2 个 IDetector) 新增 1 模块
新 AI 引擎 实现 IAIPlayer 接口 (如 Leela Zero) 新增 1 实现
新棋谱格式 实现 ISGFLike (如 UGI) 新增 1 解析器

7.2 命令模式收益

特性 实现方式
undo/redo CommandHistory 统一管理
SGF 导出 遍历 CommandHistory → 每个 command.toSGF()
操作日志 每个 command.description() → 存入日志
宏/批处理 BatchCommand 封装多个 command
回放调速 DelayedCommand 包装 + QTimer 延迟执行
测试 Mock GoGame → 验证 command.execute() 副作用

8. 目录结构

go-board-cpp/
├── src/
│   ├── core/                    # 领域层(纯 C++,不依赖 Qt)
│   │   ├── GoBoard.h/cpp        # 棋盘状态 + 规则
│   │   ├── GoGame.h/cpp         # 游戏控制器
│   │   ├── GoTimer.h/cpp        # 读秒计时
│   │   ├── GoSound.h/cpp        # 音效
│   │   └── SGFWriter.h/cpp      # SGF 导出
│   │
│   ├── command/                 # 命令层
│   │   ├── IGoCommand.h         # 命令接口
│   │   ├── Commands.h/cpp       # PlaceStone / Pass / Undo
│   │   ├── CommandHistory.h/cpp # undo/redo 栈
│   │   └── CommandFactory.h/cpp # SGF → Command[] 工厂
│   │
│   ├── input/                   # 输入适配器
│   │   ├── HumanInput.h/cpp     # UI 事件适配
│   │   ├── CameraDetector.h/cpp # OpenCV 视觉检测
│   │   ├── AIGTPClient.h/cpp    # KataGo GTP 客户端
│   │   └── SGFReplay.h/cpp      # SGF 打谱回放
│   │
│   ├── ai/                      # AI 接口
│   │   ├── IAIPlayer.h          # AI 抽象接口
│   │   └── KataGoPlayer.h/cpp   # KataGo GTP 实现
│   │
│   ├── ui/                      # Qt6 渲染层
│   │   ├── GoBoardWidget.h/cpp  # 棋盘控件
│   │   ├── StatusPanel.h/cpp    # 状态栏
│   │   ├── MainWindow.h/cpp     # 主窗口
│   │   └── ConfigDialog.h/cpp   # 配置对话框
│   │
│   ├── stream/                  # 视频流
│   │   ├── MjpegStream.h/cpp    # ESP32-CAM MJPEG
│   │   └── FrameBuffer.h/cpp    # 帧缓冲
│   │
│   └── main.cpp                 # 入口
│
├── tests/                       # 单元测试
│   ├── test_goboard.cpp
│   ├── test_gogame.cpp
│   ├── test_command.cpp
│   └── test_sgf.cpp
│
├── esp32-cam/                   # ESP32 固件
├── CMakeLists.txt
└── README.md

9. 设计原则总结

原则 体现
依赖倒置 IGoCommand / IAIPlayer / IDetector 三个接口隔离输入源
单一职责 GoBoard 只管棋盘状态,GoGame 只管流程控制,GoTimer 只管计时
开闭原则 新增输入源 = 新增 Adapter + 实现 IGoCommand,不修改 Domain
命令模式 所有操作统一为 Command,undo/redo/SGF 导出统一处理
不可变快照 GoBoard 不直接暴露 mutable 接口,playMove 要么全部成功要么回退
信号驱动 Qt 信号槽连接 Domain → UI,Domain 不依赖 Qt 头文件