智能围棋盘 —— 系统架构设计文档

版本: v3.0 | 架构师: Magic_GT | 修订: Magic_HK | 方法论: 4+1 View Model (Kruchten)

设计目标: 构建一个高内聚、低耦合、可扩展的围棋系统,支持:命令模式解耦、SGF 打谱、AI 引擎接入、摄像头棋盘/棋子检测集成、屏幕直显光标交互。

v3 变更: 完成目录重组(单层→分层),引入 core/ / command/ / engine/ / detect/ / ui/ / net/ 六模块;构建系统从 4 独立 exe 升级为 2 静态库 + 3 可执行目标;CommandInvoker 正式落实代码。


1. 4+1 视图概述

graph TB
    subgraph core["+1 场景驱动"]
        SC["用例<br>人机对弈 + 打谱 + 复盘<br>摄像头落子检测"]
    end
    subgraph logic["逻辑视图"]
        LV["类图 + 接口<br>GoBoard + GoGame<br>Command + Engine + SGF"]
    end
    subgraph process["过程视图"]
        PV["运行时序列<br>落子流程 + AI 回合<br>SGF 播放 + 线程模型"]
    end
    subgraph dev["开发视图"]
        DV["模块划分<br>src/ 目录结构<br>CMake + 依赖"]
    end
    subgraph physical["物理视图"]
        PHV["部署拓扑<br>ESP32-CAM + PC + 副屏<br>进程+线程布局"]
    end

    SC --> LV
    SC --> PV
    LV --> DV
    PV --> PHV
    DV --> PHV
    style core fill:#2563eb,color:#fff

2. 逻辑视图 — 分层架构

graph TB
    subgraph presentation["表现层 Presentation"]
        UI["GoGameWindow<br>GoBoardWidget<br>CLI 未来"]
    end

    subgraph application["应用层 Application"]
        GAME["GoGame<br>命令接收 + 状态管理"]
        CMD["Command Pattern<br>PlaceStone + Pass + Undo<br>SgfLoad + SgfStep"]
        TIMER["GoTimer<br>计时 + 读秒"]
        SOUND["GoSound<br>音效反馈"]
    end

    subgraph domain["领域层 Domain"]
        BOARD["GoBoard<br>规则引擎: 气+提+劫+计目"]
        SGF["SgfParser + SgfWriter<br>SGF 协议"]
        AI["GoEngine interface<br>GtpEngine KataGo<br>MockEngine 测试"]
        DETECT["BoardDetector interface<br>GoBoardDetector CV<br>MockDetector 测试"]
    end

    subgraph infra["基础设施 Infrastructure"]
        NET["QNetworkAccessManager<br>MJPEG Stream"]
        GTP["QProcess<br>GTP stdio"]
        CV["OpenCV<br>图像处理"]
        QT["Qt6<br>Widgets + Network"]
    end

    UI --> GAME
    GAME --> CMD
    GAME --> TIMER
    GAME --> SOUND
    CMD --> BOARD
    CMD --> SGF
    GAME --> AI
    GAME --> DETECT
    NET --> UI
    GTP --> AI
    CV --> DETECT

3. 核心接口

classDiagram
    class Move {
        +int row
        +int col
    }
    class Stone {
        <<enumeration>>
        Empty
        Black
        White
    }

    class ICommand {
        <<interface>>
        +execute(game) bool
        +undo(game) bool
        +description() string
    }

    class IGoEngine {
        <<interface>>
        +generateMove(boardState) Move
        +setLevel(level) bool
        +name() string
        +isReady() bool
    }

    class ISgfPlayer {
        <<interface>>
        +load(filePath) bool
        +next() Move
        +prev() Move
        +currentMove() int
        +totalMoves() int
    }

    class IBoardDetector {
        <<interface>>
        +processFrame(QImage) vector
        +currentBoardState() string
        +isStable() bool
        +isCalibrated() bool
        +dewarpedImage() QImage
    }

    class CommandInvoker {
        -deque~unique_ptr~ m_history
        -int m_currentIndex
        +execute(cmd, game) bool
        +undo(game) bool
        +redo(game) bool
        +undoDepth() int
    }

    class GoGame {
        -GoBoard m_board
        -GoTimer m_blackTimer
        -GoTimer m_whiteTimer
        -IGoEngine* m_engine
        -CommandInvoker* m_invoker
        +play(row,col) vector
        +pass() void
        +undo() bool
        +executeCommand(cmd) bool
        +setEngine(engine) void
        +isAiMode() bool
    }

    ICommand <|.. PlaceStoneCmd
    ICommand <|.. PassCmd
    ICommand <|.. SgfStepCmd
    IGoEngine <|.. GtpEngine
    IGoEngine <|.. MockEngine
    ISgfPlayer <|.. SgfPlayer
    IBoardDetector <|.. GoBoardDetector
    IBoardDetector <|.. MockDetector

    GoGame --> IGoEngine : has-a
    GoGame --> CommandInvoker : has-a
    CommandInvoker --> ICommand : manages
    CommandInvoker --> GoGame : executes on

