智能围棋盘 —— 系统架构设计文档
版本: v2.0 | 架构师: Magic_GT | 修订: Magic_HK | 方法论: 4+1 View Model (Kruchten)
设计目标: 构建一个高内聚、低耦合、可扩展的围棋系统,支持:命令模式解耦、SGF 打谱、AI 引擎接入、摄像头棋盘/棋子检测集成、屏幕直显光标交互。
v2 变更: 去除投影仪方案,改为屏幕直显光标;增加 CursorWindow 双屏交互;补齐风险缓解策略。
目录
- 4+1 视图概述
- 逻辑视图 Logical View
- 过程视图 Process View
- 开发视图 Development View
- 物理视图 Physical View
- 场景视图 Scenarios
- 命令模式设计
- SGF 打谱子系统
- AI 引擎接口
- 棋盘检测接入
- 接口契约定义
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行)
迁移策略:增量式,不破坏现有功能。
- 第一步: 提取
core/文件夹,GoBoard/GoTimer/GoSound 移入 - 第二步: 引入
CommandInvoker,GoGame 接收命令而非直接 play() - 第三步: 实现
IGoEngine+GtpEngine - 第四步: 实现
SgfParser+ 打谱 UI - 第五步: 引入
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 可用时自动升级为全功能。单点故障不阻塞基本对弈。