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

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

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

v2 变更: 去除投影仪方案,改为屏幕直显光标;增加 CursorWindow 双屏交互;补齐风险缓解策略。


目录

  1. 4+1 视图概述
  2. 逻辑视图 Logical View
  3. 过程视图 Process View
  4. 开发视图 Development View
  5. 物理视图 Physical View
  6. 场景视图 Scenarios
  7. 命令模式设计
  8. SGF 打谱子系统
  9. AI 引擎接口
  10. 棋盘检测接入
  11. 接口契约定义

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. 逻辑视图 (Logical View)

2.1 分层架构

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>规则引擎: 气/提/劫/计目<br>纯 C++ 零 Qt"]
        SGF["SgfParser (纯 C++ 零 Qt)<br>SgfPlayer (Qt Core 封装)<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

2.2 核心接口

classDiagram
    class IGoEngine {
        <<interface>>
        +generateMove(string boardState, Stone color) pair~int,int~
        +setLevel(int level) void
        +name() string
        +isReady() bool
        +initialize() bool
        +shutdown() void
    }

    class GtpEngine {
        -QProcess *m_process
        +generateMove(string boardState, Stone color) pair~int,int~
        +setLevel(int level) void
        +name() string
        +initialize() bool
        +shutdown() void
    }

    class MockEngine {
        +generateMove(string boardState, Stone color) pair~int,int~
        +name() string
        +initialize() bool
    }

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

    class SgfPlayer {
        -SgfNode *m_root
        -SgfNode *m_current
        +load(string filePath) bool
        +next() bool
        +prev() bool
        +currentMove() int
        +totalMoves() int
        +boardState() string
        +comment() string
        +branches() List~string~
    }

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

    class PlaceStoneCommand {
        -int row
        -int col
        -Stone color
        +execute(GoGame game) bool
        +undo(GoGame game) bool
    }

    class PassCommand {
        +execute(GoGame game) bool
        +undo(GoGame game) bool
    }
    class UndoCommand {
        +execute(GoGame game) bool
        +undo(GoGame game) bool
    }
    class SgfStepCommand {
        +execute(GoGame game) bool
        +undo(GoGame game) bool
    }

    IGoEngine <|.. GtpEngine
    IGoEngine <|.. MockEngine
    ISgfPlayer <|.. SgfPlayer
    ICommand <|.. PlaceStoneCommand
    ICommand <|.. PassCommand
    ICommand <|.. UndoCommand
    ICommand <|.. SgfStepCommand

3. 过程视图 (Process View)

3.1 人机对弈流程

sequenceDiagram
    actor Human as 👤 人类 (黑)
    participant UI as GoGameWindow
    participant Game as GoGame
    participant Board as GoBoard
    participant Timer as GoTimer
    participant Sound as GoSound
    participant AI as IGoEngine (白)

    Game->>Timer: 黑方 start()

    Human->>UI: 点击 (row,col)
    UI->>Game: play(row,col)
    Game->>Board: playMove(row,col,BLACK)
    Board-->>Game: captured[]
    Game->>Timer: 黑 stop / 白 start()
    Game->>Sound: playStone(black)
    Game->>UI: boardChanged()

    Game->>AI: generateMove(boardState)
    AI-->>Game: Move(row,col)
    Game->>Board: playMove(row,col,WHITE)
    Board-->>Game: captured[]
    Game->>Timer: 白 stop / 黑 start()
    Game->>Sound: playStone(white)
    Game->>UI: boardChanged()

3.2 SGF 打谱流程

sequenceDiagram
    actor User as 👤 用户
    participant UI as GoGameWindow
    participant Game as GoGame
    participant SGF as SgfPlayer
    participant Board as GoBoard

    User->>UI: Open SGF
    UI->>SGF: load("game.sgf")
    SGF-->>UI: metadata + moveCount

    loop 打谱
        User->>UI: Next (→)
        UI->>SGF: next()
        SGF-->>UI: Move(row,col,color,comment)
        UI->>Game: execute(SgfStepCommand)
        Game->>Board: playMove(row,col,color)
        Game->>UI: boardChanged()
        UI->>UI: 显示注释 / 分支提示
    end

    User->>UI: Prev (←)
    UI->>Game: undo()
    Game->>Board: restore previous state
    Game->>UI: boardChanged()

3.3 摄像头棋子检测接入流程

sequenceDiagram
    participant Cam as ESP32-CAM
    participant Detector as GoBoardDetector
    participant Game as GoGame
    participant AI as IGoEngine
    participant UI as GoGameWindow

    loop 每 500ms
        Cam->>Detector: MJPEG frame
        Detector->>Detector: 矫正 + 棋子识别
        Detector->>Detector: diff = S(t) ⊕ S(t-1)
        
        alt 检测到新落子
            Detector->>Game: play(row,col,color)
            Game->>UI: boardChanged()
            
            alt AI 模式
                Game->>AI: generateMove()
                AI-->>Game: Move
                Game->>Game: play(row,col,oppColor)
                Game->>UI: boardChanged()
            end
        end
    end

3.4 命令执行链

graph TD
    SRC["输入源<br>UI点击 / SGF / 摄像头 / CLI"] --> INVOKER["CommandInvoker"]
    INVOKER --> EXEC["cmd->execute()"]
    EXEC --> VALID["GoBoard 验证<br>气/劫/自杀"]
    VALID -->|"合法"| APPLY["应用 → 更新棋盘"]
    VALID -->|"非法"| REJECT["拒绝 → 错误信息"]
    APPLY --> STACK["压入 undo 栈"]
    APPLY --> EVENTS["emit 信号<br>boardChanged / movePlayed"]
    EVENTS --> LISTENERS["UI / Timer / Sound / AI<br>各自响应"]

4. 开发视图 (Development View)

4.1 模块组织

go-board-cpp/
├── src/
│   ├── core/                    # 领域层 (Qt Core, 零 Widgets)
│   │   ├── GoBoard.h/cpp        # 规则引擎 (纯 C++ 零 Qt)
│   │   ├── GoGame.h/cpp         # 游戏控制器 (QObject + 信号槽)
│   │   ├── GoTimer.h/cpp        # 计时器 (QObject)
│   │   ├── GoSound.h/cpp        # 音效 (QObject)
│   │   ├── SgfParser.h/cpp      # SGF 解析 (纯 C++ 零 Qt, std::string)
│   │   └── SgfPlayer.h/cpp      # SGF 播放 (Qt Core 封装, QString)
│   ├── engine/                  # AI 引擎接口 + 实现
│   │   ├── IGoEngine.h          # 引擎抽象接口
│   │   ├── GtpEngine.h/cpp      # GTP 协议实现 (KataGo)
│   │   └── MockEngine.h/cpp     # 测试桩
│   ├── command/                 # 命令模式
│   │   ├── ICommand.h           # 命令接口
│   │   ├── CommandInvoker.h/cpp # 命令调度器
│   │   ├── PlaceStoneCmd.h/cpp  # 落子命令
│   │   ├── PassCmd.h/cpp        # 虚手命令
│   │   ├── UndoCmd.h/cpp        # 悔棋命令
│   │   └── SgfStepCmd.h/cpp     # SGF 打谱步进命令
│   ├── detect/                  # 棋盘检测 (Part 0 迁移)
│   │   ├── IBoardDetector.h     # 检测器接口
│   │   ├── GoBoardDetector.h/cpp
│   │   ├── BoardRectifier.h/cpp
│   │   ├── LiveView.h/cpp
│   │   ├── DewarpView.h/cpp
│   │   └── MarkWidget.h/cpp
│   ├── ui/                      # 表现层
│   │   ├── GoBoardWidget.h/cpp  # 虚拟棋盘控件
│   │   ├── GameWindow.h/cpp     # 主游戏窗口
│   │   ├── TimerPanel.h/cpp     # 计时面板
│   │   ├── SoundControl.h/cpp   # 音效控制
│   │   └── SgfControl.h/cpp     # SGF 控制栏
│   ├── net/                     # 网络层
│   │   └── MjpegStream.h/cpp
│   └── main.cpp
├── doc/                         # 文档
├── CMakeLists.txt
└── build.bat

4.2 依赖方向 (单向, 无循环)

graph LR
    UI["ui/ (表现层)"] --> CMD["command/"]
    UI --> CORE["core/"]
    CMD --> CORE
    CORE --> ENGINE["engine/ (AI接口)"]
    CORE --> SGF_M["SgfParser"]
    DETECT["detect/"] --> CORE
    DETECT --> CV["OpenCV"]
    NET["net/"] --> QT["Qt6::Network"]

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

原则: 领域层 (core/) 零 Qt Widgets 依赖,但依赖 Qt6::Core(QObject / 信号槽 / moc)。其中 GoBoard 和 SgfParser 为纯 C++ 零 Qt,可脱离 QApplication 做 GTest 或移植 WASM。表现层 (ui/) 可以依赖一切,但无人依赖它。

4.3 CMake 结构

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

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

# 可执行: 完整游戏
add_executable(go-game
    src/ui/GameWindow.cpp
    src/ui/GoBoardWidget.cpp
    src/main.cpp
)
target_link_libraries(go-game PRIVATE go-core go-engine Qt6::Widgets Qt6::Network)

5. 物理视图 (Physical View)

5.1 部署拓扑

graph TB
    subgraph world["物理世界"]
        BOARD["🏁 围棋棋盘<br>19×19"]
        STONES["⚫⚪ 棋子"]
    end

    subgraph capture["采集端 ESP32-CAM"]
        CAM["📷 OV2640<br>800×600 JPEG"]
        WIFI["📡 WiFi AP"]
        HTTP["HTTP Server<br>:80 /capture /stream"]
    end

    subgraph compute["计算端 PC"]
        subgraph process["go-game 进程"]
            MAIN["Main Thread<br>Qt Event Loop<br>UI 渲染"]
            MJPEG["MJPEG 解码 (网络线程)"]
            DETECT_TH["检测线程 (可选)<br>OpenCV 处理"]
            AI_TH["AI 线程 (QProcess)"]
        end
        DISPLAY["🖥️ 主显示器<br>控制面板"]
        SCREEN["🖥️ 棋盘下方屏幕<br>光标显示"]
    end

    BOARD -->|"俯拍"| CAM
    CAM --> HTTP
    HTTP -->|"WiFi MJPEG"| MJPEG
    MJPEG --> DETECT_TH
    DETECT_TH --> MAIN
    MAIN -->|"GTP stdio"| AI_TH
    MAIN --> DISPLAY
    MAIN -->|"HDMI 第二屏"| SCREEN

5.2 线程模型

线程 职责 通信方式
Main (GUI) Qt Event Loop, 渲染, 用户交互 主控
Network MJPEG HTTP 长连接, 异步收帧 QNetworkReply 信号 → Main
Detection (可选) OpenCV 检测, 棋子识别 Qt::QueuedConnection 信号
AI Engine GTP 子进程 stdio 通信 QProcess readyRead → Main

初期: 单线程 (Main 处理一切)。OpenCV 处理和 AI 引擎都是异步信号驱动,不阻塞 UI。仅在帧率瓶颈时才将 Detection 拆分到独立线程。


6. 场景视图 (Scenarios)

6.1 用例总览

graph TB
    ACTOR["👤 用户"] --> UC1["人机对弈"]
    ACTOR --> UC2["SGF 打谱/复盘"]
    ACTOR --> UC3["摄像头自动对弈"]
    ACTOR --> UC4["双人对弈 (热座)"]
    ACTOR --> UC5["计时设置"]
    ACTOR --> UC6["音效设置"]

    UC1 --> GAME["GoGame"]
    UC2 --> SGF_M["SgfPlayer"]
    UC3 --> DETECT_M["BoardDetector"]
    UC4 --> GAME

    AI["IGoEngine"] --> UC1
    AI --> UC3
    CAM["ESP32-CAM"] --> UC3

6.2 场景卡片

场景 触发 流程 涉及组件
人机对弈 新局 + AI 模式 人落子 → AI 生成 → 自动落子 → 循环 GoGame, IGoEngine, GoTimer, GoSound
SGF 打谱 加载 .sgf 文件 解析 → 逐手步进 → 显示注释/分支 SgfPlayer, GoGame, GoBoardWidget
摄像头对弈 连接 ESP32-CAM 检测棋子变化 → 落子 → AI 回应 BoardDetector, GoGame, IGoEngine
双人对弈 新局 + 本地模式 双方交替点击落子 GoGame, GoTimer
复盘 SGF + 自动播放 定时步进 + 分支选择 SgfPlayer, QTimer

7. 命令模式设计

7.1 为什么用命令模式?

现状: UI 直接调用 GoGame::play() → UI 和逻辑耦合
目标: UI / SGF / 摄像头 / CLI 四种输入源 → 统一的命令入口
输入源 命令 示例
鼠标点击 PlaceStoneCmd(row,col,color) 用户点棋盘
SGF 步进 SgfStepCmd(forward/back) 打谱导航
摄像头检测 PlaceStoneCmd(row,col,color) 自动识别落子
CLI/脚本 PassCmd() / PlaceStoneCmd(...) 测试/自动化

7.2 接口定义

// src/command/ICommand.h
class ICommand {
public:
    virtual ~ICommand() = default;
    virtual bool execute(GoGame &game) = 0;   // 执行
    virtual bool undo(GoGame &game) = 0;       // 回退
    virtual QString description() const = 0;   // 描述 (日志/UI)
};

7.3 CommandInvoker

// src/command/CommandInvoker.h
class CommandInvoker : public QObject {
    Q_OBJECT
public:
    bool execute(ICommand *cmd, GoGame &game);
    bool undo(GoGame &game);
    bool redo(GoGame &game);
    int  undoDepth() const;
    void clear();

private:
    std::vector<ICommand*> m_history;   // 已执行命令
    int m_currentIndex = -1;            // 当前位置
};

7.4 命令执行器流程

graph TD
    CMD["new PlaceStoneCmd(row,col)"] --> INV["invoker.execute(cmd, game)"]
    INV --> CALL["cmd->execute(game)"]
    CALL --> INTERNAL["game.play(row,col)"]
    INTERNAL --> RESULT{"成功?"}
    RESULT -->|"✅"| PUSH["压入历史栈<br>m_currentIndex++"]
    RESULT -->|"❌"| DEL["delete cmd<br>不压栈"]
    PUSH --> SIGNAL["emit commandExecuted()"]

8. SGF 打谱子系统

8.1 SGF 格式简介

(;GM[1]FF[4]SZ[19]KM[6.5]
 PB[Black]PW[White]
 ;B[pd];W[dp];B[pp];W[dd]
 ;B[fq]C[This is a comment]
 (;W[cc]C[Variation A])
 (;W[cd]C[Variation B])
)
  • GM[1] = 围棋, SZ[19] = 19路
  • B[pd] = 黑落 D4 (SGF 坐标)
  • C[...] = 注释
  • (...) = 分支

8.2 双层设计:SgfParser (纯 C++) + SgfPlayer (Qt 封装)

SgfParser — 纯 C++ 零 Qt 依赖,可脱离 QApplication 独立测试或移植 WASM:

// src/core/SgfParser.h — 纯 C++,使用 std::string
class SgfParser {
public:
    bool load(const std::string &filePath);
    bool loadFromString(const std::string &sgfContent);

    // 导航
    bool next();
    bool prev();
    bool gotoMove(int n);
    bool nextBranch();

    // 查询 (纯 C++ 返回类型)
    int currentMove() const;
    int totalMoves() const;
    std::string comment() const;
    std::vector<std::string> branches() const;
    GoBoard boardState() const;

    // 元数据
    std::string blackPlayer() const;
    std::string whitePlayer() const;
    double komi() const;
    std::string date() const;
    std::string event() const;
};

SgfPlayer — Qt 封装层,在 SgfParser 之上提供信号槽和 QObject 集成,使用 QString:

// src/core/SgfPlayer.h — Qt Core 封装
class SgfPlayer : public QObject, public ISgfPlayer {
    Q_OBJECT
public:
    bool load(const QString &filePath) override;
    bool next() override;
    bool prev() override;
    bool gotoMove(int n) override;
    bool nextBranch() override;

    // ISgfPlayer 接口 (重写)
    int currentMove() const override;
    int totalMoves() const override;
    QString comment() const override;
    QStringList branches() const override;
    GoBoard boardState() const override;

    // Qt 封装元数据
    QString blackPlayer() const;
    QString whitePlayer() const;
    double komi() const;
    QString date() const;
    QString event() const;

signals:
    void moveChanged(int moveNum);
    void branchAvailable(int count);

private:
    SgfParser m_parser;   // 纯 C++ 核心
};

8.3 SGF 坐标转换

SGF 使用字母坐标 (a-s),需与内部 (row,col) 互转:

// SGF "pd" → (row=15, col=3)  (D4, 右上星位)
static int sgfColToIndex(QChar c) { return c.toLower().unicode() - 'a'; }
static int sgfRowToIndex(QChar c) { return 's' - c.toLower().unicode(); }

// (row,col) → SGF 字符串 "pd"
static QString indexToSgf(int row, int col) {
    return QString("%1%2")
        .arg(QChar('a' + col))
        .arg(QChar('a' + (18 - row)));
}

9. AI 引擎接口

9.1 IGoEngine

// src/engine/IGoEngine.h
class IGoEngine {
public:
    virtual ~IGoEngine() = default;

    /// 初始化引擎
    virtual bool initialize() = 0;

    /// 请求 AI 生成一手棋
    /// @param boardState 361 字符局面 "BWB..W."
    /// @param color      当前落子方
    /// @return pair{row, col},(-1,-1) = pass/resign
    virtual std::pair<int,int> generateMove(
        const std::string &boardState,
        GoBoard::Stone color) = 0;

    /// 设置级别 (playouts / dan-level)
    virtual void setLevel(int level) = 0;

    /// 引擎名称
    virtual std::string name() const = 0;

    /// 引擎是否就绪
    virtual bool isReady() const = 0;

    /// 清理资源
    virtual void shutdown() = 0;
};

9.2 GtpEngine (KataGo)

class GtpEngine : public IGoEngine {
    Q_OBJECT
public:
    bool initialize() override;
    std::pair<int,int> generateMove(const std::string &, GoBoard::Stone) override;
    void setLevel(int level) override;
    // ...

private:
    QProcess *m_process;        // katago 子进程
    void sendCmd(const QString &cmd);
    QString readResponse();

signals:
    void ready();
    void error(const QString &msg);
};

GTP 协议交互:

> boardsize 19
= 
> komi 6.5
= 
> play B D4
= 
> genmove W
= Q16

9.3 MockEngine (测试桩)

class MockEngine : public IGoEngine {
public:
    std::pair<int,int> generateMove(const std::string &board, GoBoard::Stone c) override {
        // 随机选空位
        for (int i = 0; i < 361; i++) {
            if (board[i] == '.') return {i/19, i%19};
        }
        return {-1, -1};
    }
    std::string name() const override { return "MockEngine"; }
    // ...
};

10. 棋盘检测接入

10.1 IBoardDetector 接口

// src/detect/IBoardDetector.h
class IBoardDetector {
public:
    virtual ~IBoardDetector() = default;

    /// 处理新帧 → 返回识别到的落子变化
    /// @return 新检测到的落子列表 (row, col, stone)
    virtual std::vector<DetectedMove> processFrame(const QImage &frame) = 0;

    /// 获取当前 19×19 局面字符串
    virtual std::string currentBoardState() const = 0;

    /// 矫正后的标准棋盘图 (600×600)
    virtual QImage dewarpedImage() const = 0;

    /// 是否检测到稳定局面 (连续 N 帧一致)
    virtual bool isStable() const = 0;
};

struct DetectedMove {
    int row, col;
    GoBoard::Stone stone;
    bool isPlacement;   // true=落子, false=提子
};

10.2 与 GoGame 集成

// 在 GoGame 中:
void GoGame::setDetector(IBoardDetector *detector) {
    m_detector = detector;
}

// MainWindow 中:
connect(m_stream, &MjpegStream::frameReady, [this](const QImage &frame) {
    if (m_autoDetect && m_detector) {
        auto moves = m_detector->processFrame(frame);
        for (auto &m : moves) {
            m_game->execute(new PlaceStoneCmd(m.row, m.col, m.stone));
        }
    }
});

11. 接口契约定义

11.1 GoGame 事件总线

// GoGame 对外的所有信号 (观察者模式)
signals:
    // 棋盘变更
    void boardChanged();
    void movePlayed(int row, int col, GoBoard::Stone color);
    void stonesCaptured(int count, GoBoard::Stone by);

    // 回合变更
    void turnChanged(GoBoard::Stone nextPlayer);
    void gameEnded(GoBoard::Stone winner, const QString &reason);

    // 计时
    void timeWarning(GoBoard::Stone player, int remainingSec);

    // 命令执行
    void commandExecuted(ICommand *cmd);
    void commandUndone(ICommand *cmd);

11.2 扩展点总结

扩展点 接口 替换/新增
AI 引擎 IGoEngine KataGo / GNU Go / Leela Zero / 自定义
棋盘检测 IBoardDetector 摄像头 / 图片 / Mock
输入源 ICommand 鼠标 / SGF / CLI / 脚本
投影显示 IProjector (未来) 副屏 / 投影仪 / LED
音效 替换 GoSound 内部实现 系统 beep / 自定义 WAV
计时 替换 GoTimer 规则 日本 / 加拿大 / Fischer

11.3 关键设计决策

决策 理由
命令模式 UI/SGF/摄像头/CLI 四个输入源共享同一执行路径
AI 接口 KataGo/GNU Go 可互换,测试用 MockEngine
检测接口 摄像头实时 / 静态图片 / 模拟输入灵活切换
SGF 双层设计 SgfParser 纯 C++ 零 Qt(GTest/WASM); SgfPlayer Qt 封装(QString+信号槽+QObject)
领域层零 Widgets GoBoard + SgfParser 纯 C++(GTest/WASM 可移植); GoGame/GoTimer/GoSound 依赖 Qt Core(QObject+信号槽)
Event Bus 松耦合,新功能通过连接信号加入

附录: 迁移路线

从当前代码到目标架构

当前 (单文件, 耦合)         目标 (分层, 解耦)
─────────────────────      ─────────────────────
go_game.cpp (600行)   →    ui/GameWindow.cpp      (~300行)
GoGame.h/cpp (350行)  →    core/GoGame.h/cpp      保留 + 精简
GoBoardWidget          →    ui/GoBoardWidget.h/cpp 归类
GoTimer/GoSound        →    core/GoTimer/Sound     归类
新增                        command/CommandInvoker  (~100行)
新增                        command/PlaceStoneCmd   (~40行)
新增                        engine/IGoEngine + GtpEngine (~200行)
新增                        core/SgfParser          (~200行)

迁移策略:增量式,不破坏现有功能。

  1. 第一步: 提取 core/ 文件夹,GoBoard/GoTimer/GoSound 移入
  2. 第二步: 引入 CommandInvoker,GoGame 接收命令而非直接 play()
  3. 第三步: 实现 IGoEngine + GtpEngine
  4. 第四步: 实现 SgfParser + 打谱 UI
  5. 第五步: 引入 IBoardDetector,接入 Part 0 的检测管线

12. 高风险点与缓解策略

12.1 风险矩阵

风险 等级 影响 缓解措施
CommandInvoker 内存泄漏 🔴 高 std::vector<ICommand*> 裸指针,undo 栈增长无界 改用 std::unique_ptr<ICommand>;设置最大 undo 深度 (默认 100);clear() 时确保 delete
GTP 子进程崩溃 🔴 高 AI 引擎无响应,对弈中断 QProcess::errorOccurred 信号监听;自动重启 + 状态恢复;超时 30s 降级为 MockEngine
MJPEG 流断连 🟡 中 ESP32-CAM WiFi 不稳定,检测中断 断线重连 (指数退避 1s/2s/4s/8s);重连超限弹窗提示;支持静态图片 fallback 模式
OpenCV 检测抖动 🟡 中 光照变化导致误检落子 连续 N 帧一致 (default N=3) 才确认;防抖窗口可配置;人工标记矫正兜底
双屏光标定位偏移 🟡 中 棋盘位置移动后光标不准 每次标定后计算映射矩阵;提供微调 UI (方向键 ±1px);定期自动重标定 (可选)
单线程瓶颈 🟢 低 高帧率+AI计算时 UI 卡顿 当前异步信号驱动足够;帧率 < 15fps 时自动启用 Detection 独立线程
SGF 文件写回缺失 🟢 低 SgfPlayer 能读不能写,无法保存对局 后期加入 SgfWriter;对局结束时自动保存 SGF;复用 SgfParser 的序列化逻辑

12.2 关键防御性设计

// CommandInvoker: 智能指针 + 深度限制
class CommandInvoker {
    static constexpr int MAX_UNDO_DEPTH = 100;
    std::deque<std::unique_ptr<ICommand>> m_history;
};

// GTP 超时保护
class GtpEngine {
    static constexpr int GTP_TIMEOUT_MS = 30000;
    QTimer m_watchdog;
    // generateMove() 启动 watchdog
    // 超时 → kill + restart 子进程 → 返回 pass
};

// MJPEG 重连策略
class MjpegStream {
    int m_retryCount = 0;
    static constexpr int MAX_RETRIES = 5;
    static constexpr int BASE_DELAY_MS = 1000;
    // 断连 → QTimer::singleShot(BASE_DELAY * 2^retry, reconnect)
};

12.3 降级路径

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

三阶段降级:实时检测 → 静态图片 → 手动点击;AI 可用时自动升级为全功能。单点故障不阻塞基本对弈。