156 lines
9.9 KiB
Markdown
156 lines
9.9 KiB
Markdown
# 代码规范(Dungeon Sweeper)
|
||
|
||
> 生效:2026-09-11 · 来源:用户在垂直切片评审中逐一指出的真实陋习与缺陷,全部成文。
|
||
> 每条规则附本项目真实反面案例(标注缺陷编号/日期)——规则不是抽象美德,是踩过的坑。
|
||
> 约束范围:`game/`(原 prototypes/ 垂直切片,2026-09-24 转正)代码同尺度;与 CLAUDE.md「硬性红线」节既有硬约束
|
||
> (数值表单点 / RNG 封闭流 / Core 零 UI / fail-fast / spec-AC 一致性)并行生效,冲突以用户裁决为准。
|
||
|
||
**写任何代码前必须通读本文件。**
|
||
|
||
---
|
||
|
||
## 1. 结构纪律——高内聚、低耦合
|
||
|
||
### 1.1 禁止上帝对象
|
||
一个类只承载一个职责。场景主脚本(main)= **组合根**:模式流转、信号接线、核心写调用的唯一出口;
|
||
渲染、文案拼装、状态装配一律拆分为独立视图/装配器类。单文件超过 ~400 行即触发审视。
|
||
|
||
> **反面案例(2026-09-11,用户:"一个工程师看了想吐")**:main.gd 随功能追加长到 ~1100 行——棋盘渲染、
|
||
> HUD、结算日志格式化、四种模态覆盖层、调整板、蒙特卡洛模拟器、关卡流程全部糊在一个类里。
|
||
> 已拆分为 9 个单一职责单元(views/ + hud_presenter),main 降到 433 行组合根。
|
||
|
||
### 1.2 一切功能以类封装,禁止裸函数堆
|
||
设计稿与实现同尺度:功能 = 类(状态 + 行为 + 信号),类是封装的最小交付单位。
|
||
工具性纯函数只允许挂在明确的宿主类下(静态装配器/工厂),不允许无主散落。
|
||
|
||
> **反面案例(2026-09-11,用户:"我看你都是函数的方式……没有对象封装吧")**:供给控制器的伪代码
|
||
> 以裸函数呈现被否;正式实现按"B4 私有组合件 RewardSupplyController"封装。
|
||
|
||
### 1.3 依赖单向,核心写调用收口
|
||
依赖方向唯一:`views →(信号)→ 组合根 →(写调用)→ core`。
|
||
视图零核心写调用(纯 setter / 信号外发优先);"读核心状态 → 拼展示文案"集中于装配器(presenter);
|
||
核心写接口(settle_batch / submit_window_selection / runner 推进)只允许在组合根出现。
|
||
|
||
### 1.4 命名 = 领域命名,禁止系统 ID 前缀
|
||
文件名与类名禁止 `a1_`/`b1_`/`c1_` 等系统 ID 前缀(`a1_board_generator.gd` → `board_generator.gd`)。
|
||
设计追溯不靠前缀:每个核心实现文件头注必须带一行
|
||
`# 实现 GDD:design/archive/gdd-2026-09-14/<id>-<slug>.md(系统 ID Xn)`(2026-09-14 起旧 GDD 体系封存于归档,头注指归档路径;对应新 systems 文档定稿后再改指 `design/gdd/systems/` 新文档);系统 ID 只作为文档与 registry 的追溯锚。
|
||
豁免:D2 schema 类与 autoload(本就领域命名);文档/夹具登记名(如 b4_golden)属文档层。
|
||
|
||
> **反面案例(2026-09-11,用户:"是没有代码规范吗")**:ID 前缀是跟随 GDD 文档名的事实约定,
|
||
> 从未成文;同目录 `config_db`/`rng_service` 无前缀与 A/B/C 系列并存——两套命名即规范缺失的铁证。
|
||
|
||
### 1.5 禁止事实约定
|
||
任何"大家默认这么做"的惯例必须成文进本文件(或 CLAUDE.md)才算存在。
|
||
写代码时发现未成文但必须遵守的约定 → 当场补条文,不许靠惯性延续。
|
||
|
||
---
|
||
|
||
## 2. 契约纪律——跨层/跨类调用的语义
|
||
|
||
### 2.1 信号与事件契约成文
|
||
每个跨类信号:参数语义、发送时点、消费方依赖,必须在发送方头注成文(参照 A2 三事件面先例)。
|
||
状态机的每次迁移路径都必须发出对应事件——**缺一条路径的事件 = 消费方永久等待 = 软锁**。
|
||
|
||
> **反面案例(playtest 缺陷②)**:A2 旗动作花完末点 AP 后相位翻 Waiting 但不发 round_end,
|
||
> B1 结算点依赖该事件 → 整局软锁。v1 冒烟没抓到是因为自动玩家不用旗——路径覆盖不全的代价。
|
||
|
||
### 2.2 跨层标识符语义单一且成文
|
||
位置(index)/ 句柄(handle)/ 键(key)是三种不同的东西。跨类传递标识符时,
|
||
传递方的语义必须与接收方契约逐字一致,禁止凭"当前恰好相等"传值。
|
||
|
||
> **反面案例(playtest 缺陷④)**:窗口选择契约 = 窗口条目位置,UI 却传库存数组句柄——
|
||
> 未消耗时二者巧合相等;消耗一件后错位 → 选中件不消耗、邻件被误耗。
|
||
|
||
### 2.3 模态覆盖层层级规则
|
||
覆盖层遮挡关系由场景树加入顺序决定。打开任何模态覆盖层时,必须显式隐藏会遮挡它的全屏层;
|
||
新增覆盖层必须登记其在 z 序中的位置,并提供"打开可见性"的场景断言。
|
||
|
||
> **反面案例(playtest 缺陷⑤)**:数值调整板已实现,但打开时被不透明的开始覆盖层完全遮住
|
||
> = "点击无反应"。功能存在 ≠ 功能可达。
|
||
|
||
### 2.4 状态字段语义显式
|
||
跨战斗/跨场景携带或恢复的状态字段,其语义来源必须显式(表值 / 明确入参),禁止用入参隐式定义。
|
||
|
||
> **反面案例**:B2 spawn 把 hero 的 max_hp 设为入参 hero_hp——跨战斗携带 HP 时上限被压扁,
|
||
> 恢复点/治疗包的满血守卫全部失效。
|
||
|
||
---
|
||
|
||
## 3. 测试纪律——"存在"必须证明"可达且正确"
|
||
|
||
### 3.1 场景级探针标配
|
||
每新增一个可交互 UI 区域(视图/覆盖层/面板),必须配套场景级 headless 探针:
|
||
真实实例化场景 → 驱动真实输入路径 → 断言可见性与状态。**只有 core 层 AC 通过不算有测试**
|
||
(GDScript 编译不查字典字段与时序,呈现层错误只在运行时暴露)。
|
||
|
||
### 3.2 覆盖层/面板必测"打开后可见"
|
||
凡模态覆盖层,探针必须断言:打开后自身 visible 且遮挡物已隐藏(缺陷⑤回归断言)。
|
||
|
||
### 3.3 缺陷修复必须沉淀回归断言
|
||
每个缺陷修复必须在探针或冒烟中留下能抓住该缺陷的断言(旗动作耗尽 AP → 必须进入第 2 轮;
|
||
选用道具 → 库存态必须变 Used)。没有回归断言的修复视为未完成。
|
||
|
||
### 3.4 提交前全量验证
|
||
交付前必须全绿:`smoke_all.gd` + 全部场景探针(ui_probe / ui_click_probe / ui_tune_probe)。
|
||
行为探针使用随机化玩家行为(旗/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。
|
||
|
||
---
|
||
|
||
## 4. 语言纪律——GDScript 4.x 实战坑(本项目踩实)
|
||
|
||
1. **多行 lambda 内禁止嵌套 match / 复杂控制流**(解析不可靠,报"Expected expression for match
|
||
pattern")——信号处理体一律抽成命名方法 + `bind` 传状态。
|
||
2. **Dictionary 取值返回 Variant,无类型推断**——参与算术或传参前显式声明类型(`var x: float = ...`)。
|
||
3. **RichTextLabel:`append_text()` 不回写 `text` 属性**——读可见文本用 `get_parsed_text()`。
|
||
4. **lambda 捕获循环变量按创建时值**——循环内先落地局部变量再捕获,或用 `bind`。
|
||
5. **重命名脚本必须同步 `.uid` 文件**(资源身份稳定);新增 `class_name` 后必须跑一次编辑器扫描
|
||
刷新全局类缓存,否则 `--headless --script` 模式解析失败。
|
||
6. **`print` 格式占位符与实参类型必须匹配**(`%d` 配 String = 静默输出字面量)——工具输出错
|
||
等于没有输出。
|
||
|
||
---
|
||
|
||
## 5. 数值与汇报纪律
|
||
|
||
### 5.1 数值口径必须显式标注尺度
|
||
任何期望值/均值汇报必须写明口径:**每格 / 每板 / 每战 / 每关**。混用口径 = 误导
|
||
("可选类 3.32"不加"每板",会被读成每格期望 >1)。
|
||
|
||
### 5.2 调参先测量,改后复测
|
||
改任何玩法数值前,先用扫描工具产出基线(通关率/难度类读数走 `balance_verifier.py` 冒险段——单关直入口口径已退役,机制类读数走 `reward_expectation.gd` 等);
|
||
改完必须复测回填数据。禁止拍脑袋调参。
|
||
|
||
### 5.3 数值设计必须跨尺度推演
|
||
在单一尺度校准的数值,必须推演其在更大尺度的复合结果(每战合理 × 关内囤积 = 关卡灾难)。
|
||
|
||
> **反面案例(2026-09-11)**:可选类 25% 格子配比在单战斗尺度校准(击杀需求 18/54),
|
||
> ×"库存关内延续"× 5 张板复合后 = 每关 24.8 件、刀总伤 180 > Boss 130 HP——预警⑥在关卡尺度坐实。
|
||
|
||
### 5.4 用户拍板的实现零发挥
|
||
拍板范围外的细节一律不自行补全——以"待办问题"清单列出等裁决。
|
||
(反面案例:清图奖励被自行设计成入库刀,用户纠正为"一次性高额伤害不入库"。)
|
||
|
||
---
|
||
|
||
## 6. 流程纪律
|
||
|
||
1. **大改动三段走**:设计稿/伪代码 → 用户审阅 → 实现 → 修改记录(README + session-state)。实现不得先于审阅。
|
||
2. **实现批收尾双登记**:切片 README 修改记录 + `production/session-state/active.md` 条目,缺一不可。
|
||
3. **规范变更必须成文进本文件**——发现未成文约定、或用户裁决产生新规则,当场补条文。
|
||
4. **呈现层缺陷的教训回写 D1 GDD 设计输入**(信号载荷 schema / 场景级测试 / 时点状态可见性)。
|
||
|
||
---
|
||
|
||
## 附:缺陷-规范对照表(本项目真实案例,2026-09-11)
|
||
|
||
| 缺陷 | 现象 | 根因 | 对应规则 |
|
||
|---|---|---|---|
|
||
| ① UI 抖动/"第3轮"消失 | 悬停文案换行推挤侧栏,末行被挤出视口 | 动态内容参与布局回流 | 1.1 拆分 + 视图定长槽(HUDView) |
|
||
| ② Boss 战 AP 用完不结算(软锁) | 插旗耗尽末点 AP 后整局冻结 | 旗动作路径缺轮末事件 | 2.1 事件发射完整性 |
|
||
| ④ 道具使用后不消失 | 选用件留存、邻件被误耗 | 窗口位置 vs 库存句柄错位 | 2.2 标识符语义单一 |
|
||
| ⑤ 调整板点击无反应 | 面板已实现但被开始层遮挡 | 模态打开未隐藏遮挡物 | 2.3 层级规则 + 3.2 |
|
||
| 上帝对象 | main.gd ~1100 行 | 职责无边界持续堆积 | 1.1–1.3 |
|
||
| ID 前缀命名 | a1_/b1_ 与领域命名并存 | 事实约定未成文 | 1.4–1.5 |
|
||
| 可选类每关 24.8 件 | 速杀退化、囤刀秒 Boss | 单尺度校准未做跨尺度推演 | 5.3 |
|