4. 过程视图 — 运行时

4.1 人机对弈流程

sequenceDiagram
    actor Human as 人类 黑
    participant UI as GoBoardWidget
    participant Cmd as PlaceStoneCmd
    participant Invoker as CommandInvoker
    participant Game as GoGame
    participant Timer as GoTimer
    participant AI as IGoEngine
    participant Snd as GoSound

    Game->>Timer: 黑方 start()

    Human->>UI: 点击交叉点
    UI->>Cmd: new PlaceStoneCmd(r,c,BLACK)
    UI->>Invoker: execute(cmd, game)
    Invoker->>Cmd: cmd->execute(game)
    Cmd->>Game: game->play(row,col)
    Game->>Game: GoBoard::playMove() 规则校验

    alt 合法
        Game->>Timer: 黑 stop + 白 start()
        Game->>Snd: playStone()
        Game-->>UI: boardChanged()
        Invoker->>Invoker: 压入历史栈
        Invoker-->>UI: commandExecuted(cmd)

        Note over Game,AI: === AI 回合 ===
        Game->>AI: generateMove(boardState)
        AI-->>Game: Move(row=3, col=15)
        Game->>Invoker: execute(new PlaceStoneCmd(3,15,WHITE), game)
        Invoker->>Game: play(3,15)
        Game->>Timer: 白 stop + 黑 start()
        Game->>Snd: playStone()
        Game-->>UI: boardChanged()
    else 非法
        Invoker->>Invoker: delete cmd 不压栈
        Game-->>UI: movePlayed(非法=禁着点)
    end

4.2 命令执行链

flowchart TD
    SRC["输入源: UI点击 + SGF + 摄像头 + AI回调"] --> NEW["new Command(...)"]
    NEW --> INV["invoker.execute(cmd, game)"]
    INV --> EXEC["cmd->execute(game)"]
    EXEC --> VALID["game.play(row,col)"]
    VALID --> CHECK{"GoBoard 验证"}
    CHECK -->|"OK"| PUSH["压入历史栈<br>m_currentIndex++"]
    CHECK -->|"OCCUPIED/SUICIDE/KO"| DELETE["delete cmd<br>不压栈"]
    PUSH --> EVENTS["emit 信号<br>boardChanged + commandExecuted"]
    EVENTS --> LISTEN["UI重绘 + Timer切换<br>Sound播放 + AI待命"]

4.3 线程模型

graph TB
    subgraph threads["线程布局"]
        MAIN["Main Thread<br>Qt Event Loop<br>UI 渲染 + GoGame"]
        NET["Network Thread<br>QNetworkAccessManager<br>MJPEG 异步收帧"]
        DETECT_T["Detection Thread 可选<br>OpenCV 处理"]
        AI_T["AI Thread<br>QProcess Katago GTP<br>stdin + stdout"]
    end

    NET -->|"QNetworkReply::readyRead signal"| MAIN
    DETECT_T -->|"Qt::QueuedConnection signal"| MAIN
    MAIN -->|"write stdin"| AI_T
    AI_T -->|"readyRead signal"| MAIN

    style MAIN fill:#059669,color:#fff
    style NET fill:#2563eb,color:#fff
    style DETECT_T fill:#d97706,color:#fff
    style AI_T fill:#7c3aed,color:#fff

5. 开发视图 — 模块分层

