2026-07-13-to-14-p2p-realtime-bidding
⟦tag:v2|task|2026-07-13-to-14-p2p-realtime-bidding⟧
kind: task
id: 2026-07-13-to-14-p2p-realtime-bidding
lane: R7
title: P2P 实时竞价 + 反狙击(Day 1-2 主,8h)
objective: 收口 W29 5 日计划 R7 lane——把 W28 落地的联机 P2P 房间骨架补成"实时可竞价",含 4 态状态机 + 反狙击 + 最小出价单位 + 最小 UI。
scope: docs/plans/active/2026-07-13-5日开发计划-v1.md § 1, docs/design/active/联机P2P房间骨架-v1.md, docs/specs/engineering/code-structure-splitting-rules-v1.md, src/data/multiplayer/**(只新增), src/utils/multiplayer/**(只新增), src/components/multiplayer-lobby/**(只新增)
acceptance:
src/data/multiplayer/realtimeBid.ts落盘,导出RealtimeBidState/RealtimeBidEvent/RealtimeBidSnapshot3 个 typesrc/utils/multiplayer/realtimeBidding.ts落盘,纯函数支持 4 态状态机waiting → bidding → antiSnipe → settled单步推进- 反狙击:最后 30s 内出价 → 自动延长 30s(纯函数可测,无 setTimeout 副作用)
- 最小出价单位:
minIncrement = currentBid * 0.05(纯函数,无副作用) - 房主 / 客机出价广播:函数签名支持
applyBid(roomState, bidEvent),WebRTC 留 v1.4 src/components/multiplayer-lobby/RealtimeBidPanel.tsx落盘,显示当前 bid + 倒计时 + 出价输入框(只读 UI,真实广播留 v1.4)- 单元测试:
realtimeBidding.regression.ts跑 5 场景全过 = ① 正常出价 ② 反狙击触发 ③ 反狙击连续触发(链式) ④ 流拍自然结束 ⑤ 异常 bid 拒绝 cmd /c npm run build通过,npm run lint无新增错误src/data/auction/**/src/utils/auctionRules.ts/src/components/auction/**任何代码未被改动RealtimeBidState跟AuctionState平级无字段交叉引用src/App.tsx主导航未被改动(只动 multiplayer tab 入口,R10 lane 范围) current state: documented blockers: none next action: 主会话 7.13 上午按本卡 prompt 段派工,短工回执时按## 回执模板段格式 summary:- 这是 W29 5 日计划 R7 lane(2026-07-13 Day 1 + 2026-07-14 Day 2 主轴),跟 R9 胜利数值拍板并行。
- 关键路径上接 W28 R3 联机 P2P 房间骨架(已落),下接 R8 5s 流拍 + 断线重连(Day 3 主轴)。
- 不实现 WebRTC 实装 / 不实现真实网络广播 / 不实现恶意抬价保护 / 不做表情包。
- 风险:跟
auctionRules.ts冲突 → 新目录 +RealtimeBidState平级 type,本卡全程不碰auction/。
prompt(给短工 / 子会话的指令)
角色:[短工]#联机-P2P实时竞价@v1.0
lane:R7
时间窗:2026-07-13 上午 9:00 ~ 2026-07-14 下午 18:00(8h 工作量,跨两天)
承接文档:docs/plans/active/2026-07-13-5日开发计划-v1.md § 1
任务清单(按顺序做)
-
type 层:
src/data/multiplayer/realtimeBid.ts导出 3 个 typeRealtimeBidState:联合类型'waiting' | 'bidding' | 'antiSnipe' | 'settled'RealtimeBidEvent:{ kind: 'startBid' | 'placeBid' | 'extendAntiSnipe' | 'settle'; payload: { bidderId: string; amount: number; timestamp: number } }RealtimeBidSnapshot:{ state: RealtimeBidState; currentBid: number; currentBidder: string | null; deadlineAt: number; extends: number }
-
状态机层:
src/utils/multiplayer/realtimeBidding.ts导出 4 个纯函数applyEvent(snapshot, event): RealtimeBidSnapshot—— 单步推进,严格按waiting → bidding → antiSnipe → settled顺序computeMinIncrement(currentBid: number): number—— 返回currentBid * 0.05shouldAntiSnipe(snapshot, now: number): boolean——now >= deadlineAt - 30_000且state === 'bidding'extendDeadline(snapshot, now: number): RealtimeBidSnapshot——deadlineAt = max(deadlineAt, now + 30_000),extends += 1- 禁止
setTimeout/setInterval/Date.now()直接调用,全部接受now: number参数注入(纯函数)
-
UI 层:
src/components/multiplayer-lobby/RealtimeBidPanel.tsx- 接收
snapshot: RealtimeBidSnapshot+onBid: (amount: number) => voidprops - 显示当前 bid + 倒计时(
useState+useEffect算deadlineAt - Date.now()) - 出价输入框:校验
amount >= snapshot.currentBid + computeMinIncrement(snapshot.currentBid),否则禁用提交 - 4 态反射:
waiting显「等待开始」/bidding显「出价中」/antiSnipe显「反狙击 +X」/settled显「已结束」 - 不接主导航,不做真实广播(
onBid回调留给 R10 集成时接线)
- 接收
-
测试层:
src/utils/multiplayer/realtimeBidding.regression.ts- 5 场景:① 正常出价 ② 反狙击触发 ③ 反狙击连续触发(链式,3 次) ④ 流拍自然结束(
now > deadlineAt无新出价) ⑤ 异常 bid 拒绝(amount < minIncrement返回原 snapshot) - 用项目现有的
node --test或 vitest 跑通,输出 PASS/FAIL
- 5 场景:① 正常出价 ② 反狙击触发 ③ 反狙击连续触发(链式,3 次) ④ 流拍自然结束(
-
验收
cmd /c npm run build通过cmd /c npm run lint无新增错误(tsc --noEmit沿用 AGENTS.md § 7.7 旧 26+ 错误不修)- 5 场景测试全过
文件边界(强制)
- ✅ 新增:
src/data/multiplayer/realtimeBid.ts - ✅ 新增:
src/utils/multiplayer/realtimeBidding.ts - ✅ 新增:
src/utils/multiplayer/realtimeBidding.regression.ts - ✅ 新增:
src/components/multiplayer-lobby/RealtimeBidPanel.tsx - ❌ 不碰:
src/data/auction/**/src/utils/auctionRules.ts/src/components/auction/** - ❌ 不碰:
src/App.tsx主导航(R10 lane 范围) - ❌ 不碰:
src/types.ts/src/data/items/**/src/utils/items/**/story/**
不做(明示)
- ❌ WebRTC 实装(留 v1.4)
- ❌ 真实网络广播 / WebSocket(留 v1.4+)
- ❌ 恶意抬价保护 / 反作弊
- ❌ 表情包 / 弹幕
- ❌ 历史出价回放
风险 + 缓解
- 跟
auctionRules.ts5s 流拍语义冲突 → 严格遵守文件边界,新目录平级实现,RealtimeBidState跟AuctionState无任何交叉引用 - 状态机死锁(比如
settled后还能placeBid)→applyEvent严格校验当前态,非法转移抛Error('illegal transition')
回执模板(短工完成时按这个格式回报主会话)
[R7 #联机-P2P实时竞价@v1.0 回执]
- 状态:✅ 完成 / ⚠️ 部分完成 / ❌ 阻塞
- 新增文件:<列出 4 个文件实际路径>
- 5 场景测试:<PASS/FAIL 列表>
- `npm run build`:✅ / ❌
- `npm run lint`:无新增 / 新增 N 个
- 风险 / 阻塞:<如有,列出;如无,写 "无">
- 残余风险:<如有,列出;如无,写 "无">
- 建议给 R8 接力点:<一句话>
(End of R7 task card)