课程定位: 围棋打谱辅助系统系列课程的 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 校验过滤
相机断连 无法检测 提示 “⚠ 相机断连” + 禁用确认
未标定 无法矫正 提示 “⚠ 未标定”