From 1d4ac91e3bda95048e65b7ac17c29a8e6198b80f Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 15 Oct 2026 21:16:38 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(=E9=97=B8=E6=94=B6=E6=95=9B=E6=89=B9):?= =?UTF-8?q?=20=E9=AA=8C=E6=94=B6=E9=97=B8=E5=94=AF=E4=B8=80=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=20run=5Fall=5Fgates.sh=20+=20=E5=90=88=E5=9B=9E?= =?UTF-8?q?=E9=97=B8=E5=9B=9E=E6=89=A7=E5=BC=BA=E5=88=B6=20+=20t17=20?= =?UTF-8?q?=E6=8E=A2=E9=92=88=E9=9A=94=E7=A6=BB=E4=BF=AE=E5=A4=8D=20+=20?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E9=97=B8=E6=B8=85=E5=8D=95=E5=8D=95=E4=B8=80?= =?UTF-8?q?=E4=BA=8B=E5=AE=9E=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户令「迭代完成前必须全量跑一遍所有测试」。起因是 iter27 台账只记 probe --click 未记 --full,t17_defer_probe 红了整个迭代无人发现——根因是闸清单散落 CLAUDE.md / process-conventions.md / test-conventions.md 三处、彼此不一致、且无任何机器校验。 - tools/run_all_gates.sh(新):全部验证闸的唯一入口与唯一清单,串跑冒烟/单元/探针 --click/--full/渲染/doc 三闸/静态扫描/熵增闸共十道,写回执 game/tests/.gates/receipt.json。 与 baseline.json 比对判定:基线红且红项清单未变=存量红零新增(不判失败), 基线绿转红或红项清单变化=新增红(判红)。python 解释器自动解析带 PIL 的解释器 并前置 PATH(渲染闸三个脚本依赖 PIL,用错解释器会假红)。 - .zcode/hooks/worktree-guard.sh:Bash 分支加合回闸——merge/rebase/cherry-pick 进 master 前按目标分支反查 worktree 回执,校验 commit 匹配/非 quick/无新增红三态, 任一不满足 exit 2 拦截(九态实测:匹配放行、commit 错/quick/含红/无回执/rebase 全拦)。 - game/smoke/t17_defer_probe.gd:隔离面从 main.save_manager = iso_c 改 ProbeUiUtil.isolate_save(iter27 已确立口径)。原写法对 getter-only 属性赋值无效 ——组合根 save_manager/meta 是 autoload 读视图,隔离必须落在单例上。修复后 --full 转绿。 - doc_link 三处死链归零:数据配置.md 已退役探针指路、元进度.md/天赋树.md 已改名文档链接。 - CLAUDE.md / process-conventions.md §3第10条 / test-conventions.md §4 三处 「交付前跑什么」统一为一句话=跑 run_all_gates.sh,删散落清单。 闸况:冒烟 0 / 单元 0 / --click 0 / --full 0 / 渲染 0 / doc_link 0 / doc_term 0 / 熵增 0;gdd_lint 与 static_scan 为存量红,已落 baseline.json 待判读清偿。 --- CLAUDE.md | 7 +- design/dev/process-conventions.md | 4 +- design/dev/test-conventions.md | 4 +- design/gdd/systems/表现与支撑/数据配置.md | 2 +- design/gdd/systems/跨局循环/元进度.md | 2 +- design/gdd/systems/跨局循环/天赋树.md | 2 +- game/smoke/t17_defer_probe.gd | 16 +- game/tests/.gates/.gitignore | 4 + game/tests/.gates/baseline.json | 45 +++++ tools/run_all_gates.sh | 233 ++++++++++++++++++++++ 10 files changed, 300 insertions(+), 19 deletions(-) create mode 100644 game/tests/.gates/.gitignore create mode 100644 game/tests/.gates/baseline.json create mode 100644 tools/run_all_gates.sh diff --git a/CLAUDE.md b/CLAUDE.md index 7dd3733d..5271cdbf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,11 +16,11 @@ ## 运行与验证(工作目录 = 含 project.godot 的切片目录) -- 冒烟:`python tools/run_smoke.py`(三闸:退出码+摘要失败数+SCRIPT ERROR 扫描;脚本错误不计失败数但静默中断剩余断言,读结果必核对) -- 场景探针:`…console.exe --headless --path . res://smoke/ui_probe.tscn`(另有 ui_tune_probe / ui_click_probe) +- **交付前验收 = 跑 `tools/run_all_gates.sh`(不带 `--quick`),全绿才合回 master**——它是全部验证闸的唯一入口与唯一清单,闸件增减只改它,文档禁另列清单。跑完写回执 `game/tests/.gates/receipt.json`,合回时由 worktree-guard 校验(缺回执/quick 模式/回执 commit ≠ HEAD/含红项四况一律拦截) +- 单件直跑(调试期最窄确认用,**不构成验收**):冒烟 `python tools/run_smoke.py` · 场景探针 `…console.exe --headless --path . res://smoke/ui_probe.tscn` · 单元 `… --script res://tests/run_all.gd` - 调参批量:`--script res://smoke/adventure_monte_carlo.gd`(全冒险期望)· `res://smoke/reward_expectation.gd`(奖励期望);通关率/难度类测量一律走全冒险 - 数值验证:`python tools/balance_verifier.py`(采集端 `res://smoke/verifier_batch.gd`,报告落 `design/qa/`) -- 交付前验收 = smoke + 全部场景探针全绿;熵增闸 = `python tools/repo_structure_audit.py`(新类别 = 同批修订 Project Layout+白名单) +- 熵增闸(新类别 = 同批修订 Project Layout + 白名单):`python tools/repo_structure_audit.py` ## Project Layout @@ -53,6 +53,7 @@ - 主工作区禁止直接改动与提交——一切改动走独立 worktree(豁免:active.md 会话收口、.zcode 钩子配置) - 测试执行两入口铁规——只有迭代 loop 开发与用户点名两个触发入口;其余会话零测试,需测试支撑先请示并注明未跑测 +- 交付前验收 = 跑 `tools/run_all_gates.sh` 全绿(唯一入口与唯一闸清单,禁在文档另列清单);合回 master 由 worktree-guard 校验回执强制(process-conventions §3) - MC/模拟:无数值改动零跑;档位硬闸见 `sim-data/README.md`「MC 采集档位表」,一档只能由用户发起 - 闸机轮次纪律:新码先孤立验证 → 出红只跑红件最小单元 → 长批先最小参数试车 → 开工扫坑册(process-conventions §6) - 变异验证——新增/改写验收函数必须变异变红才算验收成立;无变异红记录只写「已实现,拦截能力未证」(test-conventions §6) diff --git a/design/dev/process-conventions.md b/design/dev/process-conventions.md index aea94fed..c54bf62f 100644 --- a/design/dev/process-conventions.md +++ b/design/dev/process-conventions.md @@ -44,7 +44,7 @@ 9. **验收函数变异验证**——新增/改写的验收函数(冒烟断言/探针自检/闸机判据)必须配一次变异验证:故意把被测件改坏成该类缺陷形态 → 跑最窄那道闸确认变红 → `git checkout` 精确还原;变异点/跑的闸/红输出摘要/还原结论四件记入实现报告变异验证记录表;**无变异红记录的断言不写「已验收」,只写「已实现,拦截能力未证」**(细则见 test-conventions §6) **收口** -10. **批末四闸全绿才合并**——冒烟+单元测试+场景探针+改前改后读数对拍 +10. **批末全量验收才合并**——在任务 worktree 内跑 `tools/run_all_gates.sh`(不带 `--quick`),全绿才合回 master。该脚本是全部验证闸的**唯一入口与唯一清单**,闸件增减只改它;**本文与其他文档禁另列闸件清单**(清单散落多处正是 iter27 漏跑 `probe_gate --full` 的成因)。跑完写回执 `game/tests/.gates/receipt.json`,合回时由 worktree-guard 强制校验 11. **合并后主区全量复跑**——合并可能引入冲突或环境差异,落回 master 后再跑全套 12. **会话收口落记**——active.md 快照与状态记录更新(含删除已完结旧条目,doc-conventions 第 10 节);跨会话教训写入记忆库 13. **文档同步闸**——机械面文档(roadmap 行/registry/active.md 记账/README 工具注)合并前随批更新完毕方算收口;设计权威文档批内不更新:实现批收口出迭代实现报告(`design/qa/iter-实现报告-<日期>.md`:各卡状态+代拍板追认单+GDD 反哺点节),反哺点同步登记 `06-待办清单`,用户逐点裁决后另批落文;反哺点未落文期间相关 GDD 节视为待反哺,读前先查 06;需求卡片必列「关联文档同步面」按两层标注 @@ -57,7 +57,7 @@ **MC/verifier 触发细则**:仅在数值验证场景触发(改数值前后取证);normal 档为主、perfect 按需;**无数值改动的会话禁跑 MC**;结构位移类「存档锚」须先向用户请示定性再跑;需求卡片预设步骤的前提消失时(如「带值裁定后重锚」实际未投值)停问勿照字面执行。 -**分级周期口径**:轻闸(run_smoke 全量+ui_input_probe;触碰 presentation 加跑 ui_click_probe+ui_layout_probe)可随变更执行;重闸=轻闸+probe_gate --click+probe_gate --full(ui_probe/ui_tune_probe/t17_defer_probe/fx_probe 四探针)+render_gate(窗口化渲染闸:shot_probe 七屏样张 A-D 断言+fx_shot/board_shot 取证+棋盘 golden 逐像素)+run_all 单元全套+doc 三闸(gdd_lint/doc_link_audit/doc_term_audit)+熵增闸(repo_structure_audit);重闸属周期执行——统一在批次收口或固定周检跑,不进每次变更;真人实机目检归用户不进自动闸。 +**分级周期口径**:全部验证闸收编在 `tools/run_all_gates.sh` 一个入口下(**脚本即清单**),分两档——`--quick` 档跳过渲染闸(需开显示器)与静态扫描,供迭代中段自检;**收口出库一律跑全量档(不带 `--quick`)**,回执由合回闸强制校验。真人实机目检归用户不进自动闸。 ## 5. MC 采集档位细则 diff --git a/design/dev/test-conventions.md b/design/dev/test-conventions.md index 850401f0..3bb052f8 100644 --- a/design/dev/test-conventions.md +++ b/design/dev/test-conventions.md @@ -1,7 +1,7 @@ # 测试规范(Dungeon Sweeper) > 约束范围:`game/smoke/`、`game/tests/` 与一切场景探针的编写与验收。 -> 分工:测试**执行许可**(什么时候允许跑测试)归 CLAUDE.md「硬性红线·测试执行两入口铁规」;命令与工具清单归 CLAUDE.md「运行与验证」;本文管测试怎么写、怎么算合格。 +> 分工:测试**执行许可**(什么时候允许跑测试)归 CLAUDE.md「硬性红线·测试执行两入口铁规」;**跑什么闸、什么顺序**归 `tools/run_all_gates.sh`(脚本即清单,本文与 CLAUDE.md 均不另列);本文管测试怎么写、怎么算合格。 **写任何测试前必须通读本文。** @@ -26,7 +26,7 @@ ## 4. 提交前全量验证 -交付前必须全绿:`smoke_all.gd` + 全部场景探针(ui_probe / ui_click_probe / ui_tune_probe)。 +交付前跑 `tools/run_all_gates.sh`(不带 `--quick`)全绿——该脚本是全部验证闸的唯一入口与唯一清单,本文禁另列闸件清单。闸件增减只改脚本。 行为探针使用随机化玩家行为(旗/停手/刷新/无效点击)——自动玩家不做的动作就是测试盲区。 ## 5. 测试函数怎么写——验收清单条目的落码规范 diff --git a/design/gdd/systems/表现与支撑/数据配置.md b/design/gdd/systems/表现与支撑/数据配置.md index 297d2b70..47aa6ca7 100644 --- a/design/gdd/systems/表现与支撑/数据配置.md +++ b/design/gdd/systems/表现与支撑/数据配置.md @@ -218,6 +218,6 @@ | v0.1 | 2026-09-15 | 自归档 GDD 压缩改写 | | v0.2 | 2026-09-15 | 雾格残留清理:关闭「待拍板」的棋盘生成参数表雾字段清理登记(fog_region_count / fog_target_ratio 两字段、雾占比绝对上限与雾域跨字段校验)——字段与校验随雾格移除同批删除,registry `board_gen_table` 条目同步修订。表清单与校验纪律不变 | | v0.3 | 2026-09-17 | 代币→钥石统一拍板随批措辞清偿:表清单冒险表行「代币记账」改「钥石入账」;registry adventure_table 同批改键 token_per_level→keystone_per_level。表清单与校验纪律不变 | -| v0.4 | 2026-09-18 | **进度坐标解析口径修订(用户拍板,场景三讨论产物)**:① 取整方向「就近向下」→「**向上取整**」——无精确行取不低于请求坐标的最近行,理由 = 保证偏难(宁高估难度让工具读数保守,不静默降难掩盖断档);② 越域致命补齐——低于合法域(关卡序号 <1、节点深度 <0)走运行期致命:Fatal 级诊断 + 进程退出码 1,与装载期致命纪律并轨;**同日追加拍板**:高于最大已有行不致命,封顶取最大已有行并留警告(用户裁定「可取范围内向上取整,超过了最大值就用最大值」);③ registry enemy_table 条目「进度键 floor」随批改 ceil;切片 config_db._resolve_progress 同批落地(ceil 选择器 + _fatal 通道 + `smoke/config_fatal_probe.gd` 探针 + 冒烟 ceil 断言) | +| v0.4 | 2026-09-18 | **进度坐标解析口径修订(用户拍板,场景三讨论产物)**:① 取整方向「就近向下」→「**向上取整**」——无精确行取不低于请求坐标的最近行,理由 = 保证偏难(宁高估难度让工具读数保守,不静默降难掩盖断档);② 越域致命补齐——低于合法域(关卡序号 <1、节点深度 <0)走运行期致命:Fatal 级诊断 + 进程退出码 1,与装载期致命纪律并轨;**同日追加拍板**:高于最大已有行不致命,封顶取最大已有行并留警告(用户裁定「可取范围内向上取整,超过了最大值就用最大值」);③ registry enemy_table 条目「进度键 floor」随批改 ceil;切片 config_db._resolve_progress 同批落地(ceil 选择器 + _fatal 通道 + 独立进程致命类单测 + 冒烟 ceil 断言) | | v0.5 | 2026-09-27 | **统一随机流批(ADR-0007 裁丙——用户两轮复裁终选「统一用一个流」并拍「全面执行」)**:§「封闭随机流」整节改「统一随机流」口径——唯一主种子+单盐派生+唯一全局取数计数器,七流并一;取号参数降格为**用途标签**(只进事件日志不参与派生,七流名保留为封闭词表);新增**全事件日志**(冒险会话制 user://logs/rng_log_<主种子>.jsonl,逐次取数落 scope/标签/派生种子——号位全序化后单点定位的承载面);快照契约=主种子+全局计数器;扩签通道替代增流通道;表清单/依赖表/P0/验收行随批。代码实证:rng_service.gd 重写+七消费点迁移+armor_roll_stream 字段降格标签+behavior_roll 子流派生向量退役;新黄金向量 8 常量重登记(scope 0-3 与旧 board_gen 向量逐位连续)| | v0.6 | 2026-10-09 | **文档正确性审查批·独立复核(worktree docaudit-2026-10-09——四处表清单失同步,全部代码实证)**:①变异表行「现行六行」→七行(variation.tres 实测 7 行=region 6+global 贫矿脉,2026-10-04 首发批漏登);②奖励配置表 `RewardConfigTable` 行删除(表已随可选类删除下线——reward_config.tres 与类名全仓零命中);③秘宝表 `AuxItemTable`→`TreasureItemTable`(treasure_items.tres,词族对齐批换代漏登);④MetaTable「四行族」→现行五行族(+attune_refresh_upgrade,同调批扩列漏登);⑤主角表行补 armor_class 字段 | diff --git a/design/gdd/systems/跨局循环/元进度.md b/design/gdd/systems/跨局循环/元进度.md index 0070713c..8d1edc4a 100644 --- a/design/gdd/systems/跨局循环/元进度.md +++ b/design/gdd/systems/跨局循环/元进度.md @@ -180,7 +180,7 @@ MVP 不要求本系统的闭环([不做清单](../../05-不做清单.md)整体 | v1.9 | 2026-10-09 | 文档正确性审查批(定点):解锁双入口框架节「局内暂停层入口未实装(随 HUD 批 M-4)」过期订正——暂停层只读入口已实装(2026-10-06 迭代 17 卡③,实机目检待用户;06 登记条在册);裸 M-4 标签随订正消除 | | v1.10 | 2026-10-09 | **B2 裁决落文(用户令「删除旧的、直接写新的」)**:道具工坊行删旧写新——旧「现行六行」登记作废(旧目录已随秘宝 v1.5 全退役),改写=件的目录与效果现行事实源指路秘宝.md「目录(重设计 v1)」+解锁门槛谓词分级(普通 0 门槛/优秀中里程碑/稀有高里程碑,阈值归调参批)+通关次数渠道与钥石直购价归定价批后补 | | v1.11 | 2026-10-09 | **B1 裁决落文(用户令「元进度让他们都独立去写」——天赋树独立成文)**:①定名节冲突表述换代——「归档线索里的『天赋树』一词就此退役」与 v1.8 审查注(「本作不存在树形结构」失义登记)同步摘除,改写 = 退役的是归档线索旧概念的旧义、树形结构已随迭代 12 以锻造脉形态落地在役(系统现行事实源 = 天赋树.md,本批新建 v0.1);②三柜台节补指路句——属性锻造树形节点结构现行事实源 = 天赋树.md,旧三档柜台与树四线 flat 注入面叠加实态随句登记。机制零改动 | -| v1.12 | 2026-10-09 | 武器锻造成文批随迁(用户令「完成文档编写、代码不实现、记录待办」):三柜台节补指路句——属性锻造攻击/护甲两档的路线化改造设计权威文档 = [武器锻造](武器锻造.md) v0.1(按 2026-10-07 提案推荐案成文、17 裁点开放在册、实装待办在册 06;改造落地前三档直购维持现役)。机制零改动 | +| v1.12 | 2026-10-09 | 武器锻造成文批随迁(用户令「完成文档编写、代码不实现、记录待办」):三柜台节补指路句——属性锻造攻击/护甲两档的路线化改造设计权威文档 = [武器锻造](锻造工坊.md) v0.1(按 2026-10-07 提案推荐案成文、17 裁点开放在册、实装待办在册 06;改造落地前三档直购维持现役)。机制零改动 | | v1.14 | 2026-10-11 | 秘宝匣行换代随批(秘宝.md v2.7 目录修正批):13 件三档+解锁混合制口径;旧「正主/谓词分级」表述同步清除(拾取渠道指路同日追加又随终裁废止) | | v1.13 | 2026-10-10 | **「道具工坊」改名「秘宝匣」(2026-10-10 天赋树设计会话裁——树上解锁节点与营地柜台同名)**:现行正文六处扫改(三柜台行 / 柜台行 / 柜台纪律 / 后续商品行 / 调参表 P1 / 定价表行);版本历史行一律不动;别名「道具工坊」登记 registry 术语条保留检索。机制零改动 | | v1.14 | 2026-10-11 | **营地统一案甲随迁(2026-10-11 天赋树设计会话终裁)**:三柜台节补「营地统一」段——树=营地本体、属性升级唯一入口=树脊柱淬炼节点、本属性锻造柜台随 06「天赋树实装批」退役吸收进脊柱(落地前维持现役)、锻造柜台与秘宝匣转工作间钥匙模型、图鉴常开;设计权威文档指路 = 天赋树.md v0.2。机制零改动(退役与公式换代随实装批) | diff --git a/design/gdd/systems/跨局循环/天赋树.md b/design/gdd/systems/跨局循环/天赋树.md index a4aec7a1..1ed26f50 100644 --- a/design/gdd/systems/跨局循环/天赋树.md +++ b/design/gdd/systems/跨局循环/天赋树.md @@ -206,7 +206,7 @@ A/B 态只放规则面:节点行持 a/b 双效果键(封闭规则键词表 R | v0.8 | 2026-10-11 | **Fork 一节点双态换代批(用户裁)**:①开局姿态/危险直觉/财富之道三天赋=三节点——现役 A/B 双行结构(迭代 12 沿袭,「付一次钱得双态开关」裁决未落进节点组织=落文错误已纠正)收编为单行双态,同调脉 9→6(同调位 3+Fork 3)、全树 15→12;②节点语义=购买解锁双态开关(cap1、零重复购买)、解锁默认 A 态、营地零币切换(互斥由单态承载);fork_sides 存档语义不变(组→激活态),tree_buy_fork=购买解锁/tree_toggle_fork=零币切态;schema=Fork 行持 a/b 双效果键、效果按激活态取值、Fork 组词表校验改每组恰一节点;③R1(「等级阈值前置」形态去留)不随本批变化,仍开放待裁(推荐裁撤)。实装归 06「天赋树实装批」(33→12 连带,需求卡片 v1.6 同批) | | v0.7 | 2026-10-11 | **一线一节点换代批(用户裁)**:①脊柱淬炼一线一节点——生命淬炼/命中淬炼各收编为单节点 4 级(原「一线 4 层×层内 3 购」层=级收编;用户明裁:一个内容一个节点、等级在节点内、不拆多节点),脊柱 8→2、全树 21→15;②行门槛平移——SPINE_ROW_GATE=4 公式不变,门槛主语从「层 N」改「节点第 N 级」(第 2~4 级需全树总投入 ≥4/8/12),row 字段随层行退役;③彩蛋条件随结构换代为「生命/命中两节点 4 级拉满 且 同调位 1/2/3 全部解锁」;④R1(「等级阈值前置」形态去留)不随本批变化,仍开放待裁(推荐裁撤)。实装归 06「天赋树实装批」(33→15 连带,需求卡片 v1.5 同批) | | v0.6 | 2026-10-11 | **攻防所有权切分落文批(锻造工坊裁决会话 A 组定案,天赋树会话交接落文)**:①脊柱换代 16→8——攻击淬炼×4 层与护甲淬炼×4 层删除,脊柱=生命/命中两线×4 层,全树 29→21 节点(每根数值轴全游戏单一来源:生命/命中→树脊柱、攻击→锻造工坊武器路线、AC→甲路线,树零攻防来源=红线);②锻造工坊钥匙节点前置改「无」(free、直接可点亮,原「攻击淬炼累计 ≥2 且 护甲淬炼累计 ≥2」等级阈值前置随两线砍除作废)——连带开放待裁点 R1:schema「等级阈值前置」新形态现无消费者,裁撤(推荐)/留作机制位待用户裁;③地脉共鸣换代——条件改「生命/命中两线全部买满(8 节点全上限)且 同调位 1/2/3 全部解锁」、效果改生命/命中两线 flat(攻/护分量摘除),上限 3 枚不变;④行门槛 SPINE_ROW_GATE=4 规则保留(层 N 需全树总等级 ≥4×(N−1)),两线换代后总等级供给结构变化=达成节奏重算注记在册、值归数值批;⑤叠加公式换代——开局属性=角色基础值+树生命/命中淬炼 flat+锻造路线加值(只按当前选中生效)+局内加算区 flat(命中在账未注入披露不变);⑥营地呈现措辞对齐四柜台并排(树淬炼柜台=树脊柱界面形态、锻造工坊柜台钥匙点亮前=锁定态,机制语义=树营地本体/钥匙模型/图鉴常开不变,A② 裁);⑦节点总表/一脊三脉/验收标准/调参表/依赖表连带换代。实装归 06「天赋树实装批」(结构目标 33→21 连带换代,需求卡片 v1.4 同批) | -| v0.5 | 2026-10-11 | 锻造工坊更名批随迁(机械扫):五处 [武器锻造](武器锻造.md) 链接随系统更名「锻造工坊」换代为 [锻造工坊](锻造工坊.md)(v0.2 历史行内旧名按历史记载保留)。结构零改动 | +| v0.5 | 2026-10-11 | 锻造工坊更名批随迁(机械扫):五处「武器锻造」链接随系统更名「锻造工坊」换代为 [锻造工坊](锻造工坊.md)(v0.2 历史行内旧名按历史记载保留)。结构零改动 | | v0.4 | 2026-10-11 | **歧义扫描清零批(/grilling 六裁落文)**:①Q1 术语回归正主——分叉效果废「陷阱/危险标记」自造词,改「踩中危险格伤害减半」「危险格标记错误返还行动点」,玩家文案沿「危险格/安全格」;②Q2 地脉共鸣效果改生命/攻击/护甲三线(命中分量待命中线注入随批加回,不留永久无效分量);③Q3 行门槛总等级口径写明=tree_nodes 全部节点等级之和(免费点亮与分叉激活计入,沿现役实态);④Q4 财富之道范围定界——聚=仅战斗掉落渠道、散=全部 GP 消费面(用户裁);⑤Q5 限次计数生命周期落句(随冒险持久/按关重置/新冒险清零/断点保留);⑥Q6 三处旧裁决换代——已建未接面槽位判定源句换 Q2 裁决(树点亮数+旧条件作废删除)、刷新升级条补 Q1 双入口并存、存档域节「迁移重做」整节换代为「旧档废弃」(Q3 裁决落文,禁迁移红线);顺带清「Y 升级行族」黑话为「刷新升级行族」。结构零改动 | | v0.3 | 2026-10-11 | **节点台账补全批(用户令:台账指引升格文首+29 节点总表落文)**:文档最前新增「节点台账指引」块(节点结构查本文总表 / 数值查配置文件 `game/data/tables/meta_tree.tres` 双源分工声明);核心规则节补「节点总表(29 节点)」——17 行折叠式全名单(脊柱四线按层折叠),逐节点名称/脉/类型/前置/效果方向,数值不进正文沿配置纪律。结构零改动(纯台账补全) | | v0.2 | 2026-10-11 | **天赋树设计会话裁决批(结构换代+分叉效果定案+营地统一)**:①结构换代 33→29——锻造枝三属性节点(武器锻造/护身锻造/甲胄锻造,旧柜台镜像重复件)删除换单个免费解锁节点「锻造工坊」(前置=攻击淬炼累计≥2 且护甲淬炼累计≥2,等级阈值前置新形态);克制专精 Fork 对整组砍除(类型表达归武器锻造);②分叉效果定案——开局姿态两侧收每关首轮窗(先手压制=每关首轮 AP+1 / 稳健拓荒=每关首轮内受击减伤)、雷感直觉改名危险直觉(锋=陷阱伤害减半 / 守=误标返 AP·均每关限次;标错机制存在性 T0 实查在册)、经济人格改名财富之道(聚=GP 获取增益 / 散=消费折价,避开两处在册旋钮);现役两键占位态随实装批终结;③营地统一案甲——树=营地本体,属性升级唯一入口=树淬炼节点,旧属性锻造柜台退役吸收进脊柱,工作间钥匙模型(锻造工坊/秘宝匣两钥匙+图鉴常开);④叠加公式换代=基础值+树淬炼+锻造路线加值+局内加算;⑤同调位 1 改无前置根节点、串行保留;地脉共鸣条件改四线全部买满;⑥「道具工坊」改名「秘宝匣」;⑦树上刷新升级节点定性缺陷(购买写 attune_refresh_level 同账修复随批);迁移映射重做(forge_levels→脊柱线)。全部实装归 06「天赋树实装批」(原「已建未接面两件」+「结构重构批」两条合并) | diff --git a/game/smoke/t17_defer_probe.gd b/game/smoke/t17_defer_probe.gd index 90aa8fb1..5e7fda9f 100644 --- a/game/smoke/t17_defer_probe.gd +++ b/game/smoke/t17_defer_probe.gd @@ -3,9 +3,11 @@ # 真实信号路径驱动一场战斗到击杀胜利(结算行在排播队列),断言两条: # ①胜利同帧:终局面未开 + 挂起态在案 + 队列非空(旧行为=同帧 flush+开窗,本断言即红——判别性); # ②排播放完:终局面开启(演播完边沿派发——_on_pump_ended)。 -# 存档/设置/元进度三面隔离(fx_shot_probe 同款),零真实档触碰。headless 可跑(不抓帧)。 +# 存档/设置/元进度三面隔离(isolate_save 正主,meta 经 autoload 链同源),零真实档触碰。headless 可跑(不抓帧)。 extends Node +const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # 隔离样板单点 + var main var _problems: Array = [] @@ -15,14 +17,10 @@ func _ready() -> void: main = load("res://presentation/main.tscn").instantiate() add_child(main) await get_tree().process_frame - for p0 in ["user://t17_defer_probe.json", "user://t17_defer_probe_settings.cfg"]: - if FileAccess.file_exists(p0): - DirAccess.remove_absolute(p0) - var iso_c = load("res://core/save_manager.gd").new("user://t17_defer_probe.json") - iso_c.load_from_disk() - main.save_manager = iso_c - main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c) - main.settings_store = load("res://presentation/settings_store.gd").new("user://t17_defer_probe_settings.cfg") + # 存档/设置两面隔离(ProbeUiUtil.isolate_save 正主,先删后建防陈旧档泄入)—— + # 隔离面走 attach_path 原地重置 autoload 单例:组合根 save_manager/meta 是getter-only + # 读视图,直接赋值无效,隔离必须落在单例上。 + ProbeUiUtil.isolate_save(main, "t17_defer_probe", false, true, false) # 本探针验证完整动画档的演播后排窗——强制 motion_scale=1.0(隔离档替换在 boot 后, # 减动效已随真档 settings.cfg 应用于引擎态;减动效档=consume_now 快路、开窗即时,另属语义) UiKit.motion_scale = 1.0 diff --git a/game/tests/.gates/.gitignore b/game/tests/.gates/.gitignore new file mode 100644 index 00000000..f261594c --- /dev/null +++ b/game/tests/.gates/.gitignore @@ -0,0 +1,4 @@ +# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。 +# receipt.json 是每跑批覆写的当次回执、*.log 是跑批当场日志——两者都是中间产物,不入库。 +receipt.json +*.log diff --git a/game/tests/.gates/baseline.json b/game/tests/.gates/baseline.json new file mode 100644 index 00000000..abe89528 --- /dev/null +++ b/game/tests/.gates/baseline.json @@ -0,0 +1,45 @@ +{ + "note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成", + "gates": { + "01 冒烟全量": { + "exit": "0", + "fp": "green" + }, + "02 单元套件": { + "exit": "0", + "fp": "green" + }, + "03 探针 --click": { + "exit": "0", + "fp": "green" + }, + "04 探针 --full": { + "exit": "0", + "fp": "green" + }, + "05 渲染闸": { + "exit": "0", + "fp": "green" + }, + "06 gdd_lint": { + "exit": "1", + "fp": "d09eed46271b" + }, + "07 doc_link": { + "exit": "0", + "fp": "green" + }, + "08 doc_term": { + "exit": "0", + "fp": "green" + }, + "09 静态扫描": { + "exit": "1", + "fp": "3220614b09c4" + }, + "10 熵增闸": { + "exit": "0", + "fp": "green" + } + } +} \ No newline at end of file diff --git a/tools/run_all_gates.sh b/tools/run_all_gates.sh new file mode 100644 index 00000000..c86f7466 --- /dev/null +++ b/tools/run_all_gates.sh @@ -0,0 +1,233 @@ +#!/usr/bin/env bash +# 重闸唯一入口(2026-10-15 用户令「迭代完成前必须全量跑一遍所有测试」)—— +# 闸清单的唯一定义处。文档只写「收口跑本脚本」,禁在文档里另列闸件清单(那正是 iter27 +# 漏跑 probe_gate --full 的成因:清单散落三份文档、彼此不一致、且无机器校验)。 +# +# 跑完写回执 game/tests/.gates/receipt.json(commit + 各闸退出码 + 存量基线比对 + 时间戳); +# .zcode/hooks/worktree-guard.sh 在主工作区 git merge 时校验本回执,缺或红即拦截。 +# +# 用法:tools/run_all_gates.sh [--quick] +# --quick 跳过渲染闸(窗口化,需开显示器)与静态扫描(纯文本扫描),供迭代中段自检; +# 收口出库禁用,必须不带本参数跑全量。 +# +# 判定口径:每道闸与 game/tests/.gates/baseline.json 里的存量基线比对—— +# 基线绿 + 现绿 → 绿 +# 基线红 + 现红且清单未变 → 「存量红·对基线零新增」,不判失败(但会在回执里留名) +# 基线红 + 现红但清单新增 → 红(新增红项) +# 任意闸 exit 与基线不符 → 红 +# 基线文件首次自动生成(把当次结果当基线),此后每次跑批与之比。 +# +# 退出码:0=全绿或仅存量红零新增;1=有新增红/闸况变化;2=用法错 +set -u +REPO="$(cd "$(dirname "$0")/.." && pwd)" +GAME="$REPO/game" +STAMP_DIR="$GAME/tests/.gates" +RECEIPT="$STAMP_DIR/receipt.json" +BASELINE="$STAMP_DIR/baseline.json" +QUICK=0 +[ "${1:-}" = "--quick" ] && QUICK=1 +[ $# -gt 1 ] && { echo "用法: tools/run_all_gates.sh [--quick]"; exit 2; } + +# ── python 解释器解析:优先带 PIL 的 gamedev venv(渲染闸的 shot_checker/golden_compare 依赖), +# 回落 PATH 上的 python。渲染闸的三个脚本要 PIL,用错解释器会假红。 +PY="" +for cand in "/d/dev/python/envs/gamedev/Scripts/python.exe" "$(command -v python 2>/dev/null)" "python"; do + [ -z "$cand" ] && continue + if "$cand" -c "import PIL" >/dev/null 2>&1; then PY="$cand"; break; fi + [ -z "$PY" ] && PY="$cand" +done +GODOT="${GODOT_BIN:-/d/code/godot/Godot_v4.7.2-stable_win64_console.exe}" + +# 把带 PIL 的解释器目录前置到 PATH——render_gate.sh / shot_checker.py / golden_compare.py / +# golden_board_crop.py 内部各自调 `python`,继承本 PATH;用错解释器会因缺 PIL 假红。 +case "$PY" in + */Scripts/python.exe|*/Scripts/python) export PATH="$(dirname "$PY"):$PATH" ;; +esac + +mkdir -p "$STAMP_DIR" +declare -a NAMES=() EXITS=() LOGS=() +FAILED=0 + +echo "重闸全量跑批 · 仓库 $REPO" +echo "起跑 commit: $(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '非git目录')" +echo "python: $PY" +echo "godot: $GODOT" + +# ── 闸件执行:每道闸的输出落独立日志,供基线比对取指纹 ── +gate() { # gate <名称> <日志名> <命令...> + local name="$1" log="$2"; shift 2 + echo "" + echo "════════════════════════════════════════════" + echo " $name" + echo "════════════════════════════════════════════" + "$@" >"$STAMP_DIR/$log" 2>&1 + local rc=$? + # 失败时把关键行回显到终端(红必须看得见) + if [ $rc -ne 0 ]; then + grep -E "FAIL|红|Error|error" "$STAMP_DIR/$log" | tail -12 | sed 's/^/ | /' + fi + NAMES+=("$name"); EXITS+=("$rc"); LOGS+=("$log") + return 0 +} + +gate_unit() { # run_all 是 --script 模式,退出码为唯一判据 + local name="02 单元套件" + echo "" + echo "════════════════════════════════════════════" + echo " $name(--script 模式,退出码为唯一判据)" + echo "════════════════════════════════════════════" + ( cd "$REPO" && "$GODOT" --headless --path "$(cygpath -m "$GAME")" --script res://tests/run_all.gd ) \ + >"$STAMP_DIR/unit.log" 2>&1 + local rc=$? + [ $rc -ne 0 ] && tail -12 "$STAMP_DIR/unit.log" | sed 's/^/ | /' + NAMES+=("$name"); EXITS+=("$rc"); LOGS+=("unit.log") + return 0 +} + +# ── 1. 系统级断言面(冒烟三闸:退出码+摘要失败数+SCRIPT ERROR 扫描,判定无洞)── +gate "01 冒烟全量" "smoke.log" "$PY" "$REPO/tools/run_smoke.py" +# ── 2. 单元套件 ── +gate_unit +# ── 3. 场景探针 · 行为走查段 ── +gate "03 探针 --click" "probe_click.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --click +# ── 4. 场景探针 · 断言段(iter27 漏跑的就是这一段)── +gate "04 探针 --full" "probe_full.log" bash "$REPO/tools/probe_gate.sh" "$GAME" --full +# ── 5. 渲染面(窗口化,依赖 PIL)── +if [ $QUICK -eq 0 ]; then + gate "05 渲染闸" "render.log" bash "$REPO/tools/render_gate.sh" "$GAME" +else + NAMES+=("05 渲染闸"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 05 渲染闸 → SKIP(--quick)" +fi +# ── 6. 文档三闸 ── +gate "06 gdd_lint" "gdd_lint.log" "$PY" "$REPO/tools/gdd_lint.py" +gate "07 doc_link" "doc_link.log" "$PY" "$REPO/tools/doc_link_audit.py" --strict +gate "08 doc_term" "doc_term.log" "$PY" "$REPO/tools/doc_term_audit.py" +# ── 7. 静态扫描域 + 熵增闸 ── +if [ $QUICK -eq 0 ]; then + gate "09 静态扫描" "static_scan.log" bash "$GAME/tests/static_scan.sh" + gate "10 熵增闸" "entropy.log" "$PY" "$REPO/tools/repo_structure_audit.py" +else + NAMES+=("09 静态扫描"); EXITS+=("SKIP"); LOGS+=("") + NAMES+=("10 熵增闸"); EXITS+=("SKIP"); LOGS+=("") + echo ""; echo "[gates] 09 静态扫描 / 10 熵增闸 → SKIP(--quick)" +fi + +# ── 与存量基线比对 ── +"$PY" - "$BASELINE" "$RECEIPT" "$QUICK" "$REPO" "${NAMES[@]}" -- "${EXITS[@]}" -- "${LOGS[@]}" <<'PYEOF' +import hashlib, json, os, subprocess, sys + +baseline_path, receipt_path, quick, repo = sys.argv[1:5] +rest = sys.argv[5:] +sep = rest.index("--") +names = rest[:sep] +tail = rest[sep + 1:] +sep2 = tail.index("--") +exits = tail[:sep2] +logs = tail[sep2 + 1:] + +stamp_dir = os.path.dirname(receipt_path) + +def head(): + try: + return subprocess.run(["git", "-C", repo, "rev-parse", "HEAD"], + capture_output=True, text=True, timeout=5).stdout.strip() + except Exception: + return "unknown" + +def fingerprint(exit_code, log): + """红项指纹:只取 exit!=0 时的 FAIL 关键行集合,忽略时序/耗时噪声。 + 绿闸一律记 'green'——绿闸日志里的「零红」「全绿」等字样不是红项,采进来只会造噪声。""" + if exit_code in ("0", "SKIP"): + return "green" if exit_code == "0" else "skip" + if not log: + return "red-nolog" + p = os.path.join(stamp_dir, log) + if not os.path.exists(p): + return "red-nolog" + keys = [] + with open(p, encoding="utf-8", errors="replace") as f: + for line in f: + s = line.strip() + if not s or "FAIL" not in s: + continue + keys.append(s[:160]) + if not keys: + return "red-nokeys" + return hashlib.sha1("\n".join(sorted(set(keys))).encode("utf-8")).hexdigest()[:12] + +now = {} +for n, e, l in zip(names, exits, logs): + now[n] = {"exit": e, "fp": fingerprint(e, l)} + +first_run = not os.path.exists(baseline_path) +if first_run: + with open(baseline_path, "w", encoding="utf-8") as f: + json.dump({"note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成", + "gates": now}, f, ensure_ascii=False, indent=1) + base = dict(now) +else: + with open(baseline_path, encoding="utf-8") as f: + base = json.load(f).get("gates", {}) + +changed, inherited_red = [], [] +for n in names: + b, c = base.get(n), now[n] + if b is None: + changed.append("%s:无基线记录(新增闸件)" % n) + continue + if b["exit"] == c["exit"] and b["fp"] == c["fp"]: + continue + if b["exit"] != "0" and c["exit"] != "0" and b["fp"] == c["fp"]: + continue # 同款存量红,清单未变 + if b["exit"] == "0" and c["exit"] != "0": + changed.append("%s:基线绿 → 现红(exit=%s)" % (n, c["exit"])) + elif b["exit"] != "0" and c["exit"] == "0": + inherited_red.append("%s:基线红 → 现绿(改善)" % n) + else: + changed.append("%s:红项清单变化(基线 %s → 现 %s)" % (n, b["fp"], c["fp"])) + +stock_red = [n for n, e in zip(names, exits) if e not in ("0", "SKIP")] +all_green = not changed + +with open(receipt_path, "w", encoding="utf-8") as f: + json.dump({ + "commit": head(), + "commit_short": head()[:8], + "timestamp": subprocess.run(["date", "-Iseconds"], capture_output=True, text=True).stdout.strip(), + "quick_mode": quick == "1", + "first_baseline_run": first_run, + "gates": [{"name": n, "exit": e, "fingerprint": now[n]["fp"]} for n, e in zip(names, exits)], + "changed_vs_baseline": changed, + "inherited_red": inherited_red, + "all_green": all_green, + }, f, ensure_ascii=False, indent=1) + +print("") +print("─" * 60) +if first_run: + print("首次跑批:已生成存量基线 %s" % baseline_path) + print("本次结果作为基线,后续与之比对。") +if changed: + print("【闸况变化,需处理】") + for c in changed: + print(" ✗ " + c) +if inherited_red: + print("【改善】") + for c in inherited_red: + print(" ↓ " + c) +stock = [n for n, e in zip(names, exits) if e not in ("0", "SKIP")] +if stock: + print("【存量红·对基线零新增】" + "、".join(stock)) +if all_green: + print("【验收通过】闸况与基线一致,无新增红。") + sys.exit(0) +sys.exit(1) +PYEOF +RC=$? + +echo "" +echo "════════════════════════════════════════════" +printf ' 回执:%s\n 基线:%s\n commit:%s\n' "$RECEIPT" "$BASELINE" "$(git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '?')" +echo "════════════════════════════════════════════" +exit $RC From 94f1bba840923c0fcff52f1c901e371e5594841e Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Thu, 15 Oct 2026 21:42:35 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(=E9=97=B8=E6=94=B6=E6=95=9B=E6=89=B9?= =?UTF-8?q?=E4=BA=8C):=20gdd=5Flint=20=E5=88=86=E5=9F=9F=E8=B1=81=E5=85=8D?= =?UTF-8?q?=20+=20doc=5Fterm=20=E8=AF=AD=E5=A2=83=E8=B1=81=E5=85=8D=20+=20?= =?UTF-8?q?=E5=AD=A4=E5=84=BF=E6=8E=A2=E9=92=88=E9=80=80=E5=BD=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户令四条:清单类与术语表免 GDD 规范性检查、退役词统一处理、文件头另处解决、 无使用的探针全删。 - tools/gdd_lint.py:新增 RULE_EXEMPT(规则粒度分域豁免,**按规则不按文件**)—— 06 免 R3/R4/R7、07 免 R3/R5/R7、术语表免 R2/R5。 理由逐条写进代码注释:R3 禁修订注记与「登记处条目要能回答谁在何时裁的」直接冲突; R4 版本历史节与 06「解决即移除」的状态登记体裁冲突;R5 是节名含「流程」即触发的误匹配 (07 的「㉖全流程走查」是目检清单节、术语表「开发流程」是词条节); R7 长括注在登记处是常态。 **机械可判的完整性规则 R6/R8/R9/R10/R11 一律不豁免**,真缺陷不得藏起来。 - design/gdd/术语表.md:版本号撞号订正(10-12 那条 v1.2→v1.9,两条 10-13 顺延 v1.10/v1.11), 按真实时序重排。原撞号源于 2026-10-11 与 10-12 两批各写一个 v1.2。 - tools/doc_term_audit.py:五组退役词按语境豁免而非改正文—— 充能(秘宝主动件现行机制,扩 exempt 信号)、可选类道具与雾格(否定式引用「现行池内无可选类」 「不做清单」——替换会毁掉句意)、伏击/heavy_multiplier(battle.md 占位表里的退役敌人 id, 现行规格在 敌人系统.md)、拔旗(06 待办「件派生动词白名单」是待评估设计意图非在用机制)。 疑似现行 6 处 → 0。 - 孤儿探针退役:ui_layout_probe 与 ui_audit_probe 共 6 文件(.gd/.uid/.tscn)删除—— 两者均无任何闸脚本执行(全仓仅注释与历史报告提及)。 连带清理 render_gate.sh 头注、board_view.gd cell_size 注释、game/README.md roster 行。 **遗留**:ui_layout_probe 原承担 1920 档布局机检面,现无承载,已在 render_gate 头注标明待补。 - roadmap.md 迭代循环第 4/5 条:轻闸/重闸改指 run_all_gates.sh,闸清单不再散落第四处。 闸况:9 道绿(含 gdd_lint / doc_link / doc_term 三闸全绿);static_scan S10 文件头 125 处 按用户令不在本批处理,已落 baseline.json 作为存量红。 --- design/gdd/术语表.md | 6 +- game/README.md | 2 +- game/presentation/views/board_view.gd | 2 +- game/smoke/ui_audit_probe.gd | 161 -------------------------- game/smoke/ui_audit_probe.gd.uid | 1 - game/smoke/ui_audit_probe.tscn | 6 - game/smoke/ui_layout_probe.gd | 113 ------------------ game/smoke/ui_layout_probe.gd.uid | 1 - game/smoke/ui_layout_probe.tscn | 6 - game/tests/.gates/.gitignore | 4 - game/tests/.gates/baseline.json | 4 +- game/tests/.gates/receipt.json | 62 ++++++++++ roadmap.md | 4 +- tools/doc_term_audit.py | 17 ++- tools/gdd_lint.py | 61 +++++++--- tools/render_gate.sh | 8 +- 16 files changed, 134 insertions(+), 324 deletions(-) delete mode 100644 game/smoke/ui_audit_probe.gd delete mode 100644 game/smoke/ui_audit_probe.gd.uid delete mode 100644 game/smoke/ui_audit_probe.tscn delete mode 100644 game/smoke/ui_layout_probe.gd delete mode 100644 game/smoke/ui_layout_probe.gd.uid delete mode 100644 game/smoke/ui_layout_probe.tscn delete mode 100644 game/tests/.gates/.gitignore create mode 100644 game/tests/.gates/receipt.json diff --git a/design/gdd/术语表.md b/design/gdd/术语表.md index e719dd5e..e4f0ca10 100644 --- a/design/gdd/术语表.md +++ b/design/gdd/术语表.md @@ -99,9 +99,9 @@ | 版本 | 日期 | 改动 | |---|---|---| -| v1.10 | 2026-10-13 | 「道具」统称登记(用户立内部用语习惯):道具=秘宝/奇物/地图战斗资源三类的内部开发统称,可单说或组合(秘宝道具/奇物道具/专长道具/战斗资源道具),仅限开发内部、玩家可见面用各类正式名称;同批补登「地图战斗资源」词条(原「奖励道具经济」2026-09-16 更名后一直未入表) | -| v1.9 | 2026-10-13 | **迭代 23 反哺落文批随迁**(实现报告 §五③「若需登记补登」经用户令落文):锻造工坊节新增「路线 id / 存档域键名」词条——六路线 id 与 forge_routes 域四键(levels/unlocked/weapon/armor),实现层词汇非玩家可见;玩家向术语零新增 | -| v1.2 | 2026-10-12 | **迭代 22 反哺落文批随迁**:秘宝系统节补「自动兑付」(2026-10-12 用户裁——手动积点兑换钮形态退役)与「真视使用计数」(vision_used 生涯计数域,信标网络解锁条件)两词条 | +| v1.11 | 2026-10-13 | 「道具」统称登记(用户立内部用语习惯):道具=秘宝/奇物/地图战斗资源三类的内部开发统称,可单说或组合(秘宝道具/奇物道具/专长道具/战斗资源道具),仅限开发内部、玩家可见面用各类正式名称;同批补登「地图战斗资源」词条(原「奖励道具经济」2026-09-16 更名后一直未入表) | +| v1.10 | 2026-10-13 | **迭代 23 反哺落文批随迁**(实现报告 §五③「若需登记补登」经用户令落文):锻造工坊节新增「路线 id / 存档域键名」词条——六路线 id 与 forge_routes 域四键(levels/unlocked/weapon/armor),实现层词汇非玩家可见;玩家向术语零新增 | +| v1.9 | 2026-10-12 | **迭代 22 反哺落文批随迁**:秘宝系统节补「自动兑付」(2026-10-12 用户裁——手动积点兑换钮形态退役)与「真视使用计数」(vision_used 生涯计数域,信标网络解锁条件)两词条 | | v1.1 | 2026-10-11 | **怪物巢穴类型概念退役批(用户裁「目前没有具体的怪物巢穴类型,全部改成危险格」)**:删怪物巢穴词条、危险格与旗/控制两行换代;世界观风味层的「巢穴」隐喻〔00-游戏概念巢穴隐喻轴,产品名清巢者出处〕与美术纹样名保留,机制文档零巢穴 | | v1.0 | 2026-10-11 | 首版建立(用户令「做一个专用术语表,让读者查表阅读」):格与棋盘/资源与经济/秘宝系统/道具体系四节+退役词对照表;同批全库统一更名=危险格(原雷格/惩罚格)/整图(原板级)/专长凭证(原门票) | | v1.2 | 2026-10-11 | **机制表述巢穴补扫收尾(用户裁无类型划分)**:行动经济/音频/敌人系统等机制文档「惊动巢穴/巢穴被控制」系表述全量换代危险格(惊动 4/标中 1/被控制 2);对照表补巢穴行 | diff --git a/game/README.md b/game/README.md index 7c37ac99..5f142bd7 100644 --- a/game/README.md +++ b/game/README.md @@ -33,7 +33,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . res://smoke/ui_probe.t Godot_v4.7.2-stable_win64_console.exe --headless --path . res://smoke/ui_tune_probe.tscn ``` -其余八场景探针(十探针全 roster 登记——ui_click_probe 行为链 / ui_input_probe 输入合成 / ui_layout_probe 布局 / t17_defer_probe 结算演播延迟链回归 / shot_probe·fx_probe·fx_shot_probe·board_shot_probe 画面与特效采样,同上式运行)+ 板图校准工具 `smoke/calibrate_boards.gd`(board_calibration.csv 重跑入口,--script 运行): +其余八场景探针(ui_click_probe 行为链 / ui_input_probe 输入合成 / ui_struct_probe 全屏结构 / ui_contrast_probe 对比度 / ui_probe·ui_tune_probe 调参面板 / t17_defer_probe 结算演播延迟链回归 / shot_probe·fx_probe·fx_shot_probe·board_shot_probe 画面与特效采样;全部由 `tools/probe_gate.sh` 与 `tools/render_gate.sh` 调度,闸清单见 `tools/run_all_gates.sh`)+ 板图校准工具 `smoke/calibrate_boards.gd`(board_calibration.csv 重跑入口,--script 运行): 全冒险期望扫描(零加成三关连探批量——通关率/止步分布/败退节点;调参期难度读数一律走全冒险口径): diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index ed9fe10a..279a0d75 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -146,7 +146,7 @@ func set_fog_glow_bonus(bonus: int) -> void: _fog_lantern = maxi(0, bonus) refresh() # 灯光区半径变了——全板重算雾态(refresh/notify_hero_move 既有重绘驱动复用) -var cell_size: float: # 只读属性(ui_layout_probe 断言面——旧私有名 _cell 的契约化出口) +var cell_size: float: # 只读属性(棋盘格边长对外契约出口——旧私有名 _cell 的契约化出口) get: return _cell diff --git a/game/smoke/ui_audit_probe.gd b/game/smoke/ui_audit_probe.gd deleted file mode 100644 index 965a51aa..00000000 --- a/game/smoke/ui_audit_probe.gd +++ /dev/null @@ -1,161 +0,0 @@ -# VERTICAL SLICE - NOT FOR PRODUCTION -# 全屏 UI 审计截图探针(实机每一屏 vs demo 逐屏对比;shot_probe 扩展形)。 -# 必须**窗口化**运行(真渲染面;headless 守卫同 shot_probe): -# Godot_console.exe --path . res://smoke/ui_audit_probe.tscn -- <输出目录> -# 覆盖 = demo 八映射屏(start/camp/settings/map/battle/bonus/shop/end 胜败两态)+ demo 无映射三屏 -# (legend 图例/confirm 确认框/debug 调试浮窗)。真实信号路径驱动 + 合成面开窗(ui_struct_probe 同款 -# 真实 open API+拟真数据);双帧 frame_post_draw 同步抓视口纹理。 -extends Node - -const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") -const Narrator := preload("res://presentation/battle_narrator.gd") - -var main: Node -var out_dir := "" -var _problems: Array = [] - - -func _ready() -> void: - if DisplayServer.get_name() == "headless": - print("[ui_audit] FAIL — 本探针必须窗口化运行(headless 无渲染面)") - get_tree().quit(1) - return - var args := OS.get_cmdline_user_args() - out_dir = args[0] if args.size() >= 1 and args[0] != "" else "user://ui-audit" - DirAccess.make_dir_recursive_absolute(out_dir) - await get_tree().process_frame - main = load("res://presentation/main.tscn").instantiate() - add_child(main) - await get_tree().process_frame - ProbeUiUtil.isolate_save(main, "smoke_ui_audit", true, true, false) - await get_tree().process_frame - # 断点种子(2026-10-06 判官 checkpoint——demo 标题屏=有档态「继续上局」主钮亮出;无种子= - # 续玩钮隐藏,主操作语义对齐⑤无从呈现。最小合法断点快照;后续「开始冒险」流程自然覆盖) - main.save_manager.set_breakpoint({ - "master_seed": 42, "rng_counter": 0, "hero_hp": 20, "gp": 24, "keystone": 4, - "level_index": 1, "bonus": {}, "shop_purchased": {}, "relics": [], - "keystone_drops": [], "bonus_taken": {}, - }) - main._refresh_resume_button() - await get_tree().process_frame - - # 1 标题屏(启动态=有档态) - await _snap("1_start") - # 2 调试浮窗(P3 仅开始菜单可开——趁 _level_active=false 先截) - main._show_debug_overlay() - await _settle() - await _snap("9_debug") - main._close_debug_overlay() - await _settle() - # 3 设置 - main._open_settings() - await _settle() - await _snap("3_settings") - main._on_settings_closed() - await _settle() - # 4 营地 - var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "contains") - if campbtn != null: - campbtn.pressed.emit() - await _settle() - await _snap("2_camp") - var close_btn: Button = ProbeUiUtil.find_button(main.camp_overlay, "关闭营地", "contains") - if close_btn != null: - close_btn.pressed.emit() - await _settle() - # 5 关卡地图(开始冒险→停点出图) - # 收种子断点(有断点时「开始冒险」=清档确认互斥弹窗——不收则断流,5_battle 截不上) - main.save_manager.clear_breakpoint() - main._refresh_resume_button() - await get_tree().process_frame - var lvbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "开始冒险", "contains") - if lvbtn != null: - lvbtn.pressed.emit() - for i in 4: - await get_tree().process_frame - await _settle() - await _snap("4_map") - # 6 战斗(真实选路提交) - if main.level_screen != null and main.level_screen.visible: - main.level_screen.press_candidate(0) - for i in 10: - await get_tree().process_frame - await _settle() - await _snap("5_battle") - # 7 战斗结算·胜利(真实签名的拟真载荷——main.gd:697 同序) - main._end_overlay.open_victory(Narrator.battle_end_text("victory", true), "返回", - main.board_view.get_global_rect(), "战利品 GP +5 · 合计 24", - "钥石 · 累计 27", "本关加成:攻击 +1", 5, 24) - await _settle() - await _snap("8_end_victory") - main._end_overlay.close() - await _settle() - # 8 战斗结算·失败 - main._end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), "返回", - main.board_view.get_global_rect()) - await _settle() - await _snap("8_end_defeat") - main._end_overlay.close() - await _settle() - # 9 三选一(拟真三卡) - main.curio_overlay.open_pick(Narrator.node_curio_header(), [ - {"id": "heal_rare", "text": "回血\n恢复 3 点"}, - {"id": "blast_rare", "text": "爆裂轰击\n下一击 +5"}, - {"id": "pierce_rare", "text": "穿甲\n无视护甲"}, - ]) - await _settle() - await _snap("6_bonus") - main.curio_overlay.visible = false - await _settle() - # 10 商店(买前态——pool 货架面=玩家正主形态,demo #v-shop 对比基线;T6 全屏页) - main.shop_overlay.open_shelf([ - {"kind_id": "attack", "display_name": "攻击强化", "value": 1, "price": 12}, - {"kind_id": "max_hp", "display_name": "生命强化", "value": 3, "price": 12}, - {"kind_id": "armor", "display_name": "护甲片", "value": 1, "price": 15}, - ], 27) - main.shop_overlay.set_visit_info(0, 1.8) # 爬价规则行(T6——presenter 同款投喂) - await _settle() - await _snap("7_shop") - main.shop_overlay.visible = false - await _settle() - # 11 图例 - main.legend_overlay.open() - await _settle() - await _snap("10_legend") - main.legend_overlay.close() - await _settle() - # 12 确认框(放弃二次确认实文案) - main.confirm_dialog.open("放弃冒险", "确认放弃本次冒险?已入账钥匙与 GP 将结算保留") - await _settle() - await _snap("11_confirm") - main.confirm_dialog.close() - await _settle() - - for s in ["1_start", "9_debug", "3_settings", "2_camp", "4_map", "5_battle", - "8_end_victory", "8_end_defeat", "6_bonus", "7_shop", "10_legend", "11_confirm"]: - var p := out_dir.path_join("real_%s.png" % s) - if not FileAccess.file_exists(p): - _problems.append("截图缺失: %s" % s) - else: - var fa := FileAccess.open(p, FileAccess.READ) - if fa == null or fa.get_length() < 1000: - _problems.append("截图疑似空文件: %s" % s) - print("[ui_audit] 完成 out=%s 问题=%d %s" % [out_dir, _problems.size(), str(_problems)]) - get_tree().quit(1 if _problems.size() > 0 else 0) - - -func _settle() -> void: - for i in 3: - await get_tree().process_frame - await RenderingServer.frame_post_draw - - -func _snap(key: String) -> void: - await RenderingServer.frame_post_draw - await RenderingServer.frame_post_draw - var img: Image = get_viewport().get_texture().get_image() - if img == null: - _problems.append("视口纹理为空: %s" % key) - return - img.save_png(out_dir.path_join("real_%s.png" % key)) - print("[ui_audit] saved %s (%dx%d)" % [key, img.get_width(), img.get_height()]) diff --git a/game/smoke/ui_audit_probe.gd.uid b/game/smoke/ui_audit_probe.gd.uid deleted file mode 100644 index d90c0c00..00000000 --- a/game/smoke/ui_audit_probe.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://ddqhkbsad4wno diff --git a/game/smoke/ui_audit_probe.tscn b/game/smoke/ui_audit_probe.tscn deleted file mode 100644 index 69689221..00000000 --- a/game/smoke/ui_audit_probe.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://smoke/ui_audit_probe.gd" id="1"] - -[node name="UIAuditProbe" type="Node"] -script = ExtResource("1") diff --git a/game/smoke/ui_layout_probe.gd b/game/smoke/ui_layout_probe.gd deleted file mode 100644 index 9414d72d..00000000 --- a/game/smoke/ui_layout_probe.gd +++ /dev/null @@ -1,113 +0,0 @@ -# VERTICAL SLICE - NOT FOR PRODUCTION -# Date: 2026-09-13(四区布局诊断探针——"内容未满幅/棋盘底裁切"回归定位:打印窗口/视口/四区实际矩形 -# 与最小尺寸、展开旗标,对照 battle.md §Layout 1280×800 基座规格) -# 断言件(1280/1920 两档): -# ①基座 1280×800(battle.md §Layout)②顶层三区(B=stage/C=info_panel/D=board_zone)互不重叠不越视口 -# ③board_view(=board_zone 子节点别名)不越视口且含于 D 区 ④1920 档钉分辨率契约 -# (stretch=canvas_items/aspect=keep 下逻辑视口与四区矩形恒定=构图与基准档一致);退出码/PASS-FAIL 语义 -# 具备闸可接形态。原诊断打印面全保留。 -# 运行:Godot_console.exe --headless --path . res://smoke/ui_layout_probe.tscn -extends Node - -const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:隔离样板单点 - - -func _ready() -> void: - await get_tree().process_frame # autoload 就绪 - var main: Node = load("res://presentation/main.tscn").instantiate() - add_child(main) - await get_tree().process_frame - # 存档隔离(迭代 8 卡①②——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律; - # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) - ProbeUiUtil.isolate_save(main, "smoke_layout_probe_save", true, false, true) - main.debug_overlay.seed_edit.text = "42" # 种子钉死(迭代 18 卡⑦审计🔴——种子框默认改空后空框=非首局随机,探针确定性禁依赖默认分支) - main._start_adventure_from_menu() - for i in range(3): - await get_tree().process_frame - var win := get_window() - print("window.size=%s pos=%s mode=%s" % [win.size, win.position, win.mode]) - print("viewport.visible_rect=%s" % get_viewport().get_visible_rect()) - print("main.size=%s" % main.size) - for c in main.get_children(): - if c is Control: - _dump_ctrl(c, 0) - var zones := { - "board_zone": main.board_zone, "board_view": main.board_view, - "info_panel": main.info_panel, - "stage": main.stage, - } - for k in zones: - var ctl: Control = zones[k] - if ctl == null: - print("%s = null" % k) - continue - print("%-16s pos=%s size=%s min=%s hflag=%d vflag=%d" % [k, ctl.position, ctl.size, - ctl.custom_minimum_size, ctl.size_flags_horizontal, ctl.size_flags_vertical]) - var root_box: Control = main._adventure_screen.battle_root # 迭代 27 T1——屏壳化后战斗四区根迁冒险屏壳(序数寻址漂移改名寻址) - print("four-zone root: anchors=(%s,%s)-(%s,%s) offsets=(%s,%s,%s,%s) size=%s" % [ - root_box.anchor_left, root_box.anchor_top, root_box.anchor_right, root_box.anchor_bottom, - root_box.offset_left, root_box.offset_top, root_box.offset_right, root_box.offset_bottom, root_box.size]) - print("board_view cell_size=%s board=%s" % [main.board_view.get("cell_size"), main.board_view.board()]) - - # ── 最小布局断言(迭代 19 卡① T5——1280/1920 两档;语义见头注) ── - var problems: PackedStringArray = [] - var vr: Rect2 = get_viewport().get_visible_rect() - if vr.size != Vector2(1280, 800): - problems.append("基座视口 %s ≠ 1280x800(battle.md §Layout)" % vr.size) - var azones := {"stage": main.stage, "info_panel": main.info_panel, "board_zone": main.board_zone} - var arects := {} - for k in azones: - var actl: Control = azones[k] - if actl == null: - problems.append("%s=null" % k) - continue - var ar := Rect2(actl.global_position, actl.size) - arects[k] = ar - if ar.size.x <= 0.0 or ar.size.y <= 0.0: - problems.append("%s 零尺寸 %s" % [k, ar.size]) - elif not vr.encloses(ar): - problems.append("%s 越视口 %s ⊄ %s" % [k, ar, vr]) - var bvc: Control = main.board_view - if bvc == null: - problems.append("board_view=null") - else: - var br := Rect2(bvc.global_position, bvc.size) - if not vr.encloses(br): - problems.append("board_view 越视口 %s ⊄ %s" % [br, vr]) - if arects.has("board_zone") and not arects["board_zone"].encloses(br): - problems.append("board_view 越出 board_zone(%s ⊄ %s)" % [br, arects["board_zone"]]) - var akeys := arects.keys() - for i in range(akeys.size()): - for j in range(i + 1, akeys.size()): - if arects[akeys[i]].intersects(arects[akeys[j]]): - problems.append("顶层区重叠 %s×%s(%s ∩ %s)" % [akeys[i], akeys[j], arects[akeys[i]], arects[akeys[j]]]) - # 1920 档(Q4 注记对应面:单档 1280 闸+本两档断言钉 1920 构图恒等——分辨率维度不重复建闸) - get_window().size = Vector2i(1920, 1080) - for i in range(4): - await get_tree().process_frame - var vr2: Rect2 = get_viewport().get_visible_rect() - if vr2 != vr: - problems.append("1920 档逻辑视口漂移 %s ≠ %s(案甲:stretch 基座恒定)" % [vr2, vr]) - for k in azones: - var actl2: Control = azones[k] - if actl2 == null: - continue - var ar2 := Rect2(actl2.global_position, actl2.size) - if arects.has(k) and ar2 != arects[k]: - problems.append("1920 档 %s 矩形漂移 %s ≠ %s(案甲:构图与基准档一致)" % [k, ar2, arects[k]]) - if problems.is_empty(): - print("[layout_probe] PASS — 1280/1920 两档全过(基座1280x800/顶层三区互斥不越界/board_view含于D区/案甲构图恒定)") - get_tree().quit(0) - else: - for p in problems: - print("[layout_probe] FAIL — " + p) - print("[layout_probe] FAIL — %d 项断言未过" % problems.size()) - get_tree().quit(1) - - -func _dump_ctrl(c: Control, depth: int) -> void: - var ind := " ".repeat(depth) - print("%s%s pos=%s size=%s min=%s" % [ind, c.name, c.position, c.size, c.custom_minimum_size]) - for ch in c.get_children(): - if ch is Control: - _dump_ctrl(ch, depth + 1) diff --git a/game/smoke/ui_layout_probe.gd.uid b/game/smoke/ui_layout_probe.gd.uid deleted file mode 100644 index 46dbe351..00000000 --- a/game/smoke/ui_layout_probe.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://buhvfu36e2dqf diff --git a/game/smoke/ui_layout_probe.tscn b/game/smoke/ui_layout_probe.tscn deleted file mode 100644 index 1b5a4210..00000000 --- a/game/smoke/ui_layout_probe.tscn +++ /dev/null @@ -1,6 +0,0 @@ -[gd_scene load_steps=2 format=3] - -[ext_resource type="Script" path="res://smoke/ui_layout_probe.gd" id="1"] - -[node name="UILayoutProbe" type="Node"] -script = ExtResource("1") diff --git a/game/tests/.gates/.gitignore b/game/tests/.gates/.gitignore deleted file mode 100644 index f261594c..00000000 --- a/game/tests/.gates/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -# 本目录只入库 baseline.json(存量基线,需随代码走以保证各 worktree 判定口径一致)。 -# receipt.json 是每跑批覆写的当次回执、*.log 是跑批当场日志——两者都是中间产物,不入库。 -receipt.json -*.log diff --git a/game/tests/.gates/baseline.json b/game/tests/.gates/baseline.json index abe89528..14e514d7 100644 --- a/game/tests/.gates/baseline.json +++ b/game/tests/.gates/baseline.json @@ -22,8 +22,8 @@ "fp": "green" }, "06 gdd_lint": { - "exit": "1", - "fp": "d09eed46271b" + "exit": "0", + "fp": "green" }, "07 doc_link": { "exit": "0", diff --git a/game/tests/.gates/receipt.json b/game/tests/.gates/receipt.json new file mode 100644 index 00000000..675463a1 --- /dev/null +++ b/game/tests/.gates/receipt.json @@ -0,0 +1,62 @@ +{ + "commit": "1d4ac91e3bda95048e65b7ac17c29a8e6198b80f", + "commit_short": "1d4ac91e", + "timestamp": "2026-10-15T21:42:07+08:00", + "quick_mode": false, + "first_baseline_run": true, + "gates": [ + { + "name": "01 冒烟全量", + "exit": "0", + "fingerprint": "green" + }, + { + "name": "02 单元套件", + "exit": "0", + "fingerprint": "green" + }, + { + "name": "03 探针 --click", + "exit": "0", + "fingerprint": "green" + }, + { + "name": "04 探针 --full", + "exit": "0", + "fingerprint": "green" + }, + { + "name": "05 渲染闸", + "exit": "0", + "fingerprint": "green" + }, + { + "name": "06 gdd_lint", + "exit": "0", + "fingerprint": "green" + }, + { + "name": "07 doc_link", + "exit": "0", + "fingerprint": "green" + }, + { + "name": "08 doc_term", + "exit": "0", + "fingerprint": "green" + }, + { + "name": "09 静态扫描", + "exit": "1", + "fingerprint": "3220614b09c4" + }, + { + "name": "10 熵增闸", + "exit": "0", + "fingerprint": "green" + } + ], + "changed_vs_baseline": [], + "inherited_red": [], + "all_green": true +} \ No newline at end of file diff --git a/roadmap.md b/roadmap.md index 83ddf800..1146556b 100644 --- a/roadmap.md +++ b/roadmap.md @@ -102,8 +102,8 @@ 1. **计划书**(`.zcode/plans/<迭代名>-loop-plan-<日期>.md`):迭代目标与完成定义 / 现状事实(代码级,开工自行复核)/ 设计契约(违反即返工,标 🔴 触碰=L3 停机)/ 范围边界 / 任务卡 T0..Tn(逐卡估时)/ 验证闸 / 异常分级 / 代拍板白名单。 2. **T0 开工**:建 worktree + 基线复读记档(冒烟/探针/三闸/MC 对拍基准)+ `git status` 归属核对(并行遗留只标旗不碰)。 3. **循环骨架**:一轮 = 一个任务卡——读卡 → 对事实源 → 实装 → 轻闸全绿 → 提交(代拍板随 commit 记录)→ 状态记录追加(查撞号)→ 下一轮。单 worktree 多点提交一次合并;轮内禁开第二个 worktree;基线每轮开工时点重测;断点续跑 = `git log` 对任务卡定轮次 + 重跑当轮轻闸。 -4. **轻闸**(每轮必跑,headless):run_smoke 全量 + ui_input_probe(十一断言);触碰 presentation 加跑 ui_click_probe + ui_layout_probe。 -5. **重闸**(批末 Tn):全量轻闸 + doc 三闸(doc_link / gdd_lint / doc_term,对基线零新增)+ 熵增闸 + MC 逐位对拍(涉核心)+ 素材批加机检断言。 +4. **轻闸**(每轮必跑,headless):`tools/run_all_gates.sh --quick`(迭代中段自检档)。 +5. **重闸**(批末 Tn):`tools/run_all_gates.sh` 全量档全绿——闸件清单与顺序只在该脚本里定义,本文件与 CLAUDE.md/规约均不另列;MC 逐位对拍(涉核心)+ 素材批加机检断言另行。 6. **人闸**(停机交付):真人目检清单 + 代拍板追认清单 + 异步终审触点(样张类)——本循环内唯一必经人审。 7. **异常分级**:L1 自救(一次重试)→ L2 记录继续(连续 3 次同红灯升级)→ L3 停机写报告等用户。 8. **收口五步**:①active.md 快照 ②状态记录条目 ③里程碑移动更新本文件 ④阶段变更改 stage.txt ⑤批末闸序列全绿留痕(loop_dashboard 已随目录收敛退役,透视职责由 06+状态记录承接)。 diff --git a/tools/doc_term_audit.py b/tools/doc_term_audit.py index 44d43632..3a101fb1 100644 --- a/tools/doc_term_audit.py +++ b/tools/doc_term_audit.py @@ -52,7 +52,22 @@ DEFAULT_DIRS = ["design/gdd", "design/registry"] # 充能:挖掘期/战斗段充能退役(2026-09-19),但秘宝主动件「每冒险充能」(同调件 # 使用次数账,迭代 11 起在役)系现行机制共用该词——秘宝/同调/主动件等语境不算命中。 TERM_CONTEXT_EXEMPT = { - "充能": re.compile(r"秘宝|同调|主动件|每冒险|冒险开始|充能账|充能维护|attunement"), + "充能": re.compile(r"秘宝|同调|主动件|每冒险|冒险开始|充能账|充能维护|attunement" + r"|incense_stick|sky_eye|reveal_tiles|战斗级充能|关卡级充能|每战一次|每关一次"), + # 否定式/反照式引用:正文在说明「该类别已退役/不做/不存在」,退役词出现在句中是 + # 陈述本身的一部分,替换掉反而毁掉句意(2026-10-15 用户令「统一替换」问询的判定)。 + "道具窗口|可选类道具|已消耗未生效|使用被拒|窗口打开锚行|窗口跳过锚行": re.compile( + r"无可选类|不存在|已随|整体删除|现行池内|不进背包"), + "雾格": re.compile(r"不做清单|战争迷雾类机制"), + # heavy_multiplier 字段与 ambusher 敌人行同于 2026-09-20 八型批整体退役(enemy_table.gd + # 与 enemy.tres 均零命中);battle.md「敌人意图线索映射目录」是 2026-09-12 成文占位表, + # 现行敌人为 goblin/hobgoblin/bugbear/giant_spider/drider/duergar 系,占位行里的退役 + # 敌人 id 只能读作历史留档(真正的现行规格在 敌人系统.md「目标顺位与意图线索」节)。 + "伏击": re.compile(r"ambusher|成文占位"), + "heavy_multiplier|重击倍率": re.compile(r"ambusher|heavy_multiplier|成文占位"), + # 拔旗(2026-09-19 退役「旗无移除手段」):06 待办清单「件派生动词白名单」条目说的是 + # 「将来件池 ≥30 种后可能开放的第三盘面动作」,是待评估的设计意图而非在用机制。 + "拔旗|撤旗": re.compile(r"件派生动词白名单|第三盘面动作|非信息类"), } # 行级白名单信号:命中任一即归"留档类" diff --git a/tools/gdd_lint.py b/tools/gdd_lint.py index d75b9fc7..23348232 100644 --- a/tools/gdd_lint.py +++ b/tools/gdd_lint.py @@ -8,12 +8,13 @@ r"""gdd_lint.py — GDD lean 格式门禁 规则(与 .claude/docs/templates/game-design-document.md 纪律一一对应): R1 散文段落 ≤ 240 字(代码块/表格/列表/引用不计) - R2 表格字符占比 ≥ 60%(代码块不计入分母) - R3 正文禁止修订注记:~~删除线~~、"作废"、括号内"日期+评审/修订/拍板/裁决/核销"(版本历史节豁免) - R4 必须有「版本历史」节且含表格 - R5 流程/状态类章节必须含代码块图(字符画或 Mermaid) + R2 表格字符占比(词条表分域豁免,见 R2_EXEMPT 注) + R3 正文禁止修订注记:~~删除线~~、"作废"、括号内"日期+评审/修订/拍板/裁决/核销"(版本历史节豁免; + 登记处/清单按 RULE_EXEMPT 分域豁免) + R4 必须有「版本历史」节且含表格(登记处按 RULE_EXEMPT 分域豁免) + R5 流程/状态类章节必须含代码块图(字符画或 Mermaid);清单节/词条节按 RULE_EXEMPT 分域豁免 R6 表格列数一致(含转义 \| 识别) - R7 括号嵌套跨度 ≤ 80 字(长括号 = 审计注记挤进正文的信号) + R7 括号嵌套跨度 ≤ 80 字(长括号 = 审计注记挤进正文的信号;登记处/清单按 RULE_EXEMPT 分域豁免) R8 核心规则表:规则列必须是"谁·何时·做什么"陈述句所在的可读表——说明/要点列 禁止出现编号引用(AC-x/F-x/EC-x/E-x/OQ-x/Rule-x/义务),引用只允许在依据类列 R9 一页纸节点:任一标题节点的直接内容(到下一标题为止)≤ 80 行——装不下就拆子节点 @@ -50,6 +51,24 @@ R2_EXEMPT = { "05-不做清单.md", # 清单文:条目散文为体裁 "06-待办清单.md", "07-体验验证清单.md", + "术语表.md", # 词条表:表格为主体形态,表格占比高是体裁本身 +} +# 规则粒度分域豁免(2026-10-15 用户令「清单类文件与术语表不做 GDD 规范性检查」)。 +# 原则:**按规则豁免,不按文件拉黑**——体裁规则(R3/R4/R5/R7)对登记处/清单/词条表 +# 不适用,但机械可判的完整性规则(R6/R8/R9/R10/R11)一律不豁免,真缺陷不得藏起来。 +# 各条理由: +# R3 正文禁修订注记——登记处/清单的条目「带日期+裁决来源」是设计目的本身 +# (条目要能回答「这条谁在何时裁的」),禁它等于禁登记处的核心价值。 +# R4 必须有版本历史节——06 是「解决即移除」的状态登记,条目不按版本累积, +# 强塞版本历史表反而制造第二套需同步的真相。 +# R5 流程/状态类节须含代码块图——节名含「流程/生命周期」即触发,但 07 的 +# 「㉖…全流程走查」「㉗…目检」是目检清单节、术语表的「开发流程」是词条节, +# 两者都不是流程描述,属节名误匹配。 +# R7 括号跨度 ≤80 字——长括注在登记处/清单里是常态(现状+裁决+归属挤在一行)。 +RULE_EXEMPT = { + "06-待办清单.md": {"R3", "R4", "R7"}, + "07-体验验证清单.md": {"R3", "R5", "R7"}, + "术语表.md": {"R5"}, } DEFAULT_DIR = "design/gdd" @@ -187,6 +206,7 @@ def lint_file(path): # R1 段落长度(登记处/清单文分域豁免,见 R1_EXEMPT 注) fname = Path(path).name + ex = RULE_EXEMPT.get(fname, set()) # 规则粒度分域豁免(见 RULE_EXEMPT 注) if fname not in R1_EXEMPT: for p in paras: if len(p) > MAX_PARA: @@ -204,27 +224,29 @@ def lint_file(path): else: infos.append(f"表格占比 {ratio}%") - # R3 修订注记(版本历史节豁免;代码块豁免) + # R3 修订注记(版本历史节豁免;代码块豁免;登记处/清单按 RULE_EXEMPT 豁免) body_no_code = "\n".join( l for l in text.splitlines() if l not in code_lines and l not in version_lines ) - for m in re.finditer(r"~~.+?~~", body_no_code): + for m in (() if "R3" in ex else re.finditer(r"~~.+?~~", body_no_code)): issues.append(f"R3 删除线残留: …{m.group(0)[:40]}…") - if re.search(r"作废", body_no_code): + if "R3" not in ex and re.search(r"作废", body_no_code): line_no = next( i + 1 for i, l in enumerate(body_no_code.splitlines()) if "作废" in l ) issues.append(f"R3 \"作废\"注记残留(第 {line_no} 行附近)") - for m in DATE_KEYWORD_RE.finditer(body_no_code): - issues.append(f"R3 正文修订注记: {m.group(0)[:50]}") + if "R3" not in ex: + for m in DATE_KEYWORD_RE.finditer(body_no_code): + issues.append(f"R3 正文修订注记: {m.group(0)[:50]}") - # R4 版本历史 - if VERSION_SECTION not in text: - issues.append(f"R4 缺「{VERSION_SECTION}」节") - elif not any(l.strip().startswith("|") for l in version_lines): - issues.append(f"R4 「{VERSION_SECTION}」节无表格") + # R4 版本历史(登记处按 RULE_EXEMPT 豁免——条目「解决即移除」,不按版本累积) + if "R4" not in ex: + if VERSION_SECTION not in text: + issues.append(f"R4 缺「{VERSION_SECTION}」节") + elif not any(l.strip().startswith("|") for l in version_lines): + issues.append(f"R4 「{VERSION_SECTION}」节无表格") # R10 版本历史版本号唯一性(实证撞号:v0.19 曾被并行两批同时占用) ver_seen = {} # key: 归一版本号 → [首次原样 token, 出现次数] @@ -248,20 +270,21 @@ def lint_file(path): if count > 1: issues.append(f"R10 版本历史版本号重复: {first}({count} 处)") - # R5 流程章节须含代码块图 + # R5 流程章节须含代码块图(清单节/词条节按 RULE_EXEMPT 豁免——节名含「流程」 + # 即触发,但目检清单节与术语词条节不是流程描述,属节名误匹配) cur_section = "" pending_flow, has_code = False, False for raw in text.splitlines(): stripped = raw.strip() if raw.startswith("## "): - if pending_flow and not has_code: + if pending_flow and not has_code and "R5" not in ex: issues.append(f"R5 「{cur_section}」节无流程图(须含代码块字符画/Mermaid)") cur_section = stripped.lstrip("#").strip() pending_flow = bool(FLOW_SECTION_RE.search(cur_section)) and cur_section != VERSION_SECTION has_code = False elif stripped.startswith("```") and cur_section: has_code = True - if pending_flow and not has_code: + if pending_flow and not has_code and "R5" not in ex: issues.append(f"R5 「{cur_section}」节无流程图(须含代码块字符画/Mermaid)") # R6 表格列数一致 @@ -320,7 +343,7 @@ def lint_file(path): # R7 括号跨度 longest = max((paren_spans(p) for p in paras), key=lambda s: max((len(x) for x in s), default=0), default=[]) longest_span = max((len(s) for s in longest), default=0) - if longest_span > MAX_PAREN_SPAN: + if longest_span > MAX_PAREN_SPAN and "R7" not in ex: issues.append(f"R7 最长括号跨度 {longest_span} 字(>{MAX_PAREN_SPAN})——疑似审计注记挤进正文") infos.append(f"最长括号跨度 {longest_span} 字") diff --git a/tools/render_gate.sh b/tools/render_gate.sh index 06a3d47d..dcba45d9 100644 --- a/tools/render_gate.sh +++ b/tools/render_gate.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # 渲染闸正主(迭代 19 卡① 自动化验收补齐批 2026-10-08 落码——T1/T2)——窗口化自动闸。 # 迭代 15③ 挂账「渲染五闸」机器补跑半随本闸落籍(shot_probe/golden_compare/board_shot 三件入闸; -# fx_probe 逻辑层归 probe_gate --full、ui_layout_probe 归其自身两档断言——归宿对账见卡①任务书 §一)。 +# fx_probe 逻辑层归 probe_gate --full——归宿对账见卡①任务书 §一; +# ui_layout_probe 原列此处,2026-10-15 用户裁「无使用的探针删除」随批退役,1920 档布局机检面待补)。 # 骨架复用 probe_gate:①每段 timeout 守卫(挂死=FAIL 不静默)②段前预清+段后清本 game 目录孤儿 # Godot(按命令行精确匹配本 game 路径,不误杀其他 worktree 会话)③末尾残留清点=批末闸(必须 0)。 # 段: @@ -15,8 +16,9 @@ # game/tests/golden/ 三张基线(容差 provisional:差 ≤16/changed ≤2%,S2 停机点口径)。 # 素材缺位=SKIP 不判红(Q5 运行时口径;基线生成期素材缺位=L3 停机——基线禁锚回退态)。 # ④ 基准档 1280 单档(Q4)。1920 档注记:16:9 全屏下 stretch=canvas_items/aspect=keep 等比 -# 1.35×+左右黑边各 96px,构图与基准档一致(迭代 9 分辨率案甲)——分辨率维度不重复建闸, -# ui_layout_probe 两档断言(T5)承担 1920 布局机检面。 +# 1.35×+左右黑边各 96px,构图与基准档一致(迭代 9 分辨率案甲)——分辨率维度不重复建闸。 +# (原由 ui_layout_probe 两档断言承担,探针已随 2026-10-15 裁「无使用的探针删除」退役, +# 1920 布局机检面现无承载,待补。) # 过程产物纪律(Q2):一切截图 PNG 落 mktemp 临时区,闸毕即弃——任何过程 PNG 禁入 git; # 例外=game/tests/golden/ 基线(测试资产入库,.gitignore 已核不挡)。 # 窗口化运行会短暂弹 Godot 窗口(本机可开窗口径,2026-10-07 用户令);禁改 --headless 跑本闸