本文从架构师视角,用 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 头文件 |