智能围棋盘 —— 系统架构设计文档#
版本: 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 步渐进式重构,每步可验证,不破坏现有功能 |
| 智能指针 |
CommandInvoker 用 unique_ptr 防内存泄漏,MAX_UNDO_DEPTH=100 |