diff --git a/docs/superpowers/specs/2026-05-31-gobang-network-design.md b/docs/superpowers/specs/2026-05-31-gobang-network-design.md new file mode 100644 index 0000000..a3cdd50 --- /dev/null +++ b/docs/superpowers/specs/2026-05-31-gobang-network-design.md @@ -0,0 +1,159 @@ +# Gobang 网络对战设计文档 + +> 状态: 已确认 | 日期: 2026-05-31 + +## 目标 + +为 Gobang v2.0 实现基于 renet 的 P2P 网络对战,两人通过 IP:端口直连对弈。 + +## 架构 + +``` +┌─ React 前端 ───────────────────────────────────────────┐ +│ OnlineSetup GameView │ +│ 创建房间/加入房间 连接状态指示 │ +│ listen("remote-move") │ +└─────────────────────┬───────────────────────────────────┘ + │ invoke / listen +┌─ Tauri IPC ─────────┼───────────────────────────────────┐ +│ commands.rs │ │ +│ host_game(port) │ 对手落子 → │ +│ join_game(ip,port) │ app.emit("remote-move") │ +│ send_move(x,y) │ │ +│ send_undo() │ AppState.network_tx │ +│ send_resign() │ channel 发送端 │ +└─────────┬────────────┴──────────────────────────────────┘ + │ mpsc::channel (NetworkCmd / NetworkEvent) +┌─ 网络线程 ──────────────────────────────────────────────┐ +│ NetworkLoop │ +│ Server: RenetServer + NetcodeServerTransport │ +│ Client: RenetClient + NetcodeClientTransport │ +│ │ +│ loop { update → recv → process → send_packets } │ +│ 16ms 帧率,消息通过 channel 与 commands 层通信 │ +└──────────────────────────────────────────────────────────┘ +``` + +## 通信模型 + +renet 内置 Server/Client。主机运行 RenetServer,对手作为 RenetClient 连接。所有消息经主机转发。 + +### 消息协议 + +网络传输使用 serde JSON + renet ReliableOrdered 通道: + +```rust +enum NetMessage { + Move { x: usize, y: usize, turn: u32 }, + Undo { steps: u32 }, + Resign, +} +``` + +### Channel 接口 + +```rust +// commands → 网络线程 +enum NetworkCmd { + SendMove { x: usize, y: usize, turn: u32 }, + SendUndo { steps: u32 }, + SendResign, + Shutdown, +} + +// 网络线程 → commands +enum NetworkEvent { + RemoteMove { x: usize, y: usize }, + RemoteUndo { steps: u32 }, + RemoteResign, + ClientConnected, + ClientDisconnected, + Error(String), +} +``` + +## 连接流程 + +``` +主机 (Server) 对手 (Client) + host_game(port) + 绑定 UDP "0.0.0.0:{port}" + NetworkLoop::Server 启动 + 返回实际端口 + emit("waiting") ───────────────────→ 对手 join_game(ip, port) + bind UDP "0.0.0.0:0" + NetworkLoop::Client 启动 + connect to server ──→ + ← connected ───────── + 收到 ClientConnected + emit("opponent-joined") ←──────────→ emit("connected") + + 游戏开始,黑方(主机)先手正常落子 + place_piece → NetworkCmd::SendMove ─→ broadcast ─→ client recv + NetworkEvent::RemoteMove + invoke place_piece +``` + +## 生命周期 + +- 网络线程在 `new_game(Online)` 时 spawn +- 游戏结束或 AppState drop 时:发送 Shutdown → 线程退出 → join +- 对手断开:主机的 ClientDisconnected event → emit 对手获胜 + +## 前端改动 + +### OnlineSetup +- 创建房间:输入端口号(可选,默认随机),显示"我的地址: IP:端口" +- 加入房间:输入"IP:端口",连接 + +### GameView — 新增连接状态条 +- 等待中:显示"等待对手加入... (你的地址: IP:端口)" +- 已连接:显示"已连接" +- 已断开:显示"对手断开连接" + 对手获胜 + +### BoardCanvas — 监听 remote-move +```typescript +useEffect(() => { + const unlisten = listen<{ x: number; y: number }>('remote-move', (event) => { + placePiece(event.payload.x, event.payload.y); + }); + return () => { unlisten.then(fn => fn()); }; +}, []); +``` + +### GameControls +- Online 模式:不显示悔棋(需双方同意,暂不做) +- 认输:调用 send_resign + +## AppState 改动 + +```rust +pub struct AppState { + // ... 现有字段 ... + pub network_tx: Mutex>>, +} +``` + +## 依赖 + +```toml +# core/Cargo.toml & gui/Cargo.toml +renet = { version = "0.0.23", features = ["netcode"] } +renet_netcode = "0.0.15" +serde_json = "1" # 已有 +``` + +## 不做 (YAGNI) + +- Chat 功能(NetMessage 保留类型但无 UI) +- NAT 穿透 / 中转服务器 +- 断线重连 +- 观战模式 +- 悔棋双方确认(Online 模式直接禁悔棋) + +## 测试策略 + +- Rust 单元测试:NetMessage serde 往返、NetworkCmd/Event channel 通信、NetworkLoop::new 创建 +- 集成测试:renet 本地 client 模拟(server.new_local_client) +- 前端测试:OnlineSetup 组件渲染、状态文本切换 +- 手动测试:双窗口(host + join localhost)