5.1 v3 目录结构(已落地)

go-board-cpp/
├── src/
│   ├── core/                    # 领域层(纯 C++)
│   │   ├── GoBoard.h/cpp        # 棋盘状态 + 规则引擎
│   │   ├── GoGame.h/cpp         # 游戏控制器 + 命令接收
│   │   ├── GoTimer.h/cpp        # 读秒计时器
│   │   └── GoSound.h/cpp        # 音效队列
│   ├── command/                 # 命令层
│   │   ├── ICommand.h           # 命令接口
│   │   └── CommandInvoker.h/cpp # 调度器 execute/undo/redo
│   ├── engine/                  # AI 引擎层
│   │   ├── IGoEngine.h          # AI 抽象接口
│   │   └── MockEngine.h         # 测试桩 随机落子
│   ├── detect/                  # 视觉检测层
│   │   ├── GoBoardDetector.h/cpp
│   │   ├── BoardRectifier.h/cpp
│   │   ├── LiveView.h/cpp
│   │   ├── DewarpView.h/cpp
│   │   └── MarkWidget.h/cpp
│   ├── ui/                      # 表现层
│   │   ├── GoBoardWidget.h/cpp  # 虚拟棋盘控件
│   │   └── MainWindow.h/cpp     # 主窗口
│   └── net/                     # 网络层
│       └── MjpegStream.h/cpp    # MJPEG 流解码
├── doc/
│   ├── ARCHITECTURE.md
│   ├── PROJECT_ROADMAP.md
│   └── WORKFLOW.md
├── CMakeLists.txt
└── build.bat

5.2 CMake 库目标

# 静态库: 领域核心
add_library(go-core STATIC
    src/core/GoBoard.cpp src/core/GoGame.cpp
    src/core/GoTimer.cpp src/core/GoSound.cpp
    src/command/CommandInvoker.cpp
)
target_link_libraries(go-core PUBLIC Qt6::Core)

# 静态库: AI 引擎
add_library(go-engine STATIC
    src/engine/MockEngine.cpp
)
target_link_libraries(go-engine PUBLIC go-core)

# 可执行: 完整围棋对弈
add_executable(go-game go_game.cpp src/ui/GoBoardWidget.cpp src/ui/MainWindow.cpp)
target_link_libraries(go-game PRIVATE go-core go-engine Qt6::Widgets)

# 可执行: 棋盘检测标定
add_executable(go-board-calib
    go_board_calib.cpp
    src/detect/GoBoardDetector.cpp src/detect/BoardRectifier.cpp
    src/detect/LiveView.cpp src/detect/DewarpView.cpp src/detect/MarkWidget.cpp
    src/net/MjpegStream.cpp
)
target_link_libraries(go-board-calib PRIVATE go-core OpenCV Qt6::Widgets Qt6::Network)

5.3 依赖方向(单向无环)

flowchart LR
    UI["ui/"] --> CMD["command/"]
    UI --> CORE["core/"]
    CMD --> CORE
    DETECT["detect/"] --> CORE
    DETECT --> CV["OpenCV"]
    NET["net/"] --> QT["Qt6::Network"]
    CORE --> ENGINE["engine/ IGoEngine"]

    style UI fill:#7c3aed,color:#fff
    style CMD fill:#2563eb,color:#fff
    style CORE fill:#059669,color:#fff
    style ENGINE fill:#d97706,color:#fff
    style DETECT fill:#dc2626,color:#fff

6. 物理视图 — 部署拓扑

graph TB
    subgraph world["物理世界"]
        BOARD["围棋棋盘 19x19"]
        STONES["棋子 黑白"]
    end

    subgraph capture["采集端 ESP32-CAM"]
        CAM["OV2640<br>800x600 JPEG"]
        WIFI_AP["WiFi AP"]
        HTTP["HTTP Server :80<br>/stream + /capture"]
    end

    subgraph compute["计算端 PC"]
        subgraph process["go-game 进程"]
            MAIN["Main Thread<br>Qt Event Loop + UI"]
            MJPEG_T["MJPEG 解码 Qt Network"]
            AI_PROC["Katago 子进程<br>GTP stdin + stdout"]
        end
        DISPLAY["主屏: 控制面板"]
        SCREEN["副屏: 棋盘光标"]
    end

    BOARD --> CAM
    CAM --> HTTP
    HTTP -->|"WiFi MJPEG"| MJPEG_T
    MJPEG_T --> MAIN
    MAIN -->|"GTP stdio"| AI_PROC
    MAIN --> DISPLAY
    MAIN -->|"HDMI 第二屏"| SCREEN

