Phase 5 联机 P2P 房间骨架 v1.0
整理时间:2026-07-01 17:30 +08:00 整理人:Mavis(PM 视角) 依据:
docs/design/active/产品需求总览-v1.md(D4 决策点)docs/plans/active/2026-07-06-5日开发计划-v1.md(R3 主任务)docs/design/completed/gameplay/官府抄缴库房竞标会-联机版玩法设计.md(联机设计母本)docs/design/backlog/gameplay/p2p-multiplayer-playbook.md(P2P 玩法手册)docs/data/auction/+src/components/auction/(现有拍卖 UI / 配置)docs/specs/engineering/code-structure-splitting-rules-v1.md(代码拆分规则) 本文档作用:把 Phase 5 联机 P2P 房间状态机 + 接口拍清楚,Day 4 短工能据此起骨架,Day 5+ 再逐步接入实时竞价。 本版本只起房间骨架:不实现实时竞价 / 不实现断线重连 / 不动auctionRules.ts现有规则。
0. TL;DR(60 秒版)
- 房间状态机 = 5 态:
idle → waiting → bidding → settled → closed - 接口拍 4 个:
RoomHost房主 /RoomGuest客机 /RoomSeat席位 /RoomAIAI 补位 - 最小 UI 拍 2 块:房间卡片(状态总览) + 席位格子(玩家 / AI 占位)
- 目录命名
multiplayer-lobby/,跟现有auction/平级,不碰auctionRules.ts - 数据载体
MultiplayerRoomState(跟AuctionState平级,新增 type,不动现有 schema) - Day 4 验收 = type 通过 tsc + 状态机能单步推进
idle → waiting+ 最小 UI 占位 +npm run build通过
1. 房间状态机(5 态)
1.1 状态转换图
[房主创建]
↓
[idle] ───────────→ [waiting] ──→ [bidding] ──→ [settled] ──→ [closed]
↑ ↑ │ │ │
│ │ └── [guest leave] ──────┘ │
│ └────── [host cancel] ──────────────────────┘
│ ↓
└──────────── [host reopen] ←──────┘
1.2 5 态语义(只拍口径)
| 态 | 语义 | 进入条件 | 退出条件 | 持续时间 |
| :--- | :--- | :--- | :--- | :--- |
| idle | 房间未创建或已 closed 后归档 | 初始态 / closed 终态 | 房主创建 → waiting | 不限时 |
| waiting | 等待客机 / AI 补位 / 房主按"开始" | idle + 房主创建 | 满席 / 房主按开始 → bidding
房主取消 → closed
客机离开 → 回到 waiting(席不满) | 默认 5 分钟超时(可配) |
| bidding | 实时竞价中 | waiting 满足开始条件 | 流拍 / 中止 → settled
房主中止 → closed | 由实时竞价规则控制(本 v1.0 不实现) |
| settled | 本轮结算完成 | bidding 自然结束 | 房主"再来一局" → waiting
房主"关闭房间" → closed | 3-5 秒展示后自动跳到 waiting 或 closed |
| closed | 房间关闭 | 任意态房主关闭 / waiting 超时 / settled 房主关闭 | 不可逆;MultiplayerRoomState 标记 archived: true | 终态 |
1.3 状态机约束(明确)
- ❌ 不允许
idle → bidding直跳 - ❌ 不允许
settled → idle回退(只能waiting重建) - ❌ 不允许
closed复活 - ✅
bidding任意时刻房主可强制closed(安全网)
1.4 不在本 v1.0 的事项
- ❌ 实时竞价毫秒同步
- ❌ 5 秒流拍
- ❌ 恶意抬价 / 表情包互动
- ❌ 断线重连 / AI 接管
- ❌ 结算暂存 / 重连回写
- ❌
bidding/settled内部子状态机(留 v1.1)
2. 4 个接口(只拍 type,Day 4 短工可派生)
2.1 RoomHost 房主接口
interface RoomHost {
hostId: string; // 房主 playerId(从 GlobalProfile)
displayName: string;
createdAt: number; // 房间创建时间戳
authority: 'host'; // 权威节点 = 房主(已有规约)
availableActions: ReadonlyArray<
| 'cancel-room' // 任意态关闭
| 'start-bidding' // waiting → bidding
| 'force-close' // 任意态强制 closed
| 'reopen-after-settled' // settled → waiting
>;
}
2.2 RoomGuest 客机接口
interface RoomGuest {
guestId: string; // 客机 playerId
displayName: string;
joinedAt: number; // 加入时间戳
seatId: RoomSeatId; // 占哪个席位
availableActions: ReadonlyArray<
| 'leave-room' // 离开 → waiting 席不满
>;
}
2.3 RoomSeat 席位接口
type RoomSeatId = 'seat-1' | 'seat-2' | 'seat-3' | 'seat-4';
interface RoomSeat {
seatId: RoomSeatId;
occupant: RoomHost | RoomGuest | RoomAI | null;
lockedAt: number | null; // 占座时间戳
}
2.4 RoomAI AI 补位接口
interface RoomAI {
aiId: 'ai-classic-bidder' | 'ai-cautious-bidder' | 'ai-aggressive-bidder';
displayName: string; // 例如"老江湖 / 保守贾 / 豪气张"
seatId: RoomSeatId;
joinedAt: number;
availableActions: ReadonlyArray<'leave-room'>;
}
2.5 不在本 v1.0 的事项
- ❌ 客机的"出价"动作(留 v1.1 实时竞价)
- ❌ AI 的"出价"策略实现(留 v1.1)
- ❌ 客机重连(留 v1.1 断线重连)
- ❌ 观战者接口(留 v1.1)
- ❌ 房间规模 4 / 6 / 8 选(本 v1.0 默认 4 席)
3. 数据载体:MultiplayerRoomState
3.1 跟 AuctionState 的关系
AuctionState= 现有联机拍卖的"拍卖实例"数据MultiplayerRoomState= 新增的"房间实例"数据- 关系:
MultiplayerRoomState是AuctionState的"前置容器",但本 v1.0 不联通两者,各自独立 - 不改
auctionRules.ts任何字段
3.2 MultiplayerRoomState type
type RoomPhase = 'idle' | 'waiting' | 'bidding' | 'settled' | 'closed';
interface MultiplayerRoomState {
roomId: string; // 房间唯一 ID
phase: RoomPhase; // 当前态(见 § 1.2)
host: RoomHost | null; // 房主(idle 时为 null)
seats: RoomSeat[]; // 默认 4 席(seat-1 ~ seat-4)
guests: RoomGuest[]; // 客机列表(可空)
ai: RoomAI[]; // AI 补位列表(可空)
createdAt: number;
updatedAt: number;
signature: string; // 防篡改签名(沿用项目惯例)
}
3.3 落盘与隔离
- 存储 Key:
tongfu_multiplayer_room_<roomId>(跟tongfu_save_slot_*平级) - 隔离:
MultiplayerRoomState不写GlobalProfile,不写PlaythroughSaveSlot - 清理:
closed态的 room 保留 1 周后自动归档(留 v1.1)
3.4 不在本 v1.0 的事项
- ❌ 跟
AuctionState的实际联通 - ❌ 房间历史 / 战绩统计
- ❌ 房间模板 / 预设配置
- ❌ 跨设备同步(本 v1.0 是本地 P2P 模拟)
4. 最小 UI 拍 2 块(占位即可)
4.1 房间卡片(RoomCard.tsx)
┌─────────────────────────────────────────┐
│ 房间 #R-2026-07-09-001 状态:[waiting] │
│ ─────────────────────────────────────── │
│ 房主:白展堂 (host) │
│ 席位: ① 白展堂 ② 吕秀才 ③ AI-老江湖 ④ [空] │
│ ─────────────────────────────────────── │
│ [开始竞价(房主)] [关闭房间(房主)] │
│ [加入(客机)] [离开(客机)] │
└─────────────────────────────────────────┘
- 组件文件:
src/components/multiplayer-lobby/RoomCard.tsx - 状态:由 props 传入
MultiplayerRoomState,不接数据流(占位) - 样式:复用
components/auction/AuctionPanel.tsx既有拟物化类(.tongfu-parchment.tongfu-button-wooden)
4.2 席位格子(RoomSeatCell.tsx)
┌──────────┐
│ ① 席位 1 │
│ [玩家名] │
│ / [AI 名] │
│ / [空] │
└──────────┘
- 组件文件:
src/components/multiplayer-lobby/RoomSeatCell.tsx - 状态:由 props 传入
RoomSeat,不接数据流(占位) - 复用:跟
StaffRegisterModal.tsx的"员工卡"样式一致
4.3 入口占位
src/components/multiplayer-lobby/MultiplayerLobby.tsx(最小入口,只放 RoomCard 列表)- 接入位置:不接入
App.tsx主导航(留 v1.1,避免污染当前 in-flight) - 入口方式:
?tag:v2|task|2026-07-09-multiplayer-and-storyline短工验证时通过 URL query 触发
4.4 不在本 v1.0 的事项
- ❌ 实时竞价进度条 / 倒计时
- ❌ 出价历史 / 流拍动画
- ❌ 表情包 / 聊天
- ❌ 跟现有
auction/入口的联通
5. 文件边界(明确)
5.1 Day 4 短工允许动
- 新增
src/components/multiplayer-lobby/RoomCard.tsx - 新增
src/components/multiplayer-lobby/RoomSeatCell.tsx - 新增
src/components/multiplayer-lobby/MultiplayerLobby.tsx - 新增
src/data/multiplayer/types.ts(只放 type) - 新增
src/data/multiplayer/roomStateMachine.ts(状态机纯函数) - 新增
src/utils/multiplayer/roomReducer.ts(跟accountProfile.ts风格一致)
5.2 Day 4 短工不允许动
- ❌
src/data/auction/**(不动现有拍卖数据) - ❌
src/utils/auctionRules.ts(不动现有拍卖规则) - ❌
src/components/auction/**(不动现有拍卖 UI) - ❌
src/App.tsx(不接主导航) - ❌
src/types.ts(RoomHost/RoomGuest 等 type 放src/data/multiplayer/types.ts,不污染全局) - ❌
docs/handoff/index.md当前 in-flight
5.3 目录命名规范
multiplayer-lobby/(连字符)跟auction/console/既有目录风格一致- 不用
multiplayerLobby/(不用驼峰目录)
6. 验收口径(R3 短工派发时)
R3 短工派发后,完成标准 = 全部满足:
src/data/multiplayer/types.ts包含RoomHost/RoomGuest/RoomSeat/RoomAI/MultiplayerRoomState/RoomPhase6 个 typesrc/data/multiplayer/roomStateMachine.ts导出transitionRoom(phase, action) → phase纯函数,支持idle → waiting单步推进src/components/multiplayer-lobby/RoomCard.tsx+RoomSeatCell.tsx+MultiplayerLobby.tsx3 个组件落盘cmd /c npm run build通过,npm run lint无新增错误docs/handoff/tasks/2026-07-09-multiplayer-and-storyline.mdtask 卡已创建(已写)auctionRules.ts/auction//App.tsx任何文件未被改动
6.1 不算收口的情形
- ❌ 6 个 type 任一缺失
- ❌ 状态机函数跑不通
idle → waiting - ❌ 动了
auctionRules.ts或auction/** - ❌ 接入了
App.tsx主导航 - ❌ build 报错
6.2 R3 短工可保留的 residual risk
- ✅ 实时竞价毫秒同步(本 v1.0 不实现)
- ✅ 5 秒流拍 / 恶意抬价 / 表情包(本 v1.0 不实现)
- ✅ 断线重连 / AI 接管(本 v1.0 不实现)
- ✅ 跟
AuctionState实际联通(本 v1.0 独立) - ✅ 房间卡片 UI 的视觉稿细化(留 UI 重构 lane)
- ✅ 房间规模 4 / 6 / 8 选(本 v1.0 默认 4 席)
7. 不在本 v1.0 的事项汇总(明确划走)
- ❌ 实时竞价毫秒同步
- ❌ 5 秒流拍
- ❌ 恶意抬价 / 表情包
- ❌ 断线重连 / AI 接管
- ❌ 结算暂存 / 重连回写
- ❌
bidding/settled内部子状态机 - ❌ 客机"出价"动作
- ❌ AI"出价"策略
- ❌ 观战者接口
- ❌ 房间规模选(本 v1.0 默认 4 席)
- ❌ 跟
AuctionState实际联通 - ❌ 房间历史 / 战绩统计
- ❌ 房间模板 / 预设配置
- ❌ 跨设备同步
- ❌
closed后自动归档(留 v1.1) - ❌ 实时竞价进度条 / 倒计时
- ❌ 出价历史 / 流拍动画
- ❌ 表情包 / 聊天
- ❌ 跟
auction/入口联通 - ❌ 接入
App.tsx主导航 - ❌ 改
auctionRules.ts/auction/**/types.ts
8. 参考入口
| 想看什么 | 去这里 |
| :--- | :--- |
| 5 日计划上游 | docs/plans/active/2026-07-06-5日开发计划-v1.md § R3 |
| 产品总览 | docs/design/active/产品需求总览-v1.md § 8.2 D4 |
| 联机设计母本 | docs/design/completed/gameplay/官府抄缴库房竞标会-联机版玩法设计.md |
| P2P 玩法手册 | docs/design/backlog/gameplay/p2p-multiplayer-playbook.md |
| 现有拍卖实现 | src/components/auction/ + src/utils/auctionRules.ts + src/data/auction/ |
| 代码拆分规则 | docs/specs/engineering/code-structure-splitting-rules-v1.md |
| AGENTS.md 三层原则 | AGENTS.md § 1 |
| PM 角色铁律 | docs/handoff/roles/pm.md |
9. 版本与变更记录
| 版本 | 时间 | 整理人 | 关键变更 |
| :---: | :---: | :--- | :--- |
| v1.0 | 2026-07-01 17:30 | Mavis(PM 视角) | 首版:拍 5 态状态机 + 4 个接口 + MultiplayerRoomState type + 2 块最小 UI + 文件边界;不实现实时竞价 / 不动 auction 现有规则 / 不接 App.tsx 主导航 |
| v1.1(待) | — | — | 待实时竞价 spec 拍板后,补 bidding/settled 子状态机 + 出价动作 + 跟 AuctionState 联通 + 接入主导航 |
| v1.1 | 2026-07-13 | [长工]#后续任务连续执行推进@v1.0 | R7/R8/R10 跑完后增量:只增不删追加 § 10 v1.1 增量段。补 RealtimeBidState 4 态(waiting/bidding/antiSnipe/settled)+ ReconnectState 4 态(online/reconnecting/recovered/offline)+ 5 个 reducer 函数签名(applyEvent / applyTimeout / applyDisconnect / applyReconnect / transferHost)+ 3 个组件(RealtimeBidPanel / ReconnectBanner / RoomCard 升级)+ R14 跨周目联动 1 行引用 + R10 长工 = summarized-partial-gate-waived-by-p1 明示标注 + WebRTC 留 v1.4 / 跨设备同步留 Phase 7 残余风险。v1.0 段作 W28 历史母本完整保留(§ 0-§ 9 一字未动)。settled → settling 命名微调仅在 v1.1 增量段说明,不回改 v1.0 § 1.2。|
10. v1.1 增量(R7/R8/R10 跑完后)
整理时间:2026-07-13(Asia/Shanghai) 整理人:
[长工]#后续任务连续执行推进@v1.0增量范围:只增不删。本节为 v1.1 增量段,§ 0-§ 9 之上追加。v1.0 段(§ 0-§ 9)作 W28 历史母本完整保留,不回改。 承接:docs/handoff/tasks/2026-07-13-multiplayer-skeleton-sync.md(本卡)+ R7/R8/R10 实际代码产物。 依据:
- R7 实时竞价:
src/data/multiplayer/realtimeBid.ts+src/utils/multiplayer/realtimeBidding.ts+src/utils/multiplayer/realtimeBidding.regression.ts- R8 流拍 + 断线重连:
src/utils/multiplayer/timeout.ts+src/utils/multiplayer/reconnect.ts+src/utils/multiplayer/timeout.regression.ts+src/utils/multiplayer/reconnect.regression.ts- R10 UI 集成:
src/components/multiplayer-lobby/RealtimeBidPanel.tsx+ReconnectBanner.tsx+RoomCard.tsx(升级)+MultiplayerLobby.tsx+RoomSeatCell.tsx- R14 跨周目:
docs/design/active/跨周目v1.1锚点-v1.md+src/data/endgame/multiRun.ts+src/utils/endgame/multiRun.ts- 5 日计划 v1.1:
docs/plans/active/2026-07-13-5日开发计划-v1.md§ 1 / § 2 / § 4 / § 16.3
10.0 TL;DR(60 秒版,v1.1 增量)
- R7 实时竞价 =
RealtimeBidState4 态(waiting → bidding → antiSnipe → settled)+ 反狙击(最后 30s 内出价 → 自动 +30s,extends++)+ 最小出价单位 =currentBid * 0.05 - R8 5s 流拍 + 断线重连 =
ReconnectState4 态(online → reconnecting → recovered → offline,对齐RoomConnectionState)+ 5s 流拍(applyTimeout)+ 30s 重连窗口(ROOM_RECONNECT_WINDOW_MS)+ 60s 房主转移(transferHost把房主权威给第一个online/recoveredseat) - R10 UI 集成 =
RoomCard升级(v1.0 仅占位,v1.1 内嵌 ReconnectBanner + 4 态 + 当前 bid + 反狙击倒计时 + RealtimeBidPanel)+MultiplayerLobby接入 R7/R8 reducer + 主导航 multiplayer tab 入口 - 5 个 reducer 函数签名:
applyEvent/applyTimeout/applyDisconnect/applyReconnect/transferHost - 3 个新/升级组件:
RealtimeBidPanel/ReconnectBanner/RoomCard(升级) - R10 长工状态明示标注 =
summarized-partial-gate-waived-by-p1(P1 于 2026-07-12 13:06:48 +08:00 豁免;R10 仍缺真实 in-browser 4 态切换 + 掉线手验 5 场景,缺口由 R10-followup 短工 + R11 收口文档双层记录) - R10 owner review =
pending(非阻塞);视觉 checklist 与浏览器截图只认实际产物,当前不得代签为approved - R14 跨周目联动(1 行):联机房间的
settling态不写PlaythroughSaveSlot(v1.0 锁保留),跨周目 startDay 边界不触发computeLegacyPoints(房间是 transient 容器,非周目结算) - 残余风险(v1.1 增量):WebRTC 留 v1.4 / 跨设备同步留 Phase 7 / 联机 UI 动效 + 移动端适配 + 暗色模式均留 v1.1 续
- 命名微调:
settled→settling(v1.0 § 1.2 仍写settled作历史母本;v1.1 增量段说明:RoomPhase实际 type ='idle' | 'waiting' | 'bidding' | 'settling' | 'closed',代码已统一)
10.1 增量状态机(R7/R8 拍)
10.1.1 RealtimeBidState 4 态(R7 拍,type 落 src/data/multiplayer/realtimeBid.ts)
export type RealtimeBidState = 'waiting' | 'bidding' | 'antiSnipe' | 'settled';
export const REALTIME_BID_WINDOW_MS = 5_000; // 主 5s 流拍窗口
export const REALTIME_BID_ANTI_SNIPE_WINDOW_MS = 30_000; // 进入 antiSnipe 阈值(30s 前)
export const REALTIME_BID_ANTI_SNIPE_EXTENSION_MS = 30_000; // 触发后延长 30s
| 态 | 语义 | 进入条件 | 退出条件 |
| :--- | :--- | :--- | :--- |
| waiting | 未开拍 | 初始态 | 房主/客机首拍 → bidding |
| bidding | 主出价中(5s 倒计时) | startBid 事件 | 最后 30s 内新出价 → antiSnipe / 5s 无新出价 → settled |
| antiSnipe | 反狙击延长期(30s 倒计时) | bidding 最后 30s 内 placeBid | 30s 无新出价 → settled / 新出价 → 继续 antiSnipe(extends++) |
| settled | 本轮已结束 | applyTimeout 检测到期 / settle 事件 | 不可逆;房主"再来一局" → RoomPhase 重置 |
10.1.2 ReconnectState 4 态(R8 拍,type 落 src/data/multiplayer/types.ts)
export type RoomConnectionState = 'online' | 'reconnecting' | 'recovered' | 'offline';
export type ReconnectState = RoomConnectionState; // 别名,语义一致
export const ROOM_RECONNECT_WINDOW_MS = 30_000;
| 态 | 语义 | 进入条件 | 退出条件 |
| :--- | :--- | :--- | :--- |
| online | 在线 | 默认态 / recovered 后下一拍 | 收到断线信号 → reconnecting |
| reconnecting | 30s 重连窗口中 | applyDisconnect(seatId, now) | applyReconnect 在 30s 内 → recovered / 超期 → offline |
| recovered | 重连成功,保留 live bid 上下文 | applyReconnect 在 30s 内 | 下一拍自然 → online |
| offline | 重连窗口超期,标记为离线历史 | 30s 内未重连 | 房主 → 60s 触发 transferHost / 客机 → 席位标记 takeover |
10.1.3 与 v1.0 房间状态机的关系
- v1.0 § 1
RoomPhase5 态(idle / waiting / bidding / settled / closed)保留为 W28 历史母本 - v1.1 增量:代码已将 v1.0
settled重命名为settling(src/data/multiplayer/types.ts:9:type RoomPhase = 'idle' | 'waiting' | 'bidding' | 'settling' | 'closed'),v1.1 增量段沿用settling,不回改 v1.0 § 1.2 RealtimeBidState是RealtimeBidSnapshot.state字段(R7 拍),跟RoomPhase平级不互通ReconnectState是RoomSeat.connectionState字段(R8 拍),跟RoomPhase平级不互通- 关键:R7/R8 子状态机 + R10 房间状态机 = 3 层叠加(房间层 / 出价层 / 重连层),各自独立推进
10.2 增量数据流(5 个 reducer 函数签名)
约束:全部纯函数,接受
now: number注入,不直接调Date.now();不写auction/**;不写PlaythroughSaveSlot;settling态字段变更只在内存中。
10.2.1 applyEvent — R7 实时竞价事件 reducer
// src/utils/multiplayer/realtimeBidding.ts
export const applyEvent = (
snapshot: RealtimeBidSnapshot,
event: RealtimeBidEvent, // { kind: 'startBid' | 'placeBid' | 'extendAntiSnipe' | 'settle'; payload: { bidderId; amount; timestamp } }
): RealtimeBidSnapshot;
- 处理 4 类事件:
startBid/placeBid/extendAntiSnipe/settle - 自动检测 antiSnipe(
shouldAntiSnipe(snapshot, timestamp)):若placeBid落在deadlineAt - 30s之前 → 自动extendDeadline - 失败回退(非法 amount / 状态不匹配)→ 返回原 snapshot(不变)
10.2.2 applyTimeout — R8 5s 流拍 reducer
// src/utils/multiplayer/timeout.ts
export const applyTimeout = (
snapshot: RealtimeBidSnapshot,
now: number,
): RealtimeBidSnapshot;
snapshot.state === 'bidding' | 'antiSnipe'且now > deadlineAt→ 返回state: 'settled'- 其他态(等待/已结算)→ 返回原 snapshot
- 单元测试覆盖(R8 验收 5 场景之 ①):出价后 5s 无新出价 → 自动 settled
10.2.3 applyDisconnect — R8 客机/房主断线 reducer
// src/utils/multiplayer/reconnect.ts
export const applyDisconnect = (
roomState: MultiplayerRoomState,
seatId: string,
now: number,
): MultiplayerRoomState;
- 找到目标 seat →
connectionState: 'reconnecting'+reconnectDeadlineAt: now + 30_000+disconnectStartedAt: now - 同步更新
participant.ready = false realtimeBidSnapshot不变(R7 拍:断线不丢 live bid 上下文)- 单元测试覆盖(R8 验收 5 场景之 ② / ④)
10.2.4 applyReconnect — R8 重连成功 reducer
// src/utils/multiplayer/reconnect.ts
export const applyReconnect = (
roomState: MultiplayerRoomState,
seatId: string,
now: number,
): MultiplayerRoomState;
- 仅当
connectionState === 'reconnecting'且now <= reconnectDeadlineAt→connectionState: 'recovered' - 同步更新
participant.ready = true realtimeBidSnapshot不变(关键:重连后保留 live bid 上下文)- 超期调用 → 返回原 snapshot(无效)
- 单元测试覆盖(R8 验收 5 场景之 ② / ④)
10.2.5 transferHost — R8 房主断线 60s 转移 reducer
// src/utils/multiplayer/reconnect.ts
export const transferHost = (
roomState: MultiplayerRoomState,
now: number,
): MultiplayerRoomState;
- 仅当房主 seat
connectionState === 'offline'(30s 重连窗口已超期)→ 触发转移 - 找第一个
online/recovered的非房主 seat → 升级为新RoomHost - 原房主降级为
RoomGuest(convertHostToGuest)+ 原 seat 标记status: 'takeover' - 新房主 seat 标记
status: 'occupied',locked: true - 写
lastHostTransfer: { fromSeatId; toSeatId; fromOccupantId; toOccupantId; transferredAt: now } - 单元测试覆盖(R8 验收 5 场景之 ⑤)
10.2.6 5 个 reducer 的调用关系
[bidder UI click] -> onBid(amount) -> applyEvent(snapshot, { kind: 'placeBid', payload })
↓ (snapshot 变化)
RealtimeBidPanel 重渲染
↓
(setInterval 1s tick)
↓
applyTimeout(snapshot, now)
↓
(state === 'settled')
↓
handleBidSettled callback
[WebRTC onDisconnected] -> applyDisconnect(roomState, seatId, now)
↓
ReconnectBanner 渲染 "XX 正在重连 30s"
↓
(30s 内) applyReconnect → ReconnectBanner 渲染 "XX 已重连"
(30s 外) → connectionState='offline' → ReconnectBanner 渲染 "XX 已离线"
↓
(host seat, 60s) → transferHost → ReconnectBanner 渲染 "房主已转移给 YY"
10.3 增量 UI(R10 拍,3 个组件 + 1 个升级)
10.3.1 RealtimeBidPanel.tsx(新,src/components/multiplayer-lobby/)
- 位置:嵌入
RoomCard底部 - Props:
{ snapshot: RealtimeBidSnapshot; onBid: (amount: number) => void; onSnapshotChange?: (nextSnapshot) => void; onSettled?: (finalBid, finalBidder) => void } - 核心功能:
- 状态徽章 + 标签(
REALTIME_BID_STATE_LABELS):waiting / bidding / antiSnipe / settled - antiSnipe 时显「Anti-snipe +{extends}」+ 倒计时
- Highest Bid 卡 + Leader 卡
- Deadline 倒计时(
setInterval 1s tick调applyTimeout自动 settled) - 出价输入框(
min = currentBid + computeMinIncrement(currentBid)) - 提交按钮(失效条件:
amount < min或state === 'settled')
- 状态徽章 + 标签(
- 状态机边界:本组件不做
applyDisconnect/applyReconnect/transferHost,这些走ReconnectBanner - R10 视觉:复用
tongfu-parchment+tongfu-button-wooden既有类,跟auction/console/视觉一致
10.3.2 ReconnectBanner.tsx(新,src/components/multiplayer-lobby/)
- 位置:
RoomCard顶部条幅 - Props:
{ roomState: MultiplayerRoomState; now: number } - 三态渲染:
reconnectingseat 存在(任一 seat 处于reconnecting):蓝色条幅 + WifiSync 图标 + "XX 正在重连(倒计时 N 秒)"offlineseat 存在(任一 seat 处于offline):红色条幅 + WifiOff/AlertTriangle 图标(房主用 AlertTriangle)+ "XX 已离线"lastHostTransfer在 3s 内:琥珀色条幅 + Crown 图标 + "房主已转移给 YY"
- 优先级:
lastHostTransfer>reconnecting>offline(都无 → 渲染null) - R8 边界:
ReconnectBanner只读roomState,不触发 reducer;reducer 在MultiplayerLobby1s tick 里跑
10.3.3 RoomCard.tsx(升级,v1.0 占位 → v1.1 集成)
- v1.0 状态:仅占位卡片 + RoomSeatCell 4 块 + 不接数据流
- v1.1 升级:
- 顶部:内嵌
ReconnectBanner - 头部徽章:
RoomPhase(ROOM_PHASE_LABELS)+roomCode(如YM-XXXX) - 4 个数据卡:Seats(锁定/空席)/ Sync(Funds frozen or pending)/ Lots(房间 lot 数)/ Occupancy
- 4 个状态卡:Bid State(
REALTIME_BID_STATE_LABELS)/ Current Bid(leader 名字)/ Anti-snipe(倒计时 / Inactive)/ Occupancy(active seats) - 4 块
RoomSeatCell(沿用 v1.0 组件) - 底部:内嵌
RealtimeBidPanel - 底部条:阶段推进按钮(
onAdvance→advanceRoomPhase)
- 顶部:内嵌
- R10-followup 视觉调整边界:只动视觉调整,不动 props 签名(防止撞 R7/R8 reducer 集成);主导航 multiplayer tab 入口走 R10-followup 单独改
App.tsx
10.3.4 MultiplayerLobby.tsx(升级)
- v1.0 状态:最小入口,只放 RoomCard 列表(占位)
- v1.1 升级:
- 5 态阶段条(
ROOM_PHASE_ORDER+ 高亮当前 phase) - 1s tick:
setInterval调transferHost(roomState, Date.now())(只在无外部roomprops 时) handleBid(amount):构造placeBid/startBid事件 → 调applyBid(=applyEvent别名)→syncBidSnapshothandleBidSettled:state强制写settled+statusMessage写"Current lot settled with X at Y"- readOnly 模式(
readOnly=true→ 推进按钮禁用)
- 5 态阶段条(
10.3.5 不在 v1.1 增量的 UI
- ❌ 联机 panel 动效(留 v1.1 续)
- ❌ 移动端适配(留 v1.1 续)
- ❌ 暗色模式切换(留 v1.1 续)
- ❌ 表情包 / 弹幕 / 聊天(留 W30+)
- ❌ 真实网络动画 / WebRTC 进度条(留 v1.4)
10.4 跨 Lane 接入(R14 联动 + 落盘边界)
10.4.1 R14 跨周目 v1.1 锚点(1 行引用)
R14 跨周目 v1.1 锚点已落:
docs/design/active/跨周目v1.1锚点-v1.md(2026-07-13 收口),含CrossWeekAnchor/WeekRunSnapshottype +computeLegacyPoints(weekNum, baseScore, mode)纯函数 +CROSS_WEEK_LEGACY_DECAY = [1.0, 0.8, 0.6, 0.4]常量。R14 与本骨架的边界:联机房间的settling态不写PlaythroughSaveSlot,跨周目 startDay 边界不触发computeLegacyPoints(房间是 transient 容器,非周目结算)。
10.4.2 v1.0 锁保留(显式重述)
- ❌ 联机房间不写
GlobalProfile(v1.0 § 3.3 锁) - ❌ 联机房间不写
PlaythroughSaveSlot(v1.0 § 3.3 锁) - ❌ 联机房间不写
auctionRules.ts任何字段(v1.0 § 3.3 锁) - ❌ 联机房间不联通
AuctionState(v1.0 § 3.1 锁) - ✅ 联机房间的
settling态字段变更只在内存 +tongfu_multiplayer_room_<roomId>落盘 - ✅ 跨周目 startDay 边界由单机
victoryConditions.ts+computeLegacyPoints计算
10.4.3 跟 R7/R8/R9 的文件边界(强制,跟 v1.0 § 5 对齐)
- ✅ R7 落
src/data/multiplayer/realtimeBid.ts(type only)+src/utils/multiplayer/realtimeBidding.ts(纯函数)+src/components/multiplayer-lobby/RealtimeBidPanel.tsx - ✅ R8 落
src/utils/multiplayer/timeout.ts+src/utils/multiplayer/reconnect.ts+src/components/multiplayer-lobby/ReconnectBanner.tsx - ✅ R10 升级
src/components/multiplayer-lobby/RoomCard.tsx+MultiplayerLobby.tsx+RoomSeatCell.tsx(微调)+src/App.tsxmultiplayer tab 入口 3-5 行 - ❌ 任何 lane 都不碰
src/data/auction/**/src/utils/auctionRules.ts/src/components/auction/** - ❌ 任何 lane 都不碰
src/data/endgame/**/src/utils/endgame/**(multiRun.ts是 R14 独立 lane) - ❌ 任何 lane 都不碰
story/**/src/data/items/**/src/components/item/**
10.5 残余风险(v1.1 增量显式标注)
| 风险 ID | 风险描述 | 状态 | 缓解 / 后续 lane |
| :-: | :--- | :---: | :--- |
| rr-1 | WebRTC 真实网络实装缺失 | ⚠️ 留 v1.4 | 当前是 local P2P 模拟(模拟掉线 / 模拟出价都在单机内跑);v1.4 计划引入真实 WebRTC 房间信号 |
| rr-2 | 跨设备同步缺失 | ⚠️ 留 Phase 7 | tongfu_multiplayer_room_<roomId> 只存本机;Phase 7 才会接云存档 / 跨设备 |
| rr-3 | R10 长工 = summarized-partial-gate-waived-by-p1(P1 7.12 13:06 豁免) | ⚠️ 明示标注 | R10 仍缺 4 态切换 4 张 + 掉线 3 张 = 7 张真实手验截图;由 R10-followup 短工(docs/handoff/tasks/2026-07-13-r10-browser-walkthrough.md)+ R11 收口文档(docs/handoff/tasks/2026-07-17-w29-closeout.md)双层登记;豁免不等于 verified |
| rr-4 | R10 视觉对照 checklist 12+6=18 项 | ⚠️ owner review: pending(非阻塞) | docs/design/active/W29联机UI视觉对照checklist-v1.md 由 R10-followup 落盘;只认实际 checklist / 截图证据,不得代签为 approved |
| rr-5 | 联机 UI 动效 / 移动端适配 / 暗色模式 | ⚠️ 留 v1.1 续 | W29 范围外,留 v1.1 续 / v1.2 lane |
| rr-6 | settled → settling 命名微调 | ⚠️ 历史母本保留 | v1.0 § 1.2 仍写 settled(W28 历史母本);代码已统一 settling;v1.1 增量段沿用 settling |
| rr-7 | 5 场景真实手验缺口 | ⚠️ 留 R10-followup | 4 态切换 + 模拟掉线 + 房主转移 = 7 张截图;留 R10-followup 短工补手验 |
| rr-8 | settling 内部子状态机未细分 | ⚠️ 留 v1.1.1 | 当前 settling 是单态(settlement 走 applyTimeout 一步到 settled);细分(正在结算中 / 结算待确认)留 v1.1.1 |
10.6 验收 + 不做(v1.1 增量)
10.6.1 v1.1 增量验收(本 doc-sync lane 必查)
- [x]
RealtimeBidState4 态 type 落盘(src/data/multiplayer/realtimeBid.ts:6) - [x]
ReconnectState(=RoomConnectionState)4 态 type 落盘(src/data/multiplayer/types.ts:14) - [x] 5 个 reducer 函数签名落盘:
applyEvent(src/utils/multiplayer/realtimeBidding.ts:51)applyTimeout(src/utils/multiplayer/timeout.ts:8)applyDisconnect(src/utils/multiplayer/reconnect.ts:137)applyReconnect(src/utils/multiplayer/reconnect.ts:161)transferHost(src/utils/multiplayer/reconnect.ts:190)
- [x] 3 个新/升级组件落盘:
RealtimeBidPanel(src/components/multiplayer-lobby/RealtimeBidPanel.tsx)ReconnectBanner(src/components/multiplayer-lobby/ReconnectBanner.tsx)RoomCard升级(src/components/multiplayer-lobby/RoomCard.tsx)
- [x] 跨 Lane 接入段(§ 10.4)R14 1 行引用已写
- [x] 残余风险段(§ 10.5)R10 长工 =
summarized-partial-gate-waived-by-p1明示标注 - [x] v1.0 段(§ 0-§ 9)作 W28 历史母本完整保留,一字未动
- [x] 文档 v1.1 落盘 7 段真填(§ 10.0 - § 10.6)
- [x] 纯文档,
src/**零改动(本 lane 唯一动作 =docs/design/active/联机P2P房间骨架-v1.md)
10.6.2 v1.1 增量不做(明示)
- ❌ 重写 v1.0 任何段(只增不删,锁保留)
- ❌ 改
src/**任何文件(纯文档 lane) - ❌ 重开 R7 / R8 / R10 任何 lane
- ❌ 写 5 场景真实手验(留 R10-followup)
- ❌ 落 W29 视觉对照 checklist(留 R10-followup)
- ❌ 拍 P1 视觉拍板结果(短工不代决视觉)
- ❌ 改 v1.0 § 1.2
settled→settling(历史母本保留,代码已统一) - ❌ 写
PlaythroughSaveSlot/GlobalProfile/auctionRules.ts(v1.0 锁)
10.7 v1.1 增量参考入口
| 想看什么 | 去这里 |
| :--- | :--- |
| R7 task 卡 | docs/handoff/tasks/2026-07-13-to-14-p2p-realtime-bidding.md |
| R8 task 卡 | docs/handoff/tasks/2026-07-15-p2p-timeout-reconnect.md |
| R10-followup task 卡(R10 长工 = summarized-partial-gate-waived-by-p1 缺口补全) | docs/handoff/tasks/2026-07-13-r10-browser-walkthrough.md |
| R11 收口 task 卡(W29 收口) | docs/handoff/tasks/2026-07-17-w29-closeout.md |
| R14 跨周目 v1.1 锚点(本节 § 10.4 联动引用) | docs/design/active/跨周目v1.1锚点-v1.md |
| 5 日计划 v1.1 真相源 | docs/plans/active/2026-07-13-5日开发计划-v1.md § 1 / § 2 / § 4 / § 16.3 |
| R7/R8/R10 联机长工 owner 卡 | docs/handoff/tasks/2026-07-13-w29-multiplayer-p2p-owner.md(state: summarized-partial-gate-waived-by-p1)|
| R10 联机长工 session 卡 | docs/handoff/sessions/2026-07-13-w29-multiplayer-p2p-owner-*.md |
| W28 历史母本(本节 § 0-§ 9 之上) | docs/design/active/联机P2P房间骨架-v1.md § 0-§ 9 |
10.8 派工触发链(流水化)
- 本卡 = 流水化第 5 条(R10-followup callback 后立即派)
- 骨架 sync callback 收到 → 管家接力:立即派 R11 收口(
docs/handoff/tasks/2026-07-17-w29-closeout.md,已存在)+ 拉 P1 拍 W30 主题 - 本短工只回骨架 sync callback,R11 派工由管家(本 root session)接力
(End of § 10 v1.1 增量段)