Compare commits
2 Commits
2ca096f821
...
bf72c2e48d
| Author | SHA1 | Date | |
|---|---|---|---|
| bf72c2e48d | |||
| fa61eeb0b0 |
412
docs/prd/BARK_BATTLE_BDD_2026-05-11.md
Normal file
412
docs/prd/BARK_BATTLE_BDD_2026-05-11.md
Normal file
@@ -0,0 +1,412 @@
|
||||
# 汪汪声浪大作战 / bark-battle BDD 验收场景
|
||||
|
||||
## 背景
|
||||
|
||||
- 需求来源:用户提供的视频 `C:\Users\DSK\Videos\一款双方比狗叫的游戏 - 1.一款双方比狗叫的游戏(Av116504192360177,P1).mp4`,并已在 `.hermes/plans/2026-05-11_144229-bark-battle-2d-game-bdd-ddd-tdd-plan.md` 中完成抽帧分析和玩法方案整理。
|
||||
- 玩法定位:浏览器 2D 声控狗叫对战小游戏,暂定中文名 `汪汪声浪大作战`,英文代号与 play type ID 建议为 `bark-battle`。
|
||||
- 核心玩法:双方狗狗在 30 秒限时内通过麦克风输入“狗叫声”进行声浪拔河;系统依据声音强度、有效叫声次数和叫声节奏计算推动力,实时推动顶部红蓝能量条;倒计时结束后按能量条位置判定胜负或平局。
|
||||
- 文档目的:为产品、测试、前端、后端在编码前统一可验证验收口径;本文只定义 PRD/BDD 级行为与测试映射,不实现工程代码。
|
||||
|
||||
## 角色与目标
|
||||
|
||||
### 主要角色
|
||||
|
||||
- 浏览器玩家:进入 `bark-battle` 玩法,用麦克风发声参与对战。
|
||||
- 移动端玩家:在手机浏览器中进入玩法,需要看到核心游戏信息并完成授权、对战、结算。
|
||||
- 无麦克风或无 API 支持玩家:设备或浏览器不支持声控输入时,需要得到明确降级反馈并可返回。
|
||||
- 测试人员:依据本文场景验证权限、校准、叫声识别、能量条、胜负结算和布局兼容。
|
||||
- 前端实现人员:依据本文拆分 Web Audio 输入、领域规则、Phaser/DOM HUD 表现和自动化测试。
|
||||
|
||||
### 用户目标
|
||||
|
||||
- 玩家可以在开局前完成麦克风授权和环境噪音校准。
|
||||
- 玩家发出有效狗叫时,能看到叫声计数、狗狗动画、拟声词/冲击波以及能量条变化。
|
||||
- 低于阈值的背景噪音不会被误计为有效叫声。
|
||||
- 单局在 30 秒后给出明确胜负、平局和关键数据。
|
||||
- 移动端和不支持麦克风的环境不会进入不可操作状态。
|
||||
|
||||
### 非目标
|
||||
|
||||
- MVP 不要求识别“是否真实狗叫”,不引入机器学习声纹/物种分类;有效输入以音量阈值、峰值间隔、持续时间和校准结果为准。
|
||||
- MVP 不要求实时联机对战;可先按“玩家 vs AI 对手”完成单机浏览器 runtime。
|
||||
- MVP 不要求成绩持久化、作品发布、作品架、广场和排行榜;若后续接入 Genarrative 作品闭环,需要另补玩法类型集成 PRD/技术文档。
|
||||
- MVP 不要求在 UI 中长期展示大段规则说明;游戏界面应保持倒计时、能量条、狗狗、麦克风状态和结算信息为主。
|
||||
- MVP 不处理竞技级反作弊;播放录音、拍桌、喊叫等非狗叫输入只作为后续公平性风险记录。
|
||||
|
||||
## 业务规则口径
|
||||
|
||||
- 单局时长:默认 30 秒,从正式进入 `playing` 阶段开始计时。
|
||||
- 能量条:使用 `-100` 到 `100` 的连续值表示,负数偏对手侧,正数偏玩家侧,`0` 为中线。
|
||||
- 平局阈值:倒计时结束时,若能量条绝对值小于或等于 `drawThreshold`,判定平局;具体数值由实现配置,但测试需可注入固定阈值。
|
||||
- 有效叫声:一次有效叫声至少满足:音量超过校准后的有效阈值、与上一次有效峰值间隔不小于 `minBarkGapMs`、持续时长在 `minBarkDurationMs` 到 `maxBarkDurationMs` 之间。
|
||||
- 背景噪音:校准阶段采集到的环境声用于计算动态阈值;低于阈值的输入不得增加叫声次数,也不得让能量条出现可见推进。
|
||||
- 推动力:玩家推动力由音量分数、有效叫声频率和连击加成组成;能量条按玩家推动力与对手推动力差值移动,并被限制在 `-100` 到 `100`。
|
||||
- UI 反馈:有效叫声应触发可观察反馈,包括玩家侧狗狗张嘴/吠叫动画、拟声词或冲击波;反馈不应遮挡倒计时和顶部能量条。
|
||||
|
||||
## 中文 Gherkin 场景
|
||||
|
||||
### 功能: 麦克风授权与开局准备
|
||||
|
||||
```gherkin
|
||||
功能: 狗叫对战麦克风准备
|
||||
为了让玩家能用声音参与对战
|
||||
作为浏览器玩家
|
||||
我希望游戏在正式开局前请求麦克风权限并完成环境校准
|
||||
|
||||
背景:
|
||||
假如玩家进入 bark-battle 玩法页面
|
||||
而且浏览器支持 navigator.mediaDevices.getUserMedia
|
||||
|
||||
场景: 玩家允许麦克风权限后进入环境噪音校准
|
||||
当玩家同意浏览器麦克风授权
|
||||
那么系统应进入环境噪音校准阶段
|
||||
而且游戏不应在校准完成前进入 playing 阶段
|
||||
而且界面应显示麦克风已授权的可观察状态
|
||||
|
||||
场景: 校准完成后进入开局倒计时
|
||||
假如玩家已允许麦克风权限
|
||||
而且系统已采集足够的环境噪音样本
|
||||
当校准计算出有效叫声阈值
|
||||
那么系统应进入开局倒计时阶段
|
||||
而且倒计时结束后应进入 30 秒对战阶段
|
||||
而且初始能量条应位于中线
|
||||
|
||||
场景: 玩家拒绝麦克风权限后不能开始声控对战
|
||||
当玩家拒绝浏览器麦克风授权
|
||||
那么系统应停留在无法声控游玩的状态
|
||||
而且应提供重新授权或返回入口
|
||||
而且不应进入校准、倒计时或 playing 阶段
|
||||
```
|
||||
|
||||
### 功能: 环境噪音校准
|
||||
|
||||
```gherkin
|
||||
功能: 环境噪音校准
|
||||
为了减少背景噪音误触发
|
||||
作为浏览器玩家
|
||||
我希望游戏在开局前根据当前环境设置有效叫声阈值
|
||||
|
||||
场景: 安静环境生成低但非零的有效阈值
|
||||
假如校准阶段采集到的环境噪音 RMS 稳定低于默认噪音基线
|
||||
当系统完成校准
|
||||
那么有效叫声阈值应高于环境噪音平均值
|
||||
而且阈值不应低于系统配置的最小阈值
|
||||
|
||||
场景: 嘈杂环境生成更高的有效阈值
|
||||
假如校准阶段采集到的环境噪音 RMS 高于默认噪音基线
|
||||
当系统完成校准
|
||||
那么有效叫声阈值应随环境噪音上调
|
||||
而且低于该阈值的后续输入不应计为有效叫声
|
||||
|
||||
场景: 校准期间无法获得有效音频样本
|
||||
假如麦克风授权成功但音频样本持续为空或不可读
|
||||
当校准超过系统配置的最长等待时间
|
||||
那么系统应展示麦克风输入不可用状态
|
||||
而且应提供重试校准入口
|
||||
而且不应直接开始对战
|
||||
```
|
||||
|
||||
### 功能: 有效叫声计数
|
||||
|
||||
```gherkin
|
||||
功能: 有效叫声计数
|
||||
为了把玩家的狗叫行为转换为可计分输入
|
||||
作为玩家
|
||||
我希望每次符合规则的短促叫声只被计数一次
|
||||
|
||||
背景:
|
||||
假如游戏处于 30 秒 playing 阶段
|
||||
而且系统已完成环境噪音校准
|
||||
|
||||
场景: 单次超过阈值且间隔足够的叫声计数加一
|
||||
假如玩家当前叫声次数为 0
|
||||
而且上一次有效叫声时间早于 minBarkGapMs
|
||||
当麦克风输入出现一次超过有效阈值且持续时长合规的峰值
|
||||
那么玩家叫声次数应变为 1
|
||||
而且玩家侧应出现一次吠叫动画反馈
|
||||
而且画面应出现一次拟声词或冲击波反馈
|
||||
|
||||
场景: 持续噪音不会被无限计数
|
||||
假如玩家当前叫声次数为 1
|
||||
当麦克风输入持续超过阈值但没有新的峰值间隔
|
||||
那么玩家叫声次数不应在每个 tick 中持续增加
|
||||
而且系统最多只应记录当前连续声音段内的一次有效叫声
|
||||
|
||||
场景: 间隔过短的连续峰值不重复计数
|
||||
假如玩家刚刚产生一次有效叫声
|
||||
当麦克风输入在 minBarkGapMs 内再次出现峰值
|
||||
那么玩家叫声次数不应增加
|
||||
而且连击或推动力不应因该峰值重复加成
|
||||
```
|
||||
|
||||
### 功能: 声音大小和连续叫声推动能量条
|
||||
|
||||
```gherkin
|
||||
功能: 声浪推动能量条
|
||||
为了复刻双方比狗叫的核心体验
|
||||
作为玩家
|
||||
我希望更响、更连续的有效叫声能把顶部能量条推向自己一侧
|
||||
|
||||
背景:
|
||||
假如游戏处于 30 秒 playing 阶段
|
||||
而且能量条当前位于中线
|
||||
|
||||
场景: 玩家推动力高于对手时能量条向玩家侧移动
|
||||
假如玩家在短时间窗口内产生多次有效叫声
|
||||
而且玩家推动力高于对手推动力
|
||||
当系统推进一个 simulation tick
|
||||
那么能量条数值应向玩家侧增加
|
||||
而且顶部红蓝能量条的玩家侧占比应变大
|
||||
|
||||
场景: 连续大声叫声触发更强反馈
|
||||
假如玩家连续产生多次高于强叫声阈值的有效叫声
|
||||
当系统计算玩家连击加成
|
||||
那么玩家侧推动力应高于单次普通叫声推动力
|
||||
而且玩家侧声浪或冲击波反馈应比普通叫声更明显
|
||||
但是反馈不应遮挡倒计时和能量条
|
||||
|
||||
场景: 能量条到达边界后不会越界
|
||||
假如能量条已经接近玩家侧最大值
|
||||
而且玩家推动力仍高于对手推动力
|
||||
当系统推进多个 simulation tick
|
||||
那么能量条数值不应超过 100
|
||||
而且界面不应显示超出容器范围的能量条
|
||||
|
||||
场景: 对手推动力高于玩家时能量条向对手侧移动
|
||||
假如对手推动力高于玩家推动力
|
||||
当系统推进一个 simulation tick
|
||||
那么能量条数值应向对手侧减少
|
||||
而且顶部红蓝能量条的对手侧占比应变大
|
||||
```
|
||||
|
||||
### 功能: 低噪音和无效输入过滤
|
||||
|
||||
```gherkin
|
||||
功能: 背景噪音过滤
|
||||
为了避免环境声替玩家自动得分
|
||||
作为玩家
|
||||
我希望低于阈值或不合规的声音不会被当作有效狗叫
|
||||
|
||||
背景:
|
||||
假如游戏处于 30 秒 playing 阶段
|
||||
而且系统已完成环境噪音校准
|
||||
|
||||
场景: 低于阈值的背景噪音不计数
|
||||
当麦克风只接收到低于有效叫声阈值的背景噪音
|
||||
那么玩家叫声次数不应增加
|
||||
而且玩家侧不应播放吠叫动画
|
||||
而且能量条不应因为该背景噪音出现可见推进
|
||||
|
||||
场景: 过短脉冲不计为有效叫声
|
||||
假如麦克风输入峰值超过有效阈值
|
||||
但是持续时长短于 minBarkDurationMs
|
||||
当系统完成该声音段判定
|
||||
那么玩家叫声次数不应增加
|
||||
而且不应触发连击加成
|
||||
|
||||
场景: 过长持续声被削弱为单段输入
|
||||
假如麦克风输入持续超过有效阈值
|
||||
但是持续时长长于 maxBarkDurationMs
|
||||
当系统完成该声音段判定
|
||||
那么系统不应按多个叫声重复计数
|
||||
而且该声音段的推动力应按持续噪音削弱规则处理
|
||||
```
|
||||
|
||||
### 功能: 倒计时与胜负结算
|
||||
|
||||
```gherkin
|
||||
功能: 狗叫对战胜负结算
|
||||
为了让单局对抗有明确目标
|
||||
作为玩家
|
||||
我希望 30 秒倒计时结束后根据能量条位置得到胜负或平局
|
||||
|
||||
背景:
|
||||
假如游戏已经进入 30 秒 playing 阶段
|
||||
|
||||
场景: 倒计时每秒递减并在归零时停止对战输入
|
||||
当系统时间从 30 秒推进到 0 秒
|
||||
那么界面应显示倒计时归零
|
||||
而且系统应进入 finished 结算阶段
|
||||
而且归零后的麦克风输入不应再改变本局能量条和叫声次数
|
||||
|
||||
场景: 玩家侧占优时判定玩家胜利
|
||||
假如倒计时归零时能量条数值大于 drawThreshold
|
||||
当系统进入结算阶段
|
||||
那么系统应判定玩家胜利
|
||||
而且结算面板应展示玩家叫声次数、最大音量和声浪评分
|
||||
而且应提供再来一局入口
|
||||
|
||||
场景: 对手侧占优时判定玩家失败
|
||||
假如倒计时归零时能量条数值小于 -drawThreshold
|
||||
当系统进入结算阶段
|
||||
那么系统应判定对手胜利
|
||||
而且结算面板应展示玩家叫声次数、最大音量和声浪评分
|
||||
而且应提供再来一局入口
|
||||
|
||||
场景: 能量条接近平衡时判定平局
|
||||
假如倒计时归零时能量条数值位于 -drawThreshold 到 drawThreshold 之间
|
||||
当系统进入结算阶段
|
||||
那么系统应判定为平局
|
||||
而且结算面板应展示双方接近平衡的结果
|
||||
而且应提供再来一局入口
|
||||
```
|
||||
|
||||
### 功能: 再来一局与状态重置
|
||||
|
||||
```gherkin
|
||||
功能: 对战重开
|
||||
为了让玩家快速再次挑战
|
||||
作为玩家
|
||||
我希望结算后可以开始新的一局且旧状态不会污染新局
|
||||
|
||||
场景: 结算后点击再来一局重置本局状态
|
||||
假如系统处于 finished 结算阶段
|
||||
而且结算面板展示上一局结果
|
||||
当玩家选择再来一局
|
||||
那么系统应重置剩余时间为 30 秒
|
||||
而且能量条应回到中线
|
||||
而且玩家叫声次数、最大音量、连击和胜负结果应清零
|
||||
而且系统应重新进入校准或开局倒计时流程
|
||||
|
||||
场景: 结算后返回玩法入口
|
||||
假如系统处于 finished 结算阶段
|
||||
当玩家选择返回入口
|
||||
那么系统应离开当前对战运行态
|
||||
而且不应继续采集麦克风输入
|
||||
```
|
||||
|
||||
### 功能: 移动端布局
|
||||
|
||||
```gherkin
|
||||
功能: 移动端 bark-battle 布局
|
||||
为了让手机浏览器玩家可以完成声控对战
|
||||
作为移动端玩家
|
||||
我希望核心信息在窄屏中保持可见且可操作
|
||||
|
||||
场景: 移动端进入对战页面时核心元素可见
|
||||
假如玩家使用宽度不超过 430px 的移动端视口
|
||||
当玩家进入 bark-battle 页面
|
||||
那么顶部能量条应完整显示在首屏可见区域内
|
||||
而且倒计时应可见
|
||||
而且双方狗狗主体不应被权限、设置或说明面板长期遮挡
|
||||
而且非关键设置应收起到菜单或次级入口中
|
||||
|
||||
场景: 移动端授权和开始必须由用户手势触发
|
||||
假如玩家使用移动端浏览器
|
||||
当玩家点击开始或授权入口
|
||||
那么系统才应请求麦克风权限并激活音频上下文
|
||||
而且不应在页面自动加载时直接启动 AudioContext
|
||||
|
||||
场景: 移动端结算面板不遮挡主要操作
|
||||
假如玩家在移动端完成一局对战
|
||||
当系统展示结算面板
|
||||
那么胜负结果、再来一局和返回入口应在不横向滚动的情况下可见
|
||||
而且结算面板不应要求玩家阅读大段规则说明才能继续
|
||||
```
|
||||
|
||||
### 功能: 无 getUserMedia 或不可用环境降级
|
||||
|
||||
```gherkin
|
||||
功能: 无麦克风 API 降级
|
||||
为了避免不支持设备进入卡死状态
|
||||
作为无麦克风或无 API 支持玩家
|
||||
我希望系统明确告知无法声控游玩并提供退出路径
|
||||
|
||||
场景: 当前浏览器不支持 getUserMedia
|
||||
假如玩家设备不支持 navigator.mediaDevices.getUserMedia
|
||||
当玩家进入 bark-battle 页面
|
||||
那么系统应显示设备或浏览器不支持麦克风输入的状态
|
||||
而且应提供返回入口
|
||||
而且不应展示可开始声控对战的按钮
|
||||
|
||||
场景: getUserMedia 调用失败但浏览器 API 存在
|
||||
假如浏览器支持 getUserMedia
|
||||
但是请求麦克风时返回 NotFoundError 或 NotReadableError
|
||||
当系统接收到失败结果
|
||||
那么系统应展示麦克风不可用状态
|
||||
而且应提供重试授权或返回入口
|
||||
而且不应进入 playing 阶段
|
||||
|
||||
场景: 非安全上下文导致麦克风不可用
|
||||
假如页面运行在浏览器不允许麦克风的非安全上下文
|
||||
当玩家进入 bark-battle 页面
|
||||
那么系统应展示当前环境无法使用麦克风的状态
|
||||
而且应提示使用受支持的安全环境或返回
|
||||
而且不应开始对战倒计时
|
||||
```
|
||||
|
||||
### 功能: 刷新与退出时释放麦克风资源
|
||||
|
||||
```gherkin
|
||||
功能: 麦克风资源释放
|
||||
为了保护用户隐私并避免浏览器资源泄漏
|
||||
作为浏览器玩家
|
||||
我希望离开对战时麦克风采集被停止
|
||||
|
||||
场景: 对战中离开页面停止采集
|
||||
假如玩家已经授权麦克风并处于 playing 阶段
|
||||
当玩家离开 bark-battle 页面或运行态卸载
|
||||
那么系统应停止当前 MediaStream 的所有音轨
|
||||
而且不应继续推进本局 simulation tick
|
||||
|
||||
场景: 刷新页面后不沿用旧局临时状态
|
||||
假如玩家在 playing 阶段刷新页面
|
||||
当页面重新加载 bark-battle
|
||||
那么系统应重新进入权限检查或授权准备状态
|
||||
而且不应沿用刷新前的剩余时间、能量条和叫声次数作为新局结果
|
||||
```
|
||||
|
||||
## 测试映射
|
||||
|
||||
| 场景 | 测试层级 | 建议目标文件 | 自动化状态 |
|
||||
| --- | --- | --- | --- |
|
||||
| 玩家允许麦克风权限后进入环境噪音校准 | application/component | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 校准完成后进入开局倒计时 | application/unit | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/domain/BarkBattleSession.test.ts` | planned |
|
||||
| 玩家拒绝麦克风权限后不能开始声控对战 | application/component | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 安静环境生成低但非零的有效阈值 | unit | `src/games/bark-battle/domain/BarkNoiseCalibration.test.ts` | planned |
|
||||
| 嘈杂环境生成更高的有效阈值 | unit | `src/games/bark-battle/domain/BarkNoiseCalibration.test.ts` | planned |
|
||||
| 校准期间无法获得有效音频样本 | application/component | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 单次超过阈值且间隔足够的叫声计数加一 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 持续噪音不会被无限计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 间隔过短的连续峰值不重复计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 玩家推动力高于对手时能量条向玩家侧移动 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned |
|
||||
| 连续大声叫声触发更强反馈 | unit/integration/component | `src/games/bark-battle/domain/BarkBattleScoring.test.ts`, `src/games/bark-battle/ui/BarkBattleHud.test.tsx` | planned |
|
||||
| 能量条到达边界后不会越界 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned |
|
||||
| 对手推动力高于玩家时能量条向对手侧移动 | unit | `src/games/bark-battle/domain/EnergyTugOfWar.test.ts` | planned |
|
||||
| 低于阈值的背景噪音不计数 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 过短脉冲不计为有效叫声 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 过长持续声被削弱为单段输入 | unit | `src/games/bark-battle/domain/BarkDetector.test.ts` | planned |
|
||||
| 倒计时每秒递减并在归零时停止对战输入 | unit/application | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/application/BarkBattleController.test.ts` | planned |
|
||||
| 玩家侧占优时判定玩家胜利 | unit/component | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned |
|
||||
| 对手侧占优时判定玩家失败 | unit/component | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned |
|
||||
| 能量条接近平衡时判定平局 | unit/component | `src/games/bark-battle/domain/BarkBattleSession.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned |
|
||||
| 结算后点击再来一局重置本局状态 | application/component | `src/games/bark-battle/application/BarkBattleController.test.ts`, `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx` | planned |
|
||||
| 结算后返回玩法入口 | integration/smoke | `src/games/bark-battle/application/BarkBattleController.test.ts`, Playwright 或人工 smoke 清单 | planned |
|
||||
| 移动端进入对战页面时核心元素可见 | component/visual/smoke | `src/games/bark-battle/ui/BarkBattleHud.test.tsx`, Playwright 移动端视口 smoke | planned |
|
||||
| 移动端授权和开始必须由用户手势触发 | application/e2e-smoke | `src/games/bark-battle/application/BrowserMicrophoneInput.test.ts`, Playwright 移动端 smoke | planned |
|
||||
| 移动端结算面板不遮挡主要操作 | component/visual/smoke | `src/games/bark-battle/ui/BarkBattleResultPanel.test.tsx`, Playwright 移动端视口 smoke | planned |
|
||||
| 当前浏览器不支持 getUserMedia | component/application | `src/games/bark-battle/application/BrowserMicrophoneInput.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| getUserMedia 调用失败但浏览器 API 存在 | component/application | `src/games/bark-battle/application/BrowserMicrophoneInput.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 非安全上下文导致麦克风不可用 | component/application | `src/games/bark-battle/application/BrowserMicrophoneInput.test.ts`, `src/games/bark-battle/ui/BarkBattlePermissionPanel.test.tsx` | planned |
|
||||
| 对战中离开页面停止采集 | application/integration | `src/games/bark-battle/application/BrowserMicrophoneInput.test.ts`, `src/games/bark-battle/application/BarkBattleController.test.ts` | planned |
|
||||
| 刷新页面后不沿用旧局临时状态 | integration/smoke | Playwright 或人工 smoke 清单 | planned |
|
||||
|
||||
## 验收清单
|
||||
|
||||
- [ ] 权限允许、拒绝、API 不支持、麦克风不可读均有明确状态,且不会误进入 playing。
|
||||
- [ ] 校准阶段会影响有效叫声阈值,低噪音不会增加叫声计数。
|
||||
- [ ] 有效叫声计数具备阈值、峰值间隔、持续时长约束。
|
||||
- [ ] 能量条根据双方推动力差值双向移动,并限制在 `-100` 到 `100`。
|
||||
- [ ] 30 秒归零后停止本局输入影响,并按玩家胜利、对手胜利、平局三类结果结算。
|
||||
- [ ] 移动端核心元素可见,非关键设置收起,不在主画面堆叠长规则说明。
|
||||
- [ ] 离开页面或返回入口时停止麦克风采集。
|
||||
- [ ] 每个编码依据场景已在测试映射中标注测试层级和建议文件。
|
||||
|
||||
## 开放问题
|
||||
|
||||
1. MVP 是否确认只做“玩家 vs AI”,还是第一版需要双人同屏或联机对战?
|
||||
2. `drawThreshold`、`minBarkGapMs`、`minBarkDurationMs`、`maxBarkDurationMs` 的首版默认值由产品/调参阶段确认,还是先采用开发可配置默认值?
|
||||
3. 是否允许无麦克风设备提供键盘/点击备用输入?若允许,需要另补非声控模式场景;若不允许,当前降级只提供返回入口。
|
||||
4. 是否需要在结算中记录或上报成绩、最高音量、叫声次数和声浪评分?若需要,需补埋点/后端持久化场景。
|
||||
5. bark-battle 是否作为 Genarrative 正式 play type 接入创作入口、作品发布和广场,还是先作为独立 runtime 原型验证?
|
||||
6. 狗狗、背景、拟声词和冲击波素材来源是临时占位、AI 生成,还是复用项目现有素材管线?
|
||||
@@ -671,70 +671,326 @@ BarkBattleService
|
||||
- `rate_limited`
|
||||
- `impossible_bark_count`
|
||||
|
||||
## 9. TDD / 验收顺序与命令
|
||||
## 9. BDD 行为场景、TDD 落地顺序与验收命令
|
||||
|
||||
本任务只写方案,不执行代码实现。后续落地建议按以下顺序:
|
||||
本任务只写方案,不执行代码实现。后续落地必须先用 BDD 锁定可观察行为,再按 TDD 做 RED-GREEN-REFACTOR。没有先失败的测试,不进入生产代码实现。
|
||||
|
||||
### 9.1 domain 纯函数测试
|
||||
### 9.1 BDD 场景清单
|
||||
|
||||
先实现 `module-bark-battle`:
|
||||
以下场景用于约束后端行为,场景标题应映射到后续 Rust 测试名、API 测试名或 smoke 用例名。
|
||||
|
||||
#### 功能: bark-battle 发布态作品运行配置
|
||||
|
||||
```gherkin
|
||||
功能: bark-battle 发布态作品运行配置
|
||||
为了让玩家进入稳定的声控狗叫对战作品
|
||||
作为已发布作品的玩家
|
||||
我希望后端只返回发布态且版本一致的运行配置
|
||||
|
||||
背景:
|
||||
假如平台中存在 playTypeId 为 "bark-battle" 的作品配置
|
||||
|
||||
场景: 玩家请求已发布作品的 runtime config
|
||||
假如作品处于 published 状态
|
||||
而且请求携带的 configVersion 与当前发布版本一致
|
||||
当玩家请求 bark-battle runtime config
|
||||
那么后端应返回 BarkBattleRuntimeConfig
|
||||
而且响应中的 workId 应为稳定作品 ID
|
||||
而且响应中的 playTypeId 应为 "bark-battle"
|
||||
而且响应不应包含草稿态配置或创作者私有字段
|
||||
|
||||
场景: 玩家请求未发布作品的 runtime config
|
||||
假如作品处于 draft 状态
|
||||
当玩家请求 bark-battle runtime config
|
||||
那么后端应返回不可游玩的错误
|
||||
而且不应创建 runtime run
|
||||
|
||||
场景: 玩家携带过期配置版本进入 runtime
|
||||
假如作品已发布 configVersion 为 3 的版本
|
||||
当玩家使用 configVersion 为 2 的请求开始 runtime
|
||||
那么后端应拒绝开始本局或返回最新版本提示
|
||||
而且不应写入正式成绩
|
||||
```
|
||||
|
||||
#### 功能: bark-battle runtime start 与统一游玩埋点
|
||||
|
||||
```gherkin
|
||||
功能: bark-battle runtime start 与统一游玩埋点
|
||||
为了统计正式作品游玩行为
|
||||
作为数据分析人员
|
||||
我希望玩家开始 bark-battle 正式游玩时写入统一 work_play_start 事件
|
||||
|
||||
场景: 玩家成功开始已发布作品运行态
|
||||
假如存在一个已发布的 bark-battle 作品
|
||||
而且玩家有权限游玩该作品
|
||||
当玩家调用 start runtime
|
||||
那么后端应创建状态为 started 的 run
|
||||
而且应返回 runId、一次性 runToken 与 runtimeConfig
|
||||
而且应写入 work_play_start 事件
|
||||
而且事件 scope_kind 应为 work
|
||||
而且事件 scope_id 应为稳定作品 ID
|
||||
而且 metadata 应包含 playType、workId、sourceRoute 和 userId
|
||||
|
||||
场景: 无权限玩家尝试开始运行态
|
||||
假如作品不可被当前玩家访问
|
||||
当玩家调用 start runtime
|
||||
那么后端应返回权限错误
|
||||
而且不应创建 run
|
||||
而且不应写入 work_play_start 事件
|
||||
```
|
||||
|
||||
#### 功能: bark-battle runtime finish 派生成绩提交
|
||||
|
||||
```gherkin
|
||||
功能: bark-battle runtime finish 派生成绩提交
|
||||
为了保护用户隐私并保证成绩可信
|
||||
作为后端服务
|
||||
我只接受派生指标并重新裁决正式结果
|
||||
|
||||
场景: 玩家完成一局并提交合法派生指标
|
||||
假如玩家已经成功 start runtime
|
||||
而且 run 处于 started 状态
|
||||
而且提交的 runToken 与 start 返回一致
|
||||
当玩家提交 elapsedMs、finalEnergy 和 BarkBattleDerivedMetrics
|
||||
那么后端应校验派生指标范围
|
||||
而且应重新计算 serverWinner、score 和 ScoreSummary
|
||||
而且应把 run 状态改为 finished
|
||||
而且响应中的 accepted 应为 true
|
||||
而且请求与持久化记录中不应包含原始麦克风音频
|
||||
|
||||
场景: 玩家重复提交同一个 run 的 finish
|
||||
假如某个 run 已经处于 finished 状态
|
||||
当玩家再次提交 finish
|
||||
那么后端应拒绝重复提交
|
||||
而且不应重复写入成绩
|
||||
而且不应重复写入排行榜
|
||||
|
||||
场景: 玩家提交不合理的时长
|
||||
假如作品配置 durationMs 为 30000
|
||||
当玩家提交 elapsedMs 明显短于配置时长
|
||||
那么后端应返回 rejected 或 accepted_with_flags
|
||||
而且 antiCheatFlags 应包含 elapsed_too_short
|
||||
而且该结果默认不应进入排行榜
|
||||
|
||||
场景: 玩家提交超出范围的声音指标
|
||||
假如玩家已经成功 start runtime
|
||||
当玩家提交 peakVolumeMax 大于 1 或 validBarkCount 超过物理合理上限
|
||||
那么后端应拒绝或打 flag
|
||||
而且 antiCheatFlags 应包含 metric_out_of_range 或 impossible_bark_count
|
||||
```
|
||||
|
||||
#### 功能: bark-battle 排行榜投影
|
||||
|
||||
```gherkin
|
||||
功能: bark-battle 排行榜投影
|
||||
为了让玩家比较同一作品内的有效成绩
|
||||
作为玩家
|
||||
我希望排行榜只展示被后端接受的成绩
|
||||
|
||||
场景: 作品开启排行榜且成绩被接受
|
||||
假如作品 leaderboardEnabled 为 true
|
||||
而且玩家提交的 finish 结果 accepted 为 true
|
||||
当后端完成成绩写入
|
||||
那么后端应写入或更新该作品的排行榜投影
|
||||
而且排行榜排序应优先按 score 降序
|
||||
|
||||
场景: 作品关闭排行榜
|
||||
假如作品 leaderboardEnabled 为 false
|
||||
当玩家完成一局合法成绩
|
||||
那么后端应记录 run 与 score
|
||||
而且不应写入排行榜 entry
|
||||
|
||||
场景: 带反作弊 flag 的成绩不进入默认榜单
|
||||
假如玩家提交的结果为 accepted_with_flags
|
||||
当后端处理排行榜投影
|
||||
那么默认排行榜不应展示该成绩
|
||||
而且该 run 的反作弊标记应可供后台审计
|
||||
```
|
||||
|
||||
#### 功能: bark-battle 隐私边界
|
||||
|
||||
```gherkin
|
||||
功能: bark-battle 隐私边界
|
||||
为了保护玩家声音隐私
|
||||
作为平台
|
||||
我不能上传或保存原始麦克风音频
|
||||
|
||||
场景: finish 请求包含原始音频字段
|
||||
假如前端误传 audio、audioBase64、waveform 或 pcmSamples 字段
|
||||
当后端解析 finish 请求
|
||||
那么后端应拒绝请求或忽略并记录非法字段
|
||||
而且持久化记录中不应出现可还原声音的数据
|
||||
|
||||
场景: 正常 finish 只提交派生聚合指标
|
||||
假如玩家完成一局 bark-battle
|
||||
当玩家提交 finish 请求
|
||||
那么请求体应只包含峰值、均值、有效叫声次数、节奏命中、最终能量和耗时等派生指标
|
||||
而且后端只保存这些派生指标与裁决结果
|
||||
```
|
||||
|
||||
### 9.2 BDD 到测试映射
|
||||
|
||||
| BDD 场景 | 测试层级 | 建议目标文件 | RED 期望 |
|
||||
| --- | --- | --- | --- |
|
||||
| 玩家请求已发布作品的 runtime config | contract + API | `server-rs/crates/shared-contracts/src/bark_battle.rs`、`server-rs/crates/api-server/src/bark_battle.rs` | 新 DTO / route 不存在导致编译或断言失败 |
|
||||
| 玩家请求未发布作品的 runtime config | domain + API | `server-rs/crates/module-bark-battle/src/domain.rs`、`server-rs/crates/api-server/src/bark_battle.rs` | 未发布状态未被拒绝 |
|
||||
| 玩家携带过期配置版本进入 runtime | domain | `server-rs/crates/module-bark-battle/src/domain.rs` | configVersion mismatch 未返回错误 |
|
||||
| 玩家成功开始已发布作品运行态 | API + integration | `server-rs/crates/api-server/src/bark_battle.rs` | start 未返回 runToken 或未写 tracking draft |
|
||||
| 无权限玩家尝试开始运行态 | API | `server-rs/crates/api-server/src/bark_battle.rs` | 无权限仍创建 run 或写埋点 |
|
||||
| 玩家完成一局并提交合法派生指标 | domain + facade | `server-rs/crates/module-bark-battle/src/domain.rs`、`server-rs/crates/spacetime-client/src/bark_battle.rs` | score / serverWinner 未由后端计算 |
|
||||
| 玩家重复提交同一个 run 的 finish | SpacetimeDB reducer / facade | `server-rs/crates/spacetime-module/src/...`、`server-rs/crates/spacetime-client/src/bark_battle.rs` | duplicate finish 被接受 |
|
||||
| 玩家提交不合理的时长 | domain | `server-rs/crates/module-bark-battle/src/domain.rs` | 未产生 elapsed_too_short flag |
|
||||
| 玩家提交超出范围的声音指标 | domain | `server-rs/crates/module-bark-battle/src/domain.rs` | 未产生 metric_out_of_range / impossible_bark_count |
|
||||
| 作品开启排行榜且成绩被接受 | domain + projection | `server-rs/crates/module-bark-battle/src/domain.rs`、SpacetimeDB reducer 测试 | accepted 成绩未生成 leaderboard projection |
|
||||
| 作品关闭排行榜 | domain + projection | `server-rs/crates/module-bark-battle/src/domain.rs`、SpacetimeDB reducer 测试 | leaderboardEnabled=false 仍写榜 |
|
||||
| 带反作弊 flag 的成绩不进入默认榜单 | domain | `server-rs/crates/module-bark-battle/src/domain.rs` | flagged 成绩仍进入默认榜 |
|
||||
| finish 请求包含原始音频字段 | contract + API | `server-rs/crates/shared-contracts/src/bark_battle.rs`、API 反序列化测试 | DTO 接受或持久化原始音频字段 |
|
||||
| 正常 finish 只提交派生聚合指标 | contract | `server-rs/crates/shared-contracts/src/bark_battle.rs` | DTO 缺字段或包含隐私风险字段 |
|
||||
|
||||
### 9.3 TDD RED-GREEN-REFACTOR 切片
|
||||
|
||||
每个切片必须先写失败测试并运行到预期失败,再写最小实现。
|
||||
|
||||
#### Slice 1: shared contracts 固定请求/响应形状
|
||||
|
||||
RED:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p module-bark-battle
|
||||
cargo test -p shared-contracts bark_battle_contract_uses_camel_case --no-default-features
|
||||
```
|
||||
|
||||
测试覆盖:
|
||||
先新增测试,断言:
|
||||
|
||||
- runtime config 校验。
|
||||
- finish metrics 范围校验。
|
||||
- 胜负判定。
|
||||
- score / grade / leaderboard score 计算。
|
||||
- 反作弊 flags。
|
||||
- `BarkBattleRunStartRequest` 序列化为 `workId`、`configVersion`、`sourceRoute`。
|
||||
- `BarkBattleRunFinishRequest` 只包含 `runId`、`runToken`、`workId`、`configVersion`、`elapsedMs`、`finalEnergy`、`clientWinner`、`metrics`。
|
||||
- `metrics` 只包含派生聚合字段,不包含 `audio`、`audioBase64`、`waveform`、`pcmSamples`。
|
||||
|
||||
### 9.2 contracts 测试
|
||||
GREEN:新增 `shared-contracts/src/bark_battle.rs` 与 `lib.rs` 导出,让测试通过。
|
||||
|
||||
再实现 shared contracts:
|
||||
REFACTOR:统一枚举命名、字段注释和默认值策略。
|
||||
|
||||
#### Slice 2: domain 校验 runtime config 与 finish metrics
|
||||
|
||||
RED:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts bark_battle
|
||||
cargo check -p shared-contracts
|
||||
cargo test -p module-bark-battle validates_runtime_config_and_finish_metrics --no-default-features
|
||||
```
|
||||
|
||||
验收:
|
||||
先新增测试,断言:
|
||||
|
||||
- DTO 可序列化 / 反序列化。
|
||||
- 枚举值稳定。
|
||||
- 可选字段向后兼容。
|
||||
- TypeScript mirror 或契约生成产物与 Rust contract 对齐。
|
||||
- `playTypeId` 必须为 `bark-battle`。
|
||||
- `durationMs`、`energyMin`、`energyMax`、`barkThreshold` 必须在合法范围。
|
||||
- `peakVolumeMax`、`peakVolumeAvg` 必须在 `0..1`。
|
||||
- 过短时长产生 `elapsed_too_short`。
|
||||
- 不可能叫声次数产生 `impossible_bark_count`。
|
||||
|
||||
### 9.3 SpacetimeDB / api-server check
|
||||
GREEN:新增 `module-bark-battle` 纯领域类型与校验函数。
|
||||
|
||||
实现表、reducer、migration 与 facade 后:
|
||||
REFACTOR:把常量收敛为领域常量,避免 magic number 分散。
|
||||
|
||||
#### Slice 3: domain 重新裁决成绩与排行榜资格
|
||||
|
||||
RED:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo check -p spacetime-module
|
||||
cargo check -p spacetime-client
|
||||
cargo check -p api-server
|
||||
cargo test -p api-server bark_battle
|
||||
cargo test -p module-bark-battle scores_finish_result_and_filters_leaderboard --no-default-features
|
||||
```
|
||||
|
||||
如仓库有统一命令,以统一命令为准,例如:
|
||||
先新增测试,断言:
|
||||
|
||||
- 后端根据 `finalEnergy` 重算 `serverWinner`,不直接信任 `clientWinner`。
|
||||
- 合法胜利生成 `accepted=true` 与稳定 `ScoreSummary`。
|
||||
- 带 flag 的结果默认 `leaderboardEligible=false`。
|
||||
- `leaderboardEnabled=false` 时不生成榜单投影。
|
||||
|
||||
GREEN:补 `score_finish_result(...)`、`is_leaderboard_eligible(...)` 等纯函数。
|
||||
|
||||
REFACTOR:拆分 score、winner、anti-cheat、leaderboard 四组小函数。
|
||||
|
||||
#### Slice 4: SpacetimeDB 表、reducer 与 migration
|
||||
|
||||
RED:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test --workspace
|
||||
cargo check -p spacetime-module --no-default-features
|
||||
cargo test -p spacetime-client bark_battle_facade_maps_run_records --no-default-features
|
||||
```
|
||||
|
||||
验收:
|
||||
先新增 facade / mapper 测试或编译断言,预期因绑定、表或 reducer 缺失失败。
|
||||
|
||||
- migration.rs 包含新增表与变更。
|
||||
- 绑定由生成命令产出,未手改生成物。
|
||||
- api-server route handler 只做编排,不内嵌复杂计分规则。
|
||||
GREEN:新增表、reducer/procedure、`migration.rs`、生成绑定、`spacetime-client` facade。
|
||||
|
||||
### 9.4 前端 contract 对齐
|
||||
REFACTOR:确保 api-server 不直接依赖 generated bindings,mapper 命名与现有玩法一致。
|
||||
|
||||
#### Slice 5: api-server start / finish BFF
|
||||
|
||||
RED:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p api-server bark_battle_start_records_work_play_start --no-default-features
|
||||
cargo test -p api-server bark_battle_finish_rejects_duplicate_or_invalid_metrics --no-default-features
|
||||
```
|
||||
|
||||
先新增 API 测试,断言:
|
||||
|
||||
- start 成功返回 `runId`、`runToken`、`runtimeConfig`。
|
||||
- start 成功后主动补写 `work_play_start`,不只依赖 route-level 兜底。
|
||||
- finish 重复提交被拒绝。
|
||||
- finish 不接受非法 metrics。
|
||||
|
||||
GREEN:新增 Axum route、handler、错误映射、tracking 调用。
|
||||
|
||||
REFACTOR:handler 只做编排,把规则留在 domain,把持久化留在 facade。
|
||||
|
||||
#### Slice 6: 前端 contract 对齐
|
||||
|
||||
RED:
|
||||
|
||||
```bash
|
||||
npm run test -- bark-battle
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
先新增前端 contract / client 测试,预期因 TS 类型或 client 缺失失败。
|
||||
|
||||
GREEN:补前端共享 contract mirror、runtime client 调用 start / finish。
|
||||
|
||||
REFACTOR:删除重复类型,保持后端 DTO 为事实源。
|
||||
|
||||
### 9.4 后端验收命令
|
||||
|
||||
按切片逐步运行,不要等全部实现后一次性补测:
|
||||
|
||||
```bash
|
||||
cd server-rs
|
||||
cargo test -p shared-contracts bark_battle --no-default-features
|
||||
cargo test -p module-bark-battle --no-default-features
|
||||
cargo check -p spacetime-module --no-default-features
|
||||
cargo check -p spacetime-client --no-default-features
|
||||
cargo test -p api-server bark_battle --no-default-features
|
||||
```
|
||||
|
||||
如涉及 API smoke:
|
||||
|
||||
```bash
|
||||
npm run api-server
|
||||
# 另开终端执行对应 bark-battle start / finish smoke 或项目既有 API 测试脚本
|
||||
```
|
||||
|
||||
每次表结构变更后必须同步:
|
||||
|
||||
1. 更新 `migration.rs`。
|
||||
2. 重新生成 SpacetimeDB bindings。
|
||||
3. 检查 generated bindings 变更只来自生成命令。
|
||||
4. 运行 `cargo check -p spacetime-module -p spacetime-client --no-default-features`。
|
||||
|
||||
### 9.5 前端 contract 对齐验收
|
||||
|
||||
前端只在后端 contract 稳定后接入:
|
||||
|
||||
@@ -750,7 +1006,7 @@ npm run build
|
||||
- 前端 result panel 展示后端 `RunResult`。
|
||||
- 前端本地结算只作为即时反馈,正式结果以后端返回为准。
|
||||
|
||||
### 9.5 手工验收清单
|
||||
### 9.6 手工验收清单
|
||||
|
||||
- 可以创建并保存 bark-battle 草稿。
|
||||
- 可以发布成稳定作品 ID,`playTypeId = bark-battle`。
|
||||
|
||||
Reference in New Issue
Block a user