7. 接口契约

7.1 ICommand

class ICommand {
public:
    virtual ~ICommand() = default;
    virtual bool execute(GoGame* game) = 0;
    virtual bool undo(GoGame* game) = 0;
    virtual QString description() const = 0;
};

7.2 IGoEngine

class IGoEngine {
public:
    virtual ~IGoEngine() = default;
    virtual Move generateMove(const std::string& boardState) = 0;
    virtual bool setLevel(int level) = 0;
    virtual std::string name() const = 0;
    virtual bool isReady() const = 0;
};

7.3 CommandInvoker

class CommandInvoker {
    static constexpr int MAX_UNDO_DEPTH = 100;
    std::deque<std::unique_ptr<ICommand>> m_history;  // 智能指针防泄漏
    int m_currentIndex = -1;

public:
    bool execute(std::unique_ptr<ICommand> cmd, GoGame* game);
    bool undo(GoGame* game);
    bool redo(GoGame* game);
    int undoDepth() const;
    void clear();
};

7.4 GoGame 事件总线(v3 新增方法)

class GoGame : public QObject {
    Q_OBJECT
public:
    // v3 新增: 命令模式入口
    CommandInvoker* invoker() { return &m_invoker; }
    bool executeCommand(ICommand* cmd) { return m_invoker.execute(...); }
    void setEngine(IGoEngine* engine) { m_engine = engine; }
    IGoEngine* engine() const { return m_engine; }
    bool isAiMode() const { return m_engine != nullptr; }

    // 不变
    QVector<int> play(int row, int col);
    void pass();
    bool undo();

signals:
    void boardChanged();
    void movePlayed(int row, int col, GoBoard::Stone color);
    void turnChanged(GoBoard::Stone nextPlayer);
    void gameEnded(GoBoard::Stone winner, const QString& reason);
    void commandExecuted(ICommand* cmd);
};

8. 高风险点与缓解策略

8.1 风险矩阵

风险 等级 影响 缓解措施
CommandInvoker 内存泄漏 🔴 高 undo 栈裸指针增长无界 std::unique_ptr<ICommand> + MAX_UNDO_DEPTH=100
GTP 子进程崩溃 🔴 高 AI 无响应,对弈中断 QProcess::errorOccurred 监听;30s 看门狗;降级 MockEngine
MJPEG 流断连 🟡 中 ESP32-CAM WiFi 不稳定 指数退避重连 1s/2s/4s/8s;重连超限弹窗;静态图片 fallback
OpenCV 检测抖动 🟡 中 光照变化误检落子 连续 N=3 帧一致才确认;人工标记矫正兜底
双屏光标偏移 🟡 中 棋盘移动后光标不准 标定后计算映射矩阵;方向键微调 UI;自动重标定
单线程瓶颈 🟢 低 高帧率 UI 卡顿 异步信号驱动;<15fps 自动启用 Detection 独立线程
SGF 写回缺失 🟢 低 对局无法保存 对局结束自动保存 SGF;复用 SgfParser 序列化逻辑

8.2 降级路径

flowchart TD
    START["系统启动"] --> CAM{"ESP32-CAM 可用?"}
    CAM -->|"是"| LIVE["实时检测模式"]
    CAM -->|"否"| STATIC{"静态图片可用?"}
    STATIC -->|"是"| IMAGE["图片检测模式"]
    STATIC -->|"否"| MANUAL["手动点击模式"]
    
    LIVE --> AI_CHK{"AI 引擎正常?"}
    IMAGE --> AI_CHK
    MANUAL --> AI_CHK
    AI_CHK -->|"是"| FULL["全功能模式"]
    AI_CHK -->|"否 降级"| LOCAL["本地双人模式"]
    
    style START fill:#2563eb,color:#fff
    style FULL fill:#059669,color:#fff
    style LOCAL fill:#d97706,color:#fff

9. V3 重构记录

9.1 目录重组

v2 (旧)                          v3 (新)
──────────────────────────────────────────────────
src/GoBoard.h                →   src/core/GoBoard.h
src/GoBoard.cpp              →   src/core/GoBoard.cpp
src/GoGame.h                 →   src/core/GoGame.h       (新增 invoker/engine 接口)
src/GoGame.cpp               →   src/core/GoGame.cpp
src/GoTimer.h                →   src/core/GoTimer.h
src/GoTimer.cpp              →   src/core/GoTimer.cpp
src/GoSound.h                →   src/core/GoSound.h
src/GoSound.cpp              →   src/core/GoSound.cpp
src/GoBoardDetector.h        →   src/detect/GoBoardDetector.h
src/GoBoardDetector.cpp      →   src/detect/GoBoardDetector.cpp
src/BoardRectifier.h         →   src/detect/BoardRectifier.h
src/BoardRectifier.cpp       →   src/detect/BoardRectifier.cpp
src/LiveView.h               →   src/detect/LiveView.h
src/LiveView.cpp             →   src/detect/LiveView.cpp
src/DewarpView.h             →   src/detect/DewarpView.h
src/DewarpView.cpp           →   src/detect/DewarpView.cpp
src/MarkWidget.h             →   src/detect/MarkWidget.h
src/MarkWidget.cpp           →   src/detect/MarkWidget.cpp
src/GoBoardWidget.h          →   src/ui/GoBoardWidget.h
src/GoBoardWidget.cpp        →   src/ui/GoBoardWidget.cpp
src/MainWindow.h             →   src/ui/MainWindow.h
src/MainWindow.cpp           →   src/ui/MainWindow.cpp
src/MjpegStream.h            →   src/net/MjpegStream.h
src/MjpegStream.cpp          →   src/net/MjpegStream.cpp
(新增)                       →   src/command/ICommand.h
(新增)                       →   src/command/CommandInvoker.h/cpp
(新增)                       →   src/engine/IGoEngine.h
(新增)                       →   src/engine/MockEngine.h

9.2 构建系统变更

v2 v3
4 个独立可执行目标 2 个静态库 + 3 个可执行目标
go-game.exe, go-board-calib.exe, go-board-demo.exe, test_rectify.exe go-core.lib + go-engine.lib → 3 exe

9.3 GoGame 接口变更

// 新增 v3
CommandInvoker* invoker()              // 命令调度器
bool executeCommand(ICommand* cmd)     // 命令入口
void setEngine(IGoEngine* engine)      // 注入 AI
IGoEngine* engine()                    // AI 查询
bool isAiMode()                        // 模式查询

// 不变 保持兼容
QVector<int> play(int row, int col)    // 直接落子
void pass()
bool undo()

9.4 兼容性

  • ✅ 所有现有功能保持不变
  • ✅ 构建目标全部编译通过
  • ✅ 规则引擎/计时器/音效 API 未变
  • ✅ 包含路径无需手动修改(ALL_INC 自动解析)

10. 项目排期

Phase 内容 状态
P0 基础视觉 + 架构重构 ✅ Done
P1 棋子识别 + 局面感知 🎯 当前
P2 屏幕光标显示
P3 AI 对弈 + SGF 打谱
P4 产品化打磨

11. 设计原则总结

原则 体现
依赖倒置 高层依赖 ICommand / IGoEngine / IBoardDetector / ISgfPlayer 四个抽象
单一职责 GoBoard(状态) / GoGame(流程) / GoTimer(计时) / GoSound(音效) 各司其职
开闭原则 新增输入源 = 新增 ICommand 子类 + Adapter,不修改 Domain
命令模式 所有操作统一为 Command,undo/redo/SGF导出统一处理
不可变事务 GoBoard::playMove() 要么全部成功,要么完整回退
信号驱动 Qt 信号槽连接 Domain → UI,Domain 零 Widgets 依赖
Mock 优先 MockEngine + MockDetector 让 AI/检测/UI 独立开发测试
增量迁移 5 步渐进式重构,每步可验证,不破坏现有功能
智能指针 CommandInvokerunique_ptr 防内存泄漏,MAX_UNDO_DEPTH=100