课程定位: 围棋打谱辅助系统系列课程的 Part 3。本章解决"相机看到了棋子,怎么正确反映到屏幕上"的核心问题——四层架构确保虚拟棋盘始终以 GoGame 规则引擎为权威,而非直接展示原始检测结果。
前置知识: Part 0 视觉识别、Part 1 规则引擎
预计阅读: 15 分钟
实体棋盘 → 虚拟棋盘映射设计
日期:2026-05-14
问题:实体棋盘识别出的棋子如何和虚拟棋盘对应
1. 架构分层
实体棋盘上的物理棋子要正确反映到屏幕上的虚拟棋盘,需要经过四个层次,每层职责不同:
┌─────────────────────────────────────────────────────────┐
│ Layer 4: 虚拟棋盘 (GoBoardWidget) │
│ 屏幕显示, 只读展示 GoGame 的权威局面 │
│ 始终与 GoGame.state 严格一致 │
├─────────────────────────────────────────────────────────┤
│ Layer 3: 游戏引擎 (GoGame) │
│ 规则校验: 合法/自杀/劫/提子 │
│ play(row, col, color) → 返回提子列表 │
│ 权威状态: boardString() │
├─────────────────────────────────────────────────────────┤
│ Layer 2: 确认逻辑 (onPhysicalConfirm) │
│ 全盘检测 → diff → 提取"用户落子" → 调用 GoGame.play() │
│ 处理: 单子/多子/提子/噪点/无变化 │
├─────────────────────────────────────────────────────────┤
│ Layer 1: 视觉识别 (StoneDetector + 矫正) │
│ 相机帧 → 矫正 → classify → 361字符 ".BWB..W." │
│ 输出: 原始检测结果 (可能有噪声) │
└─────────────────────────────────────────────────────────┘
核心原则:虚拟棋盘始终显示 GoGame 状态,不是原始检测结果。检测是输入,GoGame 是权威。
2. 确认流程
用户按下空格键
│
▼
┌──────────────────────────────────────┐
│ Step 1: 全盘检测 │
│ 相机帧 → rectify → classify() │
│ 得到: detectedState (361字符) │
│ 如: ".......B...W....." │
├──────────────────────────────────────┤
│ Step 2: 全量刷新虚拟棋盘 │
│ 把 detectedState 显示到虚拟棋盘 │
│ → 用户看到: "相机看到了这些棋子" │
│ (用半透明/蓝色调标记检测结果) │
├──────────────────────────────────────┤
│ Step 3: 计算差异 │
│ diff = detectedState XOR lastConfirmedState │
│ addedStones: '.' → 'B'/'W' │
│ removedStones: 'B'/'W' → '.' │
├──────────────────────────────────────┤
│ Step 4: 提取落子 → GoGame.play() │
│ 预期: 刚好1个 added (用户落子) │
│ 场景A: 1 added + 0 removed │
│ → 正常落子, 调用 play(r,c) │
│ 场景B: 1 added + N removed │
│ → 落子+提子, play() 自动处理 │
│ 场景C: 0 added + 0 removed │
│ → 没变化, 可能用户还没落子 │
│ 场景D: N added (N>1) │
│ → 异常, 取最高置信度位置 │
├──────────────────────────────────────┤
│ Step 5: GoGame 返回 → 刷新虚拟棋盘 │
│ GoGame 内部的 boardString() → 设置 │
│ 到 GoBoardWidget (替换检测预览) │
│ 完成: 虚拟棋盘 = GoGame 权威状态 │
└──────────────────────────────────────┘
3. 为什么不全量同步
常见疑问:检测结果直接覆盖 GoGame 局面不就行了?
不行,因为:
| 问题 | 说明 |
|---|---|
| 劫争 | 检测不知道劫争规则,不能直接覆盖 |
| 自杀 | 检测可能把死子当活子 |
| 提子 | GoGame.play() 自动计算气+提子,检测做不到 |
| 噪点 | 偶尔误检的噪点会被 GoGame.play() 拒绝(已有棋子/禁着点) |
| 回合 | 检测不知道该谁下,可能把对方棋子当成本方落子 |
所以数据流是:检测 → 提取落子位置 → GoGame 规则引擎 → 虚拟棋盘。
4. 视觉反馈时间线
T+0ms: 用户按下空格键
T+50ms: 按钮变 "🔍 检测中..." (禁用)
T+100ms: 检测完成
T+200ms: Step2 - 虚拟棋盘刷新显示检测结果 (蓝色调)
T+400ms: Step5 - 虚拟棋盘刷新为 GoGame 权威状态 (正常色调)
消息栏显示 "✅ 确认: ⚫ D16"
T+500ms: 按钮恢复 "✅ 确认落子 (空格键)"
用户感知:按下空格 → 虚拟棋盘瞬间刷新 → 自己的棋子出现在棋盘上。
5. AI 回合的虚拟棋盘更新
当 AI 落子后(GoGame 自动调用 requestAiMove → play),虚拟棋盘自动刷新:
AI 回合:
GoGame.requestAiMove()
→ GtpEngine.generateMove() → (row, col)
→ GoGame.play(row, col) ← 规则校验
→ emit boardChanged()
→ refreshBoard() ← 虚拟棋盘更新
→ emit aiMoveReady
→ 消息: "🤖 AI 落子 D4 — 请落子"
用户看到:
虚拟棋盘上多了一个 AI 的棋子
消息提示 AI 的落子位置
等待用户实体落子后按空格键
6. 异常处理
| 场景 | 检测结果 | 处理 |
|---|---|---|
| 用户还没落子 | diff 为空 | 提示 “未检测到变化” |
| 用户放错颜色 | 检测到对面颜色 | GoGame.play() 拒绝 “禁着点” |
| 用户放到已有棋子上 | 检测到已有棋子位置 | GoGame.play() 拒绝 “该位置已有棋子” |
| 手抖/噪点 | 1-2个假检测 | 200ms 延迟 + GoGame 校验过滤 |
| 相机断连 | 无法检测 | 提示 “⚠ 相机断连” + 禁用确认 |
| 未标定 | 无法矫正 | 提示 “⚠ 未标